Skip to content

Commit aaacf1d

Browse files
hotlongclaude
andauthored
docs(spec, core): the install-time granted permission set is REGISTERED at load and refuses nothing — say so, and pin the measurement (#17147) (#17753)
## ⛔ This PR does NOT close #17147, and must not be given a closing keyword #17147 asks for the opposite of what this lands: it asks that the registered grant start **refusing**. This PR states, in every shipped carrier, that it **does not**. The card stays open and keeps its `needs-user-decision` grade — the maintainer's A/B/C question is answered **B** (say it truthfully now), ⛔ not withdrawn. `Blocked-by: #17147` on **#13458** (Phase 2, retire the legacy `string[]` arm) is unchanged and still load-bearing: retiring the legacy arm while the structured replacement refuses nothing is exactly the shape this correction exists to keep visible. ## The measurement, on `9bd4344e4` | probe | result | |---|---| | `new SecurePluginContext(` outside tests | **0** | | callers of `enforceFileRead` / `enforceFileWrite` / `enforceNetworkRequest` | **0** — `SecurePluginContext` included | | `os plugin install` / `.osplugin` loader / `sys_plugin_installation` | **none of the three exist** | | artifact `packages[]` payload | `sys_package_version.manifest_json` — metadata; the blob never reaches a runtime | | `contains_code` readers in this repo | **0** | ⇒ #13457 / #17137 gave `registerGrantedPermissions` its first production caller. Nothing queries the registry it fills, and two of the four grant classes have **no enforcement surface even in principle** — so the consented set records what is allowed and denies no operation. ## The ruling this executes Maintainer, 2026-09-12, **option B** — the same option ruled for the sibling half of this very sentence in **#11330** (2026-08-30, verbatim「同意」): *say it truthfully now*; building the per-plugin context stays a separate, separately designed direction, fenced to the ADR-0025 install-flow design effort by ruling `5486840233`. ## What changed Four shipped sentences claimed enforcement. Each is corrected to one truthful split — **registered at load · queried by nothing · refuses no operation**: - `PluginPermissionEnforcer.registerGrantedPermissions` docblock (the retracted sentence was *"so `SecurePluginContext` enforces exactly the consented surface"*) - `PluginPermissionsSchema` docblock — `spec/kernel/manifest.zod.ts` - the `manifest.loading` tombstone prescription, which ships into **six** generated reference pages - the ADR-0087 D3 entry `plugin-manifest-loading-retired`, which ships the same prescription into `docs/protocol-upgrade-guide.md` plus a `Callout` beside the permission table in the hand-written `content/docs/plugins/development.mdx`. Generated artifacts regenerated from those sources: `content/docs/references/**` (6 pages), `docs/protocol-upgrade-guide.md`, `packages/spec/spec-changes.json`, `packages/spec/src/migrations/registry.ts`. `check:docs` / `check:migration-registry` / `check:upgrade-guide` / `check:spec-changes` all report in-sync. ## The mechanical half — why this cannot rot in either direction `plugin-runtime-tier-truthful-text.test.ts`'s coordination pin held the permissions half **verbatim** so it would go red the day that half was corrected (*"#11333 owns it"*). It did its job. It is discharged and replaced by pins on the truthful text in **both** carriers, each keeping the negative assertion that stops the retracted sentence returning *beside* the new one. `packages/core/src/security/granted-permissions-not-enforced.pin.test.ts` (new) pins the **measurement** as well as the words: - zero production `SecurePluginContext` construction sites; - zero callers of the three fs/network gates; - both halves present on the docblock, retracted sentence absent; - a behavioural case: a registered grant answers `getPluginPermissions` and gates nothing; - a positive control, so a broken scan can never read as "no offenders". ⇒ **It fails the day the ADR-0025 materialize seam lands**, and its failure message names every text that becomes false at that moment — including objectui's `PluginDisclosure.tsx`, corrected in objectstack-ai/objectui#TBD. **Ablated three ways**, each turning it red: a fake production construction site; the retracted sentence restored; the tombstone reverted to `which are enforced`. Clause-②: no — no new key lands on any published payload. The diff is docblocks, two tombstone/prescription strings, one hand-written docs page, their generated projections, and one new test file; `PluginPermissionsSchema`, `EnvironmentArtifactSchema` and every other schema keep their exact key sets (`packages/spec`'s own suite, 13434 tests, is green on that). > ⚠️ **`Part-of PR must not also close its card` is red and stays red.** Its RULE 2 forbids a card-relation trailer in any *commit* message, and this branch's commit ends `Refs #17147 · …`. The gate's own log states the position: the check is advisory (absent from the required-context registry, and its workflow subscribes to no `merge_group` event), `Refs` lands as a reference and moves **no** card, and ⛔ the repair it refuses to ask for is a history rewrite — forbidden in this repository. Recorded here so the red is read rather than acted on. ## Not in this PR - **`docs/adr/0025-plugin-package-distribution.md` §3.7** (*"service/hook/file/network already enforced"*) carries the same claim. `docs/adr/**` is a governed surface (Prime Directive #14), so it is split into its own PR that stays draft for maintainer merge. - **No behaviour change.** No accept/reject, no registration, no gate added or removed. Refs #17147 · #11333 · #13457 · #13458 · #11330 · `5486840233` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent cb0c616 commit aaacf1d

16 files changed

Lines changed: 327 additions & 34 deletions
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/core': patch
4+
---
5+
6+
Say what the install-time granted permission set actually does: it is REGISTERED at load and refuses nothing.
7+
8+
Four shipped sentences claimed the structured `manifest.permissions` / `granted_permissions` set was enforced. Measured on `9bd4344e4`: `SecurePluginContext` — the only reader of `PluginPermissionEnforcer`'s service and hook gates — has zero production construction sites, and `enforceFileRead` / `enforceFileWrite` / `enforceNetworkRequest` are called by nothing at all, `SecurePluginContext` included. So #13457's binding registers a consented set that nothing queries, and the `fs` and `network` classes have no enforcement surface even in principle.
9+
10+
Corrected, each to the same truthful split ("registered at load · queried by nothing · refuses no operation"): the `registerGrantedPermissions` docblock, the `PluginPermissions` schema docblock, the `manifest.loading` tombstone prescription, and the ADR-0087 D3 entry that ships that prescription into `docs/protocol-upgrade-guide.md`. The hand-written plugin development guide gains the same note beside its permission table.
11+
12+
`plugin-runtime-tier-truthful-text.test.ts`'s coordination pin — which held the permissions half verbatim so it would go red the day that half was corrected — has been discharged and replaced by pins on the truthful text, in both carriers, each with the negative assertion that keeps the retracted sentence from returning beside it.
13+
14+
New in `@objectstack/core`: `granted-permissions-not-enforced.pin.test.ts` pins the MEASUREMENT as well as the words, so the claim cannot rot in either direction. It fails the day a production `SecurePluginContext` construction site appears — i.e. the day the ADR-0025 materialize seam lands — and names every text that then becomes false.
15+
16+
No behaviour changes: no accept/reject, no registration, no gate is added or removed.

content/docs/plugins/development.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,14 @@ Declare the capabilities your plugin needs in its manifest's `permissions` block
264264
| `network` | Network hosts the plugin may reach (e.g. `api.acme.com`) |
265265
| `fs` | Filesystem paths the plugin may access |
266266

267+
<Callout type="warn" title="Declared, not yet enforced">
268+
The install-time consent flow records this set and the loader **registers** it on the
269+
`PluginPermissionEnforcer`, but nothing queries that registry yet — so a `permissions`
270+
block **refuses nothing at runtime today**. Declare it honestly (the cloud marketplace
271+
shows it to the installer, and an upgrade that widens it forces re-consent), but do not
272+
rely on it to confine your plugin, and do not rely on it to confine anyone else's.
273+
</Callout>
274+
267275
### Error Handling
268276

269277
Always use structured errors:

content/docs/references/api/package-api.mdx

Lines changed: 6 additions & 6 deletions
Large diffs are not rendered by default.

content/docs/references/api/protocol.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1901,7 +1901,7 @@ Install package request
19011901
| **capabilities** | `never` | optional | [REMOVED] `manifest.capabilities` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no discovery path ever consulted the block: nothing read `implements`, `provides`, `requires`, `extensionPoints` or `extensions`, so the declared "interoperability and automatic discovery" never happened. Delete the key. Real dependency resolution runs off top-level `manifest.dependencies`, which stays. Capability-based discovery must be designed with an enforcing reader first, not revived here. |
19021902
| **extensions** | `never` | optional | [REMOVED] `manifest.extensions` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — an untyped map with zero readers: whatever was parked here was stored and never consulted. Delete the key. Extend the platform through the enforced channels instead: `contributes.kinds` registers metadata kinds, `navigationContributions` injects navigation into other packages' apps, and code-level extension happens in the plugin itself (`init`/`start`). |
19031903
| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +8 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages |
1904-
| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. Use the permission declarations, which are enforced. |
1904+
| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. ⛔ Nor do the permission declarations give it back: the install-time granted set is REGISTERED on the PluginPermissionEnforcer at load and queried by nothing, so it refuses no operation. Neither surface confines a plugin today — do not author either one expecting isolation. |
19051905
| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) |
19061906
| **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) |
19071907
| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares |

content/docs/references/kernel/manifest.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const result = ManifestSchema.parse(data);
4545
| **capabilities** | `never` | optional | [REMOVED] `manifest.capabilities` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — no discovery path ever consulted the block: nothing read `implements`, `provides`, `requires`, `extensionPoints` or `extensions`, so the declared "interoperability and automatic discovery" never happened. Delete the key. Real dependency resolution runs off top-level `manifest.dependencies`, which stays. Capability-based discovery must be designed with an enforcing reader first, not revived here. |
4646
| **extensions** | `never` | optional | [REMOVED] `manifest.extensions` was removed in @objectstack/spec 17 (ADR-0049 enforce-or-remove) — an untyped map with zero readers: whatever was parked here was stored and never consulted. Delete the key. Extend the platform through the enforced channels instead: `contributes.kinds` registers metadata kinds, `navigationContributions` injects navigation into other packages' apps, and code-level extension happens in the plugin itself (`init`/`start`). |
4747
| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +8 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages |
48-
| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. Use the permission declarations, which are enforced. |
48+
| **loading** | `never` | optional | [REMOVED] `manifest.loading` was removed in @objectstack/spec 17.0.0 (ADR-0049 enforce-or-remove) — the entire block (`strategy`, `preload`, `codeSplitting`, `dynamicImport`, `initialization`, `dependencyResolution`, `hotReload`, `caching`, `sandboxing`, `monitoring`) had no runtime reader in any repo, so authoring it configured nothing. Delete the key. Plugins are composed at boot — `defineStack` registers them and the kernel runs `init` then `start` in an order topologically resolved from each composed plugin's own `dependencies` / `optionalDependencies` (`resolvePluginOrder`); the set is fixed until the process restarts. ⚠️ `loading.sandboxing` in particular never isolated anything: it did not run plugins in a process, vm, iframe or web-worker, and `allowedServices` gated no call. If you were relying on it for isolation, you had none — and the plugin trust tier (`manifest.runtime`) does not give it back: that tier is enforced at the cloud marketplace PUBLISH gate only (an unverified publisher requesting the `node` tier is rejected with HTTP 422 and forced to manual review), while load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares. ⛔ Nor do the permission declarations give it back: the install-time granted set is REGISTERED on the PluginPermissionEnforcer at load and queried by nothing, so it refuses no operation. Neither surface confines a plugin today — do not author either one expecting isolation. |
4949
| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) |
5050
| **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) |
5151
| **runtime** | `Enum<'node' \| 'sandbox' \| 'worker'>` | optional | Plugin trust tier the plugin declares (ADR-0025 §3.6) — enforced at the cloud marketplace publish gate (unverified publisher requesting `node` → HTTP 422 + manual review); load-side enforcement is NOT implemented, so a locally installed plugin is not isolated by the tier it declares |

0 commit comments

Comments
 (0)