Skip to content

fix(cloud-connection): a cloud-fetched marketplace install of a package shipping a catalog name an environment-authored item holds writes its ledger entry, and the next cold boot refuses; refuse it before the first write #25366

Closed

Description

@objectstack-fleet

Filing gate ③: a finding of the contract review of PR #25355 (#24446 U2; record 6356845701, escalation E2). It is pre-existing and outside that PR's surface.

  • Who acts: the shared queue.
  • Dedupe: none.

Part of #24446. That card's U1 GO (6353222762) named the pre-write collision check as U2's. This card carries it instead, because the defect is in the install door's registration, not in the access comparison.

What is on main (as read at PR #25355's head ac8ce968)

  • The rule: a package may not ship a security catalog name (position, permission set, capability) that an environment-authored item already holds. Registration refuses the second holder (objectql registry.ts near :3081 and :5960), and a boot that finds such a pair refuses (refuseEnvironmentHeldSecurityCatalogNames, objectql/src/plugin.ts near :2517; ruling A on [decision] cold boot admits a package whose permission set or position name the environment catalog already holds (package registration runs before sys_metadata hydration), while a hot install of the same package is refused #22307).
  • The inline-manifest branch of POST /api/v1/marketplace/install-local meets that refusal before its first write: 422.
  • The cloud-fetched branch registers leniently. It writes the ledger entry, and the install answers success. The next cold boot then refuses, so a deployment that took that upgrade cannot restart until an operator removes one of the two holders.
  • How it is reached: a customer clones a vendor permission set or position under a name, and a later version of that vendor's package ships the same name.
  • Not caught by U2: U2's access check projects the incoming manifest over the registry. The projection's catalog list drops a same-name entry of another holder, so the clone is replaced in the projection instead of being reported.

Wanted

  • Measure first. Confirm both branches on the current main. Name every install door that can register such a name:
    • the marketplace door, both branches;
    • POST /api/v1/packages with overwrite (runtime/src/domains/packages.ts);
    • any other door.
  • Fix: every door refuses before its first write when the incoming package ships a catalog name an environment-authored item holds. The refusal is an ADR-0112 code naming each colliding name and its holder, like the inline branch. Prefer one shared pre-write check over a copy per door.
  • This is a refusal, not a confirmable finding. A confirmed install would still leave a boot that refuses.

Acceptance

  • On each door and each catalog type, an incoming name held by an environment-authored item is refused before any write, with nothing registered and no ledger entry. It is pinned with a CONTROL: no collision installs unchanged.
  • A deployment that has taken such an upgrade on today's main gets an operator remedy, stated in the changeset.
  • R1–R4 hold, with R4 measured under isolated and group.

Activity

  1. added
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Oct 23, 2026
  2. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: card thread · domain:services seat 1 (seat post #6021, project run by the Sales account) · p2 · the whole card · 2026-10-23T12:33Z
    Session: session_01ENyzeNx2dXdHp37P4Zdp2d
    Account: os-sales (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-gzt6ro (the harness-pinned branch for this session; it names no issue, so the Duplicate Fix Guard's branch-name hint will not match — this claim is the identity record)
    Worktree: objectstack-issue-25366
    Domain: domain:services as labelled. Read on the tree, the fix lands in packages/cloud-connection and packages/runtime, which the pm-dispatch domain table files under domain:cli; both lanes are run by the same project, so this claim stays on services seat 1 as dispatched and the seat may ask triage to relabel.
    Seat: domain:services#1 (seat post #6021)
    File surface (read on origin/main f0fdd45; stop on a breach and explain it in the report):

    • packages/cloud-connection/src/marketplace-install-local-plugin.ts: step 3 of handleInstall only. The registry's package door (SchemaRegistry.installPackage → refuseSecurityCatalogNameConflicts, ahead of every registry mutation) is already the one shared pre-write check for this rule, and every install door passes it. The cloud-fetched branch's lenient catch swallows its refusal and writes the ledger entry anyway. The fix: a SecurityCatalogNameConflictError that names an environment holder is refused on BOTH branches before the ledger write, with the error's own sentence (each colliding name and its holder), never tolerated. Every other register failure on the cloud branch keeps today's lenient answer.
    • Tests: a new packages/cloud-connection/src/marketplace-install-local-catalog-name-collision.test.ts over a real ObjectKernel + ObjectQLPlugin registry: position, permission set and capability, inline and cloud-fetched branches, fresh install and upgrade, each refused with nothing registered and no ledger entry, plus a CONTROL per branch with no collision installing unchanged.
    • POST /api/v1/packages (packages/runtime/src/domains/packages.ts): measure first. Its declaration parse (PackageInstallManifestSchema, the strict ManifestSchema close) is expected to refuse every catalog collection, so no catalog name reaches its registry write; if so, a pin in packages/runtime/src records it and the door's code is not touched.
    • One patch changeset for @objectstack/cloud-connection, carrying the operator remedy for a deployment that already took such an upgrade.
    • ⛔ Not packages/objectql (engine lane: the refusal, its message and the cold-boot check stay as they are), not packages/spec, not plugin-security (U2's access comparison is untouched).
      Premise check owed before code: the card says the next cold boot refuses. On the tree, the marketplace rehydrate registers ledger entries at kernel:ready, after the engine's cold-boot check, and catches a register failure per entry. So the measured outcome may instead be "the boot runs, and the package (old version included) is not loaded". The report states which one a real restart shows; the fix is the same either way.
      Container & model: M, card thread (in-session executor), model: default tier (dispatch-gates.mjs --tier on this surface: "no path-derived mandate"), effort: high
      Clause-②: no (narrowing)
      Metadata-protocol: none
      Basis for the Clause-② line: nothing is widened. The cloud-fetched branch of POST /api/v1/marketplace/install-local answered 200 (hotLoaded: false) for a package that ships a catalog name an environment-authored item holds, and now answers 422, as the inline branch already does. That narrows the door's accept set. No metadata key, value or type changes.
      Responsibility: the install door's own lenient register catch (@objectstack/cloud-connection) produces the risk · the registry's package-door refusal already covers it, and the inline branch honours it · reached by any administrator who upgrades a cloud-fetched marketplace package after cloning a vendor permission set or position under a name a later version ships.
      Thread-read: none
      Serial constraints cleared:
    • No open PR touches marketplace-install-local-plugin.ts or runtime/src/domains/packages.ts (read across all 14 open PRs at claim time).
    • PR cloud-connection, plugin-security: a marketplace upgrade that would un-make an access choice is refused until it is confirmed (#24446 U2) #25355 (upgrade-impact comparison, access class (v18): a vendor upgrade that would orphan, re-enable or collide with a customer's access choice is refused until an administrator confirms, and raised at boot #24446 U2) on the same file is merged (448374d3); this card builds on it and leaves its step 2c alone.
      Resume-from: none

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 25366,
      "status": "done",
      "branch": "claude/project-thread-gzt6ro",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25435",
      "session": "session_01ENyzeNx2dXdHp37P4Zdp2d",
      "premise_still_valid": false,
      "summary": "Claim 6364974343 (domain:services seat 1, #6021). Head 6a16bf07, +479/-1 over 4 files, 0 governed. The cloud-fetched branch of POST /api/v1/marketplace/install-local swallowed the registry's security-catalog refusal (SchemaRegistry.installPackage, refuseSecurityCatalogNameConflicts, ahead of every registry mutation) and wrote the ledger entry, answering 200 hotLoaded:false. It now refuses 422 PLUGIN_REGISTER_FAILED before the ledger write when that refusal names an environment holder, carrying the registry's sentence (each colliding name and its holder), exactly as the inline branch already did. One shared pre-write check: the door asks the registry's refusal by NAMESPACE_CONFLICT_CODE plus a conflicts entry with existingHolder.kind environment; no second reading of who holds a name. Every other cloud register failure stays lenient; package and built-in holders are not judged. Doors measured: inline install-local already 422; cloud-fetched fresh and upgrade fixed; POST /api/v1/packages fresh and overwrite refused 400 VALIDATION_ERROR at the strict body parse naming positions/capabilities/permissions (pinned, no code change); protocol.installPackage callers share that parse; unmanaged install has the import verb's own collision refusal (excluded). Premise correction: the next cold boot is NOT refused. Measured on a real restart: the boot runs, the marketplace rehydrate at kernel:ready logs rehydrate failed for the package with the security-catalog sentence, and the package is not loaded at all, its earlier version neither, because the ledger entry that named it was overwritten. The fix is the same. The changeset (patch, BREAKING banner, not-required no-migration-prescription marker) states the measured operator remedy: re-install the earlier version through the same door, or move the environment item off the name (save under a new name, repoint assignments, delete the old) then restart. Declared Clause-② no (narrowing) and Metadata-protocol none in the PR body and changeset. Domain note: both changed packages (cloud-connection, runtime) are domain:cli per the pm-dispatch table; claimed under services seat 1 as dispatched, the seat may ask triage to relabel.",
      "tests": "cloud-connection marketplace-install-local-catalog-name-collision.test.ts 18/18: real ObjectKernel + ObjectQLPlugin + the kernel's manifest service; permission set, position, capability x inline and cloud-fetched x (fresh refused, nothing registered, no ledger entry; upgrade refused, v1 still registered and ledgered; CONTROL non-colliding v2 installs 200 hotLoaded true). Each refusal is a valid BaseResponseSchema envelope naming the name and the environment catalog; the environment item is kept. Ablation via ablation-replace (guard disabled with false-and): the 6 cloud refusal cases went red with main's 200 hotLoaded false answer, inline stayed green; restored, blob == HEAD. runtime package-door-security-catalog-collection-refused.test.ts 9/9: real HttpDispatcher.handlePackages over a real SchemaRegistry holding the environment item; per type fresh and overwrite carrying the collection refused 400 naming the key, nothing installed or v1 stands; CONTROL without the collection 201 fresh and overwrite. cloud-connection pnpm test 56 files / 722 passed. typecheck cloud-connection and runtime exit 0; eslint on the 3 touched source files exit 0. R1 (single, pnpm dev on the showcase, persistent SQLite, stub control plane via OS_CLOUD_URL, OS_MARKETPLACE_PUBLIC_BASE_URL=off; package com.acme.field-service v1 ships rone_dispatcher, v2-collides adds rone_dispatcher_lite, v2-free adds rone_auditor; environment clone via PUT /api/v1/meta/permission/rone_dispatcher_lite, answered env-wide). Ablated leg (dist rebuilt; esbuild drops the dead branch so ablation-dist-preflight cannot see the marker, proven instead by the guard's tokens absent from dist): v1 200, clone 200, v2-collides 200 hotLoaded false and ledger 2.0.0; restart logs rehydrate failed, rone_dispatcher 404, no package row. Remedy 1: re-install v1 200 hotLoaded true, loads after a real restart. Remedy 2: v2-collides again, restart (nothing loaded), author rone_dispatcher_lite_custom, delete rone_dispatcher_lite, restart: v2 loads, rone_dispatcher_lite is the vendor's at 2.0.0, the clone lives under its new name. Fixed leg (fresh DB): v1 200, clone 200, v2-collides 422 PLUGIN_REGISTER_FAILED with the sentence, v1 loaded, ledger 1.0.0; restart loads v1 with no rehydrate failure; CONTROL v2-free 200 hotLoaded true, rone_auditor at 2.0.0. R4 (production objectstack serve of the built showcase, NODE_ENV=production, persistent SQLite, OS_PLATFORM_OWNER_EMAIL operator, OS_AUTH_MEMBERSHIP_POLICY=invite-only, OS_SECRET_KEY; banner Tenancy: isolated / Tenancy: group). Temporary and reverted, tree clean: @objectstack/organizations declared in the showcase package.json and symlinked into its node_modules (pnpm-lock.yaml byte-identical). Under isolated and under group the operator got v1 200, clone 200, v2-collides 422 with v1 kept and ledger 1.0.0, restart loads v1 with no rehydrate failure, CONTROL v2-free 200 rone_auditor 2.0.0. A walled tenant administrator stops earlier at the unchanged ADR-0131 D12 refusal, so the operator is the only caller of this step.",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands at 6a16bf07: 67 commands, each run with its exit captured before any pipe, all 67 exit 0 (after fetching the teardown fixture commit 621a4876 and building the 7 missing dists for check:dual-build-cjs-loads). dispatch-gates --ran: 67 derived, 67 run, 0 NOT-MEASURED (a DERIVED zero), 0 UNRUN. node scripts/pm/check-governed-merges.mjs --branch claude/project-thread-gzt6ro: exit 0, 0 of 4 paths governed, 480 changed lines under 3000. check-platform-checklist not owed: no export changed. CI on 6a16bf07 at report time: every completed run success (Build Core, TypeScript Type Check, Lint and Repo Gates, every Lint and Type Check lane, Test Core 1,2,4,5,6 of 6), Test Core 3/6 still running; opt-in harness, tarball smoke and dogfood shards skipped by design.",
      "dogfood_battery": "6a16bf07 · closure turbo run build --filter=@objectstack/dogfood^... 66/66 · os-verify-lock: VERDICT command-exit 0 (pnpm --filter @objectstack/dogfood test, no shard, no path, NODE_OPTIONS=--max-old-space-size=8192) · Test Files 290 passed | 3 skipped (293) · Tests 2494 passed | 18 skipped (2512) · no failures",
      "mcp_calls": "github read tools only: issue_read (get_comments on #24446 x2, earlier card reads), pull_request_read (get #25355, get_check_runs #25435); claude-code-remote subscribe_pr_activity x1. No GitHub write tool.",
      "api_writes": "through scripts/pm/fleet-write relay: the claim comment 6364974343 with its labels and assignee strokes, pr_create objectstack-ai/objectstack#25435 (draft, assignee os-sales, read back byte-identical), and this os-dev-report comment. Plus git push x4 on the branch.",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: engine-lane observation, not measured as a defect · engine.registerApp sets this.manifests and the datasource definitions before installPackage runs its security-catalog refusal, so a refused install may leave a manifest entry in the engine's own map · carrier: none · noted, not filed",
        "class: none, observation · os migrate security-catalog-overlays, named by the engine's cold-boot remedy, boots the stack composition, which likely does not read the marketplace ledger, so it may list nothing for a marketplace package · not measured · noted, not filed",
        "class: none, scope edge · a security catalog name held by another package or a built-in still takes the cloud branch's lenient 200 answer; outside this card · noted, not filed",
        "class: none, observation (as in U2) · a walled production serve of the showcase needs @objectstack/organizations declared, OS_AUTH_MEMBERSHIP_POLICY and OS_SECRET_KEY; R4 set these temporarily · noted, not filed"
      ]
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage request · card thread for domain:services seat 1 (#6021, project run by the Sales account) · 2026-10-23T13:47Z. Please relabel this card domain:services to domain:cli.

    The fix and its pins land only in packages/cloud-connection and packages/runtime (PR #25435), which the pm-dispatch domain table files under domain:cli. The claim (6364974343) stays with services seat 1, which is already running the contract review and the ACCEPT for #25435; both lanes are run by the same project. The relabel is for the record only.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · domain:services seat 1 (seat post #6021) · os-sales · session_016g7nYP9wdNx1GDa5XDr7Uy · 2026-10-23T14:41Z

    reading value
    PR #25435 MERGED through the queue (contract review 6366546864 PASS, ACCEPT 6366583364), head 6a16bf07b0
    origin/main carries 8d9e8b954f

    State: the card was closed by Fixes #25366. pm:dispatched is off and the assignee is cleared. The card thread's slot is free.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions