Repository navigation
cloud-connection, plugin-security: a marketplace upgrade that would un-make an access choice is refused until it is confirmed (#24446 U2) - #25355
Conversation
…arketplace door (#24446 U2) Claude-Session: https://claude.ai/code/session_01U9Aia8cnVwrpSUL9KgVzdu Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9Aia8cnVwrpSUL9KgVzdu Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9Aia8cnVwrpSUL9KgVzdu Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U9Aia8cnVwrpSUL9KgVzdu Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 4 package(s): 32 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 156 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 3e0152e3a74d636ffede54ce555c28ff51459a0a && git checkout 3e0152e3a74d636ffede54ce555c28ff51459a0a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 303aef069ebdfee276d4b053a11c1bc2e8fa0e90 8989149c2dc0f4f9acd7f8c820e4b7ac89a1c663 && git checkout -B drift-repro 303aef069ebdfee276d4b053a11c1bc2e8fa0e90 && git merge --no-ff 8989149c2dc0f4f9acd7f8c820e4b7ac89a1c663
node scripts/docs-audit/affected-docs.mjs --json 303aef069ebdfee276d4b053a11c1bc2e8fa0e90
|
Claude-Session: https://claude.ai/code/session_01U9Aia8cnVwrpSUL9KgVzdu Co-authored-by: Claude <noreply@anthropic.com>
…-marketplace-access-refusal
Contract reviewServed-tier: ① Derived judgmentsLine numbers are the head's unless a file is named as new (then the diff's).
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…4446 U2, review E1) Without the security plugin a re-install is not checked against the access choices it could un-make; that does not mean the composition enforces none (plugin-sharing enforces a sharing_rule switch-off on its own). Claude-Session: https://claude.ai/code/session_01U9Aia8cnVwrpSUL9KgVzdu Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: This is the delta round on the PASS record posted as comment 6356845701 on the previous head (the first record). The head moved by two commits, both read in full against the clone's objects: the first is a merge of ① Derived judgmentsThe merge (4977ef8) — right, and it changes nothing U2 claims. The merge base of E1's text ( Carried forward unchanged from the first record, re-checked against this head where the delta could have touched them: item 1 (step 2c placed after the D5e gate and before the first write, re-install only, never a fresh install, the orphaned-registry branch falling back to ② Semver levelUnchanged from the first record, and re-read on this head. The changeset at ③ Boundary flagsSeat rulings on the first record's escalations, recorded (#24446, 6356868153):
Carried forward unchanged: seat GO (a) the CLI flag in U2's surface, done; (b) Posting condition. CI on Implemented-by: VERDICT: PASS |
|
Seat ACCEPT · epic PM Seat check
|
…cords the single-posture re-slug rule ADR-0126 §6 wall 3 gains a dated note: the boot report (#25311, 54df640) and the marketplace refusal (#25355, 448374d) make it true in practice for sharing rules, permission sets, positions and capabilities. The note names who is told under `single` and under a wall, the findingSet confirm token, the two accepted limits under §9, the two doors still open (#25382, #25366), and that the other classes follow after v18. ADR-0131 D5 gains a dated note beside its 2026-10-22 note: under `single`, a slug changed to `default` is admitted only on the current Default Organization and refused 403 for every context (#25286, 09e07c2). It names the removed move and the stranding it leaves. Notes only; no decision text changes. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HbVJUgUf5HuWXPQvAo4sFg
Part of #24446
U2 of #24446: the marketplace refusal. It works to the measurement in 6355301005 and the seat's GO in 6355317159, under claim 6355237423. U1 (the boot report) landed as
54df640b38. The ADR-0126 dated note is a separate Tier H docs PR that the seat cuts. #24446 remains open for it.Clause-②: yes (narrowing): a marketplace re-install that installed before is now refused
409while it would un-make an access choice made in this system, until the same call confirms it (narrowing); the door gains theconfirmAccessUpgradebody field and theACCESS_UPGRADE_CONFIRMATION_REQUIREDcode, plugin-security gains thesecurity.accessUpgradeCheckservice and its exports, and the CLI gains--confirm-access-upgrade <token>(widening).Metadata-protocol: none
What changed
cloud-connection: step 2c ofPOST /api/v1/marketplace/install-local. It runs on a managed re-install over an installed package (findConflict === 'marketplace'), after the D5e gate and before the first write (manifest.register). A fresh install has no customer choice of its own to un-make, so it is never checked. The door asks thesecurity.accessUpgradeCheckservice as a capability question, socloud-connectiontakes no runtime dependency onplugin-security.409 ACCESS_UPGRADE_CONFIRMATION_REQUIRED, and nothing is registered or written.error.details.findingsnames each finding: its kind, type, name, the candidates and binding positions, the missing names, and a one-linedescribe.error.details.findingSetis the token, anderror.details.confirmWithis the retry.confirmAccessUpgrade: <findingSet>.trueconfirms nothing.accessUpgradeConfirmation: { findingSet, confirmedBy, confirmedAt }in its ledger entry.503 SERVICE_UNAVAILABLE, and nothing is written.warnline says no check ran.plugin-security:access-upgrade-check.ts(new), registered bySecurityPluginassecurity.accessUpgradeCheck. It runs U1'scollectAccessUpgradeFindingsunchanged and restates none of its rules. It runs it twice:packageId === P;The customer's state (the ledger, the
sys_sharing_rulerows) is never projected. The answer is the delta: what run 2 finds that run 1 does not, and a finding that only grew keeps its new parts only. So a finding that predates the upgrade, or that another package caused, never blocks it; U1's boot report keeps raising those. "P's previous version" is the door's ledger entry, or else the registry's own record of the package. Sources are resolved at every call.spec: one error-code ledger row,ACCESS_UPGRADE_CONFIRMATION_REQUIREDunder@objectstack/cloud-connection, and the three generated reference pages that list it.cli:os package install … --confirm-access-upgrade <token>. It sends the token. On the refusal, it prints every finding'sdescribeline and the exact retry.Who sees it
isolated,group): only the platform operator reaches step 2c. The existing ADR-0131 D12 refusal answers a tenant administrator403before the check runs, and shows no finding.single: the administrator (manage_metadata).Limits, as in U1 (accepted by the seat, 6353222762)
Scope held
runtime/src/domains/packages.ts(POST /api/v1/packageswithoverwrite). It is the seat's follow-up card, which calls this same service.docs/adr/**, not U1's boot report (imported read-only), and noORGANIZATION_OWNERSHIP_SHAPE_GENERATIONchange.cloud-connectiongains@objectstack/plugin-securityas a devDependency only, for its door test. There is no cycle: it already reaches plugin-security through@objectstack/runtime.Tests
cloud-connection/src/marketplace-install-local-access-upgrade.test.ts(34). The real door runs against the realcreateAccessUpgradeCheck, over an in-memory access state with version 1 installed through the door. For each of the four types (permission set, position, sharing rule, capability):single: the administrator gets 409 with the finding;isolatedandgroup: the operator gets 409;isolatedandgroup: a tenant administrator gets 403 from the wall, the check is not called, and no finding is shown;singleandisolated(200).trueand a stale token are refused; a finding that predates the upgrade does not block it; a stopped check gives 503 and writes nothing; no check service means 200 and a warning.plugin-security/src/access-upgrade-check.test.ts(15). The check asSecurityPluginregisters it, over a realObjectQLand SQL driver, for the four types with CONTROLs. It also covers a clone naming a field the upgrade removes (version 1 read from the registry), a field deny the managed set gains, the delta, the fail-closed stop, and "unbound before start".cli/test/package-install-access-upgrade.test.ts(3): the flag, its absence, and the refusal's rendering.The proof readings are in the os-dev-report on #24446: CI, the dispatch gates,
check-governed-merges --branch, R1, R4 underisolatedandgroup, the upgrade harness and the dogfood battery.Generated by Claude Code