Skip to content

fix(plugin-auth)!: under single, no slug change makes another organization the Default Organization - #25286

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-25271-single-reslug-default-anchor
Oct 22, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-25271-single-reslug-default-anchor

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #25271

Clause-②: no (narrowing)
Metadata-protocol: none

What was wrong (Step 1, measured on main at 8dfd126)

Under single the Default Organization is the organization slugged default, else the only one, and its admin_full_access grant confers platform administrator standing (ADR-0131 D5). On a store holding two organizations, org_a slugged acme and org_b slugged beta, where org_b owns an admin_full_access grant held by its owner, measured on the real stack (ObjectQL over @objectstack/driver-sql + better-sqlite3, the real last-administrator guard, a real AuthManager with the organization plugin, the real resolveAuthzContext):

door before (8dfd126) after (this PR)
better-auth POST /auth/organization/update { organizationId: org_b, data: { slug: 'default' } }, as org_b's owner 200, slug default, holder platform_admin false → true 403 PERMISSION_DENIED, slug beta, platform_admin stays false
engine update { id: org_b, slug: 'default' }, isSystem (a seed's upsert, a plugin, a script) admitted, platform_admin false → true 403 PERMISSION_DENIED, slug unchanged
data API PATCH and update-mode import (engine update in user context, with the ADR-0092 identity write guard) 403 PERMISSION_DENIED, slug is not an editable column; slug unchanged unchanged

The last-administrator guard's sys_organization update half admitted the re-slug because it only refuses a write that leaves nobody standing (ADR-0135 D5.2), and this one takes standing from nobody.

The fix, and why this one

A new hook in last-admin-guard.ts, registered on sys_organization beforeUpdate beside the update half: under single, a slug changed to default is admitted only on the organization that is already the Default Organization (the sole organization taking the slug the boot gives it). Anything else is refused 403 PERMISSION_DENIED (ADR-0112 envelope; the message names the Default Organization rule, the current standing, and the remedy: keep another slug, hold one organization, or declare a walled posture). It reads only when the payload names slug: 'default', reads nothing under a wall, and fails closed when it cannot read the organizations (a predicate write's matches, or the Default Organization).

  • Why the engine and not beforeUpdateOrganization. Every door that can change a slug is an engine update: better-auth's adapter writes through the engine, and so do seeds, plugins and the data API. A better-auth hook alone would leave the seed and plugin doors open. Measured by ablation: with only the engine hook, better-auth's organization/update answers 403 with the code PERMISSION_DENIED (the guard's refusal reaches the caller through the adapter), so a second check in auth-manager.ts would buy nothing but a different answer to a read fault. A first commit carried that pre-check; it was removed, and with it the public export it needed.
  • Why not gate every slug change the way create is gated. That refuses renaming the slug of a single-organization deployment, which moves no anchor. The narrow rule refuses only the write that captures the anchor.
  • Walled postures are unchanged: no grant anchors there and a slug is just a slug; the hook returns before any read.

Pins (packages/plugins/plugin-auth/src/single-reslug-default-anchor.test.ts, 10 cases)

  • HTTP door: refused 403 PERMISSION_DENIED, slug unchanged, no standing gained; CONTROL the sole organization takes default (200); CONTROL another slug (200); CONTROL walled isolated (200); a failed read of this rule's own query refuses.
  • Engine door: by id and by predicate, refused; a default-slugged Default Organization beside it, refused by this rule before any unique index (at HTTP, better-auth's own slug-taken check answers that shape 400 first); a failed read refuses; CONTROL walled group, admitted.
  • Ablations through scripts/ablation-replace.mjs, each restored to the HEAD blob with git diff HEAD empty (the suite imports src, no dist leg): the hook's registration removed → 6 red (every refusal pin), 4 controls green; the wall short-circuit removed → the 2 walled controls red; the Default-Organization admission removed → the sole-organization control red.

Proof (tests, gates, R1, R4, the upgrade harness, dogfood) is in the os-dev-report on #25271.

Acceptance notes

  • The refusal answers the standard PERMISSION_DENIED, the code every half of the last-administrator guard answers. A dedicated registered code would need a row in packages/spec's error-code ledger, outside this card's file surface.
  • docs/adr/** is untouched. The rule is ADR-0131 D5's definition applied to the update write; the ADR's 2026-10-22 D5 note could name this hook beside the insert half when the epic next amends it.

Generated by Claude Code

…ation the Default Organization

Under `single` the Default Organization is the organization slugged
`default`, else the only one, and its `admin_full_access` grant confers
platform administrator standing (ADR-0131 D5). Re-slugging another
organization to `default` made it the Default Organization: measured on
8dfd126, better-auth `organization/update` answered 200 and the grant
holder became platform_admin through the real resolveAuthzContext; a
system-context engine update did the same.

One judge, `refuseDefaultOrganizationSlugCapture`, refuses it unless the
organization already is the Default Organization. The last-administrator
guard runs it on `sys_organization` `beforeUpdate` (every engine write),
and `beforeUpdateOrganization` asks it before the vendor writes so the
HTTP door answers 403 PERMISSION_DENIED instead of a 500. Walled postures
read nothing and admit.

Claude-Session: https://claude.ai/code/session_01S2kKLbAZ5FRjY2StxU3jVG
Co-authored-by: Claude <noreply@anthropic.com>
…he pre-check and the export

Ablating the `beforeUpdateOrganization` pre-check showed better-auth's
`organization/update` still answers 403 PERMISSION_DENIED: the adapter
writes through the engine and the guard's refusal reaches the caller with
its code. The pre-check changed only a read fault's answer (503 rather
than the guard's fail-closed 403), so it goes, and with it the new public
export: the judge is module-private to the last-administrator guard.

Claude-Session: https://claude.ai/code/session_01S2kKLbAZ5FRjY2StxU3jVG
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 22, 2026
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 3 documentable anchor(s).

18 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 a8358581aab41981c94a697da4d2ef87faca42ac.

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

What this run could not see
  • 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 — 19 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 a8358581aab41981c94a697da4d2ef87faca42ac → packageMentionDocs.

Which tree this was computed on

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

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

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

…to, an organization

The `default moved` case re-slugged a second organization to `default`
through a system engine update: the anchor capture this card closes, now
refused 403 PERMISSION_DENIED. The case keeps both memo directions on a
path that stays legal: `default` moved off the Default Organization
withdraws standing; the second organization deleted makes the first the
only one again, which confers it. The capture itself is asserted refused.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 80315628e91fdcd3194b61d9489d3f4296be6143
Local-runs: none
Scope: packages/plugins/plugin-auth/src/last-admin-guard.ts, packages/plugins/plugin-auth/src/single-reslug-default-anchor.test.ts, packages/runtime/src/default-organization-statements.golden.test.ts + .changeset/25271-single-reslug-default-anchor.md, packages/core/src/security/default-organization.ts, packages/core/src/security/resolve-authz-context.ts, packages/plugins/plugin-auth/src/auth-manager.ts, packages/plugins/plugin-auth/src/identity-write-guard.ts, packages/plugins/plugin-auth/src/tenancy-service.ts, packages/plugins/plugin-auth/src/default-organization-invariant.ts, packages/cli/src/utils/organization-ownership-plan.ts, packages/objectql/src/engine.ts, packages/spec/src/security/tenancy-posture.ts, packages/types/src/env.ts, packages/platform-objects/src/identity/sys-organization.object.ts, docs/adr/0131-total-organization-ownership-no-null-organization-id.md, docs/adr/0135-identity-and-access-architecture.md, scripts/check-adr-0087-registration.mjs, scripts/check-changeset-no-major.mjs

Reviewed at the head the seat named after the merge of origin/main (merge commit e47a5a97e2, merge base a8358581aa = origin/main). The three code blobs are byte-identical to ec6d235613; only the changeset moved (two bullets and the operator paragraph). Net diff: 4 files, +410 / -8; 0 of 4 paths governed; same-repo head.

① Derived judgments

  1. The narrowing itself — RIGHT. guardOrganizationDefaultSlug (sys_organization beforeUpdate, priority 20) refuses a payload with slug = 'default' unless the addressed set is exactly the current Default Organization, named through findDefaultOrganization in packages/core/src/security/default-organization.ts (slug default, else the sole organization) over a system read. That is the one definition the authorization resolver anchors by (readDefaultOrganizationId, grantAnchorsPlatformStanding), so the write the hook refuses is exactly the write that would move the admin_full_access anchor onto another organization (ADR-0131 D5). The wall short-circuit reads postureEnforcesWall(resolveTenancyPosture()), the same read the resolver makes at resolve-authz-context.ts (line 1624) and the insert half makes: "no grant anchors under a wall" and "the hook reads nothing under a wall" are one fact. Refusing isSystem writes too is correct for a capture guard: the capture doors are the system ones (better-auth's adapter, seeds, plugins).

  2. Door inventory ("every door that can change a slug is an engine update") — RIGHT, verified. better-auth's adapter writes through the engine (pinned at HTTP: 403 PERMISSION_DENIED, slug unchanged, standing unchanged). The data API's user-context PATCH and update-mode import are closed one layer earlier by the ADR-0092 identity write guard (identity-write-guard.ts: slug is not in MANAGED_EXTENSION_EDITABLE_FIELDS.sys_organization, a non-whitelisted key is stripped, a payload that strips to nothing is refused 403). updateById delegates to engine.update. The boot invariant (default-organization-invariant.ts) only inserts. The ownership ceremony (organization-ownership-plan.ts) reads the Default Organization by the same rule and writes no slug. beforeUpdateOrganization in auth-manager.ts is untouched and still judges only the environment-binding slug lock; the first commit's pre-check and its export were removed, so the engine hook is the single owner of the rule (route and surface ownership, rule 1) and check:api-surface sees no export change.

  3. Predicate shapes — RIGHT. A scoped multi: true write fans beforeUpdate out per matched row with the batch options carried, and resolveTargetIds rescans the predicate, so the whole matched set is judged (pinned by predicate). An unscoped multi: true write dispatches the whole-operation call only to hooks declaring dispatchUnscopedMultiWrite (this one does not), but the per-row fan-out still reaches every registered beforeUpdate hook for every matched row, and resolveTargetIds then scans the table under maxScan: a set of two or more is refused before the unique index. An empty matched set is admitted, correctly (nothing is written). A scan over maxScan is a refusal and is re-thrown unchanged.

  4. Fail-closed and the carrier — RIGHT for this card, one asymmetry noted. The refusal is PERMISSION_DENIED + 403, the code every half of this guard answers and mapDataError maps (ADR-0112). A read fault (the addressed rows or the Default Organization unreadable) also answers 403 PERMISSION_DENIED, with text that says the read failed and to retry: that is this file's failClosed shape, and the changeset discloses it ("a read the guard cannot complete refuses as well"). It is asymmetric with the sibling slug guard in auth-manager.ts, whose read fault answers 503 SERVICE_UNAVAILABLE (slugGuardReadFaultApiError); the dev measured the move (ablation A1, 503 to 403) and chose the guard's shape. Changing the fault carrier for one half alone would split the invariant's vocabulary, and a dedicated code is a packages/spec ledger row outside this card's surface. Noted, not blocking.

  5. Controls — RIGHT. The sole organization taking default is admitted (it already is the Default Organization; its grant already anchors: platform_admin true before and after). A slug other than default costs no read. Under isolated (HTTP) and group (engine) the same write is admitted. A default-slugged Default Organization beside the target: refused by this rule before the unique index (at HTTP, better-auth's own slug-taken 400 answers first, as the test notes).

  6. Stranding, the derived consequence — RIGHT and now documented. On a single store holding several organizations and none slugged default, every re-slug to default is a capture and is refused, the previous holder included. That shape is already the tenancy census's SINGLE_POSTURE_MANY_ORGANIZATIONS error-level misconfiguration (tenancy-service.ts), and the refusal's remedies are the census's two ways out (declare a walled posture, or hold one organization). Moving the Default Organization AWAY while others exist stays with the update half (ADR-0135 D5.2, unchanged): no ADR decision is reversed (Prime Directive 13), and the changeset at this head names both the removed two-step move and the stranding.

  7. Tests — RIGHT. Ten pins on the real stack (ObjectQL over driver-sql and better-sqlite3, the real guard, a real AuthManager with the organization plugin, the real resolveAuthzContext), each refusal with its control. The find spy narrows the injected fault to this rule's own read (slug: default, limit 1), so the read-fault pins prove this hook refuses and not the update half; the dev's second A2 run is what forced that narrowing. The runtime golden test previously performed the capture in system context and asserted the standing flip, i.e. it pinned the behaviour this card removes; it is rewritten along the legal path and asserts the refusal (403 PERMISSION_DENIED), the memo retirement on withdraw (standing false) and on re-sole (standing true). The exact organizationReads: 1 count on the withdraw leg was dropped rather than corrected: with no default slug the resolver reads twice (by slug, then the count), so a 2 could have been pinned. Weaker, not wrong; non-blocking.

  8. Not a surface change elsewhere. No export added or removed in the net diff; no packages/spec change; no docs claim the removed behaviour (grep over content/docs, docs/, skills/ for re-slug and moving the Default Organization: none); no ADR anchor JSON is pinned on last-admin-guard.ts; the registration log line is extended truthfully. Not a startup-registry verdict and not a logged degradation: the failure is handed to the caller.

② Semver level

Changeset .changeset/25271-single-reslug-default-anchor.md at this head: '@objectstack/plugin-auth': patch, a fix(plugin-auth)!: summary, the **BREAKING** banner, Clause-②: no (narrowing), Metadata-protocol: none, and the ADR-0087 marker not-required (no-migration-prescription) with its why. Judged:

  • What the diff publishes: a runtime accept-set narrowing in @objectstack/plugin-auth only. The packages/runtime change is a test file; no runtime changeset is owed. Correct.
  • Direction and Clause-②: the PR body and the changeset both carry Clause-②: no (narrowing) and Metadata-protocol: none; the change widens no published payload and narrows a write. Both lines match the diff.
  • ADR-0087 disposition: no key of any metadata schema is removed, renamed or re-shaped, so objectstack migrate meta has nothing to rewrite and no tombstone is owed; no-migration-prescription is the honest category. The body's FROM and TO text is inline prose with no arrow, no vertical FROM and TO pair, no rewrite table and no migration heading, so it carries none of the prescription shapes the gate reads. The Check Changeset job, which runs check-adr-0087-registration.mjs --base and check-changeset-no-major.mjs --base, concluded success on this head, so the changeset revision at 80315628e9 has been judged by both.
  • Level: under the house rule (Metadata-protocol: none, with the BREAKING banner and the ADR-0087 marker, makes patch correct for a narrowing), patch is correct. The job's conclusion is the verdict of record; nothing was re-derived locally.
  • Migration text: FROM and TO stated; the operator remedy stated (keep another slug; move the grant, not the slug; hold one organization or declare a walled posture, as the census prescribes). The two bullets added at this head (the removed two-step move; the stranding) are accurate to the code: the update half still admits moving away, the second write is refused.

③ Boundary flags

  • premise_still_valid: true — agreed. origin/main at a8358581aa still holds the shape; main's side of the merge touched auth-manager.ts and auth-manager-actor-guards.ts but no file of this PR and not the organization update hook.
  • deviation 1, file surface grew by packages/runtime/src/default-organization-statements.golden.test.ts — accepted. The fixture pinned the removed capture, went red in Test Core 6/6 at 7aeec2d3, and is rewritten along the legal path asserting the refusal (judgment ① 7). Within the card; no scope expansion (Prime Directive 10).
  • deviation 2, worktree in the cloud container — accepted; process only.
  • deviation 3, model-free trailer pair — accepted; it is the AGENTS.md form.
  • open question (collateral of the narrow rule), options A / B / C — answered: A. The acceptance is "no slug change makes a non-Default organization the Default Organization", and A closes every door with the least collateral. The stranded shape (several organizations, none default, under single) is the census's error-level undeclared posture with two stated ways out, which the refusal repeats; the changeset at this head documents it. B changes the update half's ADR-0135 D5.2 semantics (refusing a write that leaves an administrator standing) and the claim excluded widening into that territory; C is unsound, since a grant can be written after the slug. If the epic wants the stuck shape unreachable, that is a card of its own, not a condition of this PR.
  • out-of-scope finding 1, the PERMISSION_DENIED carrier — accepted (judgment ① 4); a dedicated code is a packages/spec error-code ledger row outside this surface.
  • out-of-scope finding 2, ADR-0131 D5's 2026-10-22 note names the insert half and not this update-side rule — accepted; docs/adr/** is excluded by the claim, the ADR id is left in the code, and the epic amends.
  • Gate state on this head: 21 check-runs completed (Dogfood Regression Gate, Check Changeset, Governed Surface Queue Guard, Check PR Size, the claim checks, Check Documentation Links, Flag docs affected by code changes, Auto Label, filter, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests: success; 7 opt-in or full-battery runs skipped), 17 still in progress (Test Core 1 to 6, Build Core, the remaining Lint lanes, the Type Check lanes). The code blobs are identical to ec6d235613, where the dev reports 34 success / 7 skipped / 0 failed with Test Core 6/6 fixed. The seat confirms the 17 before posting: this PASS is read with the five required contexts concluding success on 80315628e91fdcd3194b61d9489d3f4296be6143.
  • Not measured here (read-only): the wall time of the new ten-case suite, which boots a real AuthManager per case; if a run shows it over the lane's 10 s rule, that is the lane's card, not this review's.

Implemented-by: claude/issue-25271-single-reslug-default-anchor
Reviewed-by: session_01Y5583bs2RDd53MH6kM1ca3

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat ACCEPT · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-22T22:15Z. PR #25286 at 80315628e91fdcd3194b61d9489d3f4296be6143 (#25271; claim 6349409792; report 6351599627, follow-up 6352477135). ACCEPT.

Seat check

  • Shape: 4 files, +410 / −8: plugin-auth's last-administrator guard, its new pin suite, the runtime memo-retirement golden, and one changeset. check-governed-merges --pr 25286: not governed, so this is an ordinary queue landing. It touches no ceremony, plan, harness or engine tenancy code, so no needs:upgrade-harness.
  • Review tier: the PR declares Clause-②: no (narrowing), so the review is at CONTRACT_REVIEW_TIER on the narrowing face. Record 6352123147 on this head: PASS, with no item escalated.
  • What it does (ADR-0131 D5): under single, a sys_organization slug changed to default is admitted only on the organization that already is the Default Organization. Anything else is refused 403 PERMISSION_DENIED at the engine's beforeUpdate, for every context, isSystem included. That covers better-auth's organization/update, seeds and plugins alike; the data API was already closed by the ADR-0092 identity write guard. Walled postures read nothing and admit; a failed read refuses.
  • The golden rewrite (default-organization-statements.golden.test.ts) is a declared contract change to that case's expectation, accepted. The old case performed the capture this card closes, in system context. The new one keeps the memo-retirement intent in both directions (move off withdraws standing; back to the sole organization confers it) and asserts the capture refused.
  • Open question (stranding), answered A, the dev's and the reviewer's recommendation. The PR removes one operation, with no in-place replacement: on a single store holding several organizations, making a different organization the Default Organization. After default is re-slugged away (still admitted by the update half, ADR-0135 D5.2, when an administrator stays standing), no organization can take it back. The seat judged this within the card, with no decision card, because the shape exists only where main already refuses to guess:
    • core system-write-organization.ts classifies single with several organizations as ambiguous-organization;
    • the tenancy census reports it at error level (SINGLE_POSTURE_MANY_ORGANIZATIONS);
    • both ways out are stated in the refusal and the changeset: hold one organization, or declare a wall. To move the anchor, move the grant, not the slug.
    • Making the stuck shape unreachable (option B) would change the update half's semantics and is a card of its own if the epic wants it.
  • Carried, not blocking:
  • CI on the head: 41 check-runs, every one success or skipped (Test Core 1 to 6/6, Build Core, TypeScript Type Check, Lint & Repo Gates, Governed Surface Queue Guard, Check Changeset, Dogfood Regression Gate); mergeable clean. main is merged in with a merge commit (e47a5a97e2).
  • R1–R4, measured by the dev: all on this head, after the main merge.
    • R1: a production os serve under single on a fresh store answered health, sign-up, sign-in and session 200. The sole Default Organization re-slugs both ways, 200.
    • R4: the same under isolated and under group, all 200. Two organization/create succeed in each, and there are 0 organization-less grants.
    • R2 and R3: the upgrade harness passed 16/16, both fixture legs. The dogfood battery passed 289 files and 2491 tests, 0 failed.
    • Gates: 68 of 69 exit 0. check:dual-build-cjs-loads was NOT MEASURED (it needs a full build), and CI Build Core is green. The PR changes no build or export surface.
  • Changeset: @objectstack/plugin-auth patch, fix(plugin-auth)!, **BREAKING**, Clause-②: no (narrowing), Metadata-protocol: none, ADR-0087 not-required (no-migration-prescription). Right under the house rule; Check Changeset is success on this head. The changeset names the removed move and the stranding.
  • Landing: the seat flips the PR ready and enables auto-merge. Fixes #25271 closes the card.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 22, 2026 22:16
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 22, 2026 22:16
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 22, 2026
Merged via the queue into main with commit 09e07c2 Oct 22, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-25271-single-reslug-default-anchor branch October 22, 2026 22:51
os-steve pushed a commit that referenced this pull request Oct 23, 2026
…cords the single-posture re-slug rule

ADR-0126 §6 wall 3 gains a dated note: the boot report (#25311,
54df640) and the marketplace refusal (#25355, 448374d) make it true
in practice for sharing rules, permission sets, positions and
capabilities. The note names who is told under `single` and under a wall,
the findingSet confirm token, the two accepted limits under §9, the two
doors still open (#25382, #25366), and that the other classes follow
after v18.

ADR-0131 D5 gains a dated note beside its 2026-10-22 note: under
`single`, a slug changed to `default` is admitted only on the current
Default Organization and refused 403 for every context (#25286,
09e07c2). It names the removed move and the stranding it leaves.

Notes only; no decision text changes.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HbVJUgUf5HuWXPQvAo4sFg
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/m tests tooling

Projects

None yet

2 participants