Skip to content

fix(security)!: under a wall a catalog-carrying package is the platform operator's — install-local and the /packages delete refuse tenants (ADR-0131 D3) - #22935

Merged
objectstack-fleet[bot] merged 26 commits into
mainfrom
claude/issue-15204-wall-package-refusal
Oct 12, 2026
Merged

objectstack-fleet[bot] merged 26 commits into
mainfrom
claude/issue-15204-wall-package-refusal

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #15204

Clause-②: yes (narrowing)

Stage W2 of #15204 (seat note 6114823168): the walled catalog refusal at the package doors. Stage W (#22895) refused catalog writes at /meta, the clone door and the data door. This PR refuses them where a package carries them. It asks W's predicate, walledCatalogWriteVerdict, from main.

The ruling this executes

ADR-0131 D3: "tenant admins cannot create or edit catalog items; the operator defines the catalog for every tenant (managed packages, Studio)". A package that declares positions, permission sets or capabilities is the operator's instrument. Seat note 6114823168: under group / isolated, below the operator rung (and not isSystem), install-local refuses

  • an install whose incoming version declares any catalog item;
  • an upgrade where either the installed or the incoming version declares any;
  • an uninstall of a package whose installed version declares any.

The whole package is refused, since a partial install is not well-formed. The note also asked for a measurement of DELETE /api/v1/packages/:id, and for that door to be refused the same way if the measurement showed it reachable.

Measured first

The harness was a booted examples/app-crm stack (bootStack, multiTenant: 'posture-only', plus MarketplaceInstallLocalPlugin), with two organizations. Organization A's owner holds manage_metadata through a grant scoped to organization A (rung TENANT_ADMIN). Organization B's member holds the operator's permission sets.

Edge (A, below the operator) isolated group B afterwards
install-local upgrade of the operator's package, adding manage_users to its set 200 200 gains manage_users
install-local uninstall of that package 200 200 loses the set
DELETE /packages/:id of a runtime package the operator saved a set into (PUT /meta/permission/:name?package=) 200 200 loses the set
PATCH /packages/:id/disable of such a package 200 n/a keeps the set (catalog unchanged)

Those catalog items are stored as sys_metadata rows bound to the package (package_id). The running registry does not hold them under the package, so the /packages refusal reads the stored rows, the same rows deletePackage deletes. disable changes nothing any organization resolves, so it is not refused.

What changed

  • install-local (@objectstack/cloud-connection): the admission carries the caller's posture rung and the effective tenancy posture, both from its one authorization read. refuseWalledCatalogPackage asks walledCatalogWriteVerdict once per catalog type that the incoming version, the installed version or the running registry under that id declares. It answers 403 FORBIDDEN, the envelope of the door's sibling refusals, and runs before any registration or ledger write (install) or before the ledger is touched (uninstall). error.details.catalogItems lists the items.
  • DELETE /packages/:id (@objectstack/runtime): refuseWalledCatalogPackageDelete reads the package's stored rows, and asks the predicate per stored catalog type, after the capability and writable-package gates and before anything is deleted. It answers 403 PERMISSION_DENIED, this domain's 403. The packages.ts comment that called requireManageMetadata "the operator gate" now says what that gate checks: the capability or isSystem, and no rung.
  • @objectstack/objectql exports declaredSecurityCatalogNames, the engine's own reading of what a manifest declares. The door judges the declarations the engine registers, not a second reading of them. cloud-connection now depends on objectql directly (it was already in its closure through runtime).
  • ADR-0087: the semantic entry walled-catalog-package-writes-retired; the changeset carries FROM → TO per door.
  • Docs: census row 55b names the /packages delete and anchors refuseWalledCatalogPackageDelete, with the counts regenerated. capabilities.mdx gains the clause from W's contract review (6115069587 ③ 3 ii): under a wall a sub-operator caller reads the walled refusal first, FORBIDDEN on REST and PERMISSION_DENIED on the dispatcher, before the code-only NOT_CREATABLE.

Tests

  • wall-package-refusal.dogfood.test.ts, 19 tests. Under each of isolated and group:

    • install refused (a permission set, and a position alone);
    • upgrade refused (the measured edge, and a catalog-free version over an installed catalog-carrying one);
    • uninstall refused;
    • /packages delete refused.

    B's resolution does not move. Controls under both postures: A installs, upgrades and uninstalls a catalog-free package and deletes a catalog-free runtime package; the operator installs, upgrades, uninstalls and deletes catalog-carrying packages. single control: the same manage_metadata holder does all of it as before.

  • Ablation, install-local: refuseWalledCatalogPackage was forced to admit, cloud-connection rebuilt, and the marker confirmed in dist/ by ablation-dist-preflight. The 10 install-local refusal pins went red (5 per posture), and the other 9 stayed green. Restored to the HEAD blob and rebuilt; the marker is absent from dist/ and the tree is clean.

  • Ablation, /packages: the same for refuseWalledCatalogPackageDelete (runtime). The 2 /packages refusal pins went red, and the other 17 stayed green. Restored and rebuilt; the marker is absent.

  • Full suites (pre-merge code head f4d6a8f104, the same W2 code as this head): objectql 401 files / 7,817 passed; runtime 355 files / 5,126 passed, 19 skipped; cloud-connection 43 files / 517 passed. The full dogfood suite was stopped when W landed, before it finished, so it is not counted. The pin files are counted on the merged head below.

  • dispatch-gates --ran (pre-merge, 141 derived): 140 exit 0. check-engine-split-ratio --days 90 refused on this shallow clone (exit 2, nothing measured).

  • Round-1 findings, fixed:

    • check:test-source-alias: cloud-connection's tests now reach @objectstack/objectql, so that package's vitest.config.ts gained an anchored source alias.
    • cloud-connection's own suite: fake registries lack the private catalog reader. Both doors now ask the predicate before reading anything, so under single, with no tenancy service, or for the operator, neither door reads anything new.
  • On the merged head 0d0a42082d:

    • the pins: wall-package-refusal 19/19 and wall-catalog-refusal 17/17;
    • check:system-context-census, check-adr-0087-registration --base origin/main, check-changeset-no-major and check:test-source-alias each exit 0;
    • regeneration (gen:schema, gen:docs, gen:system-context-census) leaves 0 changed bytes.

    The 124 gates dispatch-gates derives for this head are running; their verdict goes in the os-dev-report addendum on refactor(plugin-security,platform-objects,spec): retire the catalog seeders, the per-organization catalog machinery and the four catalog objects; Setup creation is an environment write under single and refused under a wall (ADR-0131 D2/D3/D5/D13) #15204.

Acceptance notes

  • Not decided here (the maintainer's): whether a walled organization's manage_metadata may install packages at all, or author the environment's objects, views and flows. This PR refuses only catalog-carrying packages, per 6114823168.
  • refuseSecurityCatalogNameConflicts (the shadow refusal) is unchanged; this refusal runs before it.

🤖 Generated with Claude Code

https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP

claude added 21 commits October 11, 2026 19:39
… door (ADR-0131 D3)

Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP
Co-authored-by: Claude <noreply@anthropic.com>
… with single and assignment controls (ADR-0131 D3)

Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP
Co-authored-by: Claude <noreply@anthropic.com>
…087 semantic entry; census row 55b

Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP
Co-authored-by: Claude <noreply@anthropic.com>
…ll-catalog-refusal

# Conflicts:
#	content/docs/permissions/system-context.mdx
…efusal (ADR-0131 D3)

Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP
Co-authored-by: Claude <noreply@anthropic.com>
… row 55b names the clone door

Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP
Co-authored-by: Claude <noreply@anthropic.com>
…y name the clone door

Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP
Co-authored-by: Claude <noreply@anthropic.com>
…ace: exact doors, capability's unchanged accept set, the D12 delegate

Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP
Co-authored-by: Claude <noreply@anthropic.com>
…he clone door and the Setup env-door write (ADR-0131 D3)

Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP
Co-authored-by: Claude <noreply@anthropic.com>
…nvironmentSets describe

Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP
Co-authored-by: Claude <noreply@anthropic.com>
Resolve content/docs/permissions/system-context.mdx: keep row 55b (this
branch) and main's row 58 rewording (#22875); counts re-derived with
pnpm gen:system-context-census.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP
…refusal' into claude/issue-15204-wall-package-refusal
…ADR-0131 D3)

Under group/isolated, below the platform operator rung, install-local
refuses an install whose incoming version declares a position, permission
set or capability, an upgrade where either version declares one, and an
uninstall of a package whose installed version declares one; DELETE
/packages/:id refuses a package holding stored catalog rows. Both ask
walledCatalogWriteVerdict once per declared type.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP
…talog-package-writes-retired

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP
…log items; alias objectql in cloud-connection tests

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP
…package-refusal

Resolve content/docs/permissions/system-context.mdx: keep this branch's
row 55b (it names the /packages delete refusal); counts re-derived with
pnpm gen:system-context-census.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP
@github-actions github-actions Bot added size/l dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Oct 12, 2026
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/cloud-connection, @objectstack/objectql, @objectstack/runtime, @objectstack/spec, touching 33 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/cloud-connection/package.json, packages/cloud-connection/vitest.config.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

47 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json a2240c4dcfc3a19f058cb0b3a4f15a351908acde.

⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/cloud-connection/package.json, packages/cloud-connection/vitest.config.ts) — pages documenting those are invisible to this run
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 145 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 a2240c4dcfc3a19f058cb0b3a4f15a351908acde → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json a2240c4dcfc3a19f058cb0b3a4f15a351908acde

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat note on the red Test Core (6/6) at b7e2eaf5f0 · epic PM session_01Rerax7QTjKMPCUZxQUtPFR (marchtian) · 2026-10-12T04:38Z

  • What failed: the shard's timing-drift step, not a test. The shard measured 2939.1s against 1871.1s predicted (1.57x; the step goes red past 1.5x). In the same job, check-test-completeness reports "14 of 14 scheduled package(s) reported … 20366 test(s) declared and all accounted for". Job 114659008804.

  • Why it is not this PR's: the overshoot is spread across packages this diff does not touch:

    • @objectstack/service-analytics 2.10x;
    • @objectstack/client 1.97x;
    • @objectstack/rest 1.45x;
    • @objectstack/plugin-approvals 1.39x.

    @objectstack/runtime (1.70x) is touched only in domains/packages.ts, and no runtime test was added. The same shard passed on feat(plugin-security,platform-objects): grant forms pick a position or permission set by name from the catalog (#15204 B2) #22939's d4c22c8fe from the same main, and on main's own e357a441c6. This reads as a slow runner, not a change in the work.

  • No fix exists in this PR's scope. The step itself says not to hand-edit the timings dataset or raise the bound.

  • The seat cannot re-run the job (the relay does not re-run jobs). The one re-run is asked of the maintainer. A second red on the re-run is treated as real and root-caused.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Re-run record · epic PM session_01Rerax7QTjKMPCUZxQUtPFR (marchtian) · 2026-10-12T05:02Z

At the maintainer's request in the seat's session, the failed jobs of CI run 38202524320 (this PR's current head b7e2eaf5f0) were re-run once. That covers Test Core (6/6), red on the timing-drift step only (6117849100), and its Test Core rollup. This is the one re-run. A second red on drift alone is treated as real: the remedy is a shard-timings refresh (scripts/measure-test-shard-timings.mjs) as its own change, outside W2.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat note: the second drift red has a root cause, and the fix is already on main · epic PM session_01Rerax7QTjKMPCUZxQUtPFR (marchtian) · 2026-10-12T05:26Z

The one re-run of run 38202524320 (6118076783) reds Test Core (6/6) again on the timing-drift step only:

  • Drift: 2963.7s measured vs 1871.1s predicted, 1.58x (red past 1.5x).
  • Biggest overshoots: @objectstack/runtime +385s, service-analytics +258s, rest +230s.
  • Tests: check-test-completeness reads OK (14 of 14 packages, 20366 tests accounted for), so every test passed.

Root cause. This head merged main at e4ad6776d1. The Test Core shard-timings refresh #22923 (1bd612bd68) landed on main right after that, so this branch still balances its shards with the 2026-10-08 dataset. 6118076783 named the remedy as a refresh in its own change. That change already exists on main as #22923, so the remedy is a merge of main into this branch (no rebase, no force-push). No new refresh PR is needed.

Next:

  • The developer merges main and pushes.
  • The seat then confirms that this PR's own diff is unchanged against the new base, so the contract review on b7e2eaf5f0 carries over.
  • The seat lands the PR once the new head is green.

Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Correction to seat note 6118313233: its root cause was wrong · epic PM session_01Rerax7QTjKMPCUZxQUtPFR (marchtian) · 2026-10-12T05:55Z

6118313233 said this branch lacked the #22923 shard-timings refresh. That does not hold:

  • PR CI checks out the merge ref with main, so both earlier runs on b7e2eaf5f0 already used chore(ci): refresh the Test Core shard-timings dataset #22923's dataset.
  • The predictions prove it. The runs predicted @objectstack/runtime at 528.6s and service-analytics at 220.5s, which are exactly the values 1bd612bd68 wrote.

The merge of main into this branch (2868402973) was harmless. This PR's own diff is identical, as the seat checked. But the merge fixed nothing.

The actual cause is main-wide and already tracked:

This PR changes nothing for either issue. Its next result on 2868402973 stands as it reads: if it is green, the seat lands it; if it is red on drift alone, the record goes on #22966.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat note: Test Core is red on one suite-duration ceiling, in a package this PR does not touch · epic PM session_01Rerax7QTjKMPCUZxQUtPFR (marchtian) · 2026-10-12T05:56Z

What reds on 2868402973: run 38208126904, Test Core → "Check the per-package suite-duration ceilings" → @objectstack/knowledge-ragflow: 4.79s, over its 3.40s ceiling (slowest file 0.05s). All six shards are green, the drift step included. Every other package is under its ceiling, and the closest is service-analytics at 0.92x.

Why it is not this PR's: the diff does not touch packages/knowledge-ragflow. The same package went over in #22874's queue build 38207850018 (4.80s) and in the queue sightings on #22949. That card is the anchor for this check. It is priority:p1 and needs-user-decision, since a ceiling rises only by a ruling (RULED_CEILING_RAISES).

No fix exists yet, and this PR's one re-run is already spent (6118076783). So the seat makes no change here. A sighting goes on #22949. The PR stays draft, and the seat lands it when the check clears, by ruling or by fix, with the contract review and the ACCEPT draft unchanged.


Generated by Claude Code

Resolve content/docs/permissions/system-context.mdx: main's count-free
prose (#22950), this branch's row 55b; no count lines.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP
`--check-drift` takes the run's event (`--event`), required. On `schedule`,
the profile scripts/test-shard-timings.json is measured on, a DRIFT exits 1
as before; on every other event it prints the same text, raises one
`::warning::` annotation and exits 0. A missing or empty event is refused.

Claude-Session: https://claude.ai/code/session_012yK1ddAdAqfzwigRHZq3r2
Co-authored-by: Claude <noreply@anthropic.com>
…one, with a 20s floor

The ceiling check takes the run's event (`--event`, required). On
`schedule`, the profile the ceilings were measured on, a package over its
ceiling reds the step as before; on every other event the same table
prints, each package over its ceiling is a `::warning::` annotation, and
the step exits 0. Refusals stay red on every event.

A package whose effective ceiling is under CEILING_FLOOR_SECONDS (20s) is
never red and prints "below floor"; the header records the floor's reading.

ci.yml passes `--event "$GITHUB_EVENT_NAME"` to both timing steps, which
keep no `if:` and no `continue-on-error`.

Claude-Session: https://claude.ai/code/session_012yK1ddAdAqfzwigRHZq3r2
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat ACCEPT: #22935 (stage W2, the walled catalog refusal at the package doors, ADR-0131 D3) at 944ebdb98d; queued on green

Epic PM session_01Rerax7QTjKMPCUZxQUtPFR (marchtian), seat epic:#15194, 2026-10-12T08:17Z.

  • Developer: session_015JgoXpZUGtNxnMDvRZsKzP.
  • Ruling: seat note 6114823168: below the operator rung under a wall, install-local refuses install, upgrade or uninstall of a catalog-carrying package (the whole package).
  • 17.7.0 measurement: 6114989076.
  • Addendum: 6117573171.
  • Contract review: PASS 6117680834 on b7e2eaf5f0 (Local-runs: none). It carries to 944ebdb98d, as the next section shows.

The head moved three times since the review; none moves W2's own change.

  1. 2868402973: a merge of main 51b43a8dc4, made with os-regen-merge.sh. The reason was two Test Core (6/6) reds on timing drift alone; the root cause turned out to be main-wide (6118613678 corrects 6118313233).
  2. 0c6a5eea21: a merge of main 117817ec28, after chore(docs,scripts): system-context page states no census totals; the census gate keeps the register #22950 took the census page off its counts.
    • The four conflict hunks were all count lines. Main's count-free prose was taken in each.
    • system-context.mdx now differs from main by row 55b alone, in W2's wording, with no markers. check:system-context-census is OK.
  3. 944ebdb98d: the two commits of ci: Test Core's two timing gates exit-grade on schedule alone (C2), with a 20s ceiling floor #22973, the fix for ci: merge-queue Test Core reds on the new suite-duration ceiling — @objectstack/cloud-connection measured 177.5 s and 200.3 s vs its 163.89 s ceiling, in two groups of PRs that do not touch it (live since 1bd612bd6) #22949's ruling C2 + floor (6119186511), cherry-picked as the maintainer's speed-up (6119234024).
  • The seat's check across all three moves: W2's 13 files carry the same hunks as at b7e2eaf5f0, apart from system-context.mdx, which differs only as described in 2. The net against main before the port is 13 files, +643/−7.

So no new contract review is owed.

Checklist, read on GitHub, not from the report:

  • PR form: draft, base main; Part of #15204, then Clause-②: yes (narrowing); no closing keyword; assignee marchtian.

  • Scope: 16 files: W2's 13 (+643/−7 against main) plus the 3 ported ci: Test Core's two timing gates exit-grade on schedule alone (C2), with a 20s ceiling floor #22973 files (+502/−84), so about 1.2k changed lines, under 3,000. .github/workflows/** is not a governed surface; check-governed-merges --pr is re-read on the final head before arming. No CHANGELOG.md and no content/docs/releases/.

  • What it closes: three package edges, measured cross-tenant under isolated and group:

    • install-local upgrade (a member gained manage_users);
    • install-local uninstall (a member lost the set);
    • DELETE /api/v1/packages/:id of a package holding the operator's set.

    Each asks walledCatalogWriteVerdict before any write and reads nothing when it admits (single, no tenancy service, the operator). PATCH …/disable was measured with no catalog change and is not refused.

  • Envelope: install-local answers 403 FORBIDDEN and the dispatcher answers 403 PERMISSION_DENIED. That is the same split stage W left on main, between the REST doors and the dispatcher, data and clone doors; the review's ① 3 judges it consistent.

  • Changeset, the ADR-0087 entry 18.walled-catalog-package-writes-retired and the semver level:

    • four packages at minor, under the launch-window rule;
    • FROM → TO with the remedy;
    • the registered marker;
    • the review's ② judges the level right, and Check Changeset is green.
  • Evidence:

    • 19 pins on isolated and group, with catalog-free, operator and single controls;
    • ablations exact: install-local open → 10 red, /packages open → 2 red, controls green, restores proven;
    • objectql 7,817, runtime 5,126, cloud-connection 517 passed;
    • dispatch-gates 123 of 124 exit 0. The one other is check-engine-split-ratio, refused on a shallow clone and so not measured.
  • CI on 944ebdb98d: 36 check-runs: 35 success and 1 skipped (Packed-tarball smoke (opt-in), on check-expected-skips's roster). 0 red. All six Test Core shards and the Test Core aggregate are success; under the ported C2 the two timing steps report instead of failing. mergeable_state is clean. check-governed-merges --pr 22935 on this final head reads NOT governed (0 of 16 paths); size 1,236 changed lines.

The review's text notes (③ 4), accepted as they stand. None moves the accept set:

  • metadata-api.mdx "a package holding positions or permission sets": a read-only package is refused 422 WRITABLE_PACKAGE_REQUIRED first, so the sentence is vacuous for it, not false.
  • "positions or permission sets" against any stored security-catalog type: no door stores a capability row.
  • The install-local docblock's "nothing is read": only the local ledger JSON is read, which no store or registry backs.

The review's escalation (③ 2) became stage W3. W3 measured publish-drafts, rollback and commits/:commitId/revert as cross-tenant (6117740670), so release-cut condition (f) is now W + W2 + W3 (6117751450). W3's fix is built on this branch and opens as its own draft once this PR is on main. This PR claims nothing about those doors.

Then: flipped ready and enqueued in this same stroke. On MERGED, a Landed record on #15204. W2 is a condition of the first next (6117174845, 6117531327).


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 12, 2026 08:17
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 12, 2026 08:17
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 12, 2026
Merged via the queue into main with commit 4806b18 Oct 12, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-15204-wall-package-refusal branch October 12, 2026 08:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants