Skip to content

fix(metadata-protocol): the in-process install door honours enableOnInstall - #19338

Queued
huangyiirene wants to merge 3 commits into
mainfrom
claude/issue-19277-in-process-install-honours-flag
Queued

huangyiirene wants to merge 3 commits into
mainfrom
claude/issue-19277-in-process-install-honours-flag

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes #19277

Clause-②: no

InstallPackageRequestSchema.enableOnInstall (packages/spec/src/kernel/package-registry.zod.ts) is the request contract of the in-process ObjectStackProtocol.installPackage / MetadataProtocol.installPackage primitive. The implementation read request.manifest and request.settings and nothing else, so a caller that asked for enableOnInstall: false got an ENABLED install — no refusal, no warning, no effect. Declared but not enforced on a published option, which ADR-0049 (enforce-or-remove) and Prime Directive #10 refuse outright.

Ruling batch #153 item 5 letter 1 (#18605, record 5724940709) kept this declaration as a COPY of the HTTP request key with the SAME meaning, so the disposition is enforce, not retire.

What changed

MetadataProtocol.installPackage now applies the same rule the HTTP door applies, through the same registry verbs PATCH /packages/:id/enable and PATCH /packages/:id/disable use:

enableOnInstall effect
true enablePackage — clears a disable, including a boot-seeded one
false disablePackage — the row and its status both move
absent no lifecycle call at all; the row the registry returned stands

=== true / === false, never a truthiness test and never a ?? default — the three states are the contract. A non-boolean value is read as absent rather than coerced.

⚠️ The card's mechanism sentence was stale; the matrix was taken from the tree

The card (written 2026-09-20T09:06Z) asks for 「the registry row's enabled (and status) follow enableOnInstall ?? true on install and on re-install」. PR #19291 (4fef271b7, 2026-09-20T11:10Z) re-ruled exactly those cells under maintainer ruling batch #157 item 5 letter C (「缺省 = 保持,有旗 = 设置」), which is younger than this card's own ruling. ?? true on re-install is precisely what the HTTP door stopped doing.

The direction 「honour it the way the HTTP door does」 is self-updating and still governs, so the matrix below was read off packages/runtime/src/domains/packages-install-enable-on-install.test.ts on origin/main, not off the card's prose. The four cells checked, and they match the dispatch's table exactly:

line case on the tree
:154 ABSENT flag, FRESH install enabled
:233 [#18877 re-ruled] re-install, flag ABSENT PRESERVES the disable
:262 re-install, enableOnInstall: true clears the durable disable
:278 [#18877 re-ruled] BARE re-install PRESERVES it too

⛔ One HTTP-door cell has no analogue at this seam: the BARE body form (a manifest posted as the whole body) does not exist in-process — InstallPackageRequest always carries manifest as a field. What is pinned instead is the third state's boundary: a non-boolean value is read as ABSENT.

⛔ What this seam does NOT write

The runtime's durable disabled-package file is keyed by environment (setPackageDisabled(environmentId, id, disabled), packages/runtime/src/package-state-store.ts), and an InstallPackageRequest carries no environment — so that key cannot even be formed here. The module also lives in @objectstack/runtime, which depends on @objectstack/metadata-protocol and not the other way round. The HTTP door owns that half and writes it from the row it returned.

So enableOnInstall through the in-process primitive moves the registry row — what every in-process reader serves from — for the life of the process. This is exactly the scope the card's acceptance names (「registry row + status」). It is stated in the code, in the changeset and here rather than left to be rediscovered; see acceptance notes for the follow-up it earns.

No behaviour change for any caller on the tree

The card's own measurement, re-verified rather than inherited. Radius: packages/**, examples/**, apps/** in this repo, at 2c8e2667c.

  • packages/runtime/src/domains/packages.ts:769protocolSvc.installPackage({ manifest, settings: body.settings }). The key is deliberately not forwarded; the door performs the flip itself.
  • packages/metadata-protocol/src/protocol.ts (duplicatePackage) — this.installPackage({ manifest: dupManifest }). Flag absent.

Those are the only two call sites. ⇒ confirmed: no existing caller sets the key, so this is observable only to a caller that sets it — one that until now got silence.

Verification

Gates — ⛔ not a list taken on trust: derived from the actual changed files with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, each exit code landed to a file before any pipe, then reconciled:

✓ dispatch-gates --ran: 61 derived famil(ies) accounted for — 61 run,
  0 NOT-MEASURED (a DERIVED zero — all 61 recorded an exit code and none of them is 3).

All 61 exit 0, measured at 2c8e2667c. Three of them (check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt) first answered exit 3 = PREREQUISITE NOT MET; that was cleared with a full workspace build and they were re-run, ⛔ never read as a pass.

Tests

run result
pnpm --filter @objectstack/metadata-protocol test 2596 passed, 19 skipped (185 files)
pnpm --filter @objectstack/objectql test 5037 passed (303 files)
pnpm --filter @objectstack/{metadata-protocol,objectql} typecheck pass
pnpm --filter @objectstack/runtime exec vitest run src/domains/packages 243 passed (16 files) — the HTTP door is unmoved
pnpm lint (repo-wide eslint . --no-inline-config) pass

Ablation — the new pin is proven able to fail. packages/objectql resolves @objectstack/metadata-protocol through its exports, i.e. dist/, with no vitest alias (it is a KNOWN_UNALIASED_TEST_IMPORTS entry), so the mutation was rebuilt and proven present in the artifact before the run's colour was read:

mutate   ablation-replace: anchor x1 -> x0, blob e7a78741e047 -> 7d197b0f7299
rebuild  pnpm --filter @objectstack/metadata-protocol build
dist     ✓ marker present in 2 built files — the ablation is live in the artifact the suite consumes
run      Tests  7 failed | 5 passed (12)            ← direction: turned RED, the ordinary direction
restore  ✓ restored: blob == HEAD (e7a78741e047) and `git diff HEAD` is empty
rebuild  pnpm --filter @objectstack/metadata-protocol build
dist     ✓ marker absent from all 24 built files
tree     ✓ working tree clean against HEAD

The 5 cases that stay green under the ablation are the control legs — fresh-absent, fresh-true, the non-boolean cell, seeded-absent and the unseeded control — none of which depends on a flag arm. Nothing of the ablation is left in the tree; the mutation script carried a trap on EXIT INT TERM with absolute paths.

Acceptance notes

1. ⭐ A published description is falsified by this PR, and it is fenced out of this card. packages/spec/src/kernel/package-registry.zod.ts:325 ships this .describe() text, which reaches the published reference page (content/docs/references/kernel/package-registry.mdx:187 and content/docs/references/api/protocol.mdx:1913):

Whether to enable immediately after install — restates the install-door request key, whose one authority is api/PackageInstallRequest; this protocol primitive does not read it

The doc block above it says the same at length (「This contract's own implementation does not read the key」), and packages/spec/src/api/package-api.zod.ts:305 carries a second copy. As of this PR all three are false. They were written by #19130, which merged at 11:10Z — two hours after this card was filed — so the card's author could not have fenced around them.

⛔ Not fixed here: the card and the dispatch both fence packages/spec out (「the declaration half belongs to #19273」), and editing a .describe() pulls in the whole spec generated-artifact family (gen:schema, gen:docs, check:generated) plus a second package's changeset — a new verification surface, so the bounded-in-place-fix exemption does not hold. It belongs to #19273, whose open question is already 「once the runtime honours 「缺省 = 保持」, what should the published enableOnInstall declaration say?」. Recorded here and in the report so it is not rediscovered as drift. No gate goes red on it: check:docs compares the generated page against the describe, and both still agree with each other.

2. The durable half of the in-process door, noted not filed. A caller that sets enableOnInstall: false in-process now gets a disable that is real in the registry and absent from the runtime's disable file, so a restart re-enables it. That is narrower than the pre-PR gap (where the key did nothing at all) but newly reachable, and it cannot be closed at this seam: the record is keyed by an environment the request does not carry. Closing it means either giving InstallPackageRequest an environment or giving the caller the durable verb — a contract decision, not an implementation one. Who would meet this: only a caller that sets the key, of which there are none on the tree today.

3. .changeset/18605-enable-on-install-one-authority.md (unreleased) states 「Its published description now records that this layer does not read it」. If it and this PR's changeset ship in the same release, one release's notes will say both. Belongs with finding 1, in #19273.

Nothing else was touched: this diff is packages/metadata-protocol/src/protocol.ts, one new test file under packages/objectql/src/, and the changeset.


Generated by Claude Code

…Install`

`MetadataProtocol.installPackage` read `manifest` + `settings` only, so a
caller that asked for `enableOnInstall: false` got an ENABLED install with no
refusal and no warning — declared but not enforced on a published option.

It now applies the same rule the HTTP door does (ruling batch #157 item 5
letter C): `true` enables, `false` disables, an ABSENT key makes no lifecycle
call at all.

Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata-protocol, touching 7 documentable anchor(s).

5 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), packages.enable (sdk, the route ledger binds it to PATCH /packages/:id/enable))
  • content/docs/api/metadata-api.mdx (via /packages/:id/disable (route, a path literal in a comment in installPackage), /packages/:id/enable (route, a path literal in a comment in installPackage))
  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/kernel/contracts/metadata-service.mdx (via /api/v1/packages/:packageId (route, bridged from symbol installPackage — its route source's handler names it))
  • content/docs/protocol/kernel/error-handling.mdx (via /api/v1/packages/:packageId (route, bridged from symbol installPackage — its route source's handler names it))

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

  • content/docs/releases/v12.mdx (via installPackage (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), /api/v1/packages/:packageId (route, bridged from symbol installPackage — its route source's handler names it))
  • content/docs/releases/v17/17-4.mdx (via /api/v1/packages/:packageId (route, bridged from symbol installPackage — its route source's handler names it))

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 — 11 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 74fb2f7a808371650b8d91e64d72156bec09aac4packageMentionDocs.

Which tree this was computed on

This run read content/docs from beb47cb30bc67b17ef9d965779ef51b26ab82e04 — the merge of head 2c8e2667c997d156b6a75272a2200bcb0c4d2855 into base 74fb2f7a808371650b8d91e64d72156bec09aac4, 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 beb47cb30bc67b17ef9d965779ef51b26ab82e04 && git checkout beb47cb30bc67b17ef9d965779ef51b26ab82e04
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 74fb2f7a808371650b8d91e64d72156bec09aac4 2c8e2667c997d156b6a75272a2200bcb0c4d2855 && git checkout -B drift-repro 74fb2f7a808371650b8d91e64d72156bec09aac4 && git merge --no-ff 2c8e2667c997d156b6a75272a2200bcb0c4d2855

node scripts/docs-audit/affected-docs.mjs --json 74fb2f7a808371650b8d91e64d72156bec09aac4

⚠️ 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 74fb2f7a808371650b8d91e64d72156bec09aac4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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

2 participants