Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/27685-manifest-liveness-cell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/spec': patch
---

docs(spec): the liveness ledger README's `manifest` cell names the type's dead set as the ledger holds it, and says why `packaging` is `planned`

Clause-②: no
Metadata-protocol: none

The `manifest` row of `liveness/README.md` said "Dead 19", counting the per-child rows of the retired `capabilities` and `configuration` containers and a `loading` row that has since left the ledger. The cell now partitions the dead set the ledger carries (the ten dead `contributes` members and the three retired containers, one tombstone row each), which sums to the count in the generated `liveness/state-counts/manifest.md` shard. It also gains the missing sentence for `packaging`, the type's only `planned` row: `bundled` is the one strategy, `manifest-deps` is refused by name, and `os plugin build` reads the key only to echo it. No ledger status, evidence, schema or accepted shape changes: only the README prose moves.
47 changes: 29 additions & 18 deletions docs/design/plugin-distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,15 +111,13 @@ A `tar.gz`:
<id>-<version>.osplugin
├── objectstack.plugin.json # compiled manifest (below)
├── dist/ # pre-built ESM bundle (tsup); @objectstack/* externalized
├── package.json # only for the manifest-deps strategy
├── pnpm-lock.yaml # only for the manifest-deps strategy
├── assets/ README LICENSE icon
└── SIGNATURE # detached signature + publisher cert chain
```

Compiled manifest adds four blocks to the existing `ObjectStackManifest`:
`engines` (protocol + platform ranges), `runtime` (`node|sandbox|worker`),
`packaging` (`bundled|manifest-deps`), `permissions` (services/hooks/network/fs
`packaging` (`bundled`, the one strategy and the default — §4.3), `permissions` (services/hooks/network/fs
+ RLS data scopes), and `integrity` (per-file hashes).

### 4.2 UI plugin (`runtime: 'ui'` — ADR-0026 §3.1)
Expand All @@ -135,15 +133,27 @@ client-scoped `permissions` (data read/write via SDK, network → CSP, navigatio
> `ManifestSchema` and `PluginSchema` refuse `ui-plugin`; this section and
> worked example C in §10 use `ui`.

### 4.3 Packaging strategies (default: bundled)

- **Bundled (default).** `tsup --noExternal` bundles third-party JS into `dist`;
no npm at install time. **`@objectstack/*` is externalized** as a host-provided
peer-dep singleton (bundling it forks the engine — duplicate registries / broken
`instanceof` / double Zod validation). Native addons disallowed.
- **manifest-deps (opt-in).** Ships `package.json` + lockfile; install runs
`pnpm install --frozen-lockfile --ignore-scripts`. For native addons / huge
deps only; disallowed for unverified publishers.
### 4.3 Packaging strategy: bundled only

- **Bundled — the one strategy, and the default.** `os plugin build` inlines
every dependency the entry imports into `dist/index.mjs`, so install is unpack +
verify with no npm or toolchain at install time: `os plugin install` and the
boot-time loader import `dist/index.mjs` as packed, and the artifact carries
neither `package.json` nor a lockfile. **`@objectstack/*` is externalized** as a
host-provided peer-dep singleton (bundling it forks the engine — duplicate
registries / broken `instanceof` / double Zod validation). A native (`.node`)
addon is refused by name at build, before any artifact is written.

> **Note (2026-10-29, #27679).** This section used to list a second, opt-in
> strategy, `manifest-deps`, which shipped `package.json` + a lockfile for an
> install-time `pnpm install`. Nothing ever ran that install, so an artifact
> built with it could not load the dependencies it left out. It is retired
> (ADR-0025 v2 §3.3): `PluginPackagingSchema`
> (`packages/spec/src/kernel/manifest.zod.ts`) accepts `bundled` alone and
> refuses `packaging: 'manifest-deps'` by name with its migration prescription
> (delete the key), and `os plugin build` no longer forks on the value. A plugin
> that needs a native addon or a very large dependency is a fresh ADR-0025 M3
> decision, not a strategy here. §4.1, §5, §6 and §8 name no second strategy.

## 5. Build → Sign → Publish

Expand All @@ -164,8 +174,8 @@ os plugin build → os plugin sign → os plugin publish
`sys_plugin_version` (`status: pending_review → published`).
- **Server gates:** schema validation; secret/malware/known-vuln scan;
**permission audit** (sensitive permissions → human review); **publish-time
tier enforcement** (an unverified publisher cannot ship `runtime: 'node'` or
`manifest-deps`); marketplace **counter-signs** with the platform key on
tier enforcement** (an unverified publisher cannot ship `runtime: 'node'`);
marketplace **counter-signs** with the platform key on
approval.
- Listed in the **contribution catalog** (not the consumer App Marketplace),
with a "contains code" badge and a permission-disclosure screen.
Expand All @@ -191,8 +201,8 @@ resolve → compat check → permission consent → download+verify → material
are wired to the KV secret store (ADR-0007) — never stored in the artifact.
4. **Download + verify.** Verify signature (publisher + marketplace keys) and each
file against `integrity`; reject on mismatch.
5. **Materialize.** Unpack to `<OS_HOME>/plugins/<env>/<id>/<version>/`;
manifest-deps runs `pnpm install --frozen-lockfile --ignore-scripts` here.
5. **Materialize.** Unpack to `<OS_HOME>/plugins/<env>/<id>/<version>/`. No
dependency install runs here: the bundle is imported as packed (§4.3).
6. **Register.** Write `sys_plugin_installation` (env_id, plugin_id, version_id,
granted_permissions incl. RLS scopes, status, is_preview). Persist to a local
store so installs survive restarts (the ADR-0016 §9.7 disable-state pattern).
Expand Down Expand Up @@ -242,8 +252,9 @@ Reuses existing components, adds two compositions:
- **Secrets.** `configuration` secret fields → KV secret store (ADR-0007) via
secret-refs; the artifact never carries credentials; the plugin reads them
through `PluginContext` gated by the network/service permission.
- **Supply chain.** Lockfile + per-file integrity; server-side secret/vuln scans;
SBOM on the version row; **always `--ignore-scripts`** (no `postinstall`).
- **Supply chain.** Per-file integrity; server-side secret/vuln scans; SBOM on
the version row; **no install-time package manager at all** (bundled only,
§4.3), so no dependency's `postinstall` ever runs.
- **Server-authoritative UI data.** UI plugins read/write only via the client SDK
under the user's session → every access re-checked by server RLS; manifest
`permissions.data` is a UI hint/guard, not the boundary. U1 iframes cannot read
Expand Down
15 changes: 12 additions & 3 deletions docs/qa/platform-checklist/areas/cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -1716,7 +1716,7 @@
"title": "os plugin build enforces the manifest at the packaging boundary: an invalid manifest is refused with located errors, `bundled` is the one packaging strategy (the retired `manifest-deps` is refused at the manifest, a native addon is refused by name), and the compiled manifest the artifact carries is readable back",
"since": "v15",
"status": "active",
"revision": 4,
"revision": 5,
"priority": "P2",
"surface": "cli",
"personas": [
Expand Down Expand Up @@ -1787,8 +1787,8 @@
"negative": [
"an artifact written from a manifest that failed the schema, or from one missing id/version, is the FAIL this item exists for — the refusal is worthless if the build still produces something publishable",
"a build that accepts `manifest-deps` — writes an artifact, or packs package.json and keeps a dependency external — is a FAIL of the bundled clause: that fork was retired because nothing ever installed the dependencies it left out, so its artifact could not load them. Likewise a native-addon refusal that prints and still writes the .osplugin hands os plugin sign the very artifact it refused",
"⛔ the `integrity` map's PRESENCE is not evidence of an integrity CHECK. os plugin build computes it and writes it into the compiled manifest, and nothing re-verifies it at unpack — #11331 is open on exactly that. Scoring the printed 'Integrity entries' count as a working integrity guarantee is the false-compliance shape the enforce-or-remove ledger exists for, and this item asserts only that the map is PRODUCED",
"⛔ likewise the plugin trust tier: build and publish both print `runtime: <value|unset>`, and nothing in this repo dispatches on it — #11330 is open. A run that reads that printed line as isolation has scored a log line, not a capability",
"⛔ the `integrity` map's PRESENCE is not evidence that this item scored an integrity CHECK. os plugin build computes the map and writes it into the compiled manifest, and this item asserts only that the map is PRODUCED. The check is another door's: os plugin install re-hashes every unpacked entry against the map and refuses the install by name (`integrity_mismatch`) before it writes anything (packages/cli/src/commands/plugin/install.ts#PluginInstall, through packages/core/src/security/plugin-artifact-integrity.ts#verifyIntegrity), and os plugin publish runs the same verifier as its own preflight — no clause here scores either. Reading the printed 'Integrity entries' count as that check scores a log line for a capability this item never exercised",
"⛔ likewise the plugin trust tier: build and publish both print `runtime: <value|unset>`, and neither dispatches on it. The dispatch is at load — the boot-time loader passes every plugin os plugin install installed through packages/runtime/src/installed-plugin-loader.ts#dispatchPluginRuntime before importing it (`node` loads in process, which is not isolation; `sandbox` and `worker` are refused by name, `runtime_not_dispatched`; an undeclared tier is refused, `runtime_undeclared`), and os plugin install warns when the next boot will refuse the tier. This item scores none of that: a run that reads the printed line as isolation has scored a log line, not a capability",
"a lint warning that fires on a manifest-less metadata fragment is a FAIL of the scoping half even though the rule itself works"
],
"traps": [
Expand All @@ -1799,6 +1799,9 @@
"packages/cli/src/commands/plugin/build.ts#formatZodErrors — manifest read + named refusal; ManifestSchema.safeParse + formatZodErrors; the explicit id/version guard; packaging default and the Loaded line; entry resolution; the bundled-only esbuild call (only `@objectstack/*` external); findNativeAddons and the native-addon refusal ahead of the integrity map; computeIntegrity + the compiled manifest; the artifact write and its printed block",
"packages/cli/src/utils/osplugin.ts#MANIFEST_FILENAME — MANIFEST_FILENAME = 'objectstack.plugin.json'; OSPLUGIN_EXT; computeIntegrity (manifest and SIGNATURE excluded, keys sorted); the ustar writer with mtime pinned to 0; readOspluginManifest",
"packages/cli/src/commands/plugin/publish.ts#version — artifact discovery; readOspluginManifest before any network call; the id/version refusal; the unsigned-artifact notice; the auth refusal",
"packages/cli/src/commands/plugin/install.ts#PluginInstall — the install-time re-verification the integrity negative points at (verifyIntegrity over the unpacked entries, `integrity_mismatch` / `integrity_malformed`, nothing installed) and the install-time tier warning; not scored here",
"packages/core/src/security/plugin-artifact-integrity.ts#verifyIntegrity — the one integrity verifier, shared by the publish preflight and the install-time check; not scored here",
"packages/runtime/src/installed-plugin-loader.ts#dispatchPluginRuntime — the boot-time tier dispatch point the trust-tier negative points at (`node` only; `sandbox` / `worker` refused by name; an undeclared tier refused); not scored here",
"packages/spec/src/kernel/manifest.zod.ts#ManifestSchema — ManifestSchema id and version (both required, version regex-pinned); PluginPackagingSchema ('bundled' only — 'manifest-deps' is a retired value refused with its migration prescription, ADR-0025 v2 §3.3); PluginRuntimeSchema (the trust tier, ADR-0025 §3.6)",
"packages/cli/src/commands/lint.ts#manifest — the protocol/missing-engines-range rule, its manifest scoping and its fix string",
"packages/rest/src/package-routes.ts#PACKAGE_MANIFEST_INVALID — the server mirror (400 PACKAGE_MANIFEST_INVALID), recorded in knownGaps rather than scored here",
Expand Down Expand Up @@ -1829,6 +1832,12 @@
"date": "2026-10-29",
"change": "A3 rewritten and a native-addon clause added: `packaging` no longer forks the artifact. PluginPackagingSchema is narrowed to `bundled` (manifest.zod.ts), so `manifest-deps` is refused at safeParse with its migration prescription and the build.ts externalize + package.json / pnpm-lock.yaml forks are gone — the old fork clause described a property the build no longer has. os plugin build now also refuses a native `.node` addon by name before writing the artifact (findNativeAddons, build.ts). Steps 2, 3, 4 and 8, the first fixture, the archive knownGap, the second negative and two source rows re-pointed to match; probes 6f-6h added for the addon refusal, and a knownGap records the addon an absolute or computed runtime path loads, which the build cannot see. Verified in the same PR through the real door (PluginBuild.run over on-disk projects, plugin-build-bundled-only.test.ts) — a verification re-run, no qa-run record. Untouched and not re-verified here: the integrity and trust-tier negatives",
"ref": "#27123"
},
{
"revision": 5,
"date": "2026-10-29",
"change": "the integrity and trust-tier negatives re-pointed, no clause changed: both said nothing in this repo verifies or dispatches, and both stopped being true with the install loop — os plugin install re-verifies the integrity map at unpack (install.ts#PluginInstall through verifyIntegrity, slice 1, 10f27b6508) and the boot-time loader dispatches on `runtime` before any import (installed-plugin-loader.ts#dispatchPluginRuntime, slice 2, d4ff1a025f). Each negative now names that code as the check this item does NOT score, in place of the tracker numbers it cited, which do not resolve in this repository; three source rows added for the code they point at. The scored clauses and steps are unchanged. Re-verified against the source at 86f2afeb23, no qa-run record",
"ref": "#27685"
}
]
},
Expand Down
Loading
Loading