From 1f360a7fdc1b7c9777224de85f9ec91e354fc447 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 16:04:40 +0000 Subject: [PATCH 1/5] feat(spec)!: PackageInstallRequestSchema's wrapped branch refuses an unknown top-level key by name The wrapped install body closes from strip to strict (strictObject), so { manifest, enabledOnInstall: false } is refused naming the misspelled key instead of installing the package ENABLED with the key dropped. The docblock's retired no-strict sentence is replaced by decision batch #227 item 3, letter A (ruling record 5856869656) and its reason. Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ Co-authored-by: Claude --- packages/spec/src/api/package-api.test.ts | 96 +++++++++++++++++++---- packages/spec/src/api/package-api.zod.ts | 59 ++++++++++---- 2 files changed, 125 insertions(+), 30 deletions(-) diff --git a/packages/spec/src/api/package-api.test.ts b/packages/spec/src/api/package-api.test.ts index 2b5ee21a81..8b7e5d0cca 100644 --- a/packages/spec/src/api/package-api.test.ts +++ b/packages/spec/src/api/package-api.test.ts @@ -900,7 +900,7 @@ describe('#18058 — install contract bound to the live door', () => { }); }); - describe('the two declared body forms — disjoint, and only ONE of them closed', () => { + describe('the two declared body forms — disjoint, and BOTH closed', () => { it('parses the WRAPPED form the client SDK sends', () => { const body = { manifest: SDK_MANIFEST, settings: undefined, enableOnInstall: true }; expect(PackageInstallBodySchema.safeParse(body).success).toBe(true); @@ -933,23 +933,91 @@ describe('#18058 — install contract bound to the live door', () => { }); /** - * ⭐ The WRAPPED branch is `z.object`, i.e. STRIP mode — it is NOT closed. + * ⭐ The WRAPPED branch is CLOSED — an unknown top-level key is refused by + * name, as the manifest's and the bare form's unknown keys are (decision + * batch #227 item 3, letter A; ruling record `5856869656`). * - * An earlier revision of the docblock claimed both branches were closed. - * They are not, and the asymmetry is the DOOR's behaviour: the handler - * reads `manifest`, `settings`, `enableOnInstall` and `overwrite` and - * ignores every other key, so dropping an unknown one is exactly what it - * does with it. ⛔ Closing this branch with `.strict()` would refuse bodies - * the door answers `201` to — the direction ruling A forbids — so what is - * pinned here is the drop, not a refusal. + * What this pinned before was the opposite: the wrapped branch was strip + * mode, so `{ manifest, enabledOnInstall: false }` parsed green with the + * misspelled key DROPPED, and the door installed the package ENABLED — the + * caller's explicit `false` inverted. Each refusal is read at BOTH doors of + * this declaration: the union the install route is bound to, and the + * wrapped branch alone, whose issue is the one the install door locates + * and surfaces. */ - it('the WRAPPED branch DROPS an unknown key rather than refusing it', () => { - const verdict = PackageInstallBodySchema.safeParse({ manifest: SDK_MANIFEST, bogus: 1 }); - expect(verdict.success).toBe(true); - expect(verdict.data && 'bogus' in verdict.data).toBe(false); + describe('the WRAPPED branch refuses an unknown top-level key by name', () => { + /** The wrapped branch's own verdict: one `unrecognized_keys` issue, at the top level, naming the key. */ + function expectRefusedByName(body: Record, key: string) { + expect(PackageInstallBodySchema.safeParse(body).success, 'the bound union refuses it').toBe(false); + const verdict = PackageInstallRequestSchema.safeParse(body); + expect(verdict.success).toBe(false); + const issues = verdict.error?.issues ?? []; + expect(issues).toHaveLength(1); + expect(issues[0]?.code).toBe('unrecognized_keys'); + expect(issues[0]?.path).toEqual([]); + expect((issues[0] as { keys?: string[] }).keys).toEqual([key]); + expect(issues[0]?.message).toContain(`\`${key}\``); + return issues[0]!.message; + } + + it('a MISSPELLED option — `enabledOnInstall` — is refused, and the declared key is offered', () => { + const message = expectRefusedByName({ manifest: SDK_MANIFEST, enabledOnInstall: false }, 'enabledOnInstall'); + // The remedy's named subject: the option the caller meant. + expect(message).toContain('`enableOnInstall`'); + }); + + it('a PRIVATE key — `_source` — is refused by name, not carried and not dropped', () => { + expectRefusedByName({ manifest: SDK_MANIFEST, _source: 'studio' }, '_source'); + }); + + it('an unknown key BESIDE declared options is refused too — the options do not buy it a pass', () => { + expectRefusedByName( + { manifest: SDK_MANIFEST, enableOnInstall: false, overwrite: true, bogus: 1 }, + 'bogus', + ); + }); + }); + + describe('controls — every declared wrapped body still round-trips', () => { + /** A value for EVERY declared install option, each one the schema accepts. */ + const DECLARED_OPTIONS = { + settings: { apiKey: 'abc123' }, + enableOnInstall: false, + overwrite: true, + platformVersion: '3.2.0', + artifactRef: { + url: 'https://marketplace.objectstack.io/artifacts/com.acme.crm/1.0.0.tgz', + sha256: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', + size: 1024000, + uploadedAt: '2026-02-01T10:00:00Z', + }, + }; + + it('the fixture names every declared option — read off the shape, never listed by hand', () => { + const declared = Object.keys(PackageInstallRequestSchema.shape).filter((k) => k !== 'manifest'); + expect(Object.keys(DECLARED_OPTIONS).sort()).toEqual(declared.sort()); + }); + + it('`manifest` alone parses green at both doors', () => { + expect(PackageInstallRequestSchema.safeParse({ manifest: SDK_MANIFEST }).success).toBe(true); + expect(PackageInstallBodySchema.safeParse({ manifest: SDK_MANIFEST }).success).toBe(true); + }); + + it('`manifest` plus every declared option parses green, each option value carried through', () => { + const body = { manifest: SDK_MANIFEST, ...DECLARED_OPTIONS }; + const request = PackageInstallRequestSchema.safeParse(body); + expect(request.error?.issues ?? []).toEqual([]); + const union = PackageInstallBodySchema.safeParse(body); + expect(union.error?.issues ?? []).toEqual([]); + for (const parsed of [request.data, union.data] as Array | undefined>) { + for (const [key, value] of Object.entries(DECLARED_OPTIONS)) { + expect(parsed?.[key], key).toEqual(value); + } + } + }); }); - it('lit control: the BARE branch IS closed — the same unknown key is refused there', () => { + it('the BARE branch is closed too — the same unknown key is refused there', () => { // `ManifestSchema` is a `strictObject`, so this is a refusal, not a drop. expect(PackageInstallBodySchema.safeParse({ ...SDK_MANIFEST, bogus: 1 }).success).toBe(false); }); diff --git a/packages/spec/src/api/package-api.zod.ts b/packages/spec/src/api/package-api.zod.ts index f1f2073210..1276f9e249 100644 --- a/packages/spec/src/api/package-api.zod.ts +++ b/packages/spec/src/api/package-api.zod.ts @@ -9,6 +9,7 @@ import { PackageArtifactSchema } from '../kernel/package-artifact.zod'; import { ManifestSchema } from '../kernel/manifest.zod'; import { ArtifactReferenceSchema } from '../marketplace/marketplace.zod'; import { retiredKey } from '../shared/retired-key'; +import { strictObject } from '../shared/strict-object'; /** * # Package API Protocol @@ -214,10 +215,26 @@ export type GetInstalledPackageRequest = z.input z.object({ +export const PackageInstallRequestSchema = lazySchema(() => strictObject({ + surface: 'this package install request', + history: + 'Until this shape was closed, an unknown top-level key on the wrapped install body parsed ' + + 'green and was silently dropped — `{ manifest, enabledOnInstall: false }`, a misspelled ' + + '`enableOnInstall`, installed the package ENABLED. Remove the key, or spell it as the ' + + 'declared install option it meant; the declared keys are enumerated by ' + + '`PackageInstallRequestSchema` (@objectstack/spec, api/package-api.zod.ts). An install ' + + 'option on a BARE manifest body is refused by the manifest instead — send the wrapped form.', +}, { /** Package manifest to install — the AUTHORING stage */ manifest: ManifestSchema.describe('Package manifest to install (AUTHORING stage: `objects` are glob patterns)'), @@ -357,7 +374,7 @@ export type PackageInstallRequestParsed = z.infer Date: Sun, 27 Sep 2026 16:07:07 +0000 Subject: [PATCH 2/5] feat(spec): register the wrapped install-request close (ADR-0087 D3) and pin it at both doors Adds the semantic entry package-install-request-unknown-keys-refused and the regenerated migrations/registry.ts region, the minor BREAKING changeset with its registered disposition, and turns the runtime install-body contract's parity pin into a status pin for the wrapped top level. Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ Co-authored-by: Claude --- .../20249-install-request-wrapped-strict.md | 62 +++++++++++++++ .../packages-install-body-contract.test.ts | 76 ++++++++++++++----- packages/spec/src/api/package-api.zod.ts | 9 +-- ...ge-install-request-unknown-keys-refused.ts | 54 +++++++++++++ packages/spec/src/migrations/registry.ts | 50 ++++++++++++ 5 files changed, 227 insertions(+), 24 deletions(-) create mode 100644 .changeset/20249-install-request-wrapped-strict.md create mode 100644 packages/spec/src/migrations/entries/semantic/18.package-install-request-unknown-keys-refused.ts diff --git a/.changeset/20249-install-request-wrapped-strict.md b/.changeset/20249-install-request-wrapped-strict.md new file mode 100644 index 0000000000..7a386b878b --- /dev/null +++ b/.changeset/20249-install-request-wrapped-strict.md @@ -0,0 +1,62 @@ +--- +'@objectstack/spec': minor +--- + +fix(spec): `PackageInstallRequestSchema`'s wrapped branch refuses an unknown top-level key by name (#20249) + +Clause-②: no (narrowing) + +**BREAKING for callers of the install door** — a WRAPPED install body +(`{ manifest, … }`) that carries any top-level key the declaration does not +name is now refused `400` / `VALIDATION_ERROR` at `POST /api/v1/packages`, and +`PackageInstallRequestSchema` / `PackageInstallBodySchema` refuse it at parse. +It used to parse green with the key silently DROPPED, and the door installed +the package and answered `201`. + +This one narrows the declaration itself. The manifest and the bare form (a +manifest as the whole body) already refused an unknown key by name; the wrapped +top level was the one position of the install contract still declared strip +mode. The sharp case is a misspelled option: `{ manifest, enabledOnInstall: +false }` had `enabledOnInstall` dropped, so the package was installed +**ENABLED** — the caller's explicit `false` inverted, with no word said. The +wrapped branch is now a `strictObject`, like the other two positions: one rule +for the whole install contract (decision batch #227 item 3, letter A; ruling +record `5856869656`). No alias and no grace window. + +The install door needs no edit and gets none: since the door started parsing +its whole body through `PackageInstallBodySchema`, it answers exactly what that +declaration says, so the refusal reaches `POST /api/v1/packages` the moment the +declaration moves. The same fact retires the sentence that had forbidden this +close — «the declaration must not refuse a body the door answers `201` to» held +only while the door did not parse its body. + +**What is not affected.** A wrapped body carrying only `manifest` and the +declared install options — `settings`, `enableOnInstall`, `overwrite`, +`platformVersion`, `artifactRef` — parses and installs exactly as before, and so +does a bare manifest. Boot-time and in-process installs reach +`SchemaRegistry.installPackage` / `ObjectQL.registerApp` directly and never pass +through this declaration. + +**Reach, measured first-party.** The SDK's `client.packages.install` sends only +`manifest`, `settings`, `enableOnInstall` and `overwrite`, and the objectui +package dialog sends only `{ manifest }`; neither breaks. +**Out-of-repo callers are NOT MEASURED** — there is no telemetry on them, so a +caller that sends its own private top-level key (a trace id, a source tag) now +gets a `400` naming that key. Check your own callers before upgrading rather +than inheriting this result. + +**Migration — FROM → TO.** The refusal names the key and, for a near-miss, +offers the declared one, so the prescription arrives with the `400`: + +- A misspelled option: FROM `{ "manifest": { … }, "enabledOnInstall": false }` + TO `{ "manifest": { … }, "enableOnInstall": false }` — respell it as the + declared option it meant. +- Any other undeclared top-level key: FROM `{ "manifest": { … }, "_source": + "studio" }` TO `{ "manifest": { … } }` — remove it. There is no place on this + request to carry it. + +It is registered as an ADR-0087 structured TODO rather than a conversion: an +unknown key has no mapping target, and deleting it automatically would repeat +the silent drop this change closes. + + diff --git a/packages/runtime/src/domains/packages-install-body-contract.test.ts b/packages/runtime/src/domains/packages-install-body-contract.test.ts index 7b399089e5..36d4436343 100644 --- a/packages/runtime/src/domains/packages-install-body-contract.test.ts +++ b/packages/runtime/src/domains/packages-install-body-contract.test.ts @@ -27,14 +27,17 @@ * enforced too: `name`, the `namespace` grammar, the closed value sets, the * retired-key tombstones and the nested blocks it closes. §8 pins that. * - * ## What is deliberately NOT refused — the declaration decides + * ## The wrapped TOP LEVEL — refused too, because the declaration closed it * - * An unknown key at the TOP LEVEL of the wrapped form. The wrapped branch is a - * plain `z.object` (strip mode) and its docblock forbids closing it here, so - * `{ manifest, bogus }` parses green with `bogus` dropped. ⛔ No case below - * pins that as intended behaviour in either direction: §5 asserts the door - * AGREES WITH THE DECLARATION on it, which stays green whichever way the - * declaration is later decided. + * An unknown key at the TOP LEVEL of the wrapped form was the one position + * this door still admitted when the whole-body parse landed: the wrapped + * branch was strip mode, so `{ manifest, enabledOnInstall: false }` — a + * misspelled `enableOnInstall` — parsed green with the key dropped and + * installed the package ENABLED. Decision batch #227 item 3, letter A (ruling + * record `5856869656`) closed that branch in `@objectstack/spec`, and the door + * followed with no edit of its own, because it asks the declaration. §5 pins + * the refusal at this door: the status, the named key, and that nothing was + * installed. * * ## Envelope * @@ -170,6 +173,8 @@ describe('§0 the declaration is what the door is held to', () => { { ...m, enableOnInstall: false }, { ...m, overwrite: true }, { ...m, settings: { a: 1 } }, + { manifest: m, enabledOnInstall: false }, + { manifest: m, _source: 'studio' }, ]; for (const body of refused) { expect(PackageInstallBodySchema.safeParse(body).success, JSON.stringify(body)).toBe(false); @@ -315,23 +320,56 @@ describe('§4 row 4 — install options on the BARE form are refused, naming the }); // ═══════════════════════════════════════════════════════════════════════ -// §5 — the door follows the declaration, including where it strips +// §5 — the wrapped TOP LEVEL: refused, because the declaration closed it // ═══════════════════════════════════════════════════════════════════════ -describe('§5 the door answers what the declaration answers', () => { - it('an unknown TOP-LEVEL key on the wrapped form: door and declaration agree, whichever way that is', async () => { - // ⛔ Deliberately a PARITY assertion, not a status pin. The wrapped - // branch strips today, so both accept; if the declaration is ever - // closed, both must refuse — and the door follows with no edit, - // because it asks the declaration. +describe('§5 an unknown TOP-LEVEL key on the wrapped form is refused by name, and nothing installs', () => { + // A STATUS pin, ⛔ not the parity assertion this section used to hold + // («door and declaration agree, whichever way that is»): parity stays green + // when the declaration is re-opened, because both would then accept, so it + // could not tell a closed branch from a strip-mode one. The ruling decided + // the direction (decision batch #227 item 3, letter A), so the direction is + // what is pinned — and each case also reads the declaration's verdict on + // the same body, so a red here names which of the two moved. + + it('a MISSPELLED option — `enabledOnInstall: false` — is refused naming the key, ⛔ never installed ENABLED', async () => { + const { registry, install } = door(); + const m = manifest('toplevel.misspelled'); + const body = { manifest: m, enabledOnInstall: false }; + expect(PackageInstallBodySchema.safeParse(body).success, 'the declaration refuses it').toBe(false); + const r = await install(body); + + expectRefused(r); + // The inversion this ruling is about: before it, this id was installed + // and ENABLED with the caller's `false` dropped. Refused, it is neither. + expect(registry.getPackage(m.id)).toBeUndefined(); + expect(persistedDisabled().has(m.id)).toBe(false); + // Named subjects, ⛔ not prose: the key the caller wrote, and the + // declared option the refusal offers in its place. + expect(messageOf(r)).toContain('`enabledOnInstall`'); + expect(messageOf(r)).toContain('`enableOnInstall`'); + }); + + it('a PRIVATE key — `_source` — is refused naming the key, and nothing installs', async () => { const { registry, install } = door(); - const m = manifest('toplevel.unknown'); - const body = { manifest: m, bogus: 1 }; - const declared = PackageInstallBodySchema.safeParse(body).success; + const m = manifest('toplevel.private'); + const body = { manifest: m, _source: 'studio' }; + expect(PackageInstallBodySchema.safeParse(body).success, 'the declaration refuses it').toBe(false); const r = await install(body); - expect(r.response?.status).toBe(declared ? 201 : 400); - expect(registry.getPackage(m.id) !== undefined).toBe(declared); + expectRefused(r); + expect(registry.getPackage(m.id)).toBeUndefined(); + expect(messageOf(r)).toContain('`_source`'); + }); + + it('control — the same body with the option spelled as declared installs, DISABLED as asked', async () => { + const { registry, install } = door(); + const m = manifest('toplevel.control'); + const r = await install({ manifest: m, enableOnInstall: false }); + + expect(r.response?.status).toBe(201); + expect(registry.getPackage(m.id)?.enabled).toBe(false); + expect(persistedDisabled().has(m.id)).toBe(true); }); }); diff --git a/packages/spec/src/api/package-api.zod.ts b/packages/spec/src/api/package-api.zod.ts index 1276f9e249..c4f6b506b4 100644 --- a/packages/spec/src/api/package-api.zod.ts +++ b/packages/spec/src/api/package-api.zod.ts @@ -229,11 +229,10 @@ export const PackageInstallRequestSchema = lazySchema(() => strictObject({ surface: 'this package install request', history: 'Until this shape was closed, an unknown top-level key on the wrapped install body parsed ' - + 'green and was silently dropped — `{ manifest, enabledOnInstall: false }`, a misspelled ' - + '`enableOnInstall`, installed the package ENABLED. Remove the key, or spell it as the ' - + 'declared install option it meant; the declared keys are enumerated by ' - + '`PackageInstallRequestSchema` (@objectstack/spec, api/package-api.zod.ts). An install ' - + 'option on a BARE manifest body is refused by the manifest instead — send the wrapped form.', + + 'green and was silently dropped, so a misspelled install option was ignored without a ' + + 'word — a caller who asked for the package DISABLED got it installed ENABLED. Remove the ' + + 'key, or spell it as the declared install option it meant; the declared keys are ' + + 'enumerated by `PackageInstallRequestSchema` (@objectstack/spec, api/package-api.zod.ts).', }, { /** Package manifest to install — the AUTHORING stage */ manifest: ManifestSchema.describe('Package manifest to install (AUTHORING stage: `objects` are glob patterns)'), diff --git a/packages/spec/src/migrations/entries/semantic/18.package-install-request-unknown-keys-refused.ts b/packages/spec/src/migrations/entries/semantic/18.package-install-request-unknown-keys-refused.ts new file mode 100644 index 0000000000..294ffdc706 --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.package-install-request-unknown-keys-refused.ts @@ -0,0 +1,54 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +// Registered as a structured TODO (ADR-0087 D3) rather than a conversion (D2), +// for the reason every strictness entry gives: an arbitrary unknown key has no +// mapping target, and deleting it automatically is the silent data loss +// ADR-0078 bans. Nothing is retired either — no key leaves the declaration — +// so there is no RETIRED_KEYS entry: the refusal is the shape's own strict +// close naming the key. HTTP-only, like the list-pagination entry beside it: +// nobody authors a PackageInstallRequest into a metadata source and nothing +// persists one, so `objectstack migrate meta` has no file to rewrite and the +// TODO lands with whoever builds the request body. +export const entry: SemanticMigration = { + id: 'package-install-request-unknown-keys-refused', + // No backticks in `surface` — build-upgrade-guide.ts renders it inside a + // code span AND a table cell. + surface: + 'api.installPackage request body, WRAPPED form — an undeclared TOP-LEVEL key beside ' + + 'manifest on POST /api/v1/packages (PackageInstallRequestSchema, the wrapped branch of ' + + 'PackageInstallBodySchema)', + replacement: + 'the declared wrapped body: `manifest`, plus any of the declared install options ' + + '(`settings`, `enableOnInstall`, `overwrite`, `platformVersion`, `artifactRef`). A ' + + 'misspelled option is respelled as the option it meant — `enabledOnInstall` → ' + + '`enableOnInstall`, which the refusal itself offers — and any other undeclared key is ' + + 'removed. The bare form (a manifest as the whole body) is unchanged: it was already ' + + 'closed, and it still carries no install options.', + reason: + 'One rule for the whole install contract (decision batch #227 item 3, letter A; ruling ' + + 'record 5856869656). The manifest and the bare form already refused an unknown key by ' + + 'name; the wrapped top level was the one position still declared strip mode, so ' + + '`{ manifest, enabledOnInstall: false }` — a misspelled `enableOnInstall` — parsed green ' + + 'with the key DROPPED, and the install door, which answers exactly what this declaration ' + + 'says since it parses the whole body (c02fa1276), installed the package ENABLED: the ' + + 'caller\'s explicit `false` inverted, with no word said. The sentence that had forbidden ' + + 'this close rested on «the declaration must not refuse a body the door answers 201 to», ' + + 'which held only while the door did not parse its body; with the door answering per ' + + 'declaration the premise became circular and constrains nothing. No alias and no grace ' + + 'window. Not losslessly convertible: an unknown key has no mapping target, and ' + + 'auto-deleting it would repeat the silent drop this closes, so each occurrence needs the ' + + 'caller\'s decision — respell or remove. First-party reach measured before the close: ' + + 'the SDK install call sends only `manifest`, `settings`, `enableOnInstall` and ' + + '`overwrite`, and the objectui package dialog sends only `{ manifest }`, so no in-repo ' + + 'caller breaks. Out-of-repo callers are NOT MEASURED — a caller that sends a private ' + + 'top-level key now gets a 400 naming it.', + acceptanceCriteria: + 'Every wrapped install body carries only `manifest` and declared install options. A body ' + + 'with any other top-level key is refused `400` / `VALIDATION_ERROR` at POST ' + + '/api/v1/packages with the key named and nothing installed, and PackageInstallRequestSchema ' + + 'answers the same body with one `unrecognized_keys` issue at the top level naming the ' + + 'key. `{ manifest }` alone, and `manifest` with every declared option, parse and install ' + + 'exactly as before.', +}; diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index ec1c9a9b92..a1edf644b9 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -11528,6 +11528,56 @@ const step18: MigrationStep = { + 'or built a route from the entries, so every request answers exactly as before — the removal ' + 'retracts a false claim, not a capability.', }, + // Registered as a structured TODO (ADR-0087 D3) rather than a conversion (D2), + // for the reason every strictness entry gives: an arbitrary unknown key has no + // mapping target, and deleting it automatically is the silent data loss + // ADR-0078 bans. Nothing is retired either — no key leaves the declaration — + // so there is no RETIRED_KEYS entry: the refusal is the shape's own strict + // close naming the key. HTTP-only, like the list-pagination entry beside it: + // nobody authors a PackageInstallRequest into a metadata source and nothing + // persists one, so `objectstack migrate meta` has no file to rewrite and the + // TODO lands with whoever builds the request body. + { + id: 'package-install-request-unknown-keys-refused', + // No backticks in `surface` — build-upgrade-guide.ts renders it inside a + // code span AND a table cell. + surface: + 'api.installPackage request body, WRAPPED form — an undeclared TOP-LEVEL key beside ' + + 'manifest on POST /api/v1/packages (PackageInstallRequestSchema, the wrapped branch of ' + + 'PackageInstallBodySchema)', + replacement: + 'the declared wrapped body: `manifest`, plus any of the declared install options ' + + '(`settings`, `enableOnInstall`, `overwrite`, `platformVersion`, `artifactRef`). A ' + + 'misspelled option is respelled as the option it meant — `enabledOnInstall` → ' + + '`enableOnInstall`, which the refusal itself offers — and any other undeclared key is ' + + 'removed. The bare form (a manifest as the whole body) is unchanged: it was already ' + + 'closed, and it still carries no install options.', + reason: + 'One rule for the whole install contract (decision batch #227 item 3, letter A; ruling ' + + 'record 5856869656). The manifest and the bare form already refused an unknown key by ' + + 'name; the wrapped top level was the one position still declared strip mode, so ' + + '`{ manifest, enabledOnInstall: false }` — a misspelled `enableOnInstall` — parsed green ' + + 'with the key DROPPED, and the install door, which answers exactly what this declaration ' + + 'says since it parses the whole body (c02fa1276), installed the package ENABLED: the ' + + 'caller\'s explicit `false` inverted, with no word said. The sentence that had forbidden ' + + 'this close rested on «the declaration must not refuse a body the door answers 201 to», ' + + 'which held only while the door did not parse its body; with the door answering per ' + + 'declaration the premise became circular and constrains nothing. No alias and no grace ' + + 'window. Not losslessly convertible: an unknown key has no mapping target, and ' + + 'auto-deleting it would repeat the silent drop this closes, so each occurrence needs the ' + + 'caller\'s decision — respell or remove. First-party reach measured before the close: ' + + 'the SDK install call sends only `manifest`, `settings`, `enableOnInstall` and ' + + '`overwrite`, and the objectui package dialog sends only `{ manifest }`, so no in-repo ' + + 'caller breaks. Out-of-repo callers are NOT MEASURED — a caller that sends a private ' + + 'top-level key now gets a 400 naming it.', + acceptanceCriteria: + 'Every wrapped install body carries only `manifest` and declared install options. A body ' + + 'with any other top-level key is refused `400` / `VALIDATION_ERROR` at POST ' + + '/api/v1/packages with the key named and nothing installed, and PackageInstallRequestSchema ' + + 'answers the same body with one `unrecognized_keys` issue at the top level naming the ' + + 'key. `{ manifest }` alone, and `manifest` with every declared option, parse and install ' + + 'exactly as before.', + }, // The carrier nobody had named: a bare `z.string()` on a published schema, // constraining nothing while its own siblings enforced a grammar. Its narrowing // is the widest of the four by accept-set area and the least likely to be felt, From 9cb95eb3c54df9a5d78723d13264eb846f39f76a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 16:11:42 +0000 Subject: [PATCH 3/5] test(spec): the declared-options round-trip fixture spells the artifact reference's defaulted format Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ Co-authored-by: Claude --- packages/spec/src/api/package-api.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/spec/src/api/package-api.test.ts b/packages/spec/src/api/package-api.test.ts index 8b7e5d0cca..732c2332d1 100644 --- a/packages/spec/src/api/package-api.test.ts +++ b/packages/spec/src/api/package-api.test.ts @@ -989,6 +989,9 @@ describe('#18058 — install contract bound to the live door', () => { url: 'https://marketplace.objectstack.io/artifacts/com.acme.crm/1.0.0.tgz', sha256: 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2', size: 1024000, + // Spelled out: the artifact reference DEFAULTS it, and a round-trip + // fixture must be one the parse hands back unchanged. + format: 'tgz', uploadedAt: '2026-02-01T10:00:00Z', }, }; From 469f5e3a72996126667a32a3e86be85460667d04 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 16:56:39 +0000 Subject: [PATCH 4/5] test(runtime): the wrapped top-level refusal pins read the door's answer before the declaration's Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ Co-authored-by: Claude --- .../src/domains/packages-install-body-contract.test.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/runtime/src/domains/packages-install-body-contract.test.ts b/packages/runtime/src/domains/packages-install-body-contract.test.ts index 36d4436343..697ffe1664 100644 --- a/packages/runtime/src/domains/packages-install-body-contract.test.ts +++ b/packages/runtime/src/domains/packages-install-body-contract.test.ts @@ -329,14 +329,14 @@ describe('§5 an unknown TOP-LEVEL key on the wrapped form is refused by name, a // when the declaration is re-opened, because both would then accept, so it // could not tell a closed branch from a strip-mode one. The ruling decided // the direction (decision batch #227 item 3, letter A), so the direction is - // what is pinned — and each case also reads the declaration's verdict on - // the same body, so a red here names which of the two moved. + // what is pinned. Each refusal case reads the declaration's verdict on the + // same body too, AFTER the door's answer, so a regression reddens the door + // assertion first and the trailing read names whether the declaration moved. it('a MISSPELLED option — `enabledOnInstall: false` — is refused naming the key, ⛔ never installed ENABLED', async () => { const { registry, install } = door(); const m = manifest('toplevel.misspelled'); const body = { manifest: m, enabledOnInstall: false }; - expect(PackageInstallBodySchema.safeParse(body).success, 'the declaration refuses it').toBe(false); const r = await install(body); expectRefused(r); @@ -348,18 +348,20 @@ describe('§5 an unknown TOP-LEVEL key on the wrapped form is refused by name, a // declared option the refusal offers in its place. expect(messageOf(r)).toContain('`enabledOnInstall`'); expect(messageOf(r)).toContain('`enableOnInstall`'); + // Read LAST, so the door's own answer is what a regression turns red first. + expect(PackageInstallBodySchema.safeParse(body).success, 'the declaration refuses it').toBe(false); }); it('a PRIVATE key — `_source` — is refused naming the key, and nothing installs', async () => { const { registry, install } = door(); const m = manifest('toplevel.private'); const body = { manifest: m, _source: 'studio' }; - expect(PackageInstallBodySchema.safeParse(body).success, 'the declaration refuses it').toBe(false); const r = await install(body); expectRefused(r); expect(registry.getPackage(m.id)).toBeUndefined(); expect(messageOf(r)).toContain('`_source`'); + expect(PackageInstallBodySchema.safeParse(body).success, 'the declaration refuses it').toBe(false); }); it('control — the same body with the option spelled as declared installs, DISABLED as asked', async () => { From 0b352e98aa1b9f641219c60296aea4b0b8ea3612 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 19:08:44 +0000 Subject: [PATCH 5/5] docs(runtime,spec): the install door's comment states the wrapped top level is closed, and item 2 cites its ruling record The install branch comment in packages/runtime/src/domains/packages.ts said PackageInstallRequestSchema was strip mode and forbidden to close; since the wrapped branch became a strictObject that is false. It now states the close, the door's 400 VALIDATION_ERROR with nothing installed, and cites ruling record 5856869656. The residual item-2 clause in package-api.zod.ts gains the same record id inline. Comments only; no code token moves. Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ Co-authored-by: Claude --- packages/runtime/src/domains/packages.ts | 19 +++++++++++-------- packages/spec/src/api/package-api.zod.ts | 3 ++- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/packages/runtime/src/domains/packages.ts b/packages/runtime/src/domains/packages.ts index 531a5f6c40..17d9ae0411 100644 --- a/packages/runtime/src/domains/packages.ts +++ b/packages/runtime/src/domains/packages.ts @@ -1000,14 +1000,17 @@ export async function handlePackagesRequest(deps: DomainHandlerDeps, path: strin // declaration's own docblock forbids it («a caller that needs an // option sends the wrapped form»). // - // ⚠️ What the union does NOT refuse, deliberately: an unknown key at - // the TOP LEVEL of the wrapped form. `PackageInstallRequestSchema` - // is a plain `z.object` (strip mode) and its docblock forbids - // closing it, so `{ manifest, bogus }` parses green with `bogus` - // dropped — and this door, reading only parsed keys, now does - // exactly what the declaration says. Refusing it here without the - // declaration would make the published contract admit a body the - // door refuses; that is the declaration's decision to make. + // ⭐ The TOP LEVEL of the wrapped form is closed too — by the + // declaration, not by this door. `PackageInstallRequestSchema` is a + // `strictObject` (ruling record `5856869656`, decision batch #227 + // item 3, letter A), so an unknown top-level key such as + // `{ manifest, enabledOnInstall: false }` fails the union by name, + // and this door answers `400` / `VALIDATION_ERROR` and installs + // nothing — with no line of its own for it: reading only parsed + // keys, it does exactly what the declaration says. ⛔ Never refuse or + // admit a key here that the declaration decides the other way: the + // published contract and this door would then answer differently, + // and which keys the body may carry is the declaration's decision. // // The verdict is answered after the `id` and `version` legs below, // which keep their own published sentences; every later read (the diff --git a/packages/spec/src/api/package-api.zod.ts b/packages/spec/src/api/package-api.zod.ts index c4f6b506b4..d86a6f85c6 100644 --- a/packages/spec/src/api/package-api.zod.ts +++ b/packages/spec/src/api/package-api.zod.ts @@ -423,7 +423,8 @@ export type PackageInstallRequestParsed = z.infer