Repository navigation
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
Copy link
Copy link
Closed
Labels
area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:servicespm:dispatchedpm:epicpriority:p2Medium: important, M3Medium: important, M3
Description
Filing gate ③: a task the maintainer directed. Decision #24443 was ruled A, with the access class in v18 (ruling of record 6259332195; the maintainer's 「其他同意」 on the line 「推荐:A,访问类进 v18,其余 v18 后」).
- Reader who acts: the epic seat (
epic:#15194,session_01Y5583bs2RDd53MH6kM1ca3). It dispatches the stages below to cloud devs, access first, measured first. - Dedupe: [Decision] A vendor package upgrade checks nothing: a rename silently re-enables what the customer switched off, and orphans, collisions and broken references go unreported, for every metadata type #24443's search found no carrier among the 105 open issues. This card and its post-v18 sibling (linked below) are the carriers.
Blocked-by: #24421 (PR #24428 adds the sharing_rule activation type that U1 reads)
What the ruling binds (verbatim, items 1–2, 4–5 of 6259332195)
- The comparison. One comparison of the customer's state (activation-ledger rows, overlays, clones, assignments) against the new registry runs at boot after a code-package change and at marketplace upgrade. It lists orphans, suspected re-enablements (the old name gone and a new name arrived in the same package, the old name carrying a switch-off or a clone), same-name collisions, and references broken inside clones and overlays.
- The access class (sharing rules, permission sets, positions, capabilities) is refused at marketplace upgrade until an administrator confirms, and raised as an administrator-visible warning at boot; operator-gated under a wall. Every other class is reported to the administrator, not only to the server log.
- Release: the access class (items 1–2 for the four access types) targets v18; the remaining classes and the retirement follow after v18.
- Record: ADR-0126 wall 3 (「no upgrade merges into or un-makes」 the customer's choices) becomes true in practice through this comparison; the stage that lands it says so in a dated note on ADR-0126 (Tier H, the maintainer merges). No rename spelling is added to the spec.
Measured starting points (origin/main e22defb13c; the seat's read-only measurement behind #24443)
- Keys are bare names.
- The activation ledger is unique on
(metadata_type, name)(packages/platform-objects/src/system/sys-metadata-activation.object.ts:189). It is read for positions and permission sets inpackages/core/src/security/resolve-authz-context.ts:945,:986-1006.package_idis stored but never compared. - Sharing rules get a ledger type under ruling A on [Decision] Declared sharing rules: what switching off a vendor's rule means once they are read from the registry #24378 (PR plugin-sharing, runtime: sharing_rule under regime C, so every same-name row is refused and declared rules are switched through the activation ledger (ruling A on #24378) #24428).
- The activation ledger is unique on
- Existing fragments to reuse:
packages/plugins/plugin-security/src/catalog-reference-report.ts: dangling catalog references, failing closed and reported at boot;objectql/src/security-catalog-namespace.ts:38-52: catalog name collisions refused at the package door;- the sharing orphan-grant sweep (
plugin-sharing/src/sharing-rule-service.ts,sweepOrphanedRuleGrants).
- The marketplace upgrade is a re-install over an existing ledger entry (
packages/cloud-connection/src/marketplace-install-local-plugin.ts:2577, answer at:1698). It withdraws nothing (:1975), and both the old manifest (in the ledger) and the new one are in hand. - A clone permission set's body is never checked against objects or fields. Unlisted fields are visible (
plugin-security/src/explain-engine.ts:886). - Every existing report is a server log line; none reaches the customer's administrator.
Stages (the seat cuts them; each measured first; each under 3,000 lines)
- U1: the comparison core, plus the boot report for the access class.
- It compares the customer's state against the registry after boot, for the four access types:
- activation rows whose name the registry no longer resolves;
- suspected re-enablements: the old name has gone and a new name has arrived in the same package, and the old name carries a switch-off or a clone;
- collisions;
- clone bodies naming an object or field the registry lacks;
- managed-set field denies that a same-object clone lacks.
- It raises an administrator-visible warning. Measure the existing channel, for example an administrator notification or a Setup health surface, and add no new surface without a stop-and-report. Under a wall, it is operator-gated.
- It carries the ADR-0126 dated note as a separate Tier H docs PR, or rides a docs-only PR the seat cuts beside it.
- It compares the customer's state against the registry after boot, for the four access types:
- U2: the marketplace refusal.
- Before the install door writes anything, it runs the comparison against the incoming manifest.
- If an access-class finding exists, it refuses with an ADR-0112 code naming each finding, until an administrator confirms (an explicit confirm flag or token on the same call).
- Before U2, the
repo:cloudseat reads cloud's remote-environment install path, which was not measured.
- Order and serial constraints.
- U1 runs after PR plugin-sharing, runtime: sharing_rule under regime C, so every same-name row is refused and declared rules are switched through the activation ledger (ruling A on #24378) #24428, which adds the sharing-rule ledger type.
- U1 and U2 meet C8 on
resolve-authz-context.ts(8c-2) andsharing-rule-service.ts(8c-3). The seat serializes them by file, and never builds ahead of an unmerged predecessor.
Acceptance
- A permission set the customer switched off. The vendor renames it in an upgrade, and its own position binds the new name.
- At boot, an administrator-visible warning names the old name, the new name and the affected positions.
- At marketplace upgrade, the install is refused with that finding, until it is confirmed.
- A sharing rule the customer switched off (ledger) or cloned. The vendor renames it. The same two outcomes apply.
- A clone set that names a field the new version removed, or that lacks a field deny the managed set gained, is reported.
- No finding: the upgrade proceeds unchanged.
- Pins, at both doors, for each of the four types:
- walled: operator-gated;
single: the administrator.
The post-v18 remainder (the other classes, and retiring packages/spec/src/kernel/package-upgrade.zod.ts) is a separate card.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:servicespm:dispatchedpm:epicpriority:p2Medium: important, M3Medium: important, M3
Activity
objectstack-fleet commentedon Oct 19, 2026
Scheduling note · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-19T04:10Z. The maintainer asked in the seat's chat: 「你所有的工作清单中,24446 这种优先级应该比较低吧」. Priority moves from p1 to p2.target:v18stays, because ruling 6259332195 item 4 puts the access class in v18.What p2 means for dispatch:
Why.
If v18's cut comes before this card is done, the seat brings it to the maintainer as the first candidate to move after v18. Moving it is the maintainer's word, not the seat's.
objectstack-fleet commentedon Oct 19, 2026
Release note · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-19T04:25Z. This card moves to 18.1. The maintainer's 「同意」 on the 18.0-fresh-installs scope (ruling record 6259608309 on #15194, item 3) amends item 4 of ruling 6259332195.target:v18is removed, and the repo has notarget:v18.1label. The scheduling note 6259462343 still applies.objectstack-fleet commentedon Oct 19, 2026
Maintainer decision: the access class moves from 18.0 to 18.1 · triage seat ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-19T07:29ZTriage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.
Provenance. The seat put its 18.0 GA leans to the maintainer in chat under the standing rule
6261447528("a metadata-protocol change is put to the maintainer for pulling forward"). The maintainer answered 「同意你的建议」.6259332195("the access class in v18"). The shape, items 1–2 and 4–5, is unchanged.target:v18and stayspm:blockedon plugin-sharing: refuse a new or renamed sys_sharing_rule row that takes a declared rule's name, and reconcile the shadowed declaration's grants on a same-name write (ruling B′ on #24378) #24421. upgrade-impact comparison after v18: report the non-access classes to the administrator, and retire the zero-consumer package-upgrade.zod.ts #24447 stays after this card.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Claim: epic seat round · 2026-10-22T22:53Z · U1 of #24446 (the access-class upgrade comparison and its boot report), next in the seat's dispatch order now that blocker #24421 is closed (PR #24428 landed as
964a23700f). It moved to 18.1 per triage record 6262048067; p2 fill-in per scheduling note 6259462343.Session:
session_01Y5583bs2RDd53MH6kM1ca3Account:
os-steveBranch:
claude/issue-24446-u1-access-upgrade-comparisonWorktree: the cloud session's own checkout (
mode:cloud)Domain:
domain:servicesSeat:
epic:#15194File surface:
packages/plugins/plugin-security/src/catalog-reference-report.ts, plus its boot wiring. It reuses that report's fail-closed and boot-report pattern, and reads the sharing ledger throughplugin-sharing.single(the administrator) and walled (operator-gated)..changeset/*.md.packages/cloud-connection/**).packages/core/src/security/resolve-authz-context.tsis READ-ONLY while fix(core): resolveLocalizationContext's direct sys_setting fallback reads tenant rows with no organization filter, taking the first row per key from any organization (ADR-0131 D8) #25248 is open, because that card's PR edits it. If U1 needs a change there, stop and report; the seat serializes it after fix(core): resolveLocalizationContext's direct sys_setting fallback reads tenant rows with no organization filter, taking the first row per key from any organization (ADR-0131 D8) #25248 lands.docs/adr/**. The ADR-0126 dated note is a separate Tier H docs PR that the seat cuts.ORGANIZATION_OWNERSHIP_SHAPE_GENERATIONchange.Container & model:
M,mode:cloud,model: TIER_DEFAULT. The review is at the tier the diff's declared face picks.Clause-②: no — expected: a boot report that raises an administrator-visible warning and refuses nothing. The dev measures and re-declares from the diff (
yesif it adds a published key, code or setting).Responsibility: after a code-package upgrade renames, removes or collides with an access-class item (sharing rule, permission set, position, capability) that the customer switched off or cloned, nothing tells the administrator, and every report is a server log line | none | reached on a store whose activation ledger rows or clone bodies name an access-class item the new registry no longer resolves, or a name the same package newly ships
Thread-read: none
Serial constraints cleared: plugin-sharing: refuse a new or renamed sys_sharing_rule row that takes a declared rule's name, and reconcile the shadowed declaration's grants on a same-name write (ruling B′ on #24378) #24421 is closed and PR plugin-sharing, runtime: sharing_rule under regime C, so every same-name row is refused and declared rules are switched through the activation ledger (ruling A on #24378) #24428 is merged (
964a23700f). fix(core): resolveLocalizationContext's direct sys_setting fallback reads tenant rows with no organization filter, taking the first row per key from any organization (ADR-0131 D8) #25248 (in flight) editsresolve-authz-context.ts, which U1 reads only. fix(plugin-auth): under single, an organization re-slugged to default through better-auth organization/update captures the platform-admin anchor — the single gate covers create only #25271 landed as09e07c259a(PR fix(plugin-auth)!: under single, no slug change makes another organization the Default Organization #25286) and freed this slot; its session is archived.12 remaining items
objectstack-fleet commentedon Oct 23, 2026
U2 measurement · os-dev for the epic seat (
session_01Y5583bs2RDd53MH6kM1ca3) · 2026-10-23T01:37Z · read-only, againstorigin/main54df640b38(U1 landed). Branchclaude/issue-24446-u2-marketplace-access-refusal(claim 6355237423; itsClaim:names this branch). ⛔ No code yet. This waits for the seat's go.1. The framework's install door:
packages/cloud-connection/src/marketplace-install-local-plugin.tsPOST /api/v1/marketplace/install-local→handleInstall(:1141). In order, at54df640b38:manage_metadata), body parse, step 0installMode:1150–1178:1180–1294ManifestSchema.shape.id. Both branches converge here.:1300refuseWalledCatalogPackage,:3695):1353:1361–1455unmanagedleaves here forinstallUnmanaged, a copy into a new base and never an upgrade:1466:1476findConflictgivesnone,marketplace(the re-install) oruser-code(refused):1506,:2577previousEntry = this.ledger.read(manifestId).entry:1533manifestService.register(converted)is the first write (the registry, plus the copy bridged into the metadata service):1584,:1592this.ledger.write(entry):1630syncSchemas, binder, side effects, seed,announceHotInstall; the answer'supgradedFrom:1638–1698findConflict === 'marketplace'case. That case is either a live ledger entry, or a package the registry holds whose ledger entry was lost. It runs the same steps as a fresh install, then answersupgradedFrom: 'previous-marketplace-version'.:1592. Nothing before it writes anything.:1533) to step 3.manifest, converted forward at:1584. The conversion is pure, so it can move up.previousEntry.manifest.findConflictthere is no old manifest. But the comparison below does not need one: the registry still holds the old package's items, each carryingpackageId.conflict === 'marketplace'.packages/cli/src/commands/package/install.ts:203. It already branches on the D5e code literal (:217) and carries--confirm-global-uniques(:114,:200);packages/cloud-connection/src/marketplace-install-local-*.test.ts, about 30 files.@objectstack/clientnames the route only in a comment (:2969). The console's install action lives in../objectui, outside this repository.POST /api/v1/packageswithoverwrite(packages/runtime/src/domains/packages.ts:1184, throughprotocol.installPackage). It also replaces an installed package. It is not the marketplace door the ruling names, so I propose not to touch it in U2. If the seat wants it covered, it calls the same check (§2) in a follow-up card.2. The comparison: U1's
collectAccessUpgradeFindingsagainst the incoming manifest, with no copy of its rulescollectAccessUpgradeFindings(engine, deps)(plugin-security/src/access-upgrade-report.ts:343) reads the registry through three injected seams only:engine.getObject(objects and fields);deps.catalog(listandresolvefor permission sets, positions and capabilities, each entry carryingpackageId);deps.sharingRules.listDeclaredRules().The customer's state is read through
engine.find: the ledger, and thesys_sharing_rulerows.So the check runs U1's function unchanged, twice, over a projected registry: the booted registry with package P's items replaced by the incoming manifest's.
packageId === Pare dropped. The incomingpermissions,positionsandcapabilities, after the forward conversion, are added as entries withpackageId: P.sharingRules.getObject: P's objects are taken from the incoming manifest. An object the old version shipped and the new one drops answersundefined.find: passed through unchanged, because the customer's state is not projected.The two runs:
accessUpgradeSummaryalready reads them. The confirm token (§3) is U1'saccessUpgradeDedupKeytaken over the delta.Where it lives, and how the door reaches it.
cloud-connectiondoes not depend onplugin-security, and should not.security.accessUpgradeCheck, beside its othersecurity.*services (security-plugin.ts:1729–1747). Its shape ischeck({ packageId, manifest }) → { findings, findingSet } | { stopped, … }.access-upgrade-precheck.ts. It builds three adapters and computes the delta.registerUninstallCleanuppattern (metadata-protocol/src/protocol.ts:7174). It costs an edit toprotocol.tsfor a single check, so I do not recommend it.Fail closed:
stopped: the ledger, the catalog or the declared rules unreadable). The door refuses503 SERVICE_UNAVAILABLE, naming the store, and writes nothing. A comparison that did not happen is not "no finding".security.accessUpgradeCheckservice (a composition without plugin-security, so no access item is enforced at all). The install proceeds, and onewarnline says that no access check ran and why.3. The refusal
The code: a new row,
ACCESS_UPGRADE_CONFIRMATION_REQUIRED, under'@objectstack/cloud-connection'inpackages/spec/src/api/error-code-ledger.zod.ts, besideUNIQUE_SCOPE_CONFIRMATION_REQUIRED(:1190).UNIQUE_SCOPE_CONFIRMATION_REQUIREDnor a standard member fits, because each would send a client down the wrong branch.CONFLICT/RESOURCE_CONFLICTalready answer 1f's unmanaged-copy refusal, which a confirmation does not lift.INSTALL_MODE_NOT_PERMITTED(:1166) was admitted on.standardSynonymOfmaps no standard member onto it: no member's tokens coverACCESS_UPGRADE_CONFIRMATION_REQUIRED. I will run the admission test to confirm.api-surfacechange; I will runcheck:generatedto prove it.Status
409, the D5e gate's status: the request is well formed and holds until the caller answers.check-governed-merges --teston the candidate list (the door, a new door test, plugin-security's two files,security-plugin.ts, the ledger file, the CLI'sinstall.tsand one changeset, about 1,920 lines) exits0: 0 of 8 paths governed, and the size is under 3,000. The ledger row is inpackages/spec, which is not on the register.The confirm shape: a token on the same call,
confirmAccessUpgrade: "<findingSet>".error.details:findings: per finding, its kind, type, old name, the candidate new names and binding positions (permission sets), the organization (sharing-rule collisions), and a one-linedescribe;findingSet: U1's dedup-key form over the delta;confirmWith: { body: { confirmAccessUpgrade: findingSet }, cli: 'os package install … --confirm-access-upgrade <findingSet>' }, the D5e gate'sconfirmWithshape.trueis not accepted. A confirmation is of the findings shown, never a standing "yes".true, because here the confirmation un-makes a customer's security choice.warnline names the confirmer, the package and the findingSet;accessUpgradeConfirmation: { findingSet, confirmedBy, confirmedAt }, likeglobalUniqueAttestation;What each caller sees. The door's existing admission already decides who can reach step 2c, so the refusal needs no audience logic of its own.
group,isolated): the D12 refusal at:1353answers every non-operator403 FORBIDDENbefore step 1c. So the only caller who can see the findings and send the token is the platform operator: operator-gated.403and no findings (nothing leaks to a tenant), and the walled operator gets409with the findings.single: the caller is amanage_metadataholder, i.e. the administrator (admin_full_access). They get409with the findings, and their token admits the install.4. Size and Tier
plugin-security/src/access-upgrade-precheck.ts(new: projection, delta, service)plugin-security/src/security-plugin.ts(registers the service)cloud-connection/src/marketplace-install-local-plugin.ts(step 2c, the refusal, token check, ledger record)spec/src/api/error-code-ledger.zod.ts(one row)cli/src/commands/package/install.ts(--confirm-access-upgrade <token>, prints the findings)singleadmin, walled operator, walled tenant admin) × (finding, CONTROL); a stale token; a baretrue; fail-closed; no service; ablation. Plus the precheck's unit testsAbout 1,700–1,900 lines. Not governed. Ordinary queue landing.
5. What the framework door needs from cloud's install path (6355241020)
Nothing, for the framework door itself: it is self-contained.
security.accessUpgradeCheck.check({ packageId, manifest })in the target runtime. That path would call it before its first write, and answer with the same code,detailsand token.repo:cloudseat's answer decides whether a cloud-side card is needed.6. Recommendation
Build as above:
handleInstall, on re-install only, between the D5e gate and the first write;409 ACCESS_UPGRADE_CONFIRMATION_REQUIRED(one new ledger row), withdetails.findingsand a findingSet token confirmed on the same call;singleadministrator already isolated by the door's existing admission.Expected declarations, to be re-declared from the diff:
Clause-②: yes (narrowing): an upgrade that installed yesterday is now refused until it is confirmed. The changeset carries the BREAKING banner, the migration line (confirm with the token) and its ADR-0087 disposition.Metadata-protocol: none: no authorable key changes, so the changeset level ispatch.Questions for the seat:
POST /api/v1/packagesoverwriteleft to a follow-up card. I recommend yes.true. I recommend the token.Waiting for the seat's go, and for the
repo:cloudseat's answer or the maintainer's ruling that U2 covers the framework door alone.Generated by Claude Code
objectstack-fleet commentedon Oct 23, 2026
Seat GO · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-23T01:39Z. U2 of this card, on measurement 6355301005 (claim 6355237423). GO: build as measured.marketplace-install-local-plugin.ts, between 2b (previousEntry) and the first write at step 3. It runs on a re-install only (findConflict === 'marketplace'); a fresh install has no customer choice of its own to un-make.collectAccessUpgradeFindingsruns unchanged, over the booted registry and over the projection with package P's items taken from the incoming manifest. The door refuses on the delta only. It is reached through the plugin-security servicesecurity.accessUpgradeCheck, socloud-connectiontakes no dependency onplugin-security. A stopped check fails closed (503).ACCESS_UPGRADE_CONFIRMATION_REQUIRED, a new error-code ledger row (check-governed-mergeson the candidate list: 0 of 8 paths governed).detailsnames each finding and carries thefindingSettoken andconfirmWith.--confirm-access-upgradeis IN U2's surface. The CLI is the door's one caller in this repository; without the flag the confirm cannot be used.POST /api/v1/packageswithoverwrite(runtime/src/domains/packages.ts) is left to a follow-up card. The ruling names the marketplace upgrade. The seat files that card once this PR addssecurity.accessUpgradeCheck, so the second door calls the same check rather than a copy.true. A stale token is refused again.singleanswers the administrator. Each type is pinned with a CONTROL at the door.repo:cloudread (6355241020) is no longer a precondition for building the framework door. The measurement shows the framework door needs nothing from cloud. The "before U2" line on this card was the seat's own sequencing guard, not part of ruling 6259332195. If cloud has its own install path, it calls the same service before its first write, in cloud's own follow-up. The request stays open for that.Clause-②: yes (narrowing)(a new service, code and confirm field; an upgrade refused) andMetadata-protocol: none. The dev re-declares from the diff.isolatedandgroupon the final head; the harness file; the dispatch gates; andcheck-governed-merges --branchon the final file list. If that list turns governed, or passes 3,000 lines, stop and report.wip: door pins and the check's own pins (#24446 U2)
wip: changeset and regenerated reference docs (#24446 U2)
wip: CLI --confirm-access-upgrade pins (#24446 U2)
docs(cli): os package install --confirm-access-upgrade (#24446 U2)
objectstack-fleet commentedon Oct 23, 2026
os-dev-report
{ "issue": 24446, "status": "done", "branch": "claude/issue-24446-u2-marketplace-access-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/25355", "session": "session_01U9Aia8cnVwrpSUL9KgVzdu", "premise_still_valid": true, "summary": "U2 only (Part of #24446), on measurement 6355301005 and GO 6355317159 (claim 6355237423). install-local gains step 2c: a managed re-install (findConflict === 'marketplace') asks the new plugin-security service security.accessUpgradeCheck, after the D5e gate and before the first write. The check runs U1's collectAccessUpgradeFindings unchanged twice, over the registry and over a projection with package P's catalog entries, declared sharing rules and objects/fields taken from the incoming manifest; it refuses on the delta only. A finding answers 409 ACCESS_UPGRADE_CONFIRMATION_REQUIRED (details.findings with a describe line each, details.findingSet token, details.confirmWith). The same call with confirmAccessUpgrade: <token> installs and records accessUpgradeConfirmation in the ledger entry; a bare true or a stale token is refused again. A stopped check answers 503 SERVICE_UNAVAILABLE and writes nothing. With no check service, the upgrade proceeds with one warn line. Walled is operator-gated by the existing D12 refusal; single answers the administrator. Also: one error-code ledger row (and its three generated reference pages), CLI --confirm-access-upgrade <token> with refusal rendering, and the flag documented in content/docs/deployment/cli.mdx. cloud-connection gains @objectstack/plugin-security as a devDependency only, for its door test; there is no cycle, since it already reaches it through runtime. Head ac8ce968, +1,471/-9 over 18 files, 0 governed, so ordinary Tier. Declared Clause-②: yes (narrowing) and Metadata-protocol: none, in the PR body and the changeset; every package is at patch, with the ADR-0087 marker not-required (no-migration-prescription). One limit added to U1's accepted ones: an object the incoming manifest adds that the registry does not yet hold is judged on its declared fields only.", "tests": "cloud-connection `pnpm test`: exit 0, 55 files / 704 passed, including the new marketplace-install-local-access-upgrade.test.ts with 34 cases. These are the four types (permission set, position, sharing rule, capability) × single admin 409 / isolated and group operator 409 / isolated and group tenant admin 403 with the check not called and no finding shown, plus CONTROLs under single and isolated (200); and the token installs and is recorded, true refused, stale token refused, a pre-existing finding does not block, a stopped check gives 503 and no write, no service gives 200 and a warning. plugin-security `pnpm test`: exit 0, 213 files / 4457 passed, 45 skipped, including access-upgrade-check.test.ts (15). That suite runs a real ObjectQL + SqlDriver and the service SecurityPlugin registers, over the four types with CONTROLs, plus a clone naming a field the upgrade removes (v1 read from the registry), a field deny the managed set gains, the delta, the fail-closed stop and unbound-before-start. cli: typecheck plus `vitest run --project unit` exit 0, 326 files / 4802 passed, including package-install-access-upgrade.test.ts (3). spec: error-code-ledger.test.ts 23/23; check:generated reported check:docs stale (the new code in three reference pages), fixed with --fix, then committed. typecheck plugin-security + cloud-connection exit 0; eslint on the touched files exit 0. Ablations, each committed first, then mutated and restored with git checkout HEAD, porcelain clean: (1) door step 2c disabled turns 21 door pins red (the 403 wall pins correctly stay green); (2) delta replaced by the whole projected report turns the 'predates' pin red in both suites; (3) catalog projection off turns 4 of the check's pins red. R1: `OS_LOG_LEVEL=info pnpm dev -- --fresh` on the showcase logs the access upgrade report clean with absent []. The same single runtime driven through the door: v1 install 200, switch-off 200, CONTROL 1.1.0 keeping the name 200, rename 2.0.0 gives 409 naming rfour_sales_rep, candidate rfour_sales_representative and position rfour_lead, bare true 409, token 200. R4 (code head 8c56e51e): production `objectstack serve` of the built showcase, persistent SQLite, NODE_ENV=production, OS_PLATFORM_OWNER_EMAIL=operator@r4.example. Temporary and reverted, tree clean: @objectstack/organizations declared in the showcase, membership policy auto with audience open (and its self-registration set) so a tenant could register, and the tenant's email_verified and a manage_metadata grant in its own organization written into the test DB. Under isolated and under group the operator got the same 200/200/200/409/409/200 sequence, with findingSet security.access_upgrade:7ce41192…. A tenant organization owner got 403 at the capability gate; given manage_metadata in their organization, 403 FORBIDDEN from the ADR-0131 D12 refusal; no findings shown, and the check never ran for them (no 'stopped' log line). An isolated restart after the confirmed upgrade: U1's boot report raised the re-enablement plus the operator line. No log carried 'was REFUSED' or 'reached no administrator'. Upgrade harness `packages/qa/dogfood/test/upgrade-18x-harness.dogfood.test.ts`, both fixture legs: exit 0, 16/16; its four upgraded boots each log the access report clean.", "gates": "`node scripts/pm/dispatch-gates.mjs --commands` exit 0, listing 133 commands, each run with its exit code captured before any pipe (head 8c56e51e): 129 exit 0 first time. Four exited 3, as PREREQUISITE refusals rather than findings. check-plugin-teardown-shape --self-test needed its pinned fixture commit 621a4876 in this shallow clone; after fetching it, exit 0. check:skill-examples, check:dual-build-cjs-loads and check:i18n-coverage needed built dists; after `pnpm build`, all exit 0. So 133/133 exit 0. After the final docs-only push: check-cli-command-ids, check-doc-anchors, check-cli-examples-parity, check-closed-ceremony-text, check-docs-transcript-drift, check:doc-authoring and check:role-word all exit 0. `node scripts/pm/check-governed-merges.mjs --branch …` exit 0: 0 of 17 paths governed, 1,463 lines ≤ 3000 at 8c56e51e. The final head adds only content/docs/deployment/cli.mdx (+17), which is not governed. CI on the final head ac8ce968, all success: Test Core (1–6/6 and the aggregate), Build Core, TypeScript Type Check, Lint & Repo Gates, Governed Surface Queue Guard, Check Changeset, Check PR Size, Part-of PR must not also close its card, The card this PR closes must claim this branch, both single-writer and same-issue guards, Spec property liveness, Validate Package Dependencies, Check Documentation Links, Dogfood Regression Gate, Build Docs, and every Lint/Type Check lane. The opt-in harness, tarball smoke and dogfood shards were skipped by design.", "dogfood_battery": "8c56e51e · `os-verify-lock: VERDICT command-exit 0` (`pnpm --filter @objectstack/dogfood test`, no shard, no path) · Test Files 289 passed | 3 skipped (292) · Tests 2491 passed | 18 skipped (2509) · no failures. The final head ac8ce968 differs only by content/docs/deployment/cli.mdx, so the battery was not re-run.", "mcp_calls": "github read tools: issue_read ×3, pull_request_read ×4 (get #25311 ×1, get_check_runs ×3); claude-code-remote: send_message ×2 (seat), subscribe_pr_activity ×1, send_later ×1. No GitHub write tool.", "api_writes": "3 through scripts/pm (fleet-write relay, POST /repos/objectstack-ai/objectstack/dispatches): the measurement comment 6355301005 on #24446, pr_create #25355 (draft, assignee os-steve), and this os-dev-report comment. Plus git push ×6 on the branch.", "open_questions": [], "out_of_scope_findings": [ "class: none (follow-up the seat already holds) · POST /api/v1/packages with overwrite (runtime/src/domains/packages.ts) also replaces an installed package and is not checked; per GO 6355317159 the seat files that card, which calls security.accessUpgradeCheck · noted, not filed", "class: none (observation, as in U1) · no example app declares @objectstack/organizations, and a walled production serve of the showcase also needs a declared membership policy (OS_AUTH_MEMBERSHIP_POLICY), an OS_SECRET_KEY, and for a self-registering tenant an audience posture with its self-registration set; R4 set these temporarily · carrier: none · noted, not filed", "class: none (pre-existing doc gap) · content/docs/deployment/cli.mdx's `os package install` options table does not list --install-mode (ADR-0131 D6); left as is, outside U2 · noted, not filed" ] }objectstack-fleet commentedon Oct 23, 2026
Seat note · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-23T03:44Z. On U2's contract review (PR #25355, PASS 6356845701), the seat rules the reviewer's three escalations:os servealways composes SecurityPlugin, and wall 3's "never writes the ledger" holds either way. The warning over-claimed, because plugin-sharing alone still enforces asharing_ruleswitch-off. Its wording is corrected in U2's next push; there is no behaviour change.repo:cloudrequest (6355241020): it stays open for cloud's own install path. It does not gate U2.fix(cloud-connection): the absent-check warning says what is true (#2…
objectstack-fleet commentedon Oct 23, 2026
Landed · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-23T05:01Z. U2: PR #25355 merged intomainas448374d3f8. The card stays open for the ADR-0126 dated note (ruling item 5).ACCESS_UPGRADE_CONFIRMATION_REQUIRED, naming each finding with afindingSettoken, until the same call confirms with that token.single: the administrator.security.accessUpgradeCheck. The CLI gains--confirm-access-upgrade.CONTRACT_REVIEW_TIERon the narrowing face. PASS 6356845701 atac8ce968, with escalations ruled in 6356868153; then PASS 6357354613 at the final head; then ACCEPT 6357361871. R1 and R4 (isolated,group; operator and tenant administrator) were measured by the dev on the final head.POST /api/v1/packagesoverwrite door (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);objectstack-fleet commentedon Oct 23, 2026
Claim: epic seat round · 2026-10-23T05:02Z · the ADR text #24446's ruling owes, now that U1 and U2 have landed (PR #25311
54df640b38; PR #25355448374d3f8). One Tier H docs PR also carries the ADR-0131 D5 line owed by #25286 (#15212 ledger 6352526831).Session:
session_01Y5583bs2RDd53MH6kM1ca3Account:
os-steveBranch:
claude/issue-24446-adr-0126-wall-3-noteWorktree: the cloud session's own checkout (
mode:cloud)Domain:
domain:servicesSeat:
epic:#15194File surface:
docs/adr/0126-packaged-metadata-customization-model.md: one dated note (ruling 6259332195 item 5). Wall 3 ("no upgrade merges into or un-makes" the customer's choices) is now true in practice for the access class:singleand to the operator's log under a wall;POST /api/v1/packagesoverwrite door (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) and the catalog-name collision before the first write (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).docs/adr/0131-total-organization-ownership-no-null-organization-id.md: one line in D5's 2026-10-22 note, or a dated note beside it. Undersingle, asys_organizationslug changed todefaultis admitted only on the current Default Organization and refused 403 for every context (PR fix(plugin-auth)!: under single, no slug change makes another organization the Default Organization #25286,09e07c259a). Name the removed move (the Default Organization among several organizations) and the stranding it leaves.Container & model:
S,mode:cloud,model: TIER_DEFAULT. The review is at the tier the diff's declared face picks.Clause-②: no — expected: ADR notes only; no published surface moves.
Responsibility: ADR-0126 wall 3 and ADR-0131 D5 do not yet record what U1, U2 and fix(plugin-auth)!: under single, no slug change makes another organization the Default Organization #25286 made true, so a reader of the ADRs cannot tell which upgrade doors are guarded and which are still open | none | reached by anyone reading the ADRs to learn the access-class upgrade guarantees or the Default Organization slug rule
Thread-read: none
Serial constraints cleared: U1 and U2 have landed. No other in-flight PR touches either ADR file.
objectstack-fleet commentedon Oct 23, 2026
os-dev-report
{ "issue": 24446, "status": "blocked", "branch": "claude/issue-24446-adr-0126-wall-3-note", "pr": null, "session": "session_01HbVJUgUf5HuWXPQvAo4sFg", "premise_still_valid": true, "summary": "Both notes are written, committed and pushed at 6cddca9 on claude/issue-24446-adr-0126-wall-3-note, one commit over origin/main 448374d3f8, +64/-0, exactly the claim's two files. ADR-0126 §6 wall 3 gains a dated note (2026-10-23) covering U1 (#25311, 54df640b38) and U2 (#25355, 448374d3f8): who is told (the administrator under single, the operator's log under a wall), the findingSet confirm token, the two §9 limits, the two open doors (#25382, #25366) and the other classes after v18. ADR-0131 D5 gains a dated note beside its 2026-10-22 note: the re-slug-to-default rule from #25286 (09e07c259a), the removed move and the stranding. No decision text changed. Blocked before opening the PR: the only write channel I have cannot open it the way the claim and the dispatch require. See open_questions.", "tests": "n/a: docs-only diff (two ADR notes); no package touched, no test owed.", "gates": "Head 6cddca9. `node scripts/pm/dispatch-gates.mjs --commands` derived 20 commands; all 20 exit 0 (each exit captured before any pipe). `pnpm --filter @objectstack/lint run check:doc-formula-expressions` first exited 3 (PREREQUISITE: formula/lint not built); after `pnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lint` under os-verify-lock (VERDICT command-exit 0) it exits 0. `dispatch-gates --ran`: '20 derived famil(ies) accounted for, 20 run, 0 NOT-MEASURED (a DERIVED zero, all 20 recorded an exit code and none of them is 3)'. `check-governed-merges --branch claude/issue-24446-adr-0126-wall-3-note`: exit 3, 'GOVERNED, landing tier: H', docs/adr/** x2, 64 changed lines (expected). Changeset: none; docs/adr/** publishes nothing and the claim allows no other file, so `skip-changeset` is to ride pr_create's labels. CI: NOT MEASURED, no PR yet.", "dogfood_battery": "n/a: no `!` in the planned title and no (narrowing) arm", "mcp_calls": "0 GitHub MCP calls; reads used `gh api` GETs", "api_writes": "1: this comment (fleet-write relay, op comment, POST /repos/objectstack-ai/objectstack/issues/24446/comments). The branch push is git, not REST.", "open_questions": [ { "question": "How should the PR be opened? The relay's pr_create (scripts/pm/fleet-write/ops.mjs) FORCES draft: true, and on a Tier H diff its governed-review follow-up POSTs requested_reviewers for every GOVERNED_APPROVERS account. The dispatch says 'never open a draft first' and 'the seat requests the maintainer's review; you do not'. No GitHub MCP create-PR tool is in this session, and a bare REST write is barred.", "options": [ "A: I open it through the relay pr_create (draft, assignee as the card's, label skip-changeset, body with Part of #24446). The executor's follow-up requests the approvers at create time. The seat readies it when it chooses.", "B: the seat opens the PR itself from head 6cddca9 with the body I hand over, so its own batching of the maintainer's request is kept. I then post the final os-dev-report and message the seat on CI.", "C: I open it via the relay and the seat accepts both deviations in writing on this card first." ], "recommendation": "A, if the seat confirms that the relay's automatic approver request is acceptable here: it is the designed path for every seat-opened Tier H PR, and the draft is what os-dev.md's DoD prescribes. Otherwise B." } ], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 23, 2026
os-dev-report
{ "issue": 24446, "status": "done", "branch": "claude/issue-24446-adr-0126-wall-3-note", "pr": "https://github.com/objectstack-ai/objectstack/pull/25384", "session": "session_01HbVJUgUf5HuWXPQvAo4sFg", "premise_still_valid": true, "summary": "PR #25384 at head 6cddca9 (+64/-0, two ADR files, notes only) supersedes the blocked report 6357957117. ADR-0126 §6 wall 3 gains a dated note: U1 (#25311, 54df640b38) and U2 (#25355, 448374d3f8) make the wall true in practice for the access class. The note covers who is told, the findingSet token, the two §9 limits, the open doors #25382 and #25366, and the other classes after v18. ADR-0131 D5 gains a dated note for #25286 (09e07c259a): the re-slug-to-default rule, the removed move and the stranding. Opened through the relay per the seat's ruling A: a forced draft, with skip-changeset, assignee os-steve and Part of #24446. The relay requested reviewers hotlong and os-zhuang, read back. The seat then readied the PR; I did not touch its draft state.", "tests": "n/a: docs-only diff; no package touched.", "gates": "Local, head 6cddca9: dispatch-gates --commands derived 20 commands, all exit 0, and --ran gives a DERIVED zero. check:doc-formula-expressions exited 3 (prerequisite) until formula and lint were built, then 0. check-governed-merges --branch: GOVERNED, Tier H, as expected. CI on 6cddca9: 51 check runs, 33 success, 18 skipped, 0 failure; mergeable_state clean.", "dogfood_battery": "n/a: no `!` in the title and no (narrowing) arm", "mcp_calls": "0 GitHub MCP calls; 2 claude-code-remote send_message to the seat", "api_writes": "3 via the fleet-write relay: POST issues/24446/comments (6357957117, blocked report); pr_create (POST /pulls, then POST issues/25384/assignees, POST issues/25384/labels, then the executor's governed-review requested_reviewers); POST issues/24446/comments (this report)", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 23, 2026
Landed and closing · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-23T07:24Z. PR #25384 (the ADR text owed by ruling 6259332195 item 5) merged intomainasb22ab8d6fb, approved by the maintainer (os-zhuang). Every part of this card's access-class scope has landed:54df640b38)448374d3f8)b22ab8d6fb)Carried in the shared queue, not owed here:
POST /api/v1/packagesoverwrite door calls the same check.package-upgrade.zod.tsfollow after v18 (ruling item 4), under their own card.Closed as completed.