You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* feat(plugin-security): authoring-time gate for package-owned permission sets + projection dogfood proof (ADR-0094 D5)
Closes the last inert-metadata hole from the ADR-0094 pure-projection
refactor (#2898), and binds the invariants into the liveness proof registry.
- metadata-protocol: new registerAuthoringGate(type, fn) seam — a per-type
gate run inside saveMetaItem before persistence; a returned rejection
becomes a thrown Error (code/status). Fail-open on a gate error (this
closes an inert-metadata hole, not a hard boundary — the data-plane
two-doors gate + the projector's refusal already protect the record).
- plugin-security: registers a `permission` gate that refuses an env-scope
saveMetaItem targeting a managed_by:package sys_permission_set record —
previously such an overlay persisted but neither projected nor enforced
(ADR-0049 violation). The package door (save carries the owning packageId)
and env-authored/new sets are unaffected. Error `package_owned` (403).
- dogfood: showcase-permission-projection proves the ADR-0094 invariants on
the real stack (write-through, awaited projection, declared-set edit →
enforced overlay, delete-as-reset, and this authoring gate), registered in
the ADR-0054 proof registry (unbound — a storage invariant has no
authorable ledger property to ratchet, kept honest with a blockedReason).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXjD7g2JkEsdqhZFZgAo1Q
* feat(plugin-security): lock permission-set API name after creation + document projection UX (ADR-0094)
Setup UI alignment for the ADR-0094 pure-projection behavior. The Setup
surface is SDUI-driven by the object metadata, so both affordances are
object-metadata changes objectui already honors (ObjectForm forwards
`readonlyWhen` to fieldRules; the engine also strips such writes server-side)
— no objectui code change needed:
- sys_permission_set.name gains `readonlyWhen: record.id != null` — the name
is the metadata identity the record projects from, so the create form
accepts it but the edit form locks it, matching the data-door's 400 rename
rejection. Verified the meta API surfaces the CEL predicate and the boot
stack loads it cleanly.
- permission-sets.mdx documents the one-authoritative-store model: Setup edits
of declared sets now enforce, the API name is immutable (clone to rename),
and deleting a packaged set resets it to the shipped definition while a
self-created set is removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXjD7g2JkEsdqhZFZgAo1Q
* docs(adr-0094): addendum generalizing the one-authoritative-store rule to sibling two-store types
Promote the permission-set-specific decision to a classification rule for
every declared-metadata ↔ queryable-record two-store type (sys_position,
sys_sharing_rule, sys_capability), keyed on which store enforcement reads at
request time:
- metadata-authoritative → record is a projection (this ADR's machinery,
reusable via registerMutationProjector / registerAuthoringGate);
- record-authoritative → the record is the authority and declared metadata is
a boot SEED only (seed-not-clobber; do NOT project — that would invert the
real authority).
Records the per-type findings (sys_sharing_rule is record-authoritative —
evaluation reads the row live; sys_position needs the seed-vs-authority
audit), so the follow-up is scoped, not rediscovered. Tracked in framework#2909.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXjD7g2JkEsdqhZFZgAo1Q
* feat(plugin-security)!: package-owned permission sets customize via the standard env metadata overlay (ADR-0094 D5 revised)
Direction confirmed by the maintainers 2026-07-14, reversing the #2898
authoring-time rejection: an env-scope overlay of a package-owned set is the
platform's standard ADR-0005 customization, made FIRST-CLASS instead of
refused — rejecting it would make `permission` the one type whose declared
allowOrgOverride:true is a lie, and clone-to-customize forks away from vendor
baseline updates (including security tightenings).
- projector: a package-owned record's facets follow the EFFECTIVE
(overlay-wins) body; managed_by:'package' + package_id provenance preserved.
- write-through: a data-door edit of a packaged set translates into that
overlay (no more flat 403); data-door "delete" removes the overlay and
RESETS the record to the shipped declaration. Single-store kernels (no
metadata overlay layer) keep the legacy two-doors refusal, now asserted by
the write-through itself.
- two-doors gate narrows to what stays structurally true: provenance forging
refused (insert/update, single/array), and transfer/restore/purge on
package rows refused (no overlay translation exists for them).
- metadata-protocol: the registerAuthoringGate seam is removed again — with
the rejection gone it had no consumer, and an unconsumed seam is exactly
the inert surface ADR-0049 forbids.
- docs: ADR-0094 TL;DR/D2/D5 revised (including the whole-document-overlay
upgrade-pinning trade and its mitigations); authorization.mdx +
permission-sets.mdx updated; dogfood proofs flipped to the new semantics
(overlay customize/reset on showcase_contributor, live).
Closes#2898 (resolved by support, not rejection).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXjD7g2JkEsdqhZFZgAo1Q
* docs(permissions): fix role-word guard violation in permission-sets.mdx (ADR-0090 D3)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SXjD7g2JkEsdqhZFZgAo1Q
---------
Co-authored-by: Claude <noreply@anthropic.com>
Package-owned permission sets are now customizable through the standard environment metadata overlay (ADR-0094 D5, revised — closes framework#2898 by making the overlay FIRST-CLASS instead of rejecting it).
6
+
7
+
- An env-scope `saveMetaItem('permission', …)` on a package-owned set is a real customization: the awaited projector applies the effective (overlay-wins) body to the `sys_permission_set` record while preserving its `managed_by:'package'` + `package_id` provenance, and the evaluator enforces it.
8
+
- A data-door edit of a packaged set (Setup PATCH) is translated into exactly that overlay — no more flat 403; a data-door "delete" removes the overlay and RESETS the record to the shipped declaration (the row survives).
9
+
- The ADR-0086 two-doors data gate narrows to what stays structurally true: forging package provenance through the admin door remains refused, as do the lifecycle ops with no overlay translation (`transfer`/`restore`/`purge`) on package rows; kernels without a metadata overlay layer keep the legacy full refusal.
10
+
- Cross-package roles compose via positions (bind several packages' sets); overlays narrow. Rationale: rejecting the overlay would make `permission` the one type whose declared `allowOrgOverride: true` is a lie, and clone-to-customize forks away from vendor baseline updates.
11
+
12
+
Note the standard overlay trade, now applicable to permission sets: while an overlay pins a set, later vendor baseline changes (including tightenings) don't take effect for that name until the overlay is reset or re-authored — surfaced by the Studio layered diff and covered by ADR-0091 recertification.
13
+
14
+
Also lands a dogfood proof (`showcase-permission-projection`) covering the full ADR-0094 invariant set — write-through, awaited projection, declared-set edit becomes an enforced overlay, package-set customize/reset lifecycle — registered in the liveness proof registry.
`registerAuthoringGate` + boot reconciliation). The record must never be an
285
+
independent authority, because it isn't the one enforcement trusts.
286
+
-**Record-authoritative** (enforcement reads the `sys_*` record live) → the
287
+
record is the authority and the declared metadata is a **boot SEED only**,
288
+
not a competing overlay. The cure is the mirror image: the seeder must not
289
+
clobber an environment-edited record, and no path may treat the declared
290
+
body as a live override that silently loses to (or fights) the record. **Do
291
+
not** apply the projection machinery here — projecting the record *from*
292
+
metadata would invert the real authority.
293
+
294
+
The generic protocol seams added by this ADR (`registerMutationProjector`,
295
+
`registerAuthoringGate`) serve the *metadata-authoritative* case and are
296
+
reusable by any such type; the record-authoritative case needs no new seam,
297
+
only a seed-not-clobber discipline.
298
+
299
+
### Per-type decisions
300
+
301
+
| Type | Enforcement reads | Class | Decision |
302
+
| :-- | :-- | :-- | :-- |
303
+
|`sys_permission_set`| metadata (`PermissionEvaluator.resolvePermissionSets` → `metadata.list('permission')`, DB row only as fallback) | metadata-authoritative |**Record is a projection — done** (this ADR). |
304
+
|`sys_sharing_rule`| the record, live (`sharing-plugin.ts` "rule evaluation reads `sys_sharing_rule` live"; `sharing-rule-service``engine.find`) |**record-authoritative**| Declared rules are a **boot seed**; the record is the authority. Do **not** project. Audit that `bootstrapDeclaredSharingRules` preserves env-edited rows (seed-not-clobber) and that the metadata overlay is not read as a live override. |
305
+
|`sys_position`| mixed — position→permission-set resolution is metadata-first for the *sets*, but the `sys_position`**record** (incl. its `permissions` field, bindings, `delegatable`, `admin` gating) is read live by the anchor gate and `DelegatedAdminGate`|**needs the seed-vs-authority audit** against the criterion above; likely record-authoritative for bindings with metadata seeding identity | Classify precisely, then either project (if the position *definition* is enforced from metadata) or make declared positions seed-only. |
306
+
|`sys_capability`| the record (curated registry read for capability existence) | record-authoritative (registry) | Seed-only; low authoring surface. Audit seed-not-clobber. |
307
+
308
+
Only `sys_permission_set` was both metadata-authoritative **and** carried a
309
+
harmful, actively-drifting split-brain, which is why it was fixed first and in
310
+
full. The others are recorded here with their class so the follow-up work is
311
+
scoped, not rediscovered — tracked in framework#2909.
312
+
313
+
### Why an addendum, not a new ADR
314
+
315
+
The decision — *one authoritative store; the other derived; enforced
316
+
structurally* — is identical; only the per-type **direction** differs. A
317
+
separate ADR would duplicate the rationale and split the classification from
318
+
the decision that motivates it. This addendum keeps the rule and its
0 commit comments