Skip to content

spec: one authority for enableOnInstall, and a read-out of its other two declarations - #19130

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-18605-enable-on-install-one-authority
Sep 20, 2026
Merged

os-sam merged 3 commits into
mainfrom
claude/issue-18605-enable-on-install-one-authority

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes #18605

Clause-②: yes — carrier: the changeset .changeset/18605-enable-on-install-one-authority.md (@objectstack/spec, minor). Three published declarations' stated meaning moves; the accept set does not move at all.

Ruling bullet 1 was already discharged by PR #18752 — this PR did not skip it

Batch #153 item 5, letter 1 carries two bullets. The first one — the install door writes the registry row's enabled from enableOnInstall ?? true — landed with PR #18752 (card #18058), and this claimant re-derived that against the merged diff before planning, rather than inheriting the card's text:

  • packages/runtime/src/domains/packages.ts reads body.enableOnInstall === false off the WRAPPED body, flips the registry row through the same call PATCH /packages/:id/disable uses, and then persists the row the door returned to the durable state file.
  • packages/runtime/src/domains/packages-install-enable-on-install.test.ts pins it, header and all: false installs disabled in all three records, false also moves status, true installs enabled, an absent key defaults to true, the disable is durable across a restart, and a re-install with true clears it.
  • Both still exist on origin/main as of this branch's merge (asserted by quoted-exact-name git grep against origin/main).

So the card's own premise — "honoured by no handler" — is false for the authority's door on today's main, and this PR carries bullet 2 and only bullet 2.

Bullet 2, verbatim

One authority: the request contract in package-api.zod.ts. The claimant re-reads the two other declarations — a copy of the request key is folded to a reference; a declaration that means something else (a stored-row field, a marketplace listing attribute) stays and says so. ⛔ No silent unification of three published declarations.

The read-out, per declaration

All three read enableOnInstall: z.boolean().default(true) with byte-identical description text, so identical shape carried no information. What distinguishes them is the request each sits on and the door that serves it.

declaration what it is disposition
api/PackageInstallRequestSchema the HTTP wire contract of POST /api/v1/packages, the door that honours the key the one authority
kernel/InstallPackageRequestSchema the request type of the in-process protocol primitive ObjectStackProtocol.installPackage a COPY of the request key — referenced
marketplace/MarketplaceInstallRequestSchema the marketplace channel's install-from-listing request, served by the control plane means something else — stays, and says so

⚠️ Both of the ruling's parenthetical guesses were falsified by the re-read, and that is recorded rather than quietly worked around. The kernel declaration is not the stored-row field: the stored-row field is InstalledPackage.enabled, a different key in the same file. The marketplace declaration is not a listing attribute: MarketplaceListingSchema does not carry it; it sits on the install request beside listingId.

The authority — api/PackageInstallRequestSchema.enableOnInstall

It is the authority because it is the request contract of the door that honours the key. Its published description now says so, so a reader of the reference page can tell which of three rows is the one that acts: "honoured at POST /api/v1/packages: the installed row's enabled is written from this key". Its doc block carries the map to the other two, so nobody has to re-derive this reading a third time.

Re-read ① — kernel/InstallPackageRequestSchema.enableOnInstall is a COPY

Same type, same default, same meaning, restated one layer down on the in-process protocol primitive. Two measured facts decide it:

  • MetadataProtocol.installPackage reads request.manifest and request.settings and nothing else (packages/metadata-protocol/src/protocol.ts). The key reaches no code that acts on it there.
  • The authority's own door does not forward it down that seam: it calls installPackage({ manifest, settings }) and performs the enable/disable flip itself afterwards, because the durable half must follow the ROW that door returned rather than the request's intent. That is deliberate and documented at the call site.

It is therefore a copy, and per the ruling it must not be left unreferenced. The reference is documentary in the declaration and MECHANICAL in a pin, for a reason that was measured rather than assumed — see the next section. packages/spec/src/api/package-install-one-authority.test.ts parses the authority and the copy over one matrix (absent, false, true, a string, null) and reds on any cell where they disagree, so the copy can no longer drift from the authority silently.

Re-read ② — marketplace/MarketplaceInstallRequestSchema.enableOnInstall means something else

Same words, a different commitment, and the difference is the subject of the request it sits on:

  • Its subject is a marketplace LISTING (listingId, version, licenseKey, tenantId). The authority's subject is a MANIFEST. Neither body can be sent where the other is expected, which the pin asserts in both directions.
  • Its door is the control plane's POST /api/v1/marketplace/install; a runtime mounts /api/v1/marketplace/* only as a read-only proxy to the configured control plane (MarketplaceProxyPlugin). docs/design/marketplace-publishing.md §4.3 spells the flow out: the channel fetches the artefact and validates the licence and only THEN maps what it holds into a platform install. So this key is what a caller asks the marketplace to request on its behalf — one translation upstream of the door key.
  • It is a different party's contract on a different release cadence: the declaration was cloud/MarketplaceInstallRequest before it moved into this namespace (packages/spec/scripts/lib/renamed-defs.ts). One shared declaration would let a narrowing at the platform door silently narrow a control-plane contract that no PR in this repo can see.

So it stays, and its published description now says which of the two it is.

The prescription that is not executable as written, and the measurement

"A copy of the request key is folded to a reference" reads naturally as enableOnInstall: PackageInstallRequestSchema.shape.enableOnInstall at the copy's site. That spelling is not available in this direction, and it is not a style preference — it is measured.

The authority sits ABOVE both copies in the module graph: PackageInstallRequestSchema is built from ManifestSchema and InstalledPackageSchema (declared in kernel/package-registry.zod.ts) and from ArtifactReferenceSchema (declared in marketplace/marketplace.zod.ts). A reference from either copy up to the authority is therefore an import cycle, and it is not a cycle the lazySchema proxy absorbs: under OS_EAGER_SCHEMAS=1 — the mode gen:schema, gen:authorable-surface-base and check:authorable-surface run in — the factory bodies evaluate at module load and the cycle dies.

Measured on this branch, both directions, each against a control that passes on the unmodified tree:

leg command result
control · kernel eager load of kernel/package-registry.zod, unmodified exit 0, enableOnInstall defaults to true
treatment · kernel the same load with enableOnInstall: PackageInstallRequestSchema.shape.enableOnInstall exit 1, ReferenceError: Cannot access 'InstalledPackageSchema' before initialization, raised from api/package-api.zod.ts through lazySchema
control · marketplace eager load of marketplace/marketplace.zod, unmodified exit 0, enableOnInstall defaults to true
treatment · marketplace the same load with the same structural reference exit 1, ReferenceError: Cannot access 'ArtifactReferenceSchema' before initialization

Under the default lazy mode both treatments load fine, which is the dangerous half: the runtime would be green and the generator would die.

Both treatments were reverted and the revert proven by blob hash against HEAD (git hash-object equal, git diff HEAD empty) before anything else was written.

⇒ The only structural fold available would be to move the key's literal into a module BELOW both copies and have the authority import it. That was deliberately not taken unilaterally: it moves the declaration out of package-api.zod.ts, which is the file the ruling names as the one authority, so it changes the ruling's own terms. It is recorded as an open question below rather than performed.

What moved on the published surface

  • Three .describe() strings — the text content/docs/references/** renders, and the only half of a doc block an author reading the reference page ever sees.
  • Three doc blocks in the source.
  • The four generated reference pages that follow from those strings (five table rows; the authority appears twice because PackageInstallBody renders its wrapped branch).
  • One new test file.

What did not move: no key added, removed, renamed or retyped, no default changed. check:api-surface, check:api-surface-declarations, check:authorable-surface, check:export-origins and check:declaration-map are all green with no regeneration — the api-surface-declarations shards this card was flagged for do not move, because a .describe() change does not change a .d.ts type.

Verification

Run on the merged tree (git merge origin/main through scripts/pm/os-regen-merge.sh), exit codes captured before any pipe.

what result
pnpm --filter @objectstack/spec test 496 files / 14536 tests passed
pnpm --filter @objectstack/spec typecheck exit 0
pnpm --filter @objectstack/spec check:generated all 16 generated artifacts up to date
eslint . --no-inline-config --format json 6879 files reached by eslint's own config, 0 errors, 0 warnings — the union, not a narrowing
gate families derived by scripts/pm/dispatch-gates.mjs and run see below

Gate families run locally, all exit 0: check:nul-bytes, check-spec-docblock-symbol-anchors, check:duration-unit-keys, check:cross-package-test-inputs, check:test-source-alias, check-adr-0087-registration --base origin/main, check-changeset-no-major --base origin/main, check-empty-changeset --base origin/main, check:changeset-gate-self-tests, check:pm-widening-tells, check:exported-any, check:dual-source-exports, check:entry-nameability, check:variant-docs, check:empty-state, check:llms-txt, check:browser-reachable-entries, check:skill-examples, check-doc-frontmatter, check-docs-section-name, check-doc-route-spelling --advisory, docs-audit/check-affected-docs, check:doc-anchors, check:docs-single-h1.

check:skill-examples first exited 1 on a build prerequisite (@objectstack/client-react had no .d.ts), not on this diff; after pnpm --filter '@objectstack/client-react^...' build it exits 0 over 258 prose examples. The remaining families the derivation names are CI's farm and are not claimed here.

Reverse verification of the new pin — the fix was committed first, the mutation landed through scripts/ablation-replace.mjs (anchor hit x1, blob 64a17a8bc364 to f2cacf8c304b), and the restore was proven against HEAD rather than against an exit code:

  • kernel's enableOnInstall default mutated true to false
  • predicted direction: the parity cells go red, the rest stay green
  • observed: Tests 2 failed | 10 passed — the two that fail are the absent-key parity cell and the same-default assertion
  • restored: blob equals HEAD (64a17a8bc364), git diff HEAD empty

Acceptance notes

Observed while reading, deliberately not fixed here and not filed:

  • packages/spec/src/contracts/package-service.ts declares a FOURTH enableOnInstall, on the plain TS interface InstallPackageInput for IPackageService. It is outside the ruling's three schemas (not Zod, not on the authorable surface), and IPackageService has no implementation in this repo — the only place the key is read is an inline fake inside package-service.test.ts (enabled: input.enableOnInstall !== false). Noted, not filed: the interface is a contract with no consumer here, so nobody is currently misled by it. Carrier if it ever needs one: whoever implements IPackageService.
  • content/docs/api/metadata-api.mdx documents the install body inline rather than from the contract, so it will not follow a future change to it. Noted, not filed: a hand-written page drifting from a schema is not one of the three filing classes, and no PR or person is presently heading for that file. Carrier: none.

Open question recorded for the seat, not answered here

The structural fold is available in exactly one shape: move the key's single literal into a module below both copies and have the request contract import it. That would give literally one Zod declaration of the key instead of a pin holding two in step — but it takes the declaration out of package-api.zod.ts, which the ruling names as the one authority. Whether the ruling prefers one literal in a lower module or the authority's file keeping its own literal with a mechanical pin is a question about the ruling's terms, so it is recorded rather than decided by the claimant. The current shape is the one that changes nothing the ruling said.

A second, separate question the re-read surfaced: the copy's own door (ObjectStackProtocol.installPackage) still does not honour the key. Making it honour the key would be new runtime behaviour at a door the ruling did not name — it is safe (every present caller omits the key, so nothing changes today), but it is not this card's to authorise.


Generated by Claude Code

…ne authority

`enableOnInstall` is declared in three published schemas. The install door's
request contract (`api/package-api.zod.ts`) is the one authority: it is the
contract of the door that honours the key. The other two are re-read here.

- `kernel/InstallPackageRequestSchema` is a COPY of the request key, restated
  on the in-process protocol primitive. It is held to the authority by a
  parity pin rather than by a structural reference: the authority sits above
  `kernel/` in the module graph, so `…Schema.shape.enableOnInstall` spelled
  there is an import cycle that dies under `OS_EAGER_SCHEMAS=1`.
- `marketplace/MarketplaceInstallRequestSchema` means something else and
  stays: its subject is a marketplace listing, its door is the control
  plane's, and its key is one translation upstream of the door key.

Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 9 documentable anchor(s).

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via packages.disable (sdk, the route ledger binds it to PATCH /packages/:id/disable))
  • content/docs/api/environment-routing.mdx (via /api/v1/packages (route, a path literal in a comment in InstallPackageRequestSchema; a path literal in a comment in PackageInstallRequestSchema))
  • content/docs/api/metadata-api.mdx (via /packages/:id/disable (route, a path literal in a comment in PackageInstallRequestSchema))
  • content/docs/getting-started/examples.mdx (via /api/v1/packages (route, a path literal in a comment in InstallPackageRequestSchema; a path literal in a comment in PackageInstallRequestSchema))
  • content/docs/kernel/contracts/metadata-service.mdx (via /api/v1/packages (route, a path literal in a comment in InstallPackageRequestSchema; a path literal in a comment in PackageInstallRequestSchema))
  • content/docs/kernel/services-checklist.mdx (via /api/v1/packages (route, a path literal in a comment in InstallPackageRequestSchema; a path literal in a comment in PackageInstallRequestSchema))
  • content/docs/permissions/permission-sets.mdx (via /api/v1/packages (route, a path literal in a comment in InstallPackageRequestSchema; a path literal in a comment in PackageInstallRequestSchema))
  • content/docs/protocol/kernel/error-handling.mdx (via /api/v1/packages (route, a path literal in a comment in InstallPackageRequestSchema; a path literal in a comment in PackageInstallRequestSchema))
  • content/docs/protocol/kernel/http-protocol.mdx (via /api/v1/packages (route, a path literal in a comment in InstallPackageRequestSchema; a path literal in a comment in PackageInstallRequestSchema))
  • content/docs/ui/apps.mdx (via /api/v1/packages (route, a path literal in a comment in InstallPackageRequestSchema; a path literal in a comment in PackageInstallRequestSchema))

2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via /api/v1/packages (route, a path literal in a comment in InstallPackageRequestSchema; a path literal in a comment in PackageInstallRequestSchema))
  • content/docs/releases/v17/17-4.mdx (via /api/v1/packages (route, a path literal in a comment in InstallPackageRequestSchema; a path literal in a comment in PackageInstallRequestSchema))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 9bb059dbfe1fdf6443cb9f76266d2a0ebff71781packageMentionDocs.

Which tree this was computed on

This run read content/docs from 20c6f3722cd63d92db0efd93c42c0b3937a7e2f6 — the merge of head 5234daa021ad86dfd5502406ad1926adb9c82ed0 into base 9bb059dbfe1fdf6443cb9f76266d2a0ebff71781, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 20c6f3722cd63d92db0efd93c42c0b3937a7e2f6 && git checkout 20c6f3722cd63d92db0efd93c42c0b3937a7e2f6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9bb059dbfe1fdf6443cb9f76266d2a0ebff71781 5234daa021ad86dfd5502406ad1926adb9c82ed0 && git checkout -B drift-repro 9bb059dbfe1fdf6443cb9f76266d2a0ebff71781 && git merge --no-ff 5234daa021ad86dfd5502406ad1926adb9c82ed0

node scripts/docs-audit/affected-docs.mjs --json 9bb059dbfe1fdf6443cb9f76266d2a0ebff71781

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 9bb059dbfe1fdf6443cb9f76266d2a0ebff71781 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 18, 2026
Merged via the queue into main with commit 596090e Sep 20, 2026
44 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] enableOnInstall is declared in three schemas and honoured by no handler — an author sets it and the runtime silently ignores it

2 participants