Skip to content

Commit d112087

Browse files
feat(plugin-security)!: a package's isDefault suggestion takes effect once a manage_metadata holder accepts it for the deployment (#15204 stage U2) (#22854)
Part of #15204 — stage U2, build. This PR builds the U2 ruling. The other stages of the card stay open. Clause-②: yes (narrowing) ## What this does This PR builds ruling #22801 → A (ruling record 6108478638). A package's `isDefault: true` permission set reaches every signed-in member only through ONE deployment-level acceptance by a caller holding `manage_metadata`, and that acceptance takes effect. Under a wall, only the platform operator can make it. - **Accept** (`POST /api/v1/security/suggested-bindings/:id/confirm`) records the decision on the suggestion row. The security plugin then re-derives the `everyone` anchor's definition as the baseline plus every accepted set (`SecurityPlugin.declareEveryone`). It also runs at `kernel:ready`, on uninstall and on a package-door publish. Every derivation bumps the metadata epoch, so the grants caches retire. - **The high-risk gate is on the definition (ADR-0090 D7).** The accept refuses a set whose definition carries a bit the `everyone` anchor forbids, with `403 PERMISSION_DENIED` (ADR-0112). The derivation asks the same `describeAnchorForbiddenBits` again on each run. A set that later fails it, or that the catalog no longer serves, is withheld. The withheld set is reported with its reason: at `error` for forbidden bits, at `warn` otherwise. It is never dropped silently. - **Reconciliation reads the definition.** "Observed" now means that `everyone`'s definition names the set. No junction row is read. - **The acceptance record belongs to the Default Organization.** Every `sys_audience_binding_suggestion` row is read and written under the Default Organization's tenant: the one with `slug='default'`, else the only organization there is. That is the derivation plugin-auth's boot invariant uses. With none, the surface answers `409 SUGGESTION_STATE` and names the remedy. - **The reading of 2c's "sealed".** An acceptance changes the derivation's input, and no caller writes the anchor's definition. The plugin, which is the definition's only declarer, re-declares it through the registry seam it already used at boot. The metadata door still refuses `PUT /meta/position/everyone` with `403`, and the dogfood pin asserts this. The reading is written into `content/docs/permissions/authorization.mdx`, beside 2c's door, and into the changeset. - **The spec texts are kept and made true.** `confirmAudienceBindingSuggestion` keeps "writes the binding", and the package-level `isDefault` note keeps "an admin confirms in Setup". Each gains the mechanism it now describes. ADR-0090 D5 is unchanged: packages suggest, admins confirm, nothing is auto-bound. **Landing point.** The producer is the one the dispatch named: `packages/plugins/plugin-security/src/suggested-audience-bindings.ts`, the `everyone` derivation in `security-plugin.ts`, and the object. The REST routes and the dispatcher twin change text only. **What stages 6b-2 and 8 need is delivered.** The module no longer imports `readDeclared` or `upsertPackagePermissionSet`. It no longer reads `sys_position`, `sys_permission_set` or `sys_position_permission_set`. It writes no junction row. Declarations come from the security catalog (`securityCatalogReaderOf(ql).list('permission')`), the source the resolver reads set bodies from. ## Decisions taken inside the ruling (also in the report's `open_questions`) 1. **`guest`: refused loudly, not honoured.** A confirm of a `guest`-anchor row answers `409 SUGGESTION_STATE` and states why: the `guest` anchor's sets are not derived from acceptances, so accepting it would record a grant that never takes effect. No producer emits a `guest` suggestion today (only `isDefault` → `everyone`). The four axes: ① zero producers; ② no speculative guest derivation; ③ declared = enforced, refused rather than silently recorded; ④ no new capability. 2. **Revoke = dismiss on an accepted row.** The dispatch's pin asks for "back to 403 after revoke". A deployment-wide widening with uninstall as its only undo would leave a gap. Dismiss now takes a `pending` row (decline, as before) or an accepted row (revoke, and `everyone` is re-derived). Paths and envelopes are unchanged. A row only the baseline names is refused `409`. 3. **Rows written before this PR are never promoted.** A new field, `deployment_decision` (boolean, default `false`), marks a row a `manage_metadata` holder resolved. Only accepted rows with that flag feed the derivation. - A Default Organization row a tenant administrator confirmed or dismissed is set back to `pending` by the sync (`reopened`), unless the baseline already names the set. - A row owned by another organization, or by none, is not read. `reconcileAudienceBindingSuggestions` counts it (`unread`) and warns once per boot or publish. Such rows granted nothing before this PR and grant nothing now. There is no migration piece (#22601 → B). 4. **All three routes take the same authority, list included.** That authority is `manage_metadata`, plus the ADR-0095 `PLATFORM_ADMIN` rung under a wall. The ADR-0126 §5 rule for install-wide rows is read off the execution context: `systemPermissions` and `posture`, never a position name. 5. **The #18031 report is kept.** It covers a package record whose `manifest.permissions` is in the ADR-0025 reading. It still reads the installed package records, but only to make that report; it never reads a declaration from them. ## What objectui needs Paths, request shapes and response envelopes are unchanged: `{ data: { suggestions, synced } }` and `{ data: { suggestion, bindingCreated } }`. `synced` gains `reopened`. Two behaviour changes reach `SuggestedBindingsPanel`: - a caller without `manage_metadata` (a tenant administrator) now gets `403 PERMISSION_DENIED` on the list. The panel should not render for them, or should render the refusal. - `POST …/dismiss` on a `confirmed` row whose resolver is set revokes it. The panel lists `pending` only, so no revoke control exists yet. ## Tests `main` was merged after 6a (#22793) landed, through `scripts/pm/os-regen-merge.sh`. - The module conflict was 6a's one comment line, inside a function this PR deletes; ours was taken. - The tenant-audit census was regenerated over the merge, and its prose counts were corrected. - Row 57 of `system-context.mdx` was re-applied, because the script takes main's side of that file. Final head: `d493e22`. - `plugin-security`, `pnpm exec vitest run` at the final head: 196 files, 3,992 passed, 45 skipped. Typecheck is clean, test typecheck included. - `suggested-audience-bindings.test.ts` is rewritten for the deployment-level record: 37 cases. Each refusal case asserts `code` and `statusCode`. - creation, the observed path and pruning, all owned by the Default Organization; - a legacy row RE-OPENED and never promoted, and another organization's row unread and reported; - a refusal when no Default Organization exists; - the authority (tenant admin 403, unauthenticated 403, a non-operator under a wall 403, with the operator and the holder as controls); - accept → `everyone` re-derived, with an anti-vacuity check that the set was absent before; `bindingCreated` false on a baseline set; - the forbidden-bits refusal, and the three #18535 capability-token cases; - the different-package refusal, the `guest` refusal, and 404 on another organization's row; - the derivation withholding a set that gained a forbidden bit or left the catalog; - revoke, and dismiss of an observed row refused; - the #12981 refusal-accounting cases and the #18031 manifest report, kept. - `suggested-audience-bindings-install-path.test.ts` (557 lines) is deleted. Its whole subject was per-organization rows, which the ruling retires. Its two pinned `engine-double-contract` rows go with it. - **Dogfood pin**, `packages/qa/dogfood/test/suggested-binding-acceptance.dogfood.test.ts`: a real showcase with a vanilla `SecurityPlugin`, so `showcase_member_default` is a package suggestion. 5/5 pass: - control: pending, and the member gets 403; - a tenant administrator's list and accept get `403 PERMISSION_DENIED`, and nothing changes; - a forbidden-bit set's accept gets `403 PERMISSION_DENIED`; - the admin's accept answers 200 with `bindingCreated: true`. `everyone`'s definition gains exactly the set and stays `com.objectstack.plugin-security`'s declaration. No `sys_metadata` row shadows it, `PUT /meta/position/everyone` still answers 403, and the member's read goes 403 → 200; - revoke takes `everyone` back to the baseline, and the member gets 403 again. - **Ablation** (one-time proof, not kept). The `await deps.rederiveEveryone()` in the confirm path was replaced through `scripts/ablation-replace.mjs` (anchor 1 → 0, blob `1c0b0a67` → `10a90e69`). `plugin-security` was rebuilt, and `ablation-dist-preflight` found the marker in 2 built files. The dogfood accept case went red (`expected false to be true`, on `bindingCreated`). The tool restored the source (blob == HEAD, `git diff HEAD` empty). After a rebuild, `--absent` confirmed the marker is gone from all 6 built files and the tree is clean. - **Full dogfood suite** at the final head: 247 files passed, 1 skipped; 2,002 tests passed, 9 skipped. - Other packages: - `cli` at the final head: unit, 281 files, 4,185 passed; typecheck clean. - Before the merge, at `d99797c`: `rest` 272 files, 5,245 passed; `runtime` 349 files, 5,038 passed; `client` 51 files, 653 passed; `spec` (`src/contracts`, `src/security`) 58 files, 762 passed. Typecheck was clean for all four. - The merge brought no change to `rest`, `runtime` or `client`. `spec` `check:generated` is green at the final head: all 14 artifacts are current. ## Gates `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 125 commands, and all were run. - Red on the base too: `check:platform-checklist` reports the same 7 problems on base `c4e7fa5` (anchors in the form-intake and attachments items), measured in a throwaway base worktree. None is in the item this PR edits. - The other results, the reconcile with `--ran`, and anything NOT MEASURED are listed in the report. - Repaired in this PR: the tenant-audit census, regenerated because the two retired write sites leave it (233 → 231 sites on the merged base), with its hand-written prose counts corrected. `system-context.mdx` row 57 is re-anchored to `assertDeploymentAuthority`. - At the final head `d493e22`: 124 of 125 exit 0. The reconcile reads "125 derived, 125 run, 0 NOT-MEASURED (a DERIVED zero — all 125 recorded an exit code and none of them is 3)". eslint (`--no-inline-config`) is clean on the 13 touched TS files; the repo-wide `pnpm lint` is CI's. ## Changesets - `.changeset/15204-su2-suggestion-acceptance.md`: `@objectstack/plugin-security` `minor`, **BREAKING**, `Clause-②: yes (narrowing)`, with ADR-0087 disposition `not-required (runtime-interface-only …)`, which the gate verified. - The behaviour narrowing for tenant administrators is stated as before and now, with its remedy. - The exported-surface moves are stated as compiler-carried facts, not as a FROM → TO block. ADR-0087 D7 refuses a prescription beside `runtime-interface-only`; the gate refused the first spelling and named that rule. - `.changeset/15204-su2-contract-text.md`: `patch` for `@objectstack/spec`, `client`, `rest` and `cli`. Text only. - `@objectstack/runtime` changes comments only and takes no changeset. ## Acceptance notes (out of scope; carrier #22619 for the ADR notes) - **ADR-0090 D9.** "the admin confirms each individually" now means a `manage_metadata` holder accepts each suggestion once for the deployment. `guest` suggestions are refused (decision 1). - **ADR-0131 D3.** "an ADR-0090 D9 suggestion, when accepted, edits the position's definition instead of inserting a row" holds, read as above: the acceptance changes the derivation's input, and the plugin re-declares the definition. - **ADR-0138 §5.** "An administrator binds a low-privilege set to `guest`, directly or by confirming a package's suggestion" names two paths that do not exist on this head. 2c sealed the anchors, so there is no direct bind, and a `guest` suggestion is refused here. - `bindBaselineToEveryone` and the junction gate `assertAudienceAnchorBindingGate` still write and guard junction rows that grant nothing. 6a's lane (#22793) owns that. - The publish-time re-derivation of `everyone` sits inside the `permission` publish materializer (`SecurityPlugin.declareEveryone` after `reconcileAudienceBindingSuggestions`). Stage 6b-2 deletes that closure and re-homes the call, for example onto `onMetadataMutation` for type `permission` (seat answer 6109520247). It also takes the contract record's flag A there: today the materializer re-derives only when the publish materialized a row and the body carries `isDefault`. - _Added by the seat at ACCEPT, from the dev's note; the head is unchanged._ ## Size Against `main` (`git diff --shortstat origin/main...HEAD`): 23 files, +1,224 / −1,599, so 2,823 changed lines. Test deletions and regenerated files are included. That is under the 3,000 cap. --- _Generated by [Claude Code](https://claude.ai/code/session_01D4f5gJKWdzmeZP57AVkRJd)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 581ca7d commit d112087

23 files changed

Lines changed: 1224 additions & 1599 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/client": patch
4+
"@objectstack/rest": patch
5+
"@objectstack/cli": patch
6+
---
7+
8+
docs: the suggested-binding acceptance is deployment-level and takes effect
9+
10+
Clause-②: no
11+
12+
Text only. These are the published sentences that describe what confirming a package's `isDefault` suggestion does. They now match the behaviour `@objectstack/plugin-security` ships in this release.
13+
14+
- `@objectstack/spec`: `ISecurityService.listAudienceBindingSuggestions`, `confirmAudienceBindingSuggestion` and `dismissAudienceBindingSuggestion` keep "writes the binding" for the confirm. The text now also says what that means: a `manage_metadata` holder accepts once for the deployment, `everyone`'s definition is derived again, and a dismiss on an accepted suggestion revokes it. The package-level `isDefault` note keeps "an admin confirms in Setup" and adds who that admin is.
15+
- `@objectstack/client`: the `security.suggestedBindings.confirm` and `.dismiss` TSDoc.
16+
- `@objectstack/rest`: the route summaries of the confirm and dismiss routes. Paths and envelopes are unchanged.
17+
- `@objectstack/cli`: the ownership-inventory citation for `sys_audience_binding_suggestion` now says the record is deployment-level and owned by the Default Organization.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
"@objectstack/plugin-security": minor
3+
---
4+
5+
feat(plugin-security)!: accepting a package's `isDefault` suggestion takes effect for the whole deployment
6+
7+
Clause-②: yes (narrowing)
8+
9+
**BREAKING.** A package's `isDefault: true` permission set now reaches every signed-in member through ONE deployment-level acceptance, and the acceptance takes effect. Before this release a confirm wrote a `sys_position_permission_set` row that granted nothing.
10+
11+
- **What an accept does.** `POST /api/v1/security/suggested-bindings/:id/confirm` records the decision once for the deployment, and the security plugin derives the `everyone` anchor's definition again as the baseline plus every accepted set. The derivation also runs at boot, on uninstall and on a package-door publish. A member's reads of the package's objects change on the next request. `bindingCreated` is `true` when the definition did not name the set before.
12+
- **Who may decide (narrowing).** All three routes now need the `manage_metadata` capability, and under a `group` or `isolated` tenancy posture the platform operator. Before this release a tenant administrator (the `*` wildcard holder) listed, confirmed and dismissed suggestions for their organization. That caller is now refused `403 PERMISSION_DENIED`. Remedy: a `manage_metadata` holder decides, once for the deployment.
13+
- **The record is the deployment's.** `sys_audience_binding_suggestion` rows are owned by the Default Organization. A deployment with no Default Organization (several organizations, none with slug `default`) answers `409 SUGGESTION_STATE` with the remedy.
14+
- **Rows written before this release are never promoted.** A Default Organization row a tenant administrator confirmed or dismissed is set back to `pending`, unless the baseline already names the set. A row another organization owns, or none, is not read. Boot logs a warning with the count. Such rows granted nothing before this release and grant nothing after it.
15+
- **The high-risk gate is on the definition (ADR-0090 D7).** Accepting a set that carries a bit the `everyone` anchor forbids answers `403 PERMISSION_DENIED`. If a publish later adds such a bit to an accepted set, the derivation withholds it and logs an `error` that names the set.
16+
- **Revoke.** `POST …/:id/dismiss` on an accepted suggestion revokes it, and `everyone` is derived again without the set. A set that only the baseline names (`confirmed`, no resolver) cannot be dismissed (`409`).
17+
- **Only `everyone`.** A `guest`-anchor suggestion is refused `409`, because accepting it would record a grant that never takes effect.
18+
- **The anchors stay sealed.** No caller writes `everyone`'s definition. An acceptance changes the derivation's input, and the plugin re-declares the definition. Reconciliation reads that definition and no binding row.
19+
- **Declarations come from the security catalog.** A suggestion is offered for an `isDefault` set that the catalog serves with an owning package, which is the source the authorization resolver reads. The module no longer reads `sys_position`, `sys_permission_set` or `sys_position_permission_set`. It also no longer materializes a set.
20+
- **New field:** `sys_audience_binding_suggestion.deployment_decision`, a boolean that is `true` when a `manage_metadata` holder resolved the row.
21+
22+
Exported TypeScript surface (the compiler names each caller):
23+
24+
- Removed: `reapOrganizationLessSuggestions`, `listSuggestionOrganizationIds` and the `SuggestionReconcileScope` type. Nothing replaces them, because the record is no longer per organization.
25+
- `reconcileAudienceBindingSuggestions` and `syncAudienceBindingSuggestions` now take `(ql, logger)`. The metadata, scope and organization parameters are gone.
26+
- `SuggestionReconcileOutcome` reports `unread` in place of `organizations` and `reaped`. `SuggestionSyncOutcome` gains `reopened`.
27+
- `SuggestionDeps` drops `resolveSets` and takes `posture` and `rederiveEveryone`.
28+
- New: `readAcceptedEveryoneSets(ql, metadata)` and the `WithheldAcceptance` type.
29+
30+
<!-- adr-0087: not-required (runtime-interface-only packages/plugins/plugin-security/src/suggested-audience-bindings.ts#SuggestionDeps, packages/plugins/plugin-security/src/suggested-audience-bindings.ts#SuggestionReconcileOutcome) TypeScript service plumbing with no metadata surface; the compiler names every caller, and no stored row or authored key changes shape (the new object field defaults to false) -->

‎content/docs/permissions/authorization.mdx‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,18 @@ as the flow and action activation switches. The two audience anchors,
428428
`everyone` and `guest`, are refused with `400` in either direction: one row
429429
would switch the authenticated or anonymous baseline off for every principal.
430430

431+
The anchors are sealed against every caller-reachable write: this door, the
432+
metadata door and an organization overlay all refuse them. A package's
433+
`isDefault` set still reaches every signed-in member, and the seal reads this
434+
way: an **acceptance changes the input of a derivation, and no caller writes
435+
the anchor's definition**. The security plugin is the only declarer of
436+
`everyone`'s definition. It derives it as the deployment's baseline plus each
437+
package default a `manage_metadata` holder accepted, and re-declares it on
438+
every accept, revoke, uninstall and package publish. The acceptance record is
439+
one row per suggestion, owned by the Default Organization, and the
440+
high-privilege gate runs on the definition each time it is derived. See
441+
[Permission sets](./permission-sets).
442+
431443
- A deactivated **permission set** contributes nothing: not its name, not its
432444
`system_permissions`, not its `tab_permissions`. As with an expired grant, a
433445
deactivated unscoped `admin_full_access` no longer derives `platform_admin`

‎content/docs/permissions/permission-sets.mdx‎

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -253,38 +253,55 @@ hand.
253253
## Package suggestion — `isDefault` (ADR-0090 D5)
254254

255255
A package may mark one of its sets `isDefault: true`: an **install-time
256-
suggestion** to bind it to the `everyone` anchor. The admin confirms each
257-
suggestion individually; nothing auto-binds, and the D7 linter rejects an
256+
suggestion** to give it to every signed-in member through the `everyone`
257+
anchor. An admin holding `manage_metadata` accepts each suggestion once, for
258+
the whole deployment; nothing auto-binds, and the D7 linter rejects an
258259
`isDefault` set that carries anchor-forbidden bits (VAMA, destructive bits,
259260
system permissions). A plain `'*'` wildcard grant is *not* an anchor-forbidden
260261
bit for `everyone` — the platform's own `member_default` baseline is exactly
261262
that shape; the wildcard ban is the stricter `guest` tier's rule.
262263

263-
Pending suggestions are materialized as `sys_audience_binding_suggestion`
264-
rows (one per organization × package × set × anchor, read-only over the data
265-
API) and resolved through the security surface — both installing a package at
266-
runtime and declaring the set in the stack produce them:
264+
Suggestions are materialized as `sys_audience_binding_suggestion` rows (one
265+
per package × set × anchor for the deployment, owned by the Default
266+
Organization, read-only over the data API) and resolved through the security
267+
surface — any `isDefault` set the security catalog serves with an owning
268+
package produces one:
267269

268270
```http
269271
GET /api/v1/security/suggested-bindings?status=pending # list (reconciles first)
270-
POST /api/v1/security/suggested-bindings/:id/confirm # create the everyone binding
271-
POST /api/v1/security/suggested-bindings/:id/dismiss # decline the prompt
272+
POST /api/v1/security/suggested-bindings/:id/confirm # accept: everyone carries the set
273+
POST /api/v1/security/suggested-bindings/:id/dismiss # decline, or revoke an acceptance
272274
```
273275

274276
`status` accepts `pending`, `confirmed` or `dismissed`, and may be omitted to
275277
list every suggestion. Any other value is rejected with a `400` naming the
276278
accepted set — it previously reached the query as a filter nothing matched, so
277279
a mistyped status returned an empty list that read as "no suggestions".
278280

279-
All three require a tenant-level administrator (the anchors are tenant-level
280-
only — D12). Confirm writes a `sys_position_permission_set` row **as the
281-
caller**, so the audience-anchor gate re-checks the forbidden-bits predicate
282-
at the data layer. That row does not grant the set: what `everyone` grants is
283-
the `permissionSets` its definition names, so a confirmed suggestion changes no
284-
one's access until the set is named there. A suggestion whose binding was created out-of-band (boot
285-
baseline, manual bind) is marked `confirmed` automatically, and uninstalling
286-
the package prunes its pending suggestions. Studio surfaces pending
287-
suggestions after marketplace installs and in the Access pillar.
281+
All three require the `manage_metadata` capability, and under a walled
282+
tenancy posture the platform operator: the decision reaches every member of
283+
every organization, so a tenant administrator cannot make it. An acceptance
284+
takes effect at once. What `everyone` grants is the `permissionSets` its
285+
definition names, and the security plugin derives that definition as the
286+
deployment's baseline plus every accepted set — again on each accept, revoke,
287+
uninstall and package publish. No caller writes the definition itself (see
288+
[Authorization](./authorization)).
289+
290+
- **The high-risk gate is on the definition.** Accepting a set that carries a
291+
bit the `everyone` anchor forbids is refused with `403 PERMISSION_DENIED`;
292+
if a later publish adds such a bit to an accepted set, the derivation
293+
withholds it and logs why.
294+
- **Revoke** an acceptance by dismissing the accepted suggestion; uninstalling
295+
the package removes its suggestions and its sets leave `everyone`.
296+
- A set the baseline already names (the served app's own `isDefault` set) is
297+
marked `confirmed` without a resolver — observed, not accepted — and cannot
298+
be dismissed here.
299+
- Only an `everyone` suggestion can be accepted. A decision a tenant
300+
administrator recorded per organization before acceptances were
301+
deployment-level is never carried over: the suggestion is pending again.
302+
303+
Studio surfaces pending suggestions after marketplace installs and in the
304+
Access pillar.
288305

289306
## Delegated administration — `adminScope` (ADR-0090 D12)
290307

‎content/docs/permissions/system-context.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ The largest single consumer — **17 of the 122 sites**.
182182
| 54 | Package domain capability gates bypassed | runtime | Get: package management and package-inventory reads without the capability | `packages/runtime/src/domains/packages.ts#requireManageMetadata`, `#requireReadCapability` |
183183
| 55 | Activation write / authoring refusals do not fire | runtime | Get: activation artifacts writable and authorable without the activation-authoring capability | `packages/runtime/src/domains/activation-gate.ts#refuseUngrantedActivationWrite`, `#refuseUngrantedActivationAuthoring` |
184184
| 56 | Automation run-state read, flow-authoring write, the paused-run caller gate (screen read and resume), the two operator run-lifecycle writes and the elevated self-triggered start at every door that starts a flow by name all pass | runtime | Get: run state, flow writes, a paused run's screen and its resume with no grant and without being the run's starter — and cancelling or restoring a suspension without the platform-operator rung. The screen read and the resume ask one predicate, so this is one bypass for both. The lifecycle bypass is the in-process owner's door: plugin-approvals' revise-window recall (ADR-0044) cancels on behalf of a decision it already authorized and recorded. And starting a flow declared `runAs: 'system'` whose type is self-triggered (`autolaunched`, `record_change`, `schedule`) through any door that starts a flow by name — the trigger route, an action of `type: 'flow'` (REST `/actions` and the MCP `run_action` bridge) and a declared endpoint of `type: 'flow'` — which every other caller, the guest principal included, is refused `403 PERMISSION_DENIED` (the maintainer's rulings, letters B and A). The three doors ask one predicate, so this is one bypass for all of them, and it is the in-process caller's, such as a job: inbound HTTP never carries the flag | `packages/runtime/src/domains/automation.ts#mayReadRunState`, `#refuseUngrantedFlowWrite`, `#isRunStarterOrRunStateReader`, `#refuseUngrantedRunLifecycleWrite`, `packages/runtime/src/flow-start-admission.ts#refusesElevatedSelfTriggeredStart` |
185-
| 57 | Audience-binding suggestion recording skipped | plugin-security | Lose: install-time suggestions are not recorded for system callers | `packages/plugins/plugin-security/src/suggested-audience-bindings.ts#assertTenantAdmin` |
185+
| 57 | Audience-binding suggestion authority gate passes | plugin-security | Get: a system caller lists, accepts and dismisses suggestions without `manage_metadata` and, under a wall, without the platform-operator rung | `packages/plugins/plugin-security/src/suggested-audience-bindings.ts#assertDeploymentAuthority` |
186186
| 58 | Email-template organization door passes; webhook provenance stamp skipped | plugin-email, plugin-webhooks | Get (`sys_email_template`): a create or update of a template row. The door refuses every other write that names a caller, `403 PERMISSION_DENIED` (ADR-0131 D6), so the seeds, the boot sweep and the projection of a Studio save are the only writers. Lose (`sys_webhook`): the row is not marked as an admin customization | `packages/plugins/plugin-email/src/email-template-door.ts#isOrganizationWrite`, `packages/plugins/plugin-webhooks/src/webhook-provenance.ts#bindWebhookProvenanceStamp` |
187187
| 59 | **Automation flow data nodes re-add the `owner_id` stamp** (the one place row 2's gap is compensated inline) | service-automation | Get: a flow-authored INSERT under system elevation still lands owned, when the run resolved a user. Fill-only — flow-authored values win | `packages/services/service-automation/src/runtime-identity.ts#stampSystemInsertOwner`, called from `packages/services/service-automation/src/builtin/crud-nodes.ts#registerCrudNodes` |
188188
| 60 | Inbox caller refusal names `isSystem` as what was carried | service-messaging | Get: nothing — the refusal still fires. The flag only shapes the diagnostic, because privilege is not an authorization subject | `packages/services/service-messaging/src/inbox-caller.ts#resolveInboxRecipient` |

0 commit comments

Comments
 (0)