Skip to content

Commit c7d6037

Browse files
feat(spec,cloud-connection,cli): the installModes manifest key and the install door's unmanaged mode (ADR-0131 D6, stage S2) (#22916)
Part of #15213: stage S2 of the ADR-0131 D6 execution card. The `installModes` manifest key lands with the install door that honours it, the `installMode` body key and `os package install --install-mode`, spelled `unmanaged` per ruling B (`6108509158`). Still on the card: S3 (the authoring docs page, which now also carries the `packages.ts` remedy text, see "Scope and size") and S4 (the objectui marketplace choice). Clause-②: yes (widening: the optional manifest key `installModes`; the install-local body field `installMode`; the CLI option `--install-mode`; new exports in `@objectstack/spec/kernel` and `@objectstack/metadata-protocol`; the error codes `STACK_UNMANAGED_INSTALL_UNCOPYABLE` and `INSTALL_MODE_NOT_PERMITTED`) > ~~Held out of the merge queue by #22949.~~ Released 2026-10-12: #22949 is no longer open. `main` has since been merged twice: at `6326fbe084`, with the net diff unchanged, and in the merge round below (`5a7ed7ca6`), whose one conflict keeps both sides. A fresh contract review of the merged head is owed before the queue. Claim `6112998649`. S1 (`importUnmanagedPackage`, PR #22879 → `72b26ed4f5`) is the verb this door calls. Changeset: `.changeset/15213-install-modes.md` (`minor` on `@objectstack/spec`, `@objectstack/cloud-connection`, `@objectstack/cli`, `@objectstack/metadata-protocol`). Nothing existing is removed. One existing answer is stricter: step 1f (below) refuses a managed install whose namespace a writable base holds with `409 RESOURCE_CONFLICT`, where a catalog install of it answered `200`. ## Merge round (seat order `6121590923`; fourth contract review `6121541034`) Head `8a713cd9e`: the merge `5a7ed7ca6` of `origin/main` at `e1deaefa8` through `scripts/pm/os-regen-merge.sh`, then one text-and-comments commit, `8a713cd9e`. No force-push, no rebase. - **Conflicts: one, not three.** The review read three `main` commits as moving paths this PR edits. Measured on `e1deaefa8` against `6326fbe084`, a driver-less `merge-tree` in a throwaway bare clone (GitHub's condition) and the driver-on `merge-tree` both name one conflicted path, `packages/cloud-connection/vitest.config.ts`. `packages/spec/src/stack.zod.ts`, `packages/cloud-connection/src/marketplace-install-local-plugin.ts` and `packages/metadata-protocol/src/protocol.ts` (main's #22954) merged without a conflict. - `vitest.config.ts`: both sides inserted an anchored source alias at the same spot. Both are kept: this PR's `@objectstack/metadata-protocol` alias first, then main's `@objectstack/objectql` alias. - `stack.zod.ts`: both collectors are in the merged file. Main's `collectDuplicateFunctionNameErrors` sits inside the cross-reference aggregate (`STACK_CROSS_REFERENCE_INVALID`, no new code), beside this PR's `validateUnmanagedInstallMode`. - The door: main's D3 `refuseWalledCatalogPackage` stays where main put it (see the order below). - **Generated artifacts.** The script's step 2 took no path from main, because no `merge=os-regen` path moved on both sides. After a fresh spec build, `check:generated` reads 14 of 14 artifacts current on the merged tree, so no regeneration commit is owed. - **The net diff is still this PR's own.** It covers the same 40 paths. For 39 of them, the per-file `git patch-id --stable` of the diff against main is the same before the merge (`6a40a09ed9..6326fbe`) and after it (`e1deaefa8..5a7ed7c`). The one that differs is `vitest.config.ts`, whose context changed. Nothing published changes, so the `Clause-②` line stands as written. - **The door's order on the merged head.** Every step comes before any write: 0. `installMode` is parsed against `InstallModeSchema` (`400 INVALID_REQUEST`), before the cloud fetch; 1. 1a, the payload check (`PLUGIN_MANIFEST_INVALID`); 2. 1b, the package id (`PLUGIN_MANIFEST_INVALID`, 400 or 502); 3. ADR-0131 D3, from main: under a wall, a caller below the operator rung gets `403 FORBIDDEN` when the incoming or installed version carries a position, a permission set or a capability; 4. 1c, the protocol handshake (`422 OS_PROTOCOL_INCOMPATIBLE`); 5. 1e, the install mode. A mode the package does not permit gets `422 INSTALL_MODE_NOT_PERMITTED`, a malformed `installModes` gets `PLUGIN_MANIFEST_INVALID`, and `unmanaged` on a walled requested posture gets `409 UNMANAGED_INSTALL_WALLED_POSTURE`; 6. 1d, code this door cannot run (`422 VALIDATION_ERROR`); 7. the `unmanaged` branch returns here; 8. 1f, the managed-after-unmanaged refusal (`409 RESOURCE_CONFLICT`), then the managed steps. One answer changes as a result. A tenant admin who asks for `unmanaged` under a wall, on a package that carries a catalog item, now gets D3's `403` instead of the `409`. Nothing else about the door's behaviour changes. The door's header docblock and the 1e comment now list D3 among the checks that run before the mode check. - **Dogfood P2 on the merged door: green.** P2's caller is the seeded platform admin, whom D3 admits. The CRM artifact it installs declares positions and permission sets, so P2 really passes through D3's admission. It still gets `409 UNMANAGED_INSTALL_WALLED_POSTURE` with zero writes, and the managed positive control still answers 200. - **The `_lock` status clause** (ruling A on #22971, `6119681934`). Five texts now carry the same clause: the lock is kept as written today, and a follow-up on #15213 strips it per #22971's ruling A. The strip itself is not in this PR. 1. The `INSTALL_MODES` TSDoc (`manifest.zod.ts`, shipped in the `.d.ts`): "keeps that lock on the copy (an open question on #15213)" now reads "keeps that lock on the copy as written today, and a follow-up on #15213 strips it per #22971's ruling A". 2. The changeset entry: "keeps that lock on the copy, and its handling is an open question on #15213." now reads "keeps that lock on the copy as written today, and a follow-up on #15213 strips it per #22971's ruling A." 3. The door's header docblock: "keeps that lock on the copy)" now reads "keeps that lock on the copy as written today, and a follow-up on #15213 strips it per #22971's ruling A)". 4. The verb's module header (`package-unmanaged-install.ts`): "keeps that lock)" now reads "keeps that lock as written today, and a follow-up on #15213 strips it per #22971's ruling A)". 5. `importUnmanagedPackage`'s TSDoc (`protocol.ts`): the same change as 4. Only texts 1 and 2 said "open question". Texts 3 to 5 said only that the lock is kept. - **Verified at `8a713cd9e`:** - spec built, and `check:generated` reads 14 of 14 current; - spec, nine suites (the seven this PR touches, plus main's `stack-duplicate-function-name` and `stack-cross-reference-envelope`): 9 files / 235 tests passed, and the census still counts eight sites; - the dogfood dependency closure built (63 of 63 turbo tasks); - dogfood `install-local-unmanaged-mode`: 17 of 17, P2's four cases included; main's `wall-package-refusal`: 19 of 19; - `@objectstack/cloud-connection`, full suite: 44 files / 536 tests; - `@objectstack/metadata-protocol`, five suites: 123 tests; - `@objectstack/cli`, `--project unit`, the four `package-install-*` suites: 23 tests; - `typecheck` exits 0 on spec, cloud-connection, metadata-protocol, cli and dogfood. - **Gates** (`dispatch-gates --commands --repo objectstack-ai/objectstack`, no paths, at `8a713cd9e`). 124 commands were derived. 122 of them ran, and 121 of those exit 0. - `check:platform-checklist` exits 1. A detached control on `main` at `e1deaefa8` shows the same 10 problems, line for line. `main`'s `794177d9b` (#22995) changed that checker after this merge; CI reads the merge ref. - `check:skill-examples` first answered exit 3, `PREREQUISITE NOT MET`. After `@objectstack/client-react` was built, it exits 0. - NOT MEASURED: `check:type-check-debt`, which the seat order excludes, and `check:dual-build-cjs-loads`, which needs a whole-workspace build (CI's `Build Core` runs it). `--ran` reconciles 124 derived, 122 run and 2 UNRUN, those two. - Artifact rosters: 44 of 45 exit 0, including the three PR-context guards, run with this PR's number, head ref and body. `check:published-readme-exports` exits 3, `PREREQUISITE NOT MET`, because it reads packages outside the built closure: NOT MEASURED. - **After the merge**, `origin/main` moved on to `794177d9b` (`67baccc55`, `794177d9b`). Neither commit touches a path of this PR. ## Patch round 2: the editability and stricter-answer wording (seat comment `6117204840`, contract review `6117191273`) The review at `618824f3bf` failed on two sentences of text; no source logic changed. Commit `57b4f4fa7f`: - **The changeset, sentence 1.** "every item is editable" is gone. It now reads: "the copy's items are this environment's own metadata, the next save edits them, and it follows no upgrade. One exception is carried: an item the package ships with an authored protection lock (`_lock`) keeps that lock on the copy, and its handling is an open question on #15213." The merge round replaced that last clause. - **The changeset's close.** "behaves exactly as before" is gone. It now reads: "an install that does not name a mode is managed, as before, with one stricter answer". It then names that answer: the 1f refusal, `409 RESOURCE_CONFLICT` before anything is written, for a managed install of a package whose namespace a writable base holds, where a catalog install of it answered `200`. This body's opening sentence carves out the same answer. - **The same claim at runtime.** - The install-local door's unmanaged answer `note` now reads "The package's metadata is now this environment's own, in the base 'BASE', and it follows no upgrade. Refresh the console to see it." - The CLI's unmanaged success line now reads "Package copied into the environment (unmanaged) — its items are this environment's own metadata, and it follows no upgrade". - No test asserted the removed wording. The CLI test matches the line's unchanged prefix, so no test changed. - **Also corrected in this body** (the review's boundary flag): the door asks the walled-posture refusal after the payload, id and handshake checks (1a to 1c, none of which writes), not "before anything else about the package". - **No sync.** GitHub reads the PR `mergeable: true`, state `blocked` (draft), neither dirty nor behind. `origin/main` has moved 4 commits past the merge base, and none touches spec source or a generated reference. - **Verified at `57b4f4fa7f`:** - the dependency closures of `@objectstack/cloud-connection` and `@objectstack/cli` were built (exit 0); - `marketplace-install-local-install-modes.test.ts`: 19 of 19; - `@objectstack/cloud-connection` `typecheck` exit 0; - the CLI's `package-install-mode.test.ts` (`--project unit`): 8 of 8; - `@objectstack/cli` `typecheck` exit 0; - `check-empty-changeset --base origin/main` exit 0. - **Gates for this round's paths.** The three files `57b4f4fa7f` changes were passed as paths to `dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, as the seat ordered; the whole-PR derivation at this head still names the same 124. That gives 61 commands, run at `57b4f4fa7f` (recorded before and after the pass): **60 of 61 exit 0**. `check:dual-build-cjs-loads` is NOT MEASURED (a whole-workspace build, excluded by the dispatch). The four `check:i18n*` / `check:lean-entry-closure` gates ran after the CLI and the four examples were built. `--ran` reconciles 61 derived, 60 run, the one UNRUN being `dual-build-cjs-loads`. The three PR-context guards (`check-closing-target-claim`, `check-partof-closing-keyword`, `check-single-claim-paths`) ran against this body with the PR's context, all exit 0. - **Round 3, comments only (seat comment `6117487130`), commit `8e9d1bfce5`.** The `INSTALL_MODES` TSDoc (`manifest.zod.ts`, shipped in the `.d.ts`) and the door's header docblock now say the copy is the environment's own metadata, which the next save edits, and name the `_lock` exception in one clause. The door's comments place the walled refusal after the payload check and steps 1b–1c, before any write. Verified at `8e9d1bfce5`: `check:generated` 14 of 14; spec and cloud-connection `typecheck` exit 0; of the 73 families derived for the two paths, 72 exit 0 and `dual-build-cjs-loads` is NOT MEASURED. - **Round 4, text and comments only (seat comment `6118016989`, contract review `6117921319`), commit `3059dee3f6`.** "Editable" with no exception beside it is gone from every shipped text the review named: the 1f refusal message, `InstallModeSchema`'s `.describe()` and the regenerated `kernel/manifest.mdx`, the CLI flag description and example line, and the changeset heading. It is also gone from the five flagged comments and from `importUnmanagedPackage`'s TSDoc in `protocol.ts`, which the sweep found. The supersede paragraph now names S1's "editable from then on" and "is fully editable"; S1's entry stays byte-equal. Verified at `3059dee3f6`, all exit 0: `check:generated` 14 of 14, `check:docs`, `check-empty-changeset`, and the spec, cloud-connection, CLI and metadata-protocol `typecheck`. Of the 110 families derived for the round's 8 paths, 109 exit 0 and `dual-build-cjs-loads` is NOT MEASURED. ## Patch round: the sync and the supersede (seat comment `6116200445`) - **The sync.** `origin/main` at `7dbb89dc5d` is merged through `scripts/pm/os-regen-merge.sh` (merge `29cfd19fd6`). `content/docs/references/index.mdx` is generated and not text-merged, so it was rendered again from the merged spec (`gen:schema` then `gen:docs`) and committed on its own (`d9add8817a`). It now reads Automation 71 schemas (main's `FlowCollectionSlot`), Kernel 158 (this PR's `InstallMode`), 1,524 in total. This was the red `check:docs` on `Type Check · source gates`. After it, `check:generated` reads 14 of 14 artifacts current, and `FlowCollectionSlot` is in the same 5 generated files on `origin/main` and on this head. - **The supersede** (`618824f3bf`). S1's pending entry `.changeset/15213-unmanaged-import-verb.md` is restored byte-equal to the merge base (blob `af966c215b` on both sides). The correction now lives in this PR's own `.changeset/15213-install-modes.md`. That entry names the earlier one, quotes its two now-false sentences, states what holds now and says it supersedes them. The sentences are "no door calls it yet, and no manifest key declares the mode yet", and seeds and translations "left to the install door, which applies them after the copy as it does for a managed install". This is the mechanism of the #22673 changeset and of `19939-flow-collection-value-slots.md`. `node scripts/check-empty-changeset.mjs --base origin/main` exits 0: no changeset from the merge base is modified or deleted. - **Q2 (`installedFrom`) and Q3 (seed data):** A, as built, per the seat. **Q1** (an authored `_lock` survives the copy): no edit in this round; the seat carries it to the card. ## What lands - **`installModes`** (`packages/spec/src/kernel/manifest.zod.ts`). An optional, non-empty list of `'managed'` / `'unmanaged'` (`InstallModeSchema`, `INSTALL_MODES`). There is **no parse-time default** (Q9): absence survives a parse, and `resolveInstallModes(manifest)` is the one reader of what it means (`DEFAULT_INSTALL_MODES`, `['managed']`). The liveness row is `live`; its evidence is the door and the authoring refusal. - **Refused at authoring (Q5).** `defineStack`, and so `os validate` and `os build`, refuses a package that permits `unmanaged` while it carries what the copy cannot write: - a non-empty collection of a type the registry declares runtime-uncreatable (`allowRuntimeCreate: false`, read off `DEFAULT_METADATA_TYPE_REGISTRY`, never a list); - code; - an extension of another package's items. The code is `STACK_UNMANAGED_INSTALL_UNCOPYABLE` (422), the eighth site of the `StackRefusalError` family, with one `issues` entry per key. The key classification the import verb used to keep privately moved to spec as `UNMANAGED_INSTALL_KEY_DISPOSITIONS` (`kernel/unmanaged-install.ts`). The authoring refusal and the door's import now read **one** table, held total against `ManifestSchema` and the stack definition by a spec test. So the PR that adds a schema key is the PR that reddens. - **The door** (`packages/cloud-connection/src/marketplace-install-local-plugin.ts`, `POST /api/v1/marketplace/install-local`): - `installMode` is parsed against `InstallModeSchema`. Absent means `managed`, so every existing caller is unchanged; a value outside the enum answers `400 INVALID_REQUEST` before the cloud fetch. - Step 1e, on both sources, before any write: - a mode the package's `installModes` does not permit answers `422 INSTALL_MODE_NOT_PERMITTED` with `details.permitted`; - a malformed `installModes` answers `PLUGIN_MANIFEST_INVALID`, with the id gate's 400/502 split; - `unmanaged` on a walled REQUESTED posture answers `409 UNMANAGED_INSTALL_WALLED_POSTURE` (Q2). This is the import verb's own refusal, `unmanagedInstallPostureRefusal`, now exported. The door asks it after the payload check, step 1b (the id), main's ADR-0131 D3 walled-catalog refusal and step 1c (the handshake), none of which writes, and before any other judgement of the package; the verb still asks it for every other caller. The full merged order is under "Merge round". - The `unmanaged` branch hands the verb ONE flat package body. A compiled artifact's nested `manifest:` is flattened, the collections winning, as `AssembledPackageBodySchema` records. The branch runs none of the managed steps: no conflict classification, no D5e gate, no `manifest.register`, no ledger entry, no `syncSchemas`, no binder, no `applySideEffects`. The answer carries `installMode`, `targetPackageId`, `copiedCount`, `copied` and `installedFrom`; `seeded: { mode: 'skipped' }` appears when the package declares seed data. The verb's refusals are relayed with their own code, status, message and `details`; a 5xx message is withheld and logged. - Step 1f (Q6), the managed-after-unmanaged refusal, on BOTH sources before any write. A managed install of a package whose namespace (declared, or derived from its id) is held by a **writable** base answers `409 RESOURCE_CONFLICT`, naming the base in the message and `details`. A namespace held by another code package keeps the lenient branch's existing behaviour, whose fate is a separate decision. - **`os package install --install-mode managed|unmanaged`** (`packages/cli/src/commands/package/install.ts`): - Without the option, nothing is sent. - An unmanaged answer prints the base, the item count and `installedFrom`, and none of the managed lines (hot-load; "re-registers on every boot"). - Asking for `unmanaged` and getting an answer without it means the runtime predates the mode and installed MANAGED. The command then exits 1 and says so. - The command's prose says two *sources* (catalog, air-gapped), so "mode" has one meaning. - **The import verb** (`packages/metadata-protocol/src/package-unmanaged-install.ts`), as S1's carry list asks: - `translations` are now copied, each locale of each bundle as one `translation` item `NAMESPACE_LOCALE` (`crm_zh_cn`), the bundles merged in order; - `installModes` is read and never written onto the base; - an empty code container (`functions: {}`) is no code. - The `MetadataWriteFace` docblock is corrected: the verb forwards `cause.issues`, but its one door relays message + code + `details` only, so the full-prose default on the 422 stays right. - **Carried prose.** - The `specVersion` guidance names `marketplace/template-manifest.zod.ts`; the retired `cloud/` path is pinned absent. - `18.meta-doors-organization-scope-retired.ts:30` reads "an unmanaged install". - S1's pending changeset is left byte-equal to the base. This PR's changeset supersedes the two sentences in it that this stage made false: "no door calls it yet", and seeds/translations "left to the install door". See "Patch round". ## Zone 2 readings (measured on `origin/main` at the base `45e206e498`) 1. **File list.** Every stage-0 S2 site is where the plan put it: - `ManifestSchema` at `kernel/manifest.zod.ts:401`, with the retired path at `:418`; - `defineStack`'s validators in `stack.zod.ts`; - `liveness/manifest.json`, which had 0 `installModes` rows; - the door's `handleInstall` at `:930`; - `install.ts:7/10/15/58/98` ("mode" prose); - `packages.ts:384/421/524` (the "ADR-0005 org overlay" remedies); - `18.meta-doors…:30`. Beyond it, necessarily: S1's `package-unmanaged-install.ts` (its private key table moved to spec; without a row, `installModes` would have been refused by the import as an unrecognised key); `metadata-protocol/src/index.ts` (one export); the two `defineStack` refusal censuses; and S1's pending changeset. 2. **The door** (base `45e206e498`): - catalog versus air-gapped branch at `if (inlineManifest)` `:969`; - step 1d at `:1149`; - the managed branch's `findConflict` at `:1195`, the D5e gate at `:1222`, `manifest.register` at `:1272`, the ledger write at `:1310` and `applySideEffects` at `:1343`. The `unmanaged` branch returns before `:1195`, after 1d, and none of these run (pinned: no register, no sync, no ledger file). The import's three registry reads (`protocol.ts:24472` `getPackage` / `manifests.has`, `:24479` `getNamespaceOwners`) were measured on the REAL registry by the dogfood suite. See P3. 3. **Translations and `installed_from`.** - Translations travel in the copied items. Measured: `translation/crm_zh_cn` is served after P1 and again after the restart. - `installed_from` is recorded on the door's answer (`installedFrom: { packageId, version }`) and on the operator log line at `info`. **No durable carrier**: it needs a contract decision (the install record or `sys_packages.metadata`), which is left to the seat. See Acceptance notes. - The keys S1 left to the door: `translations`, now copied by the verb; and `data`, not loaded, and the answer says so. 4. **The acceptance on `examples/app-crm`.** The fixture is the CRM's built artifact with `installModes: ['managed', 'unmanaged']` on its manifest. Suite: `packages/qa/dogfood/test/install-local-unmanaged-mode.dogfood.test.ts`, 17 cases, green on the merged tree `799c30127b` and again at `677d7fad45`. - **P1** (`single`, one database file across a restart): - 200, answering `targetPackageId: 'com.example.crm-copy'` and `installedFrom: { packageId: 'com.example.crm', version: '4.0.0' }`; - every copied item served by `GET /meta`, never with a non-`org` provenance; - every copied item saved by `PUT` with its own served body, `defineView` containers included (`view/crm_lead`); - every row has `package_id` = the base and `organization_id` NULL; - no ledger file and no registry package under `com.example.crm`; - after the restart, every item is still served, the source is still unregistered, and the base still holds the `crm` namespace. - **P2** (`isolated`): - `409 UNMANAGED_INSTALL_WALLED_POSTURE`, `details: { posture: 'isolated' }`; - zero writes: no base, no `sys_metadata` row, no ledger file; - positive control: the managed install of the same artifact on the same boot answers 200. - **P3**: - after the restart, a managed install of `4.0.1` answers `409 RESOURCE_CONFLICT` with `details: { namespace: 'crm', base: 'com.example.crm-copy' }`; - every copied row's `checksum|metadata` is byte-identical before and after; - a second unmanaged install answers 409 "'com.example.crm-copy' already exists" (`getPackage` on the real registry); - a target that is the booted code package `com.example.showcase` answers 409 (`manifests.has`); - a second target over the owned namespace answers 409 with `details: { namespace: 'crm', owners: ['com.example.crm-copy'] }` (`getNamespaceOwners`). - **S1's open notes:** - the container expansion is measured clean (above); - the authored protection lock is measured as a **gap**, under Acceptance notes. 5. **Size.** See "Scope and size". ## Ablation Dropping the requested-posture refusal turns P2 red. - **Mutation.** `unmanagedInstallPostureRefusal` always answers `null`, via `scripts/ablation-replace.mjs` in wrap mode (anchor ×1 → ×0, blob `87618e8d12` → `c3f5b8c7b9`). `@objectstack/metadata-protocol` was rebuilt, and `ablation-dist-preflight` found the marker in 2 built files. The build's `.d.ts` step refused the always-true condition (TS2872), but the JS the dogfood consumes was emitted. - **Mutated P2:** 3 failed, 1 passed (the posture precondition), as predicted: - the refusal answered 200; - writes happened; - the positive control then met the managed-after-unmanaged refusal. - **Restore:** blob `87618e8d12` == HEAD, `git diff HEAD` empty, rebuild exit 0, `ablation-dist-preflight --absent` marker absent from all 24 built files, tree clean. P2 green again (4 passed). ## Tests (each line names the commit it ran at) - **This round, at `618824f3bf` (after the sync merge `29cfd19fd6`), `@objectstack/spec`:** - the seven suites this PR touches: 7 files / 205 tests passed (`kernel/manifest`, `kernel/manifest-unknown-keys`, `kernel/unmanaged-install`, `stack-unmanaged-install-mode`, `stack-refusal-envelopes`, `stack-conversions-record`, `type-alias-convention.pin`); - `gen:migration-registry` current (423 semantic), then `--project repo` (`test:repo`): 55 files / 971 tests passed; - `typecheck` exit 0. - The merge brought no change to `cloud-connection` or `metadata-protocol` source. The lines below are the earlier round's, after the merge `682620e7dc`. - **`@objectstack/spec`**: - at `bbe1b2be2b` (the merged tree): `--project local` 650 files / 19,405 tests passed (1 todo); `--project repo` 55 / 971; `typecheck` exit 0; - at `677d7fad45`, after the ADR-0122 pin: the four touched suites 106 / 106 (`type-alias-convention.pin`, `kernel/manifest`, `kernel/unmanaged-install`, `stack-unmanaged-install-mode`); `typecheck` exit 0. - The defineStack refusal censuses (`stack-refusal-envelopes`, `stack-conversions-record`) now count eight sites. - **`@objectstack/cloud-connection`**, full suite at `677d7fad45`: 44 files / 536 tests. The new `marketplace-install-local-install-modes.test.ts` has 19 cases. `typecheck` exit 0. - **`@objectstack/metadata-protocol`** at `bbe1b2be2b`: 5 files / 123 tests (`package-unmanaged-install`, both face inventories, the `lookupArtifactItem` call-site census, `tenant-authored-write`). `typecheck` exit 0. - **`@objectstack/cli`**, `--project unit`, the four `package-install-*` suites at `bbe1b2be2b`: 4 files / 23 tests, `package-install-mode.test.ts` 8 of them. `typecheck` exit 0 at `677d7fad45`. The `integration` tier is declared to CI: no spawn entry and no integration file is touched. - **`@objectstack/runtime`**, `src/domains/` at `bbe1b2be2b`: 108 files / 2,712 tests; `typecheck` exit 0. The runtime diff is empty after the move to S3. - **Dogfood:** - the new suite plus the six existing install-local suites at `799c30127b` (merged tree, closure rebuilt): 7 files / 56 tests; - the new suite again at `677d7fad45` (cloud-connection rebuilt): 17 / 17; - `typecheck` exit 0. ## Gates (`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, no paths, derived again and run at `618824f3bf`) The derivation at `618824f3bf` names the same 124 commands as at `677d7fad45`. Union at `618824f3bf` (`git rev-parse --short HEAD`, recorded before and after the pass): **122 of 124 exit 0**. Reconciled with `--ran`: 123 of 124 derived families accounted with an exit code; the one UNRUN is `dual-build-cjs-loads` (below). - In the first pass, 8 commands answered exit 3 `PREREQUISITE NOT MET`, which measures nothing: the worktree was re-created for this round, so the built outputs they read were absent. The 8 are `check:doc-formula-expressions`, `check:doc-security-posture`, `check:skill-examples`, `check:docs-transcript-drift`, `check:i18n`, `check:i18n-coverage`, `check:i18n-walk-parity` and `check:lean-entry-closure`. The packages their own fix lines name were built with their dependencies (69 packages, exit 0, tree clean after), and all 8 were run again at the same head: exit 0. The record above carries the second run. - `pnpm check:dual-build-cjs-loads`: NOT MEASURED. It needs a whole-workspace build, which the dispatch excludes. - `node scripts/check-empty-changeset.mjs --base origin/main`: **exit 0**. "No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)" and "No changeset from the merge base modified or deleted by this diff". `check-changeset-no-major` exit 0. - `pnpm check:platform-checklist`: **red on `origin/main` too, not this PR.** The same 8 problems (7 `ABSENT SYMBOL` anchors in `docs/qa/platform-checklist/areas/*.json` naming `protocol.ts` / `attachment-access-hooks.ts` symbols, plus one anchor floor) are identical, line for line, on a detached `origin/main` at `eab7236ff7`, measured this round. This PR touches no checklist file and no named symbol. - Named by the seat, all exit 0 at `618824f3bf`: `check:generated`, `check:docs` ("226 generated files in sync"), `check:api-surface` ("public API surface + factory signatures unchanged"), `check:authorable-surface` and `check-empty-changeset`. Also exit 0: `check:liveness`, `check:route-envelope`, `check:error-code-casing` and `check:nul-bytes`. - Fixed in this PR by the first round's own findings: `check:slot-lookup`, `check:spec-parsed-alias`, `check:test-source-alias` and `check:empty-state`. - Artifact rosters (the block the no-path run prints). 42 of its 45 commands ran at `618824f3bf`, all exit 0; 14 of those are checker-health `--self-test` runs, which cannot fail for this diff. The three PR-context guards ran against this body with the PR's context, all exit 0: `check-closing-target-claim`, `check-partof-closing-keyword` and `check-single-claim-paths`. - After the sync, `origin/main` moved one commit, `eab7236ff7` (#22907). It touches no spec source, no generated reference and no file of this PR, so the reference index stays current against the merge ref. CI reads the current merge ref. ## Scope and size After the merge round, the change set is 2,623 changed lines (+2,451 / −172, the same 40 files), generated included, against the merge base `e1deaefa8`, under the 3,000 human-merge line. Before that round it was 2,602 changed lines (+2,433 / −169) against the merge base `7dbb89dc5d`. It read 2,604 / 41 files before this round, which counted S1's changeset edit that is now restored. It measured 2,667 with the `packages.ts` remedies, past the stage plan's ~2.6k mark. As the plan prescribes, **the `packages.ts` "ADR-0005 org overlay" remedy text (`:421`, `:524`, docblock `:384`) moves to S3**, untouched here. S3 therefore does not fold in. ## Acceptance notes - **Gap, measured, and since ruled (Q1); the changeset names it as the one carried exception to editability.** An item a package ships with an authored protection lock (`_lock: 'full'`, ADR-0010) is copied as written. The next `PUT /api/v1/meta/view/NAME` then answers `403 ITEM_LOCKED`, so that copied item is not editable, against D6's "editable in Studio like anything authored there". Measured on the real showcase stack: the import is called directly; the install-local door calls the same verb. The remedy is now ruled: A on #22971 (`6119681934`), the import strips `_lock` / `_lockReason` / `_lockSource` and names each stripped lock. A follow-up on #15213 carries the strip. This PR keeps the lock as written today, and every shipped text says so (see "Merge round"). - `installed_from` has no durable carrier (see Zone 2 item 3). The seat ruled this A, as built (Q2). - Seed data is not loaded by an unmanaged install. The reseed and purge doors act on ledger entries this install never writes, so seeded rows would be rows nothing here could clear. The answer says so. The seat ruled this A, as built (Q3). - ADR-0126's status line (`docs/adr/0126-packaged-metadata-customization-model.md:3`, "template = copied once…") still spells the old word. It is a governed Tier H surface, so it is not edited here. - Observation, unmeasured: a managed install of a compiled artifact through this door hands `registerApp` a body whose `namespace` sits only on the nested `manifest:`, so the registry's namespace gate may not see it. The managed-after-unmanaged refusal reads the declared namespace itself, so it does not depend on that. - An unrecognised `OS_TENANCY_POSTURE` is refused by the door's install-capability admission (401 in the unit harness) before step 1e. The import's own suite pins that value as walled. --- _Generated by [Claude Code](https://claude.ai/code/session_01KNKBCRDJCu5tGy3TEbvtrF)_ --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: objectstack-fleet[bot] <332303061+objectstack-fleet[bot]@users.noreply.github.com>
1 parent 2acff87 commit c7d6037

40 files changed

Lines changed: 2451 additions & 172 deletions

‎.changeset/15213-install-modes.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/cloud-connection': minor
4+
'@objectstack/cli': minor
5+
'@objectstack/metadata-protocol': minor
6+
---
7+
8+
A package declares how it may be installed — `installModes: ['managed', 'unmanaged']` — and `os package install --install-mode unmanaged` copies it once into the environment as the environment's own metadata (ADR-0131 D6)
9+
10+
Clause-②: yes (widening: the new optional manifest key `installModes`; the new exports `INSTALL_MODES`, `InstallModeSchema`, `InstallMode`, `DEFAULT_INSTALL_MODES`, `resolveInstallModes`, `UNMANAGED_INSTALL_KEY_DISPOSITIONS`, `UNMANAGED_INSTALL_REFUSING_DISPOSITIONS`, `UnmanagedInstallKeyDisposition`, `unmanagedInstallValueCarries` and `unmanagedInstallPostureRefusal`; the install-local body field `installMode`; the CLI option `--install-mode`; the error codes `STACK_UNMANAGED_INSTALL_UNCOPYABLE` and `INSTALL_MODE_NOT_PERMITTED`)
11+
12+
A package installs one of two ways. **Managed**: it is registered as code, stays the authority, upgrades, and nothing it ships is edited in place. **Unmanaged**: its metadata is copied once into this environment under a new writable base; the copy's items are this environment's own metadata, the next save edits them, and it follows no upgrade. One exception is carried: an item the package ships with an authored protection lock (`_lock`) keeps that lock on the copy as written today, and a follow-up on #15213 strips it per #22971's ruling A. This entry adds the manifest key that declares the modes a package permits, and the install door that honours it; the copy itself is the companion entry `15213-unmanaged-import-verb`.
13+
14+
**This entry supersedes parts of that earlier entry, `15213-unmanaged-import-verb`, in this same release.** It says of the import "no door calls it yet, and no manifest key declares the mode yet". What holds now: the install door calls it for `installMode: 'unmanaged'`, and the manifest key `installModes` declares the modes a package permits. It says seed `data` and `translations` are "left to the install door, which applies them after the copy as it does for a managed install". What holds now: the import copies `translations` itself, as `translation` items, and an unmanaged install loads no seed `data` — neither the import nor the door applies it, and the door's answer says so under `seeded`. It also calls the copy "editable from then on" and says each item "is fully editable"; what holds is the sentence above: the copy's items are this environment's own metadata and the next save edits them, except an item shipped with an authored `_lock`, which keeps that lock. Where the two entries disagree, this one is current.
15+
16+
- **`installModes`** (`@objectstack/spec`, `ManifestSchema`). Optional, a non-empty list of `'managed'` / `'unmanaged'`. Absent means `['managed']`, read through one resolver, `resolveInstallModes(manifest)`; there is no parse-time default, so no existing manifest changes by a byte. A package may permit `unmanaged` alone.
17+
- **Refused at authoring.** `defineStack` — so `os validate` and `os build` — refuses a package that permits `unmanaged` while it carries what the copy cannot write as environment metadata: a non-empty collection of a type the metadata-type registry declares runtime-uncreatable (`agents`, `apis`, `capabilities`, `jobs`, `picklists`), code (`functions`, `onEnable`, `plugins`, a plugin artifact's `main` / `runtime` / `packaging` / `integrity`), or an extension of another package's items (`objectExtensions`, `picklistExtensions`, `navigationContributions`, `contributes`). The refusal is `STACK_UNMANAGED_INSTALL_UNCOPYABLE` (422), one `issues` entry per key. Fix: remove `'unmanaged'` from `manifest.installModes`, or move what cannot be copied out of the package. The install door judges the same body with the same key classification (`UNMANAGED_INSTALL_KEY_DISPOSITIONS`), so a package that passes `os validate` is not refused at install for a key nobody named.
18+
- **The install door** (`@objectstack/cloud-connection`, `POST /api/v1/marketplace/install-local`). The body takes `installMode: 'managed' | 'unmanaged'`; absent means `managed`, so every existing caller is unchanged. Before anything is written, on the catalog and the air-gapped source alike:
19+
- a mode the package's `installModes` does not permit answers `422 INSTALL_MODE_NOT_PERMITTED`, with `details.permitted`;
20+
- `unmanaged` on a deployment whose requested tenancy posture walls organizations (`group`, `isolated`, or an unrecognised value) answers `409 UNMANAGED_INSTALL_WALLED_POSTURE`, naming the posture;
21+
- a managed install of a package whose namespace a writable base holds — its unmanaged copy — answers `409 RESOURCE_CONFLICT`, naming that base: a managed install would otherwise take the namespace and stop serving the edited copies.
22+
An unmanaged install writes no install-ledger entry, registers nothing under the package's id and does not load its sample data (the answer says so under `seeded`). The answer names the new base, the items copied and `installedFrom` (the source package id and version, for information only). The package's translation bundles are copied as `translation` items, so they reload at every boot with the environment's own translations.
23+
- **`os package install --install-mode managed|unmanaged`** (`@objectstack/cli`). Without the option, nothing is sent and the runtime installs managed. An unmanaged answer prints the base, the item count and the source. A runtime that predates the install mode ignores the option and installs managed; the command then exits 1 and says so. The command's help now calls the catalog and the air-gapped artifact the two *sources*, so "mode" means one thing.
24+
- **Copy semantics** (`@objectstack/metadata-protocol`). `importUnmanagedPackage` copies `translations` (each locale of each bundle as one `translation` item named `<namespace>_<locale>`, bundles merged in order), reads `installModes` without writing it onto the base, and no longer refuses an empty code container (`functions: {}`, `plugins: []`). `unmanagedInstallPostureRefusal` is exported, so the door and the import answer the walled posture with one sentence.
25+
26+
Nothing existing is removed: every new key, field and option is optional, and an install that does not name a mode is managed, as before, with one stricter answer. That answer is the refusal above: a managed install of a package whose namespace a writable base holds now answers `409 RESOURCE_CONFLICT` before anything is written, where a catalog install of it answered `200`.

‎content/docs/references/api/contract.mdx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const result = ApiErrorSchema.parse(data);
2828

2929
| Property | Type | Required | Description |
3030
| :--- | :--- | :--- | :--- |
31-
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +323 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
31+
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +325 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
3232
| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112) |
3333
| **message** | `string` | ✅ | Readable error message |
3434
| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces `message`. |
@@ -199,6 +199,7 @@ const result = ApiErrorSchema.parse(data);
199199
* `HOOK_UNSCOPED_DATA_ACCESS`
200200
* `IMPORT_JOB_CREATE_FAILED`
201201
* `IMPORT_ROW_FAILED`
202+
* `INSTALL_MODE_NOT_PERMITTED`
202203
* `INTERNAL`
203204
* `INVALID_ARTIFACT_PACKAGES`
204205
* `INVALID_ARTIFACT_PACKAGE_ENTRY`
@@ -339,6 +340,7 @@ const result = ApiErrorSchema.parse(data);
339340
* `STACK_SCHEMA_INVALID`
340341
* `STACK_SINGLE_APP_VIOLATION`
341342
* `STACK_TRIGGER_CAPABILITY_REQUIRED`
343+
* `STACK_UNMANAGED_INSTALL_UNCOPYABLE`
342344
* `STORED_TYPE_NOT_CANONICAL`
343345
* `STORE_UNAVAILABLE`
344346
* `SUGGESTION_CONFIRM_FAILED`

‎content/docs/references/api/error-code-ledger.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,7 @@ const result = ErrorCode.parse(data);
366366
* `HOOK_UNSCOPED_DATA_ACCESS`
367367
* `IMPORT_JOB_CREATE_FAILED`
368368
* `IMPORT_ROW_FAILED`
369+
* `INSTALL_MODE_NOT_PERMITTED`
369370
* `INTERNAL`
370371
* `INVALID_ARTIFACT_PACKAGES`
371372
* `INVALID_ARTIFACT_PACKAGE_ENTRY`
@@ -506,6 +507,7 @@ const result = ErrorCode.parse(data);
506507
* `STACK_SCHEMA_INVALID`
507508
* `STACK_SINGLE_APP_VIOLATION`
508509
* `STACK_TRIGGER_CAPABILITY_REQUIRED`
510+
* `STACK_UNMANAGED_INSTALL_UNCOPYABLE`
509511
* `STORED_TYPE_NOT_CANONICAL`
510512
* `STORE_UNAVAILABLE`
511513
* `SUGGESTION_CONFIRM_FAILED`

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ Installed package row whose manifest is the assembled package body
8989
| **version** | `string` | ✅ | Package version (SemVer 2.0.0 — e.g. 1.2.3, 2.0.0-beta.1, 1.0.0+20230101) |
9090
| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package |
9191
| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project |
92+
| **installModes** | `Enum<'managed' \| 'unmanaged'>[]` | optional | Install modes this package permits (ADR-0131 D6): managed \| unmanaged. Absent means ['managed']. |
9293
| **name** | `string` | ✅ | Human-readable package name |
9394
| **description** | `string` | optional | Package description |
9495
| **permissions** | `{ name: string; label?: string; description?: string; packageId?: string; … }[]` | optional | Permission Sets — the ADR-0090 collection half of `permissions`; at the manifest/authoring stage the same key is the ADR-0025 capability grant instead (`ManifestSchema.permissions`) |
@@ -270,6 +271,7 @@ Installed package with runtime lifecycle state
270271
| **version** | `string` | ✅ | Package version (SemVer 2.0.0 — e.g. 1.2.3, 2.0.0-beta.1, 1.0.0+20230101) |
271272
| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package |
272273
| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project |
274+
| **installModes** | `Enum<'managed' \| 'unmanaged'>[]` | optional | Install modes this package permits (ADR-0131 D6): managed \| unmanaged. Absent means ['managed']. |
273275
| **name** | `string` | ✅ | Human-readable package name |
274276
| **description** | `string` | optional | Package description |
275277
| **permissions** | `{ services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }` | optional | Required permissions at the AUTHORING stage: the structured plugin block `{ services, hooks, network, fs }` (ADR-0025 §3.2) — the legacy flat string list is retired — and at the assembled stage the same key is the ADR-0090 `PermissionSet[]` collection instead (`AssembledPackageBodySchema`) |
@@ -333,6 +335,7 @@ Installed package row whose manifest is the assembled package body
333335
| **version** | `string` | ✅ | Package version (SemVer 2.0.0 — e.g. 1.2.3, 2.0.0-beta.1, 1.0.0+20230101) |
334336
| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package |
335337
| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project |
338+
| **installModes** | `Enum<'managed' \| 'unmanaged'>[]` | optional | Install modes this package permits (ADR-0131 D6): managed \| unmanaged. Absent means ['managed']. |
336339
| **name** | `string` | ✅ | Human-readable package name |
337340
| **description** | `string` | optional | Package description |
338341
| **permissions** | `{ name: string; label?: string; description?: string; packageId?: string; … }[]` | optional | Permission Sets — the ADR-0090 collection half of `permissions`; at the manifest/authoring stage the same key is the ADR-0025 capability grant instead (`ManifestSchema.permissions`) |

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ Install package request
146146
| **version** | `string` | ✅ | Package version (SemVer 2.0.0 — e.g. 1.2.3, 2.0.0-beta.1, 1.0.0+20230101) |
147147
| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package |
148148
| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project |
149+
| **installModes** | `Enum<'managed' \| 'unmanaged'>[]` | optional | Install modes this package permits (ADR-0131 D6): managed \| unmanaged. Absent means ['managed']. |
149150
| **name** | `string` | ✅ | Human-readable package name |
150151
| **description** | `string` | optional | Package description |
151152
| **permissions** | `{ services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }` | optional | Required permissions at the AUTHORING stage: the structured plugin block `{ services, hooks, network, fs }` (ADR-0025 §3.2) — the legacy flat string list is retired — and at the assembled stage the same key is the ADR-0090 `PermissionSet[]` collection instead (`AssembledPackageBodySchema`) |
@@ -190,6 +191,7 @@ Install package request
190191
| **version** | `string` | ✅ | Package version (SemVer 2.0.0 — e.g. 1.2.3, 2.0.0-beta.1, 1.0.0+20230101) |
191192
| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| 'module' \| 'gateway' \| 'adapter'>` | ✅ | Type of package |
192193
| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project |
194+
| **installModes** | `Enum<'managed' \| 'unmanaged'>[]` | optional | Install modes this package permits (ADR-0131 D6): managed \| unmanaged. Absent means ['managed']. |
193195
| **name** | `string` | ✅ | Human-readable package name |
194196
| **description** | `string` | optional | Package description |
195197
| **permissions** | `{ services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }` | optional | Required permissions at the AUTHORING stage: the structured plugin block `{ services, hooks, network, fs }` (ADR-0025 §3.2) — the legacy flat string list is retired — and at the assembled stage the same key is the ADR-0090 `PermissionSet[]` collection instead (`AssembledPackageBodySchema`) |
@@ -307,6 +309,7 @@ Install package request
307309
| **version** | `string` | ✅ | Package version (SemVer 2.0.0 — e.g. 1.2.3, 2.0.0-beta.1, 1.0.0+20230101) |
308310
| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package |
309311
| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project |
312+
| **installModes** | `Enum<'managed' \| 'unmanaged'>[]` | optional | Install modes this package permits (ADR-0131 D6): managed \| unmanaged. Absent means ['managed']. |
310313
| **name** | `string` | ✅ | Human-readable package name |
311314
| **description** | `string` | optional | Package description |
312315
| **permissions** | `{ services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }` | optional | Required permissions at the AUTHORING stage: the structured plugin block `{ services, hooks, network, fs }` (ADR-0025 §3.2) — the legacy flat string list is retired — and at the assembled stage the same key is the ADR-0090 `PermissionSet[]` collection instead (`AssembledPackageBodySchema`) |
@@ -440,6 +443,7 @@ Upgrade package request
440443
| **version** | `string` | ✅ | Package version (SemVer 2.0.0 — e.g. 1.2.3, 2.0.0-beta.1, 1.0.0+20230101) |
441444
| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package |
442445
| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project |
446+
| **installModes** | `Enum<'managed' \| 'unmanaged'>[]` | optional | Install modes this package permits (ADR-0131 D6): managed \| unmanaged. Absent means ['managed']. |
443447
| **name** | `string` | ✅ | Human-readable package name |
444448
| **description** | `string` | optional | Package description |
445449
| **permissions** | `{ services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }` | optional | Required permissions at the AUTHORING stage: the structured plugin block `{ services, hooks, network, fs }` (ADR-0025 §3.2) — the legacy flat string list is retired — and at the assembled stage the same key is the ADR-0090 `PermissionSet[]` collection instead (`AssembledPackageBodySchema`) |
@@ -535,6 +539,7 @@ Resolve dependencies request
535539
| **version** | `string` | ✅ | Package version (SemVer 2.0.0 — e.g. 1.2.3, 2.0.0-beta.1, 1.0.0+20230101) |
536540
| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package |
537541
| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project |
542+
| **installModes** | `Enum<'managed' \| 'unmanaged'>[]` | optional | Install modes this package permits (ADR-0131 D6): managed \| unmanaged. Absent means ['managed']. |
538543
| **name** | `string` | ✅ | Human-readable package name |
539544
| **description** | `string` | optional | Package description |
540545
| **permissions** | `{ services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }` | optional | Required permissions at the AUTHORING stage: the structured plugin block `{ services, hooks, network, fs }` (ADR-0025 §3.2) — the legacy flat string list is retired — and at the assembled stage the same key is the ADR-0090 `PermissionSet[]` collection instead (`AssembledPackageBodySchema`) |

‎content/docs/references/api/protocol.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1950,6 +1950,7 @@ Install package request
19501950
| **version** | `string` | ✅ | Package version (SemVer 2.0.0 — e.g. 1.2.3, 2.0.0-beta.1, 1.0.0+20230101) |
19511951
| **type** | `Enum<'plugin' \| 'ui' \| 'driver' \| 'server' \| 'app' \| 'theme' \| 'agent' \| 'objectql' \| …>` | ✅ | Type of package |
19521952
| **scope** | `Enum<'cloud' \| 'system' \| 'project'>` | optional (default: `"project"`) | Deployment scope: cloud \| system \| project |
1953+
| **installModes** | `Enum<'managed' \| 'unmanaged'>[]` | optional | Install modes this package permits (ADR-0131 D6): managed \| unmanaged. Absent means ['managed']. |
19531954
| **name** | `string` | ✅ | Human-readable package name |
19541955
| **description** | `string` | optional | Package description |
19551956
| **permissions** | `{ services?: string[]; hooks?: string[]; network?: string[]; fs?: string[] }` | optional | Required permissions at the AUTHORING stage: the structured plugin block `{ services, hooks, network, fs }` (ADR-0025 §3.2) — the legacy flat string list is retired — and at the assembled stage the same key is the ADR-0090 `PermissionSet[]` collection instead (`AssembledPackageBodySchema`) |

0 commit comments

Comments
 (0)