Skip to content

fix(cloud-connection)!: refuse a cloud-fetched marketplace install whose catalog name an environment-authored item holds - #25435

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-gzt6ro
Oct 23, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-gzt6ro

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Sales · project thread

Fixes #25366

Clause-②: no (narrowing)
Metadata-protocol: none

Claim 6364974343 (domain:services seat 1, #6021). The branch is the harness-pinned claude/project-thread-gzt6ro. It names no issue, so the claim is the identity record for it.

What changed

cloud-connection, step 3 of POST /api/v1/marketplace/install-local. When the registry refuses the package because an environment-authored item already holds a position, permission set or capability name the package ships, the cloud-fetched branch now refuses 422 PLUGIN_REGISTER_FAILED before the ledger write. The answer carries the registry's own sentence, which names each colliding name and its holder. The inline (file-import) branch already answered exactly this. Before, the cloud branch answered 200 with hotLoaded: false, wrote the ledger entry, and said the package would load at the next restart.

  • One shared pre-write check. The registry's package door (SchemaRegistry.installPackage → refuseSecurityCatalogNameConflicts) already runs ahead of every registry mutation, for every door that installs a package. The door asks that refusal rather than keeping a second reading of who holds a name. refusesEnvironmentHeldCatalogName recognises it by NAMESPACE_CONFLICT_CODE plus a conflicts entry whose holder is environment. The engine keeps the error class unexported, so this is a code comparison and not instanceof.
  • Narrow on purpose. Every other register failure on the cloud branch keeps its lenient answer. A name another package or a built-in holds is not judged here.
  • ⛔ Engine lane untouched: the refusal, its message and the cold-boot check in packages/objectql stay as they are.

Every install door, measured

door before after
marketplace install-local, inline manifest 422, nothing written unchanged
marketplace install-local, cloud-fetched, fresh 200 hotLoaded:false, ledger written 422, nothing written
marketplace install-local, cloud-fetched, upgrade 200 hotLoaded:false, ledger overwritten 422, earlier version stays
POST /api/v1/packages, fresh and overwrite 400 VALIDATION_ERROR at the body parse, naming positions / capabilities / permissions unchanged, now pinned
protocol.installPackage callers (duplicate, AI blueprint) same strict parse unchanged
unmanaged install the import verb's own collision refusal unchanged, out of scope

Boot-time plugin registrations are not install doors.

The premise, measured

The card says the next cold boot is refused. A real restart shows something different. The boot runs. The marketplace rehydrate, at kernel:ready, after the engine's own cold-boot check, logs rehydrate failed for com.acme.field-service with the security-catalog sentence. The package is then not loaded at all. On an upgrade its earlier version does not load either, because the ledger entry that named that version was overwritten. The deployment does not stop, but the package's apps, objects and permission sets disappear while GET /api/v1/marketplace/install-local still lists the package. The fix is the same either way.

R1–R4

My reading of R1–R4, taken from #24446 U2 practice:

  • R1: a dev-server run under single that drives the door.
  • R4: a production objectstack serve run under walled isolated and group that drives the door.
  • R2 and R3: the package tests and the dispatch gates.

The test manifests are served by a local stub control plane (OS_CLOUD_URL pointed at it, OS_MARKETPLACE_PUBLIC_BASE_URL=off). Package com.acme.field-service:

  • v1 ships rone_dispatcher;
  • v2-collides ships rone_dispatcher and rone_dispatcher_lite;
  • v2-free ships rone_dispatcher and rone_auditor.

The environment's clone is PUT /api/v1/meta/permission/rone_dispatcher_lite, which answers "Saved permission … (env-wide)".

R1, single (pnpm dev on the showcase, persistent SQLite).

The ablated leg (guard disabled with ablation-replace, dist rebuilt; esbuild drops the dead branch, and the dist has no existingHolder):

  1. v1 installs 200.
  2. Clone 200.
  3. v2-collides answers 200 hotLoaded:false ("registers it again at its next restart"), and the ledger reads 2.0.0.
  4. A restart logs rehydrate failed. rone_dispatcher answers 404, and /api/v1/packages has no row for the package.

Then the two remedies, each across a real restart:

  • Re-install v1: 200 hotLoaded:true, and it loads again after a restart.
  • Rename the clone: author rone_dispatcher_lite_custom, delete rone_dispatcher_lite, then restart. v2 loads, rone_dispatcher_lite is the vendor's at 2.0.0, and the clone lives on under its new name.

The fixed leg (restored, blob == HEAD, dist rebuilt), on a fresh database:

  1. v1 200, then clone 200.
  2. v2-collides answers 422 PLUGIN_REGISTER_FAILED with the sentence. v1 stays loaded and the ledger stays at 1.0.0.
  3. A restart loads v1, with no rehydrate failed.
  4. CONTROL: v2-free answers 200 hotLoaded:true, upgradedFrom set, and rone_auditor is at 2.0.0.

R4, production objectstack serve of the built showcase. Settings: NODE_ENV=production, persistent SQLite, OS_PLATFORM_OWNER_EMAIL set to the operator, OS_AUTH_MEMBERSHIP_POLICY=invite-only, and OS_SECRET_KEY. Each banner prints Tenancy: isolated or Tenancy: group.

The run needed temporary changes, all reverted afterwards: @objectstack/organizations declared in the showcase's package.json and linked into its node_modules (no lockfile change, compared byte for byte), plus the test databases and ledgers under scratch. The tree is clean.

Under both isolated and group, the operator saw the same sequence:

  1. v1 200, then clone 200.
  2. v2-collides answers 422 PLUGIN_REGISTER_FAILED. v1 stays and the ledger stays at 1.0.0.
  3. A restart loads v1, with no rehydrate failed.
  4. CONTROL: v2-free 200, and rone_auditor is at 2.0.0.

A walled tenant administrator is stopped earlier by the unchanged ADR-0131 D12 refusal (403), so the operator is the only caller who reaches this step.

Tests

  • cloud-connection/src/marketplace-install-local-catalog-name-collision.test.ts (18). It runs over a real ObjectKernel with ObjectQLPlugin and the kernel's real manifest service. The cases cover permission set, position and capability, on both the inline and the cloud-fetched branch:
    • a fresh install is refused with nothing registered and no ledger entry;
    • an upgrade is refused and v1 stays registered and ledgered;
    • CONTROL: a non-colliding v2 installs.
    • Each refusal is a valid BaseResponseSchema envelope naming the name and "the environment catalog". The environment item is kept.
    • Ablation (guard disabled) turns the 6 cloud-branch refusal cases red, with main's 200 hotLoaded:false answer. The inline cases stay green.
  • runtime/src/package-door-security-catalog-collection-refused.test.ts (9). It runs the real HttpDispatcher.handlePackages over a real SchemaRegistry holding the environment's item. For each type, a fresh install and an overwrite carrying the collection are refused 400 naming the key, and nothing is installed or the earlier version stands. CONTROL: the same manifests without the collection install 201. POST /api/v1/packages gets no check of its own, because it cannot carry a catalog name.
  • The cloud-connection suite: 56 files and 722 tests pass. Typecheck passes for cloud-connection and runtime. eslint on the touched files passes.

Acceptance notes

  • Out of scope, engine lane (not measured as a defect): engine.registerApp sets this.manifests and the datasource definitions before installPackage runs its refusal. So a refused install may leave a manifest entry in the engine's own map. Not touched here.
  • Out of scope, observation: os migrate security-catalog-overlays, named by the engine's cold-boot remedy, boots the stack composition, which likely does not include the marketplace ledger. It may therefore list nothing for a marketplace package. Not measured.
  • A name held by another package or a built-in still takes the lenient cloud answer. That is outside this card.
  • The changeset carries the operator remedy for a deployment that already took such an upgrade. Both remedy steps were measured in R1 across a real restart.

The proof readings are in the os-dev-report on #25366: the dispatch gates, check-governed-merges --branch (0 of 4 paths governed, 480 lines), R1, R4 and the dogfood battery.


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01ENyzeNx2dXdHp37P4Zdp2d

…se catalog name an environment-authored item holds

The cloud-fetched branch's lenient register catch swallowed the registry's
security-catalog refusal and wrote the ledger entry anyway, answering 200
with hotLoaded false. No restart heals that refusal: the environment's item
is loaded at every boot, so the ledger entry can never load. Both branches
now refuse it with 422 PLUGIN_REGISTER_FAILED before the ledger write.

Claude-Session: https://claude.ai/code/session_01ENyzeNx2dXdHp37P4Zdp2d
Co-authored-by: Claude <noreply@anthropic.com>
…ent-held catalog name on both branches

Claude-Session: https://claude.ai/code/session_01ENyzeNx2dXdHp37P4Zdp2d
Co-authored-by: Claude <noreply@anthropic.com>
…ction at its body parse

Claude-Session: https://claude.ai/code/session_01ENyzeNx2dXdHp37P4Zdp2d
Co-authored-by: Claude <noreply@anthropic.com>
…e environment-held catalog name refusal

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 5 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 77392e901cce58316e803144971f2741ef049fc4 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 77392e901cce58316e803144971f2741ef049fc4

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6a16bf07b0e66885315645fa69bfda96ff26d270
Local-runs: none
Scope: packages/cloud-connection/src/marketplace-install-local-plugin.ts, packages/cloud-connection/src/marketplace-install-local-catalog-name-collision.test.ts, packages/runtime/src/package-door-security-catalog-collection-refused.test.ts + packages/objectql/src/registry.ts, packages/objectql/src/security-catalog-namespace.ts, packages/objectql/src/index.ts, packages/objectql/src/engine.ts, packages/objectql/src/plugin.ts, packages/metadata-protocol/src/protocol.ts, packages/runtime/src/domains/packages.ts, packages/runtime/src/http-dispatcher.ts, packages/spec/src/api/package-api.zod.ts, packages/spec/src/api/error-code-ledger.zod.ts, packages/cloud-connection/package.json

Inputs: card #25366 (body, claim 6364974343, os-dev-report 6366332431), PR #25435 (body, 4 files, net diff origin/main...6a16bf07), the 41 check-runs on 6a16bf07 (33 success, 8 skipped — the opt-in harness, tarball smoke, console pin, temporal conformance, docs build and the dogfood matrix shards; no failure), and the closed sibling card #25382 with its seat record 6365067991, read for its measurement of POST /api/v1/packages. Read by ref only; nothing built, run or re-run.

① Derived judgments

  1. The narrowing itself — right. POST /api/v1/marketplace/install-local, cloud-fetched branch, step 3: when manifest.register throws the registry's security-catalog refusal naming an environment holder, the door now answers 422 PLUGIN_REGISTER_FAILED and returns before step 4. On main it fell to the lenient hotLoadError path and reached this.ledger.write(entry). Read in the diff and the surrounding code: the new if sits inside the step-3 catch, after the unchanged inline if (inlineManifest) return and before hotLoadError = registerError; step 4 (ledger write), 4b (syncSchemas), the binder and the side effects all follow the catch. Steps 1 through 2c on the cloud branch read and judge (fetch, id parse, handshake, install mode, conflict check, previousEntry read, the global-unique and access-upgrade gates) and write nothing. The registry's own refusal (SchemaRegistry.installPackage → refuseSecurityCatalogNameConflicts, registry.ts:5960) runs after the namespace gate and the co-owned object collision gate and ahead of every registry mutation, so a refused package leaves no record, no namespace ownership and no claim. "Refused before the first write" holds on both sides of the seam.

  2. "Identical to the inline branch" — right on the contract, with one wording difference. Same status (422), same ledger-registered code (PLUGIN_REGISTER_FAILED), the same registry sentence interpolated (each colliding name, its type label and its holder, from SecurityCatalogNameConflictError's constructor). The message prefix differs: inline says Failed to register imported manifest: …, cloud says Package '…' cannot be installed: …. The ledger registers the code, not the prose, and the test pins the sentence's content (the quoted name and "the environment catalog") on both branches, so this is a wording choice and not a contract difference.

  3. One shared pre-write check, recognised and not copied — right. refusesEnvironmentHeldCatalogName compares err.code to NAMESPACE_CONFLICT_CODE and asks for a conflicts entry whose existingHolder.kind is environment. On main the producers of that code are exactly two classes: SecurityCatalogNameConflictError (door registry.ts:3095, item seam :5054, cold boot plugin.ts:2529), which carries conflicts, and NamespaceConflictError, which shares the code and carries no list, so it keeps the lenient answer unchanged. Holder kinds are closed (package, environment, built-in; securityCatalogHoldersOtherThan, registry.ts:3038). The door reads nothing of its own about who holds a name. The error reaches the door unwrapped: the manifest service's register calls ql.registerApp (plugin.ts:813), which calls registry.installPackage (engine.ts:9291) with no catch between, and the test runs the real kernel's service.
    Could it misfire on a non-environment holder? No. A conflicts list holding only package or built-in holders answers false and takes the lenient path as before. A mixed list (one environment holder among package holders) answers true, which is right: the environment holder alone makes every later load fail, so the lenient promise would be false whatever the other holders are.
    Code comparison rather than instanceof — right. The engine keeps the class unexported; the constant is exported from the index.ts barrel (:82), not from core.ts, and the import is from the barrel.

  4. A new module edge, gates green. import { NAMESPACE_CONFLICT_CODE } from '@objectstack/objectql' is the first static import from @objectstack/objectql in packages/cloud-connection/src outside tests (on main there is none). @objectstack/objectql is already a declared dependencies entry of cloud-connection, and objectql does not depend on cloud-connection, so there is no cycle. Build Core, every Lint and Type Check lane and the dispatch gates' dual-build check (per the report) are green on this head. Named, not a finding.

  5. POST /api/v1/packages, fresh and overwrite — pinned, no code change; right. HttpDispatcher.handlePackages is a thin delegate to handlePackagesRequest in domains/packages.ts (http-dispatcher.ts:2489), which parses the body with PackageInstallBodySchema (:1231) ahead of the write at :1427 / :1442. That schema is a union of two closed shapes (package-api.zod.ts:517): the strict wrapped request and the strict manifest, which refuses positions and capabilities by name and refuses a permission-set LIST under permissions because the manifest-stage key is the ADR-0025 grant block. The new pin asserts 400 VALIDATION_ERROR naming the collection key, nothing installed or the earlier version standing, with a CONTROL at 201 fresh and as an overwrite. Card fix(runtime): POST /api/v1/packages with overwrite replaces an installed package without the access upgrade check the marketplace door runs — call security.accessUpgradeCheck before the first write #25382's seat record read the same door independently on f0fdd45: it "stores the manifest alone" and its probe's leg C measured the same 400s. Two readings agree. (The marketplace door accepts the same permissions list as permission sets; the two doors speak two body dialects, both pre-existing and outside this card.)

  6. Every door named — right. (a) Marketplace inline: already 422, unchanged. (b) Marketplace cloud-fetched, fresh and upgrade: now 422, nothing written, earlier version kept. (c) POST /api/v1/packages: item 5. (d) protocol.installPackage (protocol.ts:32756) and its callers (duplicate :28801, AI blueprint :29177): the declaration parse ahead of it refuses collections, and the registry refusal propagates through it with no lenient catch. (e) Unmanaged install (ADR-0131 D6): writes through the import verb into the environment's own ledger and registers no package-held name, so it cannot create a package-versus-environment pair; the import verb's own collision rule applies. Out of scope, correctly. (f) Boot-time AppPlugin registrations are code-booted and judged by the engine's cold-boot check (plugin.ts:1223); the marketplace rehydrate is a load, not a door, and catches per entry. No door is left that can register such a name leniently.

  7. The premise correction — right in code and measured. The card said the next cold boot is refused. refuseEnvironmentHeldSecurityCatalogNames runs inside ObjectQLPlugin.start after restoreMetadataFromDb (plugin.ts:1223), over the packages registered by then. The marketplace rehydrate registers ledger entries at kernel:ready, after that check, and its loop catches per entry and logs rehydrate failed for the package id (marketplace-install-local-plugin.ts rehydrate, the per-entry catch). So the boot runs, the package is not loaded, and on an upgrade the earlier version is not loaded either because step 4's ledger.write replaces the entry. refusedAtRehydrate is set only for a protocol-range refusal, so the GET listing still shows the package. R1's ablated leg observed exactly this across a real restart. The fix is the same either way, as the claim anticipated. premise_still_valid: false is correctly reported.

  8. The changeset's operator remedy — right. Remedy 1 (re-install the earlier version through the same door): a marketplace-over-marketplace conflict takes the upgrade path and step 4 replaces the ledger entry; measured in R1 (200 hotLoaded:true, loads after a restart). Remedy 2 (save the environment item under a new name, repoint assignments, delete the old, then restart or re-install): measured in R1. The changeset's warning that an assignment left on the old name grants the package's definition from then on follows from the registry rule (an assignment names by bare name). The remedy does not lean on os migrate security-catalog-overlays, which the engine's cold-boot message names and which the dev notes may not read the marketplace ledger. Right to state the measured remedy instead.

  9. Tests — right for what they pin. marketplace-install-local-catalog-name-collision.test.ts (18): real ObjectKernel + ObjectQLPlugin, the kernel's own manifest service, both branches × three types × fresh refused / upgrade refused / CONTROL, each refusal a valid BaseResponseSchema envelope naming the quoted name and "the environment catalog", the registry and the ledger read back unchanged, the environment item kept with no _packageId. The capability case pins that the door judges the refusal and not the type. The dev reports an ablation (guard disabled: 6 cloud cases red with main's 200 hotLoaded:false, inline green); that is a report reading, not a check-run, and this review does not re-run it. package-door-security-catalog-collection-refused.test.ts (9): item 5.

  10. No other published surface moves. No export, type, flag, route, metadata key or error code is added, removed or renamed. GET /api/v1/marketplace/install-local is untouched. The engine lane is untouched, as the claim's ⛔ required.

② Semver level

  • Changeset .changeset/25366-marketplace-env-catalog-name-refusal.md: @objectstack/cloud-connection: patch, Clause-②: no (narrowing), Metadata-protocol: none, a **BREAKING** banner, an ADR-0087 marker not-required (no-migration-prescription); PR title fix(cloud-connection)!:; the PR body carries the same two lines. Matches what the diff publishes. One door's accept set narrows (a 200 becomes a 422 for an install that could never load); nothing widens; no metadata key, accepted value, enum member or type changes, so Metadata-protocol: none is the true kind.
  • The level follows ruling A as check-changeset-no-major.mjs states it: a no (narrowing) with Metadata-protocol: none beside it is a non-protocol narrowing, ships patch, and keeps its BREAKING carriers (banner and !). Both carriers are present. Check Changeset is success on this head.
  • @objectstack/runtime receives a test file only and owes no changeset. @objectstack/objectql is unchanged.
  • Clause-②: no (narrowing) — read as declared and correct: the narrowing is of the whole PR, on one door's answer; no path narrows a schema or a governed rule (check-governed-merges --branch: 0 of 4 paths governed, per the report; Governed Surface Queue Guard success).
  • The ADR-0112 question the card raised ("an ADR-0112 code naming each colliding name and its holder, like the inline branch"): PLUGIN_REGISTER_FAILED is ledger-registered (error-code-ledger.zod.ts:1185), and the ledger's own NAMESPACE_CONFLICT entry records that this door answers the registry's 422 refusals with its own PLUGIN_REGISTER_FAILED, interpolating the message (:1002). The card's wording asks for the inline branch's answer, and this is it. Acceptable. The gap, non-blocking: the answer carries the names and holders in prose only (no details list), and the registry's own registered code is dropped at the door, so an AI installer or the console must parse the sentence. Not owed by this card; a follow-up candidate if a consumer needs it.

③ Boundary flags

  • Dev flag: engine residue on a refused install (engine.registerApp sets this.manifests and datasource definitions before installPackage refuses, engine.ts ahead of :9291). Read and confirmed. On a refused upgrade the engine's manifests map holds the refused body while the registry keeps the installed version. The same residue exists on the inline branch today, the registry and the ledger (what the card's acceptance names) are untouched, and the claim's ⛔ keeps the engine lane out of this PR. Answered: out of scope here, correctly noted and not filed. A follow-up card on the engine lane would be the place.
  • Dev flag: os migrate security-catalog-overlays may list nothing for a marketplace package (it boots the stack composition, likely without the ledger). Not measured; the changeset's remedy does not depend on it. Answered: observation, not a defect of this PR.
  • Dev flag: a name held by another package or a built-in keeps the lenient 200 hotLoaded:false. The card and claim are about environment-authored items, so this is the stated scope. Noted for the record: a built-in holder can no more be cleared by a restart than an environment one can, so the lenient promise is as false for it; a package holder can be cleared by an uninstall, so leniency has meaning there. Answered: outside this card; a candidate follow-up for the built-in case.
  • Dev flag: R4 needed @objectstack/organizations declared and linked, OS_AUTH_MEMBERSHIP_POLICY and OS_SECRET_KEY, temporarily. The diff carries none of it (4 files, lockfile untouched). Answered: a rig note, as in U2.
  • open_questions: [] — none to escalate.
  • Reviewer flag: the first static @objectstack/objectql import in cloud-connection source (① item 4). Declared dependency, no cycle, gates green. Answered: named, not a finding.
  • Reviewer flag: message prefix differs between the two branches (① item 2). Contract unchanged. Answered: wording.
  • Reviewer flag: domain label. Both changed packages are domain:cli by the dispatch table; the claim records this and leaves relabelling to triage. A board matter, not this review's.
  • Premise: the card body's "the next cold boot refuses" is corrected by the PR body and the changeset (① item 7). The acceptance ("refused before any write, nothing registered and no ledger entry, pinned with a CONTROL; operator remedy in the changeset; R1–R4 with R4 under isolated and group") is met on the inputs read.

Implemented-by: claude/project-thread-gzt6ro
Reviewed-by: session_016g7nYP9wdNx1GDa5XDr7Uy

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT · 2026-10-23T13:56Z

domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · card #25366 (card thread; os-dev-report 6366332431) · reviewed head 6a16bf07b0e66885315645fa69bfda96ff26d270

  • Contract review record 6366546864 (CONTRACT_REVIEW_TIER, narrowing face): PASS. The seat adopts it.
    • Pre-write refusal. The cloud-fetched branch of POST /api/v1/marketplace/install-local now refuses before its first write (the ledger) with the inline branch's 422 PLUGIN_REGISTER_FAILED, when the registry's security-catalog refusal names an environment holder.
    • No second copy of the rule. The door recognises the registry's own refusal: NAMESPACE_CONFLICT_CODE plus an environment holder in conflicts. A list holding only package or built-in names stays lenient, and a mixed list refuses.
    • Every door named.
    • Premise correction, accepted. A restart does not refuse the cold boot. The rehydrate logs rehydrate failed and the package (on an upgrade, its earlier version too) does not load. The changeset's two operator remedies match the code, and R1 measured them across real restarts.
    • Changeset. patch + BREAKING banner + fix(cloud-connection)!: with Clause-②: no (narrowing) / Metadata-protocol: none is the non-protocol narrowing shape that check-changeset-no-major admits. Check Changeset is green.
    • Noted, not owed here. Names and holders reach the caller as prose only, with no details list, and the registry's own code is dropped at the door. This is a follow-up candidate, not a defect of this PR. It is also cloud-connection's first static import from @objectstack/objectql: a declared dependency, no cycle, and the gates are green.
  • Dev readings (report). R1 (single, dev, ablation and fix legs) and R4 (production serve, isolated and group). dispatch-gates 67/67 exit 0 with 0 NOT MEASURED. The full dogfood battery passes on this head: 290 files / 2494 tests.
  • CI on this head. 33 success and 8 expected skips, none failed. git merge-tree onto today's main is clean.
  • Body. The attribution header and both footers are present. The seat requested the os-sales reviewer through the relay.
  • Lane. The files are in cloud-connection and runtime (domain:cli by the domain table). Both lanes are run by this project, so the card stays under services seat 1 and no relabel is asked.
  • Landing. Not governed (check-governed-merges --pr 25435: 480 lines). Ready + auto-merge through the relay. Fixes #25366 closes the card on merge, and the card thread follows the PR to MERGED.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 23, 2026 13:57
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 23, 2026 13:57
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 8d9e8b9 Oct 23, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-gzt6ro branch October 23, 2026 14:30
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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants