Skip to content

plugin-auth, plugin-sharing: the last-admin guard, self-registration, default-organization and sharing-rule readers read the security catalog and the activation ledger (#15204 stage 2b) - #22745

Merged
objectstack-fleet[bot] merged 47 commits into
mainfrom
claude/issue-15204-s2b-auth-sharing-readers
Oct 11, 2026
Merged

objectstack-fleet[bot] merged 47 commits into
mainfrom
claude/issue-15204-s2b-auth-sharing-readers

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Stacked on stage 1 (#22723). The base is claude/issue-15204-s1-position-permission-sets, not main. GitHub retargets this PR to main when stage 1 merges, because the repository deletes merged head branches. Until then the diff below is this stage's alone.

Part of #15204 (stage 2b of the cutover batch: the plugin-auth and plugin-sharing readers). Stages 2a, 2c and the later stages stay on the card; #15204 remains open.

Clause-②: yes (narrowing: a zero-administrator write is refused when an unscoped grant names a set the security catalog does not hold, and self-registration is refused when its declared set is not in the catalog or is switched off in the activation ledger; widening: writes to a sys_permission_set row are no longer judged by the guard, and a ledger write switching admin_full_access off is refused only when it would leave no administrator)

Size: 2,251 changed lines (additions + deletions against stage 1's head bf6bdf071: 1,184 additions, 1,067 deletions, 24 files), under the 3,000 line.

What this does

Stage 1 moved the authorization resolver onto the security catalog (a set or position exists when the catalog bound to the engine holds its definition) and onto the activation ledger (sys_metadata_activation, one row per (metadata_type, name), for deactivation). This stage moves the remaining plugin-auth and plugin-sharing readers onto the same two places, so they cannot disagree with the resolver. ⛔ No catalog row's active is read any more, and no transition piece is built (#22601 → B).

plugin-auth/src/last-admin-guard.ts

  • The enumeration. A grant-anchored platform administrator is counted exactly as the resolver derives one: an unscoped, in-window grant naming admin_full_access, which the catalog holds (securityCatalogReaderOf(engine), resolved at the write) and which the ledger does not switch off. An engine with no catalog bound counts none, as the resolver grants none. All SystemObjectName.PERMISSION_SET reads are gone.
  • The ledger half (stage 1 Q5 → B). The flat refuseLedgerSwitchingAdminOff is removed. A ledger insert or update that would switch admin_full_access off is now judged by the guard's own enumerate–simulate–enumerate, like every other standing write: refused only when it would leave nobody who can sign in. The simulation re-reads the rows an update addresses (an update can move another row onto the pair) and judges an inserted row. A payload that touches no standing key, or an insert that is switched on, costs no reads. A ledger delete is not judged: no row means active.
  • The sys_permission_set hooks are removed, with PERMISSION_SET_STANDING_KEYS and the two op words. The resolver reads no row, so a delete, rename, active or organization_id write to it moves no administrator. The correspondence gate's reverse check (the guard has no standing-key list for a table the resolver does not derive from) already states that principle for lists.
  • The zero-administrator diagnosis (:1214–:1242 on stage 1's head) keeps its meaning. Two states still read as "emptied, not fresh": an unscoped, in-window grant naming a set the catalog does not hold (or none), and admin_full_access switched off in the ledger while such grants name it. The remedy for the second is to switch it back on, and that write is permitted by the existing restore exemption.
  • A composition that registers no ledger object issues no ledger read, and an unprovisioned ledger table reads as no row, both as in the resolver. Any other failed read refuses the write (fail closed).

plugin-auth/src/catalog-set-in-effect.ts (new, 61 lines)

permissionSetInEffect(engine, read, name): the catalog holds it and the ledger leaves it on. Used by the two readers below.

plugin-auth/src/auth-manager.ts (the :5018 reference)

  • Admission: the declared selfRegistrationPermissionSet must be in effect (catalog + ledger), and a sys_permission_set row must exist, because the grant's required permission_set_id is written with its id. The row's active is not read.
  • Settlement: the row is read for that id only; its active filter is removed.
  • Fate of the id read: it stays until the grant id column goes (stage 8 or C7b, per the stage-0 order correction 4).

plugin-auth/src/ensure-default-organization.ts (the :465 reference)

The legacy-anchor lookup asks permissionSetInEffect for admin_full_access instead of reading a sys_permission_set row. A failed read decides nothing (no_admin), as tryFind did.

plugin-sharing/src/sharing-rule-service.ts (readPositionActive)

A position recipient confers no share while the ledger has a position row of that name with active false (or 0). The verdict is deployment-wide, as the ledger is, so the per-organization row choice is gone. A name with no ledger row keeps sharing (today's verdict for a name that resolves nowhere), and a failed read still grants for the pass, as before.

plugin-sharing/src/sharing-service.ts (3 references)

No change. The three names sit in the sharing bypass list (sys_position, sys_permission_set, sys_position_permission_set). The objects still exist until stage 8, and taking them off the list would apply record sharing to them. Stage 8 removes them with the objects.

Deviations from the dispatch hypotheses, stated

  1. readPositionActive reads the ledger only, not the position definition. The definition decides nothing here: a name the catalog does not hold has no ledger row and keeps today's verdict anyway. The resolver drops a position by its ledger row alone, whether or not the catalog holds it, so reading the ledger alone keeps the two access-conferring readers identical. Recorded in the report's open_questions.
  2. The self-registration settlement still reads a sys_permission_set row, for the id its required lookup column needs. That is the write's id, not a grant decision; its retirement is the grant-id stage.

Tests

  • plugin-auth: 136 files, 2,769 passed / 10 skipped. typecheck exit 0 (source, examples and the test layer).
  • plugin-sharing: 41 files, 1,006 passed. typecheck exit 0.
  • Downstream consumers (...^@objectstack/plugin-auth, ...^@objectstack/plugin-sharing direction), after building their closure: plugin-security 4,126 passed / 45 skipped; organizations 152; plugin-approvals 1,023; cloud-connection 514; service-automation 2,348; example-crm 46; example-showcase 409; rest 5,178 passed / 327 skipped (275 files); runtime 5,685 passed / 19 skipped; cli unit 4,126; dogfood 1,903 passed and 7 failed in two files (me-apps-and-everyone-baseline, showcase-fls-read-mask-strip), which stage 1's patch round 96fb3e981 fixes: after merging it, those two files pass 14/14. The consumer runs were on head de4d000e8 (before the two stage 1 merges, which touch neither package); both packages' own suites and typecheck were re-run on the final head 315d0df16.
  • The resolver's batch-equivalence golden and the delegated-admin pins live in core and plugin-security, which this diff does not touch; plugin-security ran green above.
  • Fixtures that defined a set only as a row now bind the catalog their rows name (stage 1's bindCatalogFromTables / bindTestSecurityCatalog testkit). The pins that drove sys_permission_set writes as shape (4) now drive ledger writes, and one new block pins that writing the row is no standing input.
  • Ablations (committed state, scripts/ablation-replace.mjs, restored byte-identical and git diff HEAD empty both times):
    • readPositionActive forced to true: sharing-rule.test.ts 5 failed / 110 passed.
    • the guard's ledger switch-off reading forced to false: 17 failed / 114 passed across last-admin-guard.test.ts, .activation-ledger, .re-pricing and grant-readers-by-name.golden.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 127 families against the merge base with origin/main (stage 1's paths included). --ran reconciled 127 derived, 127 run, 0 unrun. On the final tree every family carries an exit code: 127 exit 0 and check:platform-checklist exits 1 with the same six absent-symbol anchors in metadata-protocol/src/protocol.ts and service-storage/src/attachment-access-hooks.ts that stage 1 measured identical on main (files this diff does not touch). 126 families were swept on bd916d342; the four touched by the last merge (the three changeset gates and nul-bytes) and the two it added (cli-test-child-env, i18n-walk-parity) were re-run on 315d0df16. Targeted eslint over the 22 changed .ts files: 0 errors, 0 warnings (a targeted run, not the repo-wide lint, which CI owns)

Acceptance notes

  • Stale prose outside this stage's landing zone, noted and not edited: core/src/security/security-catalog.ts's module doc still says deactivation "lives on the catalog ROW"; core/src/security/admin-standing-surface.ts:12 names PERMISSION_SET_STANDING_KEYS; the permission-set-active / position-active rows of qa/dogfood/test/authz-conformance.matrix.ts still describe the row predicate. Carrier: the stage that next touches each file.
  • Stage 1's changeset sentence on refuseLedgerSwitchingAdminOff describes an export this stage removes before any release; this stage's changeset states the later state.
  • Setup's Deactivate on a position still writes the row's active, which now stops neither the resolver nor sharing rules. That is the window recorded in the release-cut condition (stage 2c's ledger door, then C7b).

Generated by Claude Code

claude added 30 commits October 10, 2026 08:31
…d bodies from the catalog

Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
Co-authored-by: Claude <noreply@anthropic.com>
…declared at kernel:ready

Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
Co-authored-by: Claude <noreply@anthropic.com>
…ledger regenerated

Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
Co-authored-by: Claude <noreply@anthropic.com>
…ssionSets through the write-through

Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
Co-authored-by: Claude <noreply@anthropic.com>
…activation ledger

The resolver honours ADR-0049 deactivation through sys_metadata_activation
(types position and permission) and no longer reads the catalog row's active
column. convertDeactivatedCatalogRows turns existing active=false rows into
ledger rows for the upgrade ceremony; it is not wired into boot. A composition
whose registry has no ledger object issues no ledger read.

The admin-standing surface now derives from the ledger; the last-admin guard
refuses a ledger write that switches admin_full_access off. The name-fold
warning names permissionSets as the governed remedy.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
The resolver reads deactivation from sys_metadata_activation; the install
fixtures' grant maps answer it empty, as they answer the other authz tables.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
…-position-permission-sets

# Conflicts:
#	packages/plugins/plugin-security/src/security-plugin.ts
The merged capability declarations are told apart by _packageId; the test
registry now stamps it as the engine registry does.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
…Sets; the ledger-guard test double holds the caller's bound

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
… zh-CN, ja-JP and es-ES

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
…g and its switch from the ledger

Claude-Session: https://claude.ai/code/session_01ByzDx6dv5xWV1SCpCviLRU
Co-authored-by: Claude <noreply@anthropic.com>
…ion and sharing-rule readers ask the catalog and the ledger

Claude-Session: https://claude.ai/code/session_01ByzDx6dv5xWV1SCpCviLRU
Co-authored-by: Claude <noreply@anthropic.com>
…w active pins move to the ledger

Claude-Session: https://claude.ai/code/session_01ByzDx6dv5xWV1SCpCviLRU
Co-authored-by: Claude <noreply@anthropic.com>
…ermission-sets' into claude/issue-15204-s2b-auth-sharing-readers
…-position-permission-sets

# Conflicts:
#	packages/spec/src/migrations/registry.ts
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 10, 2026
@github-actions

github-actions Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-auth, @objectstack/plugin-sharing, touching 31 documentable anchor(s).

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 bfc15d275ba3fed8406cd5836b826e617150fae0.

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

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: organization_id (literal, 33 pages)
  • 8 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 — 20 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 bfc15d275ba3fed8406cd5836b826e617150fae0 → packageMentionDocs.

Which tree this was computed on

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

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

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

…migration registry

Byte-identical to that PR's head 2a4ccb4 for render-projection-diff.ts and
its test; a no-op once main carries it.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AGgRrdom7nizSbHc5Gws2U
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 315d0df16f3d635b64f4243f92e665b9c66cdb22
Local-runs: none

Inputs: card #15204 (body and all 44 comments, the rulings 6094045326 → B / Q (a), 6028793924 → B, 6094985249 → A, the stage plan 6094501866, stage 0 6095755866, the 2b amendment 6101897723, stage 1's answers 6101876814, the 2b answers 6103398921, the dev report 6103372612), PR #22745 (body, 24 files, the net diff against its base bf6bdf071d, stage 1's tip), and the 32 check-runs on this head: 30 success, 2 roster skips (Build Docs, Console Pin Gate), 0 failures. Check Changeset, Lint & Repo Gates, TypeScript Type Check, all Test Core and Dogfood Regression Gate shards are green, so every derived gate family is answered on the head itself. Where a judgment below says "as the resolver reads it", it was read against core/src/security/resolve-authz-context.ts (readDisabledCatalogNames, ledgerUnregistered) and admin-standing-surface.ts at this head.

① Derived judgments

  1. Guard enumeration (resolveAdminUserIds) — right. A grant-anchored platform administrator is an unscoped, in-window grant naming admin_full_access while securityCatalogReaderOf(engine).resolve('permission', …) holds the definition and the ledger has no (permission, admin_full_access) row with active === false || === 0; no catalog bound → none; registry asked first, missing table → no row; any other failed read → failClosed refuses. That is the resolver's own reading, line for line. The narrowing arm (a zero-administrator write is refused when an unscoped grant names a set the catalog does not hold, where a sys_permission_set row used to suffice) is pinned on a real engine: activation-ledger.test.ts "a grant naming a set the catalog does not hold is DANGLING", "an engine with NO catalog bound counts no grant-anchored administrator", plus the fresh-environment control. Fail-closed direction, as the diagnosis has always rounded.

  2. Ledger writes judged by enumerate–simulate–enumerate — right, as ruled (stage 1 Q5 → B in 6101876814; the 2b amendment). beforeInsert runs only when the payload's active is false/0; beforeUpdate when the payload touches metadata_type|name|active; the simulation re-reads addressed rows outside the (permission, admin_full_access) scan, applies the patch, appends the inserted row, so an update moving another row onto the pair is caught (re-pricing "moving another ledger row ONTO admin_full_access, switched off, is still refused"). The flat refuseLedgerSwitchingAdminOff is gone. The widening arm (a switch-off with another administrator standing now lands) is pinned both ways: re-pricing OBSOLETE/KEPT blocks, activation-ledger "a second administrator left standing permits the switch-off". Two edges I checked beyond the dev's claims: a batch insert dispatches beforeInsert per row (objectql/src/engine.ts:14185), and the ledger object's active defaults to true (sys-metadata-activation.object.test.ts:158), so the payload pre-filter misses no switch-off. A ledger delete is not judged: no row means active, so a delete can only restore. In a genuinely fresh environment an insert switching the set off is permitted by the bootstrap exemption and then read as "emptied, not fresh" once a grant names it, with the switch-back-on exempt as the way out — pinned ("a switched-off set is an EMPTIED environment", "…switching it back on is the way out").

  3. sys_permission_set hooks and PERMISSION_SET_STANDING_KEYS removed — right (Q2 → A; [Decision] ADR-0131 剩余部分(C2 余下阶段 + C3):继续分段、合并成一次切换,还是改成「registry 支撑的对象」 #22601 → B forbids keeping a guard on a table the resolver does not read). STANDING_KEYS_BY_TABLE on the head is exactly sys_member, sys_user_permission_set, sys_metadata_activation, sys_user = core's adminStandingTables() (derives rows of ADMIN_STANDING_SURFACE), and the correspondence pin last-admin-standing-keys.test.ts is green in CI. The widening arm (delete, rename, active, organization_id writes to the set row land) is pinned with the administrator still protected afterwards (re-pricing "the sys_permission_set ROW is no standing input"; guard test "the last administrator's set ROW can be deactivated, renamed, re-scoped and deleted — and the admin is still protected").

  4. Public surface of @objectstack/plugin-auth — right, carried by the changeset. index.ts re-exports the guard wholesale, so two exports leave the package: PERMISSION_SET_STANDING_KEYS (released) and refuseLedgerSwitchingAdminOff (stage 1's, unreleased). The changeset names both, what stands in their place (nothing; registerLastAdminGuard judges ledger writes itself) and the consumer count (none outside the package). check:api-surface reads spec dist only, so the changeset is the only carrier, and it carries. catalog-set-in-effect.ts is not re-exported (no index.ts change) → no new public surface; METADATA_ACTIVATION only moved within the file (stage 1's export, unchanged). The literal "FROM → TO" words are absent because check:adr-0087-registration's no-migration-prescription refuses that literal on a body — the bind the 6b-1c ACCEPT (6101787808) recorded; the mapping is stated in substance.

  5. Self-registration admission and settlement — right. Admission = permissionSetInEffect (catalog holds it, ledger leaves it on) AND a sys_permission_set row with an id; the active filter is gone at admission and at settlement. Narrowing (a catalog-less or ledger-off set → 403 AUDIENCE_CONFIG_ERROR, pinned) and widening (a row flagged active: false admits and grants, pinned) both follow the resolver, which grants such a holder. The row read for permission_set_id is a required column's value, not a grant decision (Q4 → A, order correction 4); with the active filter gone the settlement's candidate set can include a row-deactivated copy, but the resolver reads the grant by name, so which id is written changes no grant.

  6. ensureDefaultOrganization legacy anchor — right. permissionSetInEffect(ql, system read, 'admin_full_access'); a failed read decides nothing (no_admin), as tryFind did. Pinned both ways with a control.

  7. catalog-set-in-effect.ts — right. Registry-first, missing table → not off, false/0 → off, other failures throw and each caller keeps its prior direction (guard refuses, admission refuses, default-org no_admin, sharing grants). The one divergence from the resolver is that ledgerUnregistered wraps getObject in a try/catch and this helper does not; no pin reaches a throwing registry.

  8. Sharing readPositionActive — right (Q1 → A). Ledger only, (position, name), deployment-wide, limit 1, false/0 → off, no row → on, failed read → on with a warn; the per-organization row choice is gone with the per-organization semantics. Both ruling directions stay pinned: the access-conferring path stops and revokes; the addressing primitive expandPositionUsers stays a raw read. Accept-set reading: widening (a position deactivated in Setup — row active: false — confers shares again; pinned "the sys_position row's active switches nothing") and narrowing (a ledger-off position confers none). Two observations, neither a defect of this stage: (a) the string spellings '0'/'false' that isRowActive honoured are dropped — the resolver reads only false/0 and the store writes a boolean, so agreement wins; (b) unlike the resolver, the guard and the admission, this reader does not ask the registry before reading, so a composition that registers no ledger object logs one warn per position per pass and grants — the same behaviour it had on a stack without sys_position.

  9. sharing-service.ts bypass list untouched — right until stage 8 retires the objects.

  10. Dispatch exclusions held. No retired module touched (batch Release version 0.4.0 #310 item 4), no transition piece (the id read is a column need, not a row fallback), no change to who writes the catalog ([Decision] 切换后谁能在 Setup 里新建/编辑岗位和权限集:保持今天的权限(持 manage_metadata 者),还是放开给组织管理员(安全边界) #22621 → A), nothing under content/docs/releases/, landing zone plugin-auth/src + plugin-sharing/src only, 2,251 changed lines (under 3,000).

  11. Pins. The 29 removed it()s are the sys_permission_set hook and deactivated-row family; each has a ledger/catalog equivalent (reproduction, control, org-admin-elsewhere, no-reads, other set/other type, org-scoped grant, predicate sweep, fail-closed, amplifier pin kept, way-back-open, not-evidence, fresh-environment control, unguarded reverse). No skip/only added. The dev's two ablations (sharing forced on; ledger switch-off forced off) are the red/green evidence for the two moved readers; the batch-equivalence golden lives in core/plugin-security, untouched here and green in Test Core.

② Semver level

  • @objectstack/plugin-auth: minor, feat(plugin-auth)!:, BREAKING under the launch-window convention, adr-0087 marker not-required (no-migration-prescription) — right. No authorable key moves; the two removed exports have no importer outside the package. Its Clause-② line reads yes (narrowing: …; widening: …); scripts/pm/clause2-line.mjs reads the value yes and the arm narrowing (the arm word is the first token in the parenthetical, trailing prose is tolerated by design), i.e. the established spelling for a diff that widens AND narrows — yes (narrowing), as ten changesets on main and the gate's own fixture spell it. (narrowing) is BREAKING → carried by the bang and the banner; yes → at least minor → carried. One arm token, so AGENTS.md's "at most one arm" holds in the machine reading; the second direction is prose. Q3 → A stands.
  • @objectstack/plugin-sharing: patch, fix(plugin-sharing), Clause-②: no — wrong level. The diff reverses an access-conferring behaviour of a released package: a position deactivated in Setup (row active: false) confers sharing-rule shares again, and a ledger-off position stops conferring them. The changeset's own body states the window ("a deactivation made in Setup … stops neither the resolver nor sharing rules until the upgrade ceremony converts it into a ledger row"). That is the identical contract move the plugin-auth changeset in this same PR and stage 1's core changeset spell as BREAKING, shipped as minor; in this batch patch has meant "no behaviour change" (6c-prep) and every authorization-path behaviour change has gone minor. A fix at patch with no opts out of check-changeset-no-major's grade check and hides the reversal from an upgrader grepping the CHANGELOG. Fix: '@objectstack/plugin-sharing': minor, the BREAKING sentence under the launch-window convention with the window text it already carries, and the arm yes (narrowing) on that changeset's Clause-② line so the gate carries the grade (the PR body's line already reads yes (narrowing)). One file; no code changes. This is the FAIL reason.
  • The PR body's Clause-② line is on its own line and reads value yes, arm narrowing — right.
  • Stage 1's changeset (.changeset/15204-s1-position-permission-sets.md, on the base) still says a new hook refuseLedgerSwitchingAdminOff refuses the ledger write and "its sys_permission_set hooks stay registered, unchanged" — false after this diff. It is not in this PR's file list; the seat scheduled its amendment for the round after the retarget (6103398921). Owed on the retargeted head, where the PASS record checks it.

③ Boundary flags

Each dev flag (PR body Deviations 1–2, Acceptance notes 1–3; report open_questions 1–4, out_of_scope_findings 1–4) and the form of the PR, answered or escalated:

  • Q1 readPositionActive reads the ledger only (Zone 2 deviation) → A (6103398921). Judged right in ①.8: the resolver drops a position by its ledger row alone; the 2c door is where a name that resolves nowhere is refused.
  • Q2 remove the sys_permission_set hooks now → A. Judged right in ①.3; keeping them is the transition piece [Decision] ADR-0131 剩余部分(C2 余下阶段 + C3):继续分段、合并成一次切换,还是改成「registry 支撑的对象」 #22601 → B forbids.
  • Q3 one mixed Clause-② declaration with minor → A. Right for plugin-auth (② above). The plugin-sharing changeset was outside Q3's framing and is the FAIL item.
  • Q4 the settlement's row read for permission_set_id → A (order correction 4 of 6095755866). Right; not a grant decision.
  • Stacked PR opened after the correction 6103293232 → the seat let it stand: GitHub retargets to main on stage 1's merge, origin/main is merged before landing, nothing lands stacked (6103398921). AGENTS.md's stated hazard, a base-relative ADR-0087 disposition, does not bite: not-required (no-migration-prescription) holds against main as against stage 1, since no authorable key moves on either base. Answered.
  • Acceptance note 1 — stale prose in core/src/security/security-catalog.ts (module doc), admin-standing-surface.ts:12 (PERMISSION_SET_STANDING_KEYS), and the permission-set-active / position-active rows of qa/dogfood/test/authz-conformance.matrix.ts → carriers named (the stage that next touches each file); prose only; accepted by the seat. Answered.
  • Acceptance note 2 — stage 1's changeset sentence → escalated to the retarget round (6103398921); see ② last bullet.
  • Acceptance note 3 — Setup's Deactivate writes a row flag that now stops neither the resolver nor sharing rules → covered by the release-cut condition (6101876814, extended in 6102862135: (a) stage 2 with 2c's door, (b) C7b, (c) C9 with stage 7, (d) the suggestion-confirm path). This PR is part of (a); nothing in it widens that window beyond what stage 1 opened. Escalated as already recorded.
  • 2b × 2c pin rule (6103398921): catalog-activation-door.dogfood.test.ts on feat(runtime): activation door for positions and permission sets on sys_metadata_activation (stage 2c) #22736 pins stage 1's flat 403; after this diff the booted showcase case answers 200 because the seeded administrator also owns the Default Organization. The later lander rewrites it to the ruled pair (403 with no row when nobody who can sign in remains; 200 restored in finally as the control). Not on this head (the file lives on 2c's branch); the landing round's record checks it.
  • check:platform-checklist exit 1 → pre-existing on main, the two anchor files are not in this diff, Lint & Repo Gates is green on the head. Answered.
  • Downstream consumer suites run on de4d000e8 and 7 dogfood reds before stage 1's patch round → superseded by the check-runs on 315d0df16: Dogfood Regression Gate 1/3–3/3 and Test Core 1/6–6/6 all success. Answered by the head's own checks.

Implemented-by: session_01ByzDx6dv5xWV1SCpCviLRU (cloud dev, branch claude/issue-15204-s2b-auth-sharing-readers)
Reviewed-by: session_01Rerax7QTjKMPCUZxQUtPFR

VERDICT: FAIL

One reason, one file: the @objectstack/plugin-sharing changeset is patch for a BREAKING behaviour reversal in an access-conferring reader (② above). Every derived judgment in ① holds and every flag in ③ is answered or escalated, so the record on the patched head is a delta check of that changeset (and, once retargeted, of stage 1's amended sentence).


Generated by Claude Code

…; a composition with no ledger object issues no ledger read

Claude-Session: https://claude.ai/code/session_01ByzDx6dv5xWV1SCpCviLRU
Co-authored-by: Claude <noreply@anthropic.com>
…ermission-sets' into claude/issue-15204-s2b-auth-sharing-readers
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3f38a2be5489bd0317938052f92a6c4bcc3c29ae
Local-runs: none

Supersedes 6103669202 (FAIL on 315d0df16f). Delta review: the diff against the PR's base (stage 1's tip, now d0cab9f80d) was re-read per file against the FAIL head's diff; the 22 plugin-auth pieces and the plugin-auth changeset are byte-identical (index lines aside), and only three files moved — .changeset/15204-s2b-plugin-sharing-ledger-deactivation.md, plugin-sharing/src/sharing-rule-service.ts, plugin-sharing/src/sharing-rule.test.ts — exactly the file list of the dev's one patch commit 2aa6ad4bc1. The other seven commits between the heads are stage 1's tip (d0cab9f80d, the #22750 port) and the four main commits it carries, plus the two merges; the PR diff is still 24 files, +1,208 / −1,067 (2,275 changed lines, under 3,000). Check-runs on this head: 32, of which 30 success and 2 roster skips (Build Docs, Console Pin Gate), 0 failures; Check Changeset, Lint & Repo Gates, TypeScript Type Check, every Test Core and Dogfood Regression Gate shard are green. Every judgment of 6103669202 not named below stands unchanged.

① Derived judgments

  1. Items 1–7, 9–11 of 6103669202 stand — the guard enumeration, the ledger-write simulation, the removed sys_permission_set hooks and PERMISSION_SET_STANDING_KEYS, the plugin-auth public surface, self-registration, the default-organization anchor, catalog-set-in-effect.ts, the sharing bypass list, the dispatch exclusions and the pins. Not one of those files changed between the two heads.

  2. Sharing readPositionActive — right, and observation (b) of 6103669202 ①.8 is closed. The reader now asks engine.registry.getObject('sys_metadata_activation') first and answers "on" with no read when the composition registers no ledger object, which is the resolver's ledgerUnregistered decision (and the same shape the guard's scanLedger and catalog-set-in-effect.ts take). The remaining difference from the resolver — no try/catch around getObject — is shared by all three plugin-auth/plugin-sharing readers and reached by no pin. Pinned: "a composition that registers no ledger object issues no ledger read, and the position still shares" (registry stub answers undefined for the ledger, a switched-off row is seeded, ledgerReads is asserted 0, the share lands). The ablation the dev reports (1 fail / 115 pass with the check removed, restored) is what that assertion does by construction. Accept set unchanged by this item: a name with no ledger row, or no ledger object, shares — as before the patch.

  3. The merge of stage 1's tip d0cab9f80d adds nothing to this stage's diff. Stage 1's fix(spec): render-projection-diff generates a base's git-ignored migration registry #22750 port touches render-projection-diff.ts and the migration-registry build; main's four commits (build(spec): the migration registry is generated at build and leaves git #22706, fix(objectql): an item registered through the registry path is served with its package's _packageVersion #22724, fix(cli): os package install reports hotLoaded: false as installed, loads at the next restart #22734 and the /meta read gate) touch spec, objectql, cli and rest; none of them is in this PR's 24 files, and the 22 unchanged pieces prove the merge rewrote no stage-2b hunk.

② Semver level

  • @objectstack/plugin-sharing: now minor, feat(plugin-sharing)!:, BREAKING under the launch-window convention, adr-0087 marker not-required (no-migration-prescription) — right; the FAIL reason of 6103669202 is cleared. The body states FROM → TO (deactivation comes from sys_metadata_activation, no longer from the sys_position row's active column), the window (a Setup deactivation stops neither the resolver nor sharing rules until the upgrade ceremony converts it), the no-ledger-object behaviour the patched code has, and the fail-open read. Its Clause-② line reads yes (narrowing: a position switched off in the ledger stops receiving shares in every organization; widening: a row flagged active: false receives them again); clause2-line.mjs reads value yes, arm narrowing — the established spelling for a diff that widens AND narrows — so (narrowing) → BREAKING is carried by the bang and the banner and yes → at least minor is carried by the level. The marker is right: no authorable key moves and the reader asks the store whose move is registered as position-permission-sets-declared. The two changesets of this PR now spell the same contract move the same way.
  • @objectstack/plugin-auth changeset: unchanged, judged right in 6103669202 ②.
  • The PR body's Clause-② line is unchanged (value yes, arm narrowing) — right. Its size sentence (2,251) is stale by 24 lines under the os-dev one-write rule; the size of record is the 2,275 above, under the line.

③ Boundary flags

The seat's answers to Q1–Q4 (6103398921), the stacked-form decision, the stale-prose carriers and the release-cut condition stand as recorded in 6103669202 ③. Two items are restated because this head changes their standing, and the owed round is named:

  • Owed after stage 1 merges — one main merge round on this PR, confirmed by a record on that head:
    1. Stage 1's changeset sentence (.changeset/15204-s1-position-permission-sets.md: "A new hook refuses a ledger write that switches admin_full_access off (refuseLedgerSwitchingAdminOff). Its sys_permission_set hooks stay registered, unchanged.") is false after this diff and is on the base, not in this PR's files. Amend it in that round (6103398921).
    2. content/docs/permissions/authorization.mdx, "Grant lifecycle: the active switch (ADR-0049)". On this head the section is stage 1's pre-round-3 text: :399–400 dates the switch to sys_permission_set.active / sys_position.active, :434–435 says isRowActive is used by every reader "including the last-administrator guard's simulation", and :438 says deactivating the break-glass set is refused. Stage 1's patch round 3 (23d3545e64, 6103784577, not yet merged into this head) moves the section onto the ledger but keeps two sentences this diff makes false — "the guard's enumeration still reads the row active, so it refuses more, never less" and "a ledger write switching admin_full_access off is refused outright". After this PR the guard reads the catalog and the ledger, no row, and a switch-off is refused only when it would leave no administrator who can sign in. That paragraph is rewritten in the same round, on top of stage 1's text.
    3. Sharing docs. On this head content/docs/permissions/sharing-rules.mdx and positions.mdx carry no sentence stating the row predicate (sharing-rules.mdx:229 is about the rule's own active), so nothing is false in those two today; the round re-derives the list with scripts/docs-audit/affected-docs.mjs on the merged tree and rewrites any page it names that still describes a position's deactivation as sys_position.active.
  • 2b × 2c pin rule (6103398921) — unchanged: catalog-activation-door.dogfood.test.ts on feat(runtime): activation door for positions and permission sets on sys_metadata_activation (stage 2c) #22736 (now at 306de5fe, 6103827113) still pins stage 1's flat 403; the later lander of 2b and 2c rewrites it to the ruled pair (403 with no row when nobody who can sign in remains; 200 restored in finally as the control). Not on this head.
  • Stacked form. Still stacked on stage 1's branch, which has moved again (23d3545e64) since this head merged d0cab9f80d; the seat's rule holds — nothing lands while stacked, the PR retargets to main on stage 1's merge and merges origin/main first. The adr-0087 disposition not-required (no-migration-prescription) is the same against either base, so AGENTS.md's base-relative hazard does not bite.
  • check:platform-checklist exit 1 and the pre-patch consumer runs: answered as in 6103669202 by the green check-runs on this head.

Implemented-by: session_01ByzDx6dv5xWV1SCpCviLRU (cloud dev, branch claude/issue-15204-s2b-auth-sharing-readers)
Reviewed-by: session_01Rerax7QTjKMPCUZxQUtPFR

VERDICT: PASS

The one FAIL reason of 6103669202 is cleared by the plugin-sharing changeset as rewritten; the optional registry check closes the one observation that record left open; the stage 1 merge moves no stage-2b hunk. What remains is the main merge round named in ③, owed after stage 1 lands and confirmed by a record on that head.


Generated by Claude Code

Base automatically changed from claude/issue-15204-s1-position-permission-sets to main October 11, 2026 01:48
…b-auth-sharing-readers

# Conflicts:
#	packages/plugins/plugin-auth/src/last-admin-guard.activation-ledger.test.ts
#	packages/plugins/plugin-auth/src/last-admin-guard.ts
#	packages/plugins/plugin-auth/src/last-admin-standing-keys.test.ts
…d check, not a flat refusal

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 66c4c5aedca8b750a4e96d5ad7d56d1cc1b4343b
Local-runs: none

Supersedes 6103969012 (PASS on 3f38a2be) and 6103669202 (FAIL on 315d0df16). The PR is now based on main (bfc15d275ba3, stage 1's squash) and the diff judged is its net diff against main: 26 files, +1,222 / −1,079 (2,301 changed lines, under 3,000). Inputs: that diff and file list, the PR body, card #15204 through its 51st comment (the landing record 6104351995 names what this round owed 2b; 6103991518 names a carrier item), the check-runs on this head, and check-empty-changeset.mjs's own two-class text for the one expected red. No dev addendum for the merge round was on the PR (3 comments) or the card (52 comments; the newest, 6104498446, is the 6b-1b/6b-1c landing ruling) when this record was rendered at 02:14Z; the dev's statement is the commit message of 66c4c5aedc ("docs, changeset: the last-admin guard's ledger switch-off is the ruled check, not a flat refusal").

Check-runs on this head, all complete at 02:13Z: 35, of which 32 success, 2 roster skips (Console Pin Gate, Packed-tarball smoke) and 1 failure: Check Changeset. Lint & Repo Gates, TypeScript Type Check (all four lanes), Build Core, every Test Core shard and the aggregate, every Dogfood Regression Gate shard and the aggregate, Dogfood Verify CLI, Temporal Conformance, Spec property liveness, Build Docs and Check Documentation Links are green. The one red is the foreign-changeset rule (#17712) on .changeset/15204-s1-position-permission-sets.md and nothing else — judged in ② as the DELIBERATE CORRECTION class this record confirms. No other red exists, so the rule stated for this review (PASS if that is the only red) applies.

① Derived judgments

  1. The 24 stage-2b files are byte-identical to the diff judged PASS on 3f38a2be (index lines aside): both 2b changesets, the 20 plugin-auth files and the two plugin-sharing files. The retarget to main and the origin/main merge (f4db2f35d2) moved no stage-2b hunk, so every derived judgment of 6103669202 ① and 6103969012 ① stands: the guard's catalog + ledger enumeration, the ledger-write simulation (insert / update, batch inserts dispatched per row, active defaulting true), the removed sys_permission_set hooks and PERMISSION_SET_STANDING_KEYS with STANDING_KEYS_BY_TABLE equal to core's adminStandingTables(), the two removed plugin-auth exports carried by their changeset, self-registration admission, the default-organization anchor, catalog-set-in-effect.ts, and readPositionActive with its registry check. Nothing else in plugin-auth or plugin-sharing changed.

  2. Two files are new to the diff, both prose: no accept set and no public surface moves. .changeset/15204-s1-position-permission-sets.md (one sentence; judged in ②) and content/docs/permissions/authorization.mdx (+13 / −11, all inside "Grant lifecycle: the active switch (ADR-0049)"). The doc rewrite, sentence by sentence, each read against the guard on this head (last-admin-guard.ts, which imports neither isRowActive nor SystemObjectName.PERMISSION_SET):

    • Base: "The last-administrator guard's enumeration still reads the catalogue row's active, so it counts a row-deactivated admin_full_access as off and refuses more than it must, never less." → New: "The last-administrator guard reads the same two places the resolver does: whether the catalogue holds admin_full_access, and whether the ledger switches it off. It reads no catalogue row, so a row's active changes nothing it counts." True after this PR: adminSetDefined() resolves the catalog, readAdminSetSwitchedOff() reads the ledger, and no sys_permission_set read remains (pinned: "the last administrator's set ROW can be deactivated, renamed, re-scoped and deleted — and the admin is still protected").
    • Base heading: "Switching the break-glass set off is refused." → New: "Switching the break-glass set off is refused while it would leave no administrator." True: the two ledger hooks go through enforceStanding, the same enumerate–simulate–enumerate as every standing write.
    • Base: "A ledger row switching admin_full_access off would un-make every platform admin who holds it through a grant row, in one write — and switching it back on needs the permission just lost." → New: the same sentence with "un-makes". True (the grant-anchored population; the config anchor never reads a set, as the paragraph above it says).
    • Base: "The last-administrator guard refuses such a ledger write outright, whatever the administrator population (ADR-0135 D5.2), and refuses one whose target row it cannot read." → New: "The last-administrator guard judges such a ledger write like every other write that can take standing away (ADR-0135 D5.2): it is refused only when nobody who can sign in would still administer the environment." True: the flat refusal is gone with refuseLedgerSwitchingAdminOff; survivors' banned state is re-read, which is what "who can sign in" names (pinned: "a second administrator left standing permits the switch-off — the flat refusal is gone"; re-pricing KEPT block for the single shape).
    • New: "A configured administrator or an organization owner or admin still standing permits it." True: the enumeration counts the OS_PLATFORM_OWNER_EMAIL anchor and sys_member rows by isOrgAdminGrade (pinned: re-pricing OBSOLETE block with a configured administrator; "an org admin elsewhere keeps the switch-off legal").
    • New: "A ledger read that fails refuses the write." True: scanLedger rethrows anything but a missing table and failClosed turns it into PERMISSION_DENIED (pinned: "a ledger read that fails refuses the write it was asked about — fail closed"). This sentence also carries the one qualification the next one needs.
    • Base and new: "Switching it back on is never refused." True as the standing rule: a switch-on cannot shrink the population, and in the emptied state the restore exemption permits it (pinned: "…and switching it back on is the way out"); the sentence before it states the fail-closed exception for an unreadable store, so the paragraph reads correctly as a whole.
    • Unchanged in this PR and re-checked as still true after it: the section's opening (the ledger decides; "a catalogue row's own active column … no longer decides a grant", :407–409), the three deactivation bullets (the resolver's behaviour, stage 1's), and the "what it does not touch" paragraph. The explain-engine paragraph is also unchanged; it describes the explainer's deactivated state, which is 2a's file and 2a's move (its phrase "the catalogue row somebody switched off" is 2a's wording to settle, not made false by this diff). A whole-file grep finds no remaining isRowActive, refuseLedger, "refused outright" (the one hit at :490 is the packaged-object clause) or "guard's simulation" sentence.
  3. "The sharing docs" owed by 6104351995: nothing false found, so nothing was edited. Of the 22 content/docs/**/*.mdx pages on this head whose names concern sharing, positions, activation or lifecycle, none states that a sharing rule reads sys_position.active, that a deactivated position's shares are decided by the row, or that the guard refuses a ledger switch-off outright; sharing-rules.mdx:229 is about the rule's own active, and positions.mdx says only that a rule may target a position. The dev's commit touched no sharing page, and that is the right outcome on this measurement.

  4. Dispatch exclusions still hold on the main-based diff: no retired module, no transition piece, no change to who writes the catalog, nothing under content/docs/releases/, and the only files outside plugin-auth/src + plugin-sharing/src are the two prose files above, both owed by the seat's landing record.

② Semver level

  • The corrected stage 1 note: .changeset/15204-s1-position-permission-sets.md (@objectstack/spec, core, plugin-security, plugin-auth minor, platform-objects patch; on the merge base, not added by this PR). One paragraph changed, the @objectstack/plugin-auth one. Sentence by sentence:

    • Kept: "The last-administrator guard follows what the resolver reads." — true after this PR (①.1).
    • Kept: "Its standing-key map judges sys_metadata_activation (metadata_type, name, active) in place of sys_permission_set." — true: STANDING_KEYS_BY_TABLE on this head maps sys_member, sys_user_permission_set, sys_metadata_activation, sys_user.
    • Removed: "A new hook refuses a ledger write that switches admin_full_access off (refuseLedgerSwitchingAdminOff)." — false after this PR: that export and the flat refusal are gone, so the removal is right; the 2b plugin-auth changeset names the export's removal.
    • Removed: "Its sys_permission_set hooks stay registered, unchanged." — false after this PR: both hooks are removed; removal right.
    • New: "A ledger write that switches admin_full_access off is refused only when it would leave no administrator who can sign in, and switching it back on is never refused." — true of main once this PR lands, as the standing rule (①.2's two matching sentences; the fail-closed refusal of an unreadable store is the guard's general discipline, stated in the 2b changeset and the doc, not a second standing rule).
    • New: "The guard no longer judges sys_permission_set writes, because the resolver no longer reads that row." — true: core's ADMIN_STANDING_SURFACE lists no sys_permission_set row since stage 1, and the guard's two hooks on it are removed here.

    Stage 1 and this PR are consumed by the same release, so the note as corrected describes the state that release ships; left as it was, the release note would have named an export and two hooks that do not exist in the published package. The class is therefore DELIBERATE CORRECTION under finding: random changeset filenames collide silently across parallel agents — a round overwrote a sibling PR's minor changeset and every gate stayed green #17712 / fix(scripts): the foreign-changeset refusal prescribes restoring a release note the same PR made false — name the second class (ruling D on #17712) #18160 ruling D, not COLLISION (the PR's own changesets carry distinct names and the stage 1 file's front matter and every other paragraph are untouched), and the gate's remedy for that class — "do NOT restore it; say so on the PR and get it confirmed" — is met by this record, which names the note and judges each rewritten sentence. The Check Changeset red is that rule and only that rule: its annotations carry one failure on .changeset/15204-s1-position-permission-sets.md with the two-class text, and notice-level ADR-0087 exemptions for the two 2b changesets (both accepted); check-adr-0087-registration and check-changeset-no-major raised nothing. The gate stays red by design; per landing-operations the seat lands it with this record on file.

  • @objectstack/plugin-auth changeset (minor, feat!, BREAKING, not-required (no-migration-prescription), line value yes arm narrowing): unchanged, right (6103669202 ②).

  • @objectstack/plugin-sharing changeset (minor, feat!, BREAKING, FROM → TO, not-required (no-migration-prescription), line value yes arm narrowing): unchanged, right (6103969012 ②).

  • The PR body's Clause-② line is unchanged (value yes, arm narrowing) — right. Its size sentence is stale under the os-dev one-write rule; the size of record is 2,301 above.

③ Boundary flags

Implemented-by: session_01ByzDx6dv5xWV1SCpCviLRU (cloud dev, branch claude/issue-15204-s2b-auth-sharing-readers)
Reviewed-by: session_01Rerax7QTjKMPCUZxQUtPFR

VERDICT: PASS

The stage-2b diff is the one judged PASS on 3f38a2be, now against main; the two prose files it adds are the corrections the landing record owed, each sentence judged true of main after this PR lands; the one red check is the foreign-changeset rule on the stage 1 note, DELIBERATE CORRECTION class, which this record confirms. One carrier item (row-active.ts's comment) is escalated to the seat, not held against the diff.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat note: Check Changeset is red by design here, and this PR lands with it

Epic PM session_01Rerax7QTjKMPCUZxQUtPFR, seat epic:#15194, 2026-10-11T02:16Z.


Generated by Claude Code

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

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants