From d5335df04f4668ab779d816f3ee85fc4680e7dbf Mon Sep 17 00:00:00 2001 From: Bonanza Date: Sat, 25 Jul 2026 20:10:59 -0700 Subject: [PATCH 1/4] docs(site): document AppOS Catalog Bundle Layout v1 (fn-167.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - manifest/index.md: new 'Catalog bundle layout' section — dual-manifest zip layout, installer root-first resolution + appos/runtime/plugin.json normalize-at-install fallback, verification-before-manifest-resolution, exactly-one-candidate + zip-root-relative-paths rules - manifest/reference.md (via generate-docs-data.mjs pageManifestReference): note on the published-bundle manifest location, regenerated through the drift-gated pipeline - getting-started/first-plugin.md: 'Where to go next' pointer to the layout section (no contract duplication) Validation: npm ci (0), npm run check-drift (0), npm run build (0) --- docs-site/scripts/generate-docs-data.mjs | 8 ++++ .../docs/getting-started/first-plugin.md | 2 + docs-site/src/content/docs/manifest/index.md | 47 +++++++++++++++++++ .../src/content/docs/manifest/reference.md | 8 ++++ 4 files changed, 65 insertions(+) diff --git a/docs-site/scripts/generate-docs-data.mjs b/docs-site/scripts/generate-docs-data.mjs index 02b3e30..51edec4 100644 --- a/docs-site/scripts/generate-docs-data.mjs +++ b/docs-site/scripts/generate-docs-data.mjs @@ -279,6 +279,14 @@ Generated from **Always required:** ${requiredList.map((f) => `\`${f}\``).join(", ")}. ${conditional} +:::note +In a dev tree this file is \`plugin.json\` at the plugin root. In a *published +catalog bundle* it rides at \`appos/runtime/plugin.json\` (the zip root holds the +catalog \`manifest.json\` instead) and the desktop installer copies it back to +the bundle root at install time — see +[Catalog bundle layout](/manifest/#catalog-bundle-layout). +::: + ## Fields ${renderSchemaObject(manifestSchema, requiredList, 2, "")} diff --git a/docs-site/src/content/docs/getting-started/first-plugin.md b/docs-site/src/content/docs/getting-started/first-plugin.md index 5ecc195..09c63e0 100644 --- a/docs-site/src/content/docs/getting-started/first-plugin.md +++ b/docs-site/src/content/docs/getting-started/first-plugin.md @@ -110,3 +110,5 @@ node scripts/validate-schema.mjs path/to/plugin.json - [API namespaces](/reference/namespaces/) — the full `context.*` surface - [Manifest & permissions](/manifest/) — every manifest field and scope - [Extension points](/extension-points/) — contributing to core plugins via `extensions[]` +- [Catalog bundle layout](/manifest/#catalog-bundle-layout) — how a published + catalog bundle's zip layout differs from this dev layout diff --git a/docs-site/src/content/docs/manifest/index.md b/docs-site/src/content/docs/manifest/index.md index 4eb62c4..bb15067 100644 --- a/docs-site/src/content/docs/manifest/index.md +++ b/docs-site/src/content/docs/manifest/index.md @@ -42,6 +42,53 @@ Add a `$schema` reference for editor autocomplete: - **[Extension points](/extension-points/)** — the `extensions[]` array for contributing to core-plugin extension points. +## Catalog bundle layout + +A dev tree keeps `plugin.json` at the plugin root — that is what local and +sideload installs load. Bundles published to the AppOS catalog use **AppOS +Catalog Bundle Layout v1**, which carries TWO manifests because the catalog's +submit validation and the desktop installer read different schemas: + +- `manifest.json` at the **zip root** — the catalog `manifest-v1` document + (`schema`, `slug`, `kind`, `version`, `title`, `license`, `capabilities`, + `permissions`, `compatibility`, `entry`; unknown keys rejected). Validated at + publish time. +- `appos/runtime/plugin.json` — the AppOS runtime manifest (the schema + documented on these pages). Read by the desktop app at install time. + +```text +space-appos-myplugin-1.0.0.zip +├── manifest.json # catalog manifest-v1 +├── appos/ +│ └── runtime/ +│ └── plugin.json # AppOS runtime manifest (this schema) +├── dist/ +│ └── main.js # runtime payload at the zip root +└── webview/ assets/ README.md LICENSE ... +``` + +Rules: + +- **Installer resolution is root-first.** A root `plugin.json` always wins; + otherwise the desktop installer consults the single well-known fallback + `appos/runtime/plugin.json` and copies it to the bundle root at install time + ("normalize-at-install"); neither present fails the install with + `manifestMissing`. There is no globbing — the fallback is one constant path. +- **Verification precedes manifest resolution.** SHA-256 (and, on the catalog + install path, Ed25519 signature verification) runs over the exact zip bytes + before extraction — always before any manifest is read. +- **Exactly one catalog-manifest candidate.** Nothing named `plugin.json` or + `manifest.json` may exist at the zip root or one level deep except the single + catalog manifest — the submit scan rejects zero candidates (`no_manifest`) + and more than one (`ambiguous_bundle_root`). Depth 2 keeps the runtime + manifest invisible to that scan. +- **Runtime-manifest paths are relative to the ZIP ROOT**, not to + `appos/runtime/` — e.g. `"entrypoint": "dist/main.js"`. +- A dev-layout bundle (root `plugin.json`, no `manifest.json`) installs locally + but is NOT publishable to the catalog: its root `plugin.json` would be + selected as the catalog-manifest candidate and fail `manifest-v1` validation + (`manifest_invalid`). + ## Validation This repo ships a validator you can run locally against any manifest: diff --git a/docs-site/src/content/docs/manifest/reference.md b/docs-site/src/content/docs/manifest/reference.md index 1446c75..1883e50 100644 --- a/docs-site/src/content/docs/manifest/reference.md +++ b/docs-site/src/content/docs/manifest/reference.md @@ -17,6 +17,14 @@ Generated from **Always required:** `id`, `name`, `version`, `runtime`. Plugins with `"runtime": "javascript"` must also declare `entrypoint`. +:::note +In a dev tree this file is `plugin.json` at the plugin root. In a *published +catalog bundle* it rides at `appos/runtime/plugin.json` (the zip root holds the +catalog `manifest.json` instead) and the desktop installer copies it back to +the bundle root at install time — see +[Catalog bundle layout](/manifest/#catalog-bundle-layout). +::: + ## Fields | Field | Type | Required | Description | From 6cd698869e9e9a17ccd30a2b91a39fe634868623 Mon Sep 17 00:00:00 2001 From: Bonanza Date: Sat, 25 Jul 2026 20:15:02 -0700 Subject: [PATCH 2/4] docs(site): include catalog-path publisher gate in verification bullet (fn-167.4 review r1) --- docs-site/src/content/docs/manifest/index.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs-site/src/content/docs/manifest/index.md b/docs-site/src/content/docs/manifest/index.md index bb15067..a04f3ef 100644 --- a/docs-site/src/content/docs/manifest/index.md +++ b/docs-site/src/content/docs/manifest/index.md @@ -74,9 +74,11 @@ Rules: `appos/runtime/plugin.json` and copies it to the bundle root at install time ("normalize-at-install"); neither present fails the install with `manifestMissing`. There is no globbing — the fallback is one constant path. -- **Verification precedes manifest resolution.** SHA-256 (and, on the catalog - install path, Ed25519 signature verification) runs over the exact zip bytes - before extraction — always before any manifest is read. +- **Verification precedes manifest resolution.** SHA-256 (both install paths) + and, on the catalog install path, the publisher-verified gate plus Ed25519 + signature verification all run before extraction — the hash and signature + checks cover the exact zip bytes, so no manifest is read until verification + passes. - **Exactly one catalog-manifest candidate.** Nothing named `plugin.json` or `manifest.json` may exist at the zip root or one level deep except the single catalog manifest — the submit scan rejects zero candidates (`no_manifest`) From 04f89149b977a0fae4dedce71e792cd3eed55d85 Mon Sep 17 00:00:00 2001 From: Bonanza Date: Sun, 26 Jul 2026 23:19:53 -0700 Subject: [PATCH 3/4] docs(site): scope validate-schema.mjs to runtime plugin.json; document server-side catalog manifest-v1 validation (fn-167.4 review r2) --- .../content/docs/getting-started/first-plugin.md | 4 +++- docs-site/src/content/docs/manifest/index.md | 13 ++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/docs-site/src/content/docs/getting-started/first-plugin.md b/docs-site/src/content/docs/getting-started/first-plugin.md index 09c63e0..043f607 100644 --- a/docs-site/src/content/docs/getting-started/first-plugin.md +++ b/docs-site/src/content/docs/getting-started/first-plugin.md @@ -99,7 +99,9 @@ npx esbuild src/main.ts --bundle --format=iife --target=es2020 --outfile=dist/ma `npx` runs the esbuild binary you installed as a devDependency during [installation](/getting-started/installation/) — no global install needed. -Validate your manifest against the schema shipped in this repo: +Validate your runtime `plugin.json` against the schema shipped in this repo +(runtime schema only — the catalog `manifest.json` of a published bundle is +validated server-side at submit time): ```bash node scripts/validate-schema.mjs path/to/plugin.json diff --git a/docs-site/src/content/docs/manifest/index.md b/docs-site/src/content/docs/manifest/index.md index a04f3ef..1dc4192 100644 --- a/docs-site/src/content/docs/manifest/index.md +++ b/docs-site/src/content/docs/manifest/index.md @@ -93,8 +93,19 @@ Rules: ## Validation -This repo ships a validator you can run locally against any manifest: +This repo ships a validator for the **runtime `plugin.json` manifest only** — +it checks against `schemas/plugin-v1.json`, the schema documented on these +pages: ```bash node scripts/validate-schema.mjs path/to/plugin.json ``` + +The catalog `manifest.json` at the zip root uses a different schema — catalog +`manifest-v1` — and has **no local validator in this repo**, so don't point +`validate-schema.mjs` at it: you'd get irrelevant runtime-schema errors. +Catalog manifests are validated server-side at publish time — the submit step +re-extracts `manifest.json` from your uploaded zip and validates it strictly +(unknown keys rejected; `schema`, `slug`, `title`, and `entry` required). +Failures surface as `manifest_invalid` errors in the publish API's +`prepare → submit` response. From 905834bfd3beb62682eadee3d7ac2cc2614e96f5 Mon Sep 17 00:00:00 2001 From: Bonanza Date: Sun, 26 Jul 2026 23:43:04 -0700 Subject: [PATCH 4/4] =?UTF-8?q?docs(site):=20qualify=20manifest-overview?= =?UTF-8?q?=20intro=20=E2=80=94=20root=20plugin.json=20is=20dev/installed?= =?UTF-8?q?=20layout;=20catalog=20zip=20nests=20it=20at=20appos/runtime/?= =?UTF-8?q?=20(fn-167.4=20review=20r3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs-site/src/content/docs/manifest/index.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs-site/src/content/docs/manifest/index.md b/docs-site/src/content/docs/manifest/index.md index 1dc4192..598bfcb 100644 --- a/docs-site/src/content/docs/manifest/index.md +++ b/docs-site/src/content/docs/manifest/index.md @@ -5,9 +5,13 @@ sidebar: order: 1 --- -Every AppOS plugin ships a `plugin.json` manifest at its root. The manifest -declares the plugin's identity, when it activates, which permissions it needs, -and any system or plugin dependencies. +Every AppOS plugin keeps its `plugin.json` manifest at the plugin root — in +the dev tree and in the installed plugin directory alike. The one exception is +a catalog-published zip, which nests the runtime manifest at +`appos/runtime/plugin.json` until the installer copies it to the root at +install time — see [Catalog bundle layout](#catalog-bundle-layout) below. The +manifest declares the plugin's identity, when it activates, which permissions +it needs, and any system or plugin dependencies. The manifest is validated against the JSON Schema at [`schemas/plugin-v1.json`](https://github.com/appos/plugin-sdk/blob/main/schemas/plugin-v1.json).