Skip to content

manifest.runtime — the ADR-0025 §3.6 plugin trust tier — is read only to be echoed in two CLI lines, and loading's own tombstone tells upgrading authors it is enforced #11330

Description

@os-sam

Measured while seeding the plugin-manifest liveness ledger for #10728 (verdict recorded as dead in packages/spec/liveness/manifest.json). Filed, not fixed — a removal or an enforcement is its own decision, and the measurement has a scope limit (below).

Measured on origin/main (b9e9227e3).

What is declared

packages/spec/src/kernel/manifest.zod.ts:563runtime: PluginRuntimeSchema.optional(), an enum of node / sandbox / worker described as the "Plugin trust tier (ADR-0025 §3.6)":

  • node — in-process, full PluginContext (first-party / verified only)
  • sandbox — QuickJS-WASM, capability-gated surface
  • worker — out-of-process (reserved)

What is measured

The key is read — twice — and both reads only echo the value into a progress line:

packages/cli/src/commands/plugin/build.ts:127 printStep(`Loaded ${id}@${version} (runtime: ${manifest.runtime ?? 'unset'}, packaging: ${packaging})`)
packages/cli/src/commands/plugin/publish.ts:94 printStep(`${id}@${version} (… KB, runtime: ${manifest.runtime ?? 'unset'})`)

Nothing dispatches on the tier. There is no runtime === 'sandbox' branch anywhere in the monorepo, and the QuickJS runner (packages/runtime/src/sandbox/) is the hook / action script-body sandbox — reached from the body-runner factories, never from a plugin's declared tier. objectui: zero reads.

So authoring runtime: 'sandbox' changes one line of build output and nothing else. Note the asymmetry with its immediate neighbour: packaging is genuinely live on a behavioural fork (build.ts:159-161manifest-deps keeps declared dependencies external instead of bundling them), which is what a read that matters looks like in the same file.

Controls — an uncontrolled zero is not a reading

  • The same 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). The pattern is not blind to this file or this object.
  • objectui control: manifest.(id|name|namespace|version) returns 46 hits there, so manifest reads are findable in that repo; the zero for runtime is about the path, not the probe.

Why this is security-shaped rather than cosmetic

The retirement of manifest.loading (#4914) removed loading.sandboxing precisely because it "never isolated anything", and its tombstone — shipped in manifest.zod.ts:527-530 and again in the ADR-0087 D3 entry 17.plugin-manifest-loading-retired — redirects every upgrading author, verbatim:

⚠️loading.sandboxing in particular never isolated anything … If you were relying on it for isolation, you had none — use the plugin trust tier (manifest.runtime) and the permission declarations, which are enforced.

Measured here, that redirection points at a key with no enforcement in this repo. An author who follows the prescription gets the same nothing they were being warned about, one key over. That is ADR-0049's false-compliance shape with a shipped migration message attached.

(The other half of that sentence — "the permission declarations" — is filed separately: the structured PluginPermissionsSchema arm of manifest.permissions also has zero readers. Cf. the closed #7500, which recorded that PluginPermissionEnforcer has no production caller.)

⚠️ Scope limit — measure cloud before acting

The cloud repo is unmeasured, carried unchanged from #10627: it could not be attached to the measuring session (add_repo → "you don't have access to objectstack-ai/cloud"). Plugin trust-tier enforcement is exactly the kind of thing that could live in the control plane — manifest.zod.ts:10-18 says cloud mirrors these shapes when it validates a published .osplugin. So the dead verdict is scoped to objectstack + objectui, and the cloud leg must be measured, with its own control probe, before either fork is executed — the same precondition #10724 already carries.

The fork (not prejudged)

  • A — Enforce. Route plugin loading through the declared tier. This is the option the shipped tombstone already promises, and the one that makes the sentence true. Cost: a real isolation path plus the capability-gating surface sandbox implies.
  • B — Retire. Tombstone runtime under ADR-0049 and correct the loading migrate string in the same change, since that string is the thing actively misdirecting authors. Note the migrate string is shipped in a published spec version, so the correction is itself a spec change.
  • C — Mark unenforced. Add an [EXPERIMENTAL — not enforced] marker to the .describe() so the ledger's marker vocabulary classifies it honestly, correct the migrate string now, and leave the schema pending a real pull.

Whichever fork is taken, the migrate string's claim should stop being made before the fork is resolved — it costs nothing and it is the part actively telling authors they have isolation they do not have.

Filed unassigned for triage.

Blocked-by: #10812


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions