Measured while seeding the plugin-manifest liveness ledger for #10728. The ledger row is live (the key is read) with the split recorded in its note; this card carries the half that a one-level ledger row cannot fail on. Filed, not fixed.
Measured on origin/main (b9e9227e3).
What is declared
packages/spec/src/kernel/manifest.zod.ts:54-57 — ManifestPermissionsSchema is a union of two arms:
exportconstManifestPermissionsSchema=z.union([z.array(z.string()),// legacy flat listPluginPermissionsSchema,// structured (ADR-0025 §3.2)]);
and the doc above it says, in the schema itself:
Backward-compatible manifest permissions value … New code should prefer the structured form.
The structured arm (:33-45, .strict()) declares services / hooks / network / fs, and its TSDoc states the install-time consent flow (ADR-0025 §3.5 step 2) "turns this declaration into the persisted granted_permissions set enforced at load by the PluginPermissionEnforcer."
What is measured
One reader, on the legacy arm only.
packages/plugins/plugin-security/src/suggested-audience-bindings.ts:252
const declared = Array.isArray(manifest?.permissions) ? manifest.permissions : [];
The Array.isArray guard is the whole finding: an author who follows the schema's own "prefer the structured form" advice produces an object, the guard is false, declared is [], and the declaration contributes nothing. The recommended spelling is the one nothing reads.
No other reader exists in objectstack or objectui.
PluginPermissionEnforcer is not the missing consumer
packages/core/src/security/plugin-permission-enforcer.ts:94-104 registers the set the install-time consent flow persisted to sys_package_installation.granted_permissions, and its own comment says that set is "independent of whatever the manifest requested". Nothing in this repo feeds manifest.permissions into it. Cf. the closed #7500, which recorded that PluginPermissionEnforcer has no production caller at all and that the plugin-distribution task doc's ✅ on F4 overstated — this is the producer-side half of that same gap.
Controls
- The probe finds real reads of sibling manifest keys:
.packaging (cli/src/commands/plugin/build.ts:126), .data (runtime/src/app-plugin.ts:946), .contributes (objectql/src/engine.ts:4504). objectui: manifest.(id|name|namespace|version) → 46 hits, so manifest reads are findable there; zero for either permissions arm.
Why file it separately from the ledger row
The liveness gate classifies at one level. manifest.permissions is one key, it is read, so the honest one-level verdict is live — and that verdict, correct as it is, cannot express "live on the arm we tell authors not to use". A union whose recommended member is inert is invisible to the ratchet by construction, which is the same class of blind spot #10728 just closed one level up. Recording it as a card is the only place it can be seen.
⚠️ Scope limit
cloud is unmeasured (carried from #10627: add_repo → "you don't have access to objectstack-ai/cloud"), and the install-time consent flow of ADR-0025 §3.5 is a control-plane path — so the structured arm plausibly has its consumer there. Measure the cloud leg with a control probe before acting, the same precondition #10724 carries. If cloud does consume it, this card becomes a documentation/scope correction (the arm is live cross-repo, and the ledger note should say so) rather than an enforce-or-remove.
Related
Directly related to #11330 (manifest.runtime): the loading tombstone's migrate string redirects upgrading authors to "the plugin trust tier (manifest.runtime) and the permission declarations, which are enforced". This card measures the second half of that sentence; #11330 measures the first.
Filed unassigned for triage.
Blocked-by: #10812
Generated by Claude Code
Measured while seeding the plugin-manifest liveness ledger for #10728. The ledger row is
live(the key is read) with the split recorded in its note; this card carries the half that a one-level ledger row cannot fail on. Filed, not fixed.Measured on
origin/main(b9e9227e3).What is declared
packages/spec/src/kernel/manifest.zod.ts:54-57—ManifestPermissionsSchemais a union of two arms:and the doc above it says, in the schema itself:
The structured arm (
:33-45,.strict()) declaresservices/hooks/network/fs, and its TSDoc states the install-time consent flow (ADR-0025 §3.5 step 2) "turns this declaration into the persistedgranted_permissionsset enforced at load by the PluginPermissionEnforcer."What is measured
One reader, on the legacy arm only.
The
Array.isArrayguard is the whole finding: an author who follows the schema's own "prefer the structured form" advice produces an object, the guard is false,declaredis[], and the declaration contributes nothing. The recommended spelling is the one nothing reads.No other reader exists in
objectstackorobjectui.PluginPermissionEnforceris not the missing consumerpackages/core/src/security/plugin-permission-enforcer.ts:94-104registers the set the install-time consent flow persisted tosys_package_installation.granted_permissions, and its own comment says that set is "independent of whatever the manifest requested". Nothing in this repo feedsmanifest.permissionsinto it. Cf. the closed #7500, which recorded thatPluginPermissionEnforcerhas no production caller at all and that the plugin-distribution task doc's ✅ on F4 overstated — this is the producer-side half of that same gap.Controls
.packaging(cli/src/commands/plugin/build.ts:126),.data(runtime/src/app-plugin.ts:946),.contributes(objectql/src/engine.ts:4504).objectui:manifest.(id|name|namespace|version)→ 46 hits, so manifest reads are findable there; zero for either permissions arm.Why file it separately from the ledger row
The liveness gate classifies at one level.
manifest.permissionsis one key, it is read, so the honest one-level verdict islive— and that verdict, correct as it is, cannot express "live on the arm we tell authors not to use". A union whose recommended member is inert is invisible to the ratchet by construction, which is the same class of blind spot #10728 just closed one level up. Recording it as a card is the only place it can be seen.cloudis unmeasured (carried from #10627:add_repo→ "you don't have access to objectstack-ai/cloud"), and the install-time consent flow of ADR-0025 §3.5 is a control-plane path — so the structured arm plausibly has its consumer there. Measure the cloud leg with a control probe before acting, the same precondition #10724 carries. If cloud does consume it, this card becomes a documentation/scope correction (the arm is live cross-repo, and the ledger note should say so) rather than an enforce-or-remove.Related
Directly related to #11330 (
manifest.runtime): theloadingtombstone's migrate string redirects upgrading authors to "the plugin trust tier (manifest.runtime) and the permission declarations, which are enforced". This card measures the second half of that sentence; #11330 measures the first.Filed unassigned for triage.
Blocked-by: #10812
Generated by Claude Code