You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Security: an external organisation role whose members do not see the member directory and reach data only through explicit grants (portal users on hosted environments) #25233
On hosted environments the organisation invite is the only way in (objectstack-ai/cloud#3132 ruling), so a supplier's portal users are organisation members.
The baseline member sets carry the sys_user_org_members row scope (id in current_user.org_user_ids; packages/plugins/plugin-security/src/objects/default-permission-sets.ts:447, repeated on viewer_readonly), and sys_user declares the same defaultRowScope (packages/platform-objects/src/identity/sys-user.object.ts).
RLS policies OR-combine, so an app permission set cannot narrow it.
Result: supplier A's portal users list supplier B's portal accounts, and every supplier sees every employee's name and email.
ADR-0138 (the v18 guest model) covers the anonymous principal only, not a signed-in external member: this is a new population, not an unused capability.
The ruling (what binds)
An organisation invite may carry the role external, beside owner / admin / member. The better-auth organization plugin's roles are configurable.
A person holding external in the organisation is, in every environment of that organisation, a member whose:
baseline excludes the member directory: no sys_user_org_members row scope, own row only;
data comes only through explicit grants (positions, permission sets, sharing rules);
access is not reached by the everyone-anchor default sets.
The JIT path stamps the population from the role.
Self-hosted deployments get the same role through the environment's own organisation.
The record: a dated note on ADR-0135 D2 / D4, mirrored in cloud's ADR-0024. objectstack-ai/cloud#3132 is not reopened; it gains one line.
Acceptance
An external member of an organisation with two suppliers' portal users reads sys_user and sys_member and sees its own row only.
With a permission set granting one object, it reads that object and nothing else.
An ordinary member is unchanged.
The cloud's invite flow can issue external (cloud follows with its own card when this lands).
Claim: PM loop round 8
Session: session_01EjZr5fYfHhxeJA2EjdEEkn (the card thread; seat session session_01B2EeooonSXrANpkpgUSfFZ posts this claim)
Account: os-bill
Branch: claude/project-thread-md96s8 (the card thread's designated push branch, at main42b60b9; claude/issue-N-slug is the convention, and this is the measured platform exception the pm-dispatch runbook records)
Worktree: objectstack-issue-25233
Domain: domain:spec
Seat: domain:spec#2 (seat post #18549)
File surface: packages/plugins/plugin-security/src/objects/default-permission-sets.ts, packages/platform-objects/src/identity/ (the sys_user / sys_member row scopes), packages/plugins/plugin-auth/src/ (the organization role config and the JIT population stamp), the spec contract the role or population is declared in if one is owed, their tests, one changeset; a separate docs-only PR for the dated ADR-0135 D2/D4 note (Part of #25233, Tier H, the maintainer's merge) (stop on breach; explain in the report)
Container & model: L, mode:thread, model: default (dispatch-gates --tier: no path-derived mandate), effort: xhigh (p1, security); a published-contract widening, if the diff makes one, owes one CONTRACT_REVIEW_TIER review of record before the queue
Clause-②: yes (expected widening: a new organisation role the invite and the population accept; the dev re-measures from the diff and the PR body states the measured value)
Responsibility: the baseline member sets' sys_user_org_members row scope and sys_user's defaultRowScope produce the directory exposure | no platform path covers a signed-in external member (ADR-0138 covers the anonymous principal only) | reached by portal users invited as organisation members on hosted environments (hotsrm's supplier portal design, cloud#3149); measured use today: none in production per the card
Thread-read: none
Serial constraints cleared: #25238 (open, claude/issue-25112-magic-link-ttl) touches plugin-auth/src/auth-manager.ts and a mail-locale test only, a different region; whoever lands second merges main. No other open PR touches the surface.
{
"issue": 25233,
"status": "needs_decision",
"branch": "claude/project-thread-md96s8",
"pr": null,
"session": "session_01EjZr5fYfHhxeJA2EjdEEkn",
"premise_still_valid": true,
"summary": "Stopped before code at a fork, per the dispatch order. The leak is real and measured at main 3e07389: sys_user_org_members sits on three shipped sets (organization_admin :447, member_default :858, viewer_readonly :1159), sys_user's defaultRowScope repeats it, sys_member carries no member-side row scope, RLS OR-combines, and better-auth's list-members and get-full-organization doors check membership only. The fork: the card's sub-claim 'a new population, not an unused capability' is false. ExecutionContext.audience 'external' (a published, zero-consumer spec key) and the EXTERNAL posture rung (a test-locked dead branch) are reserved for exactly this population, and three accepted records lock its semantics against the 2026-10-22 ruling: ADR-0095 D2 and ADR-0099 D3/I7 (explicitly shared rows only; sharing rules, OWD and held bits never apply; 'model EXTERNAL as a restricted MEMBER' rejected twice) and ADR-0090 D11 as amended by ruling of record 6159300530 (external principals come from an identity pool separate from members). The 2026-10-22 ruling puts portal users in the member pool and reaches them through positions, permission sets and sharing rules, and names none of those records. The full analysis (options with customer-visible effect and workload, the four axes, re-check commands) goes to the seat with this report for the decision card.",
"tests": "none run: stopped at a fork before any code (os-dev, when to stop and not write code). Every reading is a file:line at objectstack main 3e07389, plus better-auth 1.7.12 dist (routes/crud-members.mjs:465, routes/crud-org.mjs:370).",
"gates": "not run: no diff (the branch sits at origin/main 3e07389 with zero commits).",
"dogfood_battery": "n/a: no diff",
"mcp_calls": "0",
"api_writes": "1: POST repos/objectstack-ai/objectstack/issues/25233/comments (this report, through scripts/pm/post-stamped.mjs). The empty-branch probe was a git push, not a REST write.",
"open_questions": [
{
"question": "Is the external organisation role the platform's reserved EXTERNAL principal (ExecutionContext.audience 'external', the ADR-0095/0099 rung), and if so which reach channels does that rung admit? The 2026-10-22 ruling (member pool; positions, permission sets, sharing rules) contradicts ADR-0095 D2, ADR-0099 D3/I7 and ADR-0090 D11 as amended on 2026-10-14 (separate pool; explicit shares, sharing sets and share links only).",
"options": [
"A - two meanings: the role is a restricted member (posture MEMBER, audience unstamped), the EXTERNAL rung stays reserved for a separate pool; cross-notes on the three records. Smallest; leaves audience reporting a portal partner as internal and two 'external's for an AI to conflate. Spec + engine, 1 code PR + 1 Tier H docs PR, no measured precedent.",
"B - one meaning, rung re-ruled: the role IS the EXTERNAL principal (audience 'external', derivePosture returns EXTERNAL), and the rung admits explicit grants only (shares, sharing rules, position and permission-set grants with their RLS), never the internal OWD baseline, the baseline or everyone, or the directory; Tier H amendments to ADR-0095 D2, ADR-0099 D3/I7, ADR-0090 D11. A plus the OWD seam and matrix cells, 1-2 code PRs + 1 Tier H docs PR, est. 1.5-2x A.",
"C - the rung as locked: explicitly shared rows only, sharing rules and held bits never apply. Withdraws the ruling's channels, card acceptance item 2 fails as written, and a supplier portal needs per-record shares until sharing sets (unbuilt) exist."
],
"recommendation": "B, because on long-term soundness it leaves one external concept whose reserved shapes get their first consumer, in the mainstream shape (Salesforce external users: permission-set object access plus sharing rules, sharing sets and manual shares; Power Pages table permissions); it serves hotsrm's supplier portal today as A does; it removes the AI trap A creates (wiring audience from the role silently empties every portal under the locked rung); and the workload is under 3x A. Confidence gap: cloud#3149's own wording was not read (out of this session's reach)."
},
{
"question": "Execution parameters to rule in the same stroke (they hold under A and B).",
"options": [
"E1 the population marker is sys_member.role, per organisation, read at the resolver's one provisioning boundary (ADR-0095 D3), not a sys_user column (a stored tier ADR-0095 rejects; cannot say external in one org and member in another)",
"E2 the better-auth directory doors (list-members, get-full-organization) are narrowed for an external caller in the same code PR (same leak, second door)",
"E3 the invitation grant row for external is 'any' issuer that reaches invite-member (it confers less than member, by the table's own rule)",
"E4 an external's self-service baseline is a new shipped anchor-safe set: member_default's own-row carve-outs without sys_user_org_members, plus sys_member own row; app isDefault sets and accepted package defaults do not reach it",
"E5 external is exclusive: a membership role list carrying it with another role is refused at write"
],
"recommendation": "E1 to E5 as stated."
}
],
"out_of_scope_findings": []
}
Triage: one lane, domain:spec. domain:services removed, needs-user-decision kept
Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-22T15:53Z. ⛔ Not a claim.
Why one lane: a card carries one lane. The design round, and the fork the dev reported (6344121515), belong to the spec seat that claimed it. The fork: the 2026-10-22 ruling puts portal users in the member pool, against the accepted ADR-0095 D2, ADR-0099 D3/I7 and ADR-0090 D11.
The services half: the read-scope and directory doors get their own card once the maintainer rules the fork.
Claim: PM loop round 2
Session: session_01AZbN6PU5Xz4WTi6NKmyKhN (the card thread; seat session session_013odhA26dvzy7pQmevdTTvy posts this claim)
Account: os-tesla
Branch: claude/project-thread-cmvqwf
Worktree: objectstack-issue-25233
Domain: domain:spec
Seat: domain:spec#1 (seat post #6017)
File surface: stage 1 of ruling 6355206139 only — packages/plugins/plugin-security/src/** (default sets, anchoring), packages/platform-objects/src/identity/**, packages/plugins/plugin-auth/src/** (role config, directory doors, invitation grant, exclusivity), the spec file declaring membership roles if one exists, tests, one changeset (stop on breach; explain in the report). ⛔ no audience / derivePosture / OWD seam / matrix column (stage 2), no ADR edit, no services half.
Container & model: L, mode:thread, model: default tier (dispatch-gates --tier: "no path-derived mandate", floor sonnet · default opus · ceiling fable), effort: high
Clause-②: yes (widening; the dev measures for any narrowing of an existing holder)
Metadata-protocol: added
Responsibility: n/a — not a defect card
Thread-read: none
Serial constraints cleared: seat 2's design-round claim 6343811714 released by this seat after seat 2's demotion on #18549. #25234 is dispatched in parallel; its PR ① is spec-only, and its plugin-security PR ② is serialised against this card's stage 2, not stage 1.
The platform lets the card thread push only its designated thread branch, so Branch: records that branch, not claude/issue-25233-<slug>. Dispatch order: the project's shared folder, spec-seat-1/dispatch-25233.md. The seat runs a CONTRACT_REVIEW_TIER review before the queue.
{
"issue": 25233,
"status": "done",
"branch": "claude/project-thread-cmvqwf",
"pr": "https://github.com/objectstack-ai/objectstack/pull/25515",
"session": "session_01AZbN6PU5Xz4WTi6NKmyKhN",
"premise_still_valid": true,
"summary": "Stage 1 of ruling 6355206139 letter B, one draft PR. Adds the external membership role (better-auth role with no statements; spec vocabulary, reserved identity org_external, invitation grant row 'any'). E1: the resolver projects sys_member.role=external in the active organization to org_external and withholds the everyone anchor (per organization, fail closed with no tenant). E4: new shipped set external_default (member_default's own-row carve-outs without sys_user_org_members, plus a sys_member own-row policy) bound by the org_external built-in position, and plugin-security's additive baseline is swapped to it on an external context, so neither an app isDefault set nor an accepted package default reaches it. E2: list-members and get-full-organization answer an external caller its own member row and own invitations only. E5: external with another role is refused at write on sys_member and sys_invitation (400 VALIDATION_FAILED, invalid_value). E6: an external is refused any business-unit placement (BU membership, BU-anchored position, invitation business_unit_id, role change while placed).",
"tests": "New: core resolve-authz-context.external-population.test.ts 5/5 (4 red before the resolver change); plugin-security external-population.test.ts 11/11; plugin-auth external-membership.test.ts 16/16 and external-directory-door.test.ts 8/8; spec membership-role-external.test.ts; dogfood external-member-directory.dogfood.test.ts 6/6 on a real showcase boot with the showcase isDefault set wired (external reads only own sys_user/sys_member at the data door and both better-auth doors; one-object grant reads that object, 403 on the app-default object; member unchanged; update-member-role ['external','member'] -> 400, stored role unchanged). Suites after merging main 05cc3ee9: plugin-security Test Files 215/215 Tests 4478 passed; plugin-auth 191/191, 3607 passed; core src/security 53/53, 1092 passed; spec src/identity 11/11, 182 passed; role/identity pin tests in 13 other packages all pass; dogfood membership-role-vocabulary 6/6 (pin updated), security-catalog-showcase and org-admin-affordance-reach pass. typecheck exit 0 for spec, core, plugin-security, plugin-auth, platform-objects; dogfood tsc --noEmit exit 0 with an 8 GB heap (the default heap aborted with 134, environmental).",
"gates": "dispatch-gates --commands re-derived after the changeset and the main merge: 109 commands, each run with </dev/null, exit codes landed in ran.txt before any pipe; dispatch-gates --ran: 109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0. Fixed on the way: check:where-matcher (two new test doubles now refuse combinators), check:engine-double-contract (ledger --write for the new pin). Prerequisite exits 3 cleared by building, not waived (spec dist rebuilt after merge; check:dual-build-cjs-loads after building seven unrelated unbuilt packages and rebuilding @objectstack/organizations with declarations). CI check-runs: pending on the draft PR.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm; targeted dogfood files run, see tests",
"line_budget": "45 files, +1650/-67 vs origin/main; non-test, non-generated source: 13 files, +649/-18",
"files_changed": "spec (membership-role.ts, eval-user.zod.ts, invitation-role-grant.ts, index.ts, api-surface + export-origins regenerated); core (security/resolve-authz-context.ts); plugin-security (objects/default-permission-sets.ts, builtin-positions.ts, security-plugin-permission-sets.ts, translations regenerated); plugin-auth (auth-manager.ts, auth-plugin.ts, new external-membership.ts, new external-directory-door.ts); platform-objects (translations regenerated); qa/dogfood (new test, vocabulary pin); scripts/engine-double-contract.pinned.json; .changeset/25233-external-org-role.md",
"deviations": "File surface: packages/core/src/security/resolve-authz-context.ts is outside the order's listed surface; it is the resolver's one provisioning boundary E1 names, so the stamp could not land elsewhere. Also outside the list: spec eval-user.zod.ts/index.ts (the reserved identity), generated i18n and spec surface files, the engine-double ledger, and dogfood tests. No platform-objects identity object file changed (only generated translations). Changeset levels are minor (Metadata-protocol: added) for spec, core, plugin-security, plugin-auth, platform-objects. Pins moved deliberately: built-in positions 6->7, reserved identity names 4->5, delegated_admin/org_admin invite offers include external, the closed better-auth role map includes external, and in the delegated posture test org_external meets the delegated-admin gate's 403 before the reserved-name 400 (both store nothing).",
"mcp_calls": "0 — no MCP GitHub calls (subscribe_pr_activity on #25515 is a session subscription, not a GitHub MCP tool)",
"api_writes": "2 — POST repos/objectstack-ai/objectstack/pulls (#25515, draft, via fleet-write relay run 39995653474); POST repos/objectstack-ai/objectstack/issues/25233/comments (this report, via post-stamped.mjs)",
"open_questions": [],
"out_of_scope_findings": [
"class: b · reach: public door — better-auth /organization/list-teams and /organization/list-team-members, and get-full-organization's teams member, check membership only, so an external still sees team names and team rosters · evidence: better-auth 1.7.12 crud-team routes, same membership-only shape as listMembers · dedupe words: external teams directory door",
"class: b · reach: named producer — member_default grants read on sys_team, sys_sso_provider and the OAuth resource tables with no row scope (externals no longer hold it) · dedupe words: member_default unscoped team sso read",
"class: c · reach: stage 2 — explain/describe surfaces still read baselinePermissionSets, not the external swap; externals have no owner_only OWD floor until the stage 2 OWD seam · dedupe words: external explainer baseline owd floor",
"carrier: PR #25515 body · noted, not filed"
]
}
From the contract review on PR #25515 (stage 1, verdict PASS). Two items the review judged out of stage 1 are folded into stage 2 of this card instead of a new card, because both are the same leak one door over from E2 and touch the response stage 1 already rewrites:
Team names reach an external.list-teams and the teams member of get-full-organization are not narrowed. No member row leaks through them (list-team-members needs the caller on the team), but an external portal user can read the organization's team names, which is internal structure the ruling's "no member directory" rung means to hide. Stage 2 narrows both for an external holder.
Pending invitees' emails, unverified. Whether better-auth's /organization/list-invitations is membership-only was not readable in the review container. If it is, an external reads other invitees' emails there. Stage 2 measures it first, and narrows it to the caller's own email if so (the rule get-full-organization.invitations already follows).
Docblock line. The resolver treats a role list that contains external as external; the directory door requires exactly external. They agree on every row the engine admits (E5 refuses mixed lists). Stage 2 adds one line saying so to the door's docblock.
Business check: the customer is the app whose supplier/portal contacts sign in as externals; a portal contact seeing the tenant's team list or other invitees' emails is a confidentiality complaint the first time it is noticed.
{
"issue": 25233,
"status": "done",
"branch": "claude/project-thread-cmvqwf",
"pr": "https://github.com/objectstack-ai/objectstack/pull/25541",
"session": "session_01AZbN6PU5Xz4WTi6NKmyKhN",
"premise_still_valid": true,
"summary": "Stage 2 of ruling 6355206139 letter B (item 2 plus scope note 6371125645), one draft code PR, body Closes #25233, one changeset. The resolver stamps audience 'external' on a holder and derivePosture returns EXTERNAL (PLATFORM_ADMIN > EXTERNAL > TENANT_ADMIN > MEMBER); the assembled context carries the audience, agent envelopes included. OWD seam: core owdForAudience floors an external's effective model to private (owner + shares) in plugin-sharing buildReadFilter/buildWriteFilter/checkEdit/checkDelete; plugin-security lifts the floor per object and operation class when a held set's RLS using policy applies, via operation-private stamps (__externalReadGrant/__externalWriteGrant) that name the object; the object's declared default row scope and the platform ownership floor are not grants. external_default carries owner_only_writes/owner_only_deletes bound to org_external (same provenance key as the member floor), keeping EXTERNAL within MEMBER on writes. authz-matrix-gate gains the EXTERNAL column. Explain/describe resolve the baseline the principal actually gets (external_default, 'external baseline (org_external)') and explain's OWD layer reports the owner-only floor. list-teams and get-full-organization teams answer an external the teams it sits on; list-invitations (measured membership-only in better-auth 1.7.12 crud-invites listInvitations) answers it its own email's invitations. Door docblock notes includes('external') vs exactly 'external'. sharing-rules.mdx external section updated.",
"tests": "New: core external-owd-floor.test.ts 6/6; plugin-sharing external-owd-floor.test.ts 15/15; plugin-security external-owd-grant-stamp.test.ts 9/9, explain-external-principal.test.ts 6/6, authz-matrix-gate 54/54 with the EXTERNAL column; plugin-auth door tests (teams, full-org teams, invitations) 37/37 with external-membership; core posture-ladder/resolver/assembly cells. Dogfood external-member-directory 10/10 on a real showcase boot: external reads its own + shared row of four on public_read showcase_announcement; an external with a select-RLS grant reads exactly the one matching row; list-teams/full-org teams and list-invitations narrowed, member controls armed. Full suites: plugin-security 4602, core 2806, plugin-sharing 1224, plugin-auth 3620, plugin-approvals 1233, rest 6608 passed (before the main merge, which touched only plugin-auth among these; plugin-auth tsc and the 37 door tests re-passed after it). tsc clean: core, plugin-security (incl. tsconfig.test.json), plugin-sharing, plugin-auth, dogfood (8 GB heap).",
"gates": "dispatch-gates --commands: 121 commands, each with </dev/null, exit codes recorded before any pipe; dispatch-gates --ran: 121 derived, 121 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0. Fixed on the way: check:engine-double-contract (dropped unused update/delete from a read-only fake, no ledger change). CI on head 917632215: all 42 check-runs success or skipped (opt-in/path-scheduled); one interim red, Reserved-word ('role') docs ratchet on the docs edit, fixed by rewording.",
"dogfood_battery": "n/a — no ! in the title and no (narrowing) arm; targeted dogfood file run, see tests",
"line_budget": "36 files, +1556/-148 vs origin/main; under the 3000-line threshold, not split",
"files_changed": "spec (kernel/execution-context.zod.ts, security/explain.zod.ts; generated reference mdx); core (security posture-ladder.ts, resolve-authz-context.ts, assemble-execution-context.ts, new external-owd-floor.ts, index.ts); plugin-sharing (sharing-service.ts); plugin-security (new external-baseline.ts, new external-owd-grant-stamp.ts, security-plugin.ts, security-plugin-sharing-verdicts.ts, security-plugin-permission-sets.ts, security-plugin-explain-access.ts, explain-engine.ts, objects/default-permission-sets.ts, platform-ownership-policies.ts docblock); plugin-auth (external-directory-door.ts, auth-manager.ts); qa/dogfood test; content/docs/permissions/sharing-rules.mdx; .changeset/25233-external-posture-owd-floor.md",
"deviations": "Changeset: minor on spec, core, plugin-security, plugin-sharing, plugin-auth; Clause-②: yes (widening) for the new core exports, with the note that the narrowings apply only to the external role, which no release has shipped (stage 1 changeset still pending); Metadata-protocol: semantic. Write floor on external_default is an addition the order did not name: without it an external's write reach on public objects was org-wide, wider than a member's (I2). Pin moved: platform-ownership-policies.test.ts deduplicates the floor by identity key (two carriers) and pins the carriers. Outside the order's file list: platform-ownership-policies.ts docblock, sharing-rules.mdx.",
"mcp_calls": "read-only only: issue_read (#25233), get_job_logs x3, pull_request_read get_check_runs x2; 0 MCP writes",
"api_writes": "3 — POST pulls (#25541, draft, fleet-write relay run 40025862331); PATCH issues/25541 body (relay run 40025968100, corrected the verification sentence); POST issues/25233/comments (this report, post-stamped.mjs)",
"open_questions": [
"'Access matrix' was read as the EXTERNAL role column of authz-matrix-gate; ADR-0090 D11 also names a D6 explain access-matrix external-audience column. If that was meant, it is a follow-up."
],
"out_of_scope_findings": [
"class: b · reach: public door — creating a share on a public_read/public_read_write object is refused as inert (inertGrantReason), but such an object is now owner-only for an external, so the create gate closes the share channel to externals on public objects (dogfood writes its share row directly) · dedupe words: external share inert public object create gate",
"class: b · reach: public door — better-auth /organization/list-team-members still checks membership only, so an external reads a team roster for a team id it knows (list-teams is narrowed, list-team-members is not; not in the stage 2 order) · dedupe words: external list-team-members roster door",
"carrier: PR #25541 body · noted, not filed"
],
"next": "Tier H docs PR (ADR-0095 D2, ADR-0099 D2 column/D3/I7, ADR-0090 D11, dated note on ADR-0135 D2/D4) drafted locally; opens from the thread branch restarted at main once #25541 merges."
}
{
"issue": 25233,
"status": "done",
"branch": "claude/project-thread-cmvqwf",
"pr": "https://github.com/objectstack-ai/objectstack/pull/25557",
"session": "session_01AZbN6PU5Xz4WTi6NKmyKhN",
"premise_still_valid": true,
"summary": "Tier H docs PR for ruling 6355206139 letter B, item 2, opened as a draft from the thread branch restarted at main 45f996231 after #25541 merged. ADR-0095: a dated Amended line, TL;DR item 2, the D2 enum comment and EXTERNAL bullet (channel list: shares and share links, sharing rules, held position and permission-set grants with RLS; owner-only OWD and write floors; the controlled_by_parent exemption; the named ownerless-object limit; the original quoted), and a D3 bullet (EXTERNAL derives from the reserved org_external position projected from sys_member.role = external). ADR-0099: a dated header line, the D2 EXTERNAL column rewritten with a note superseding the 2026-07-18 'EXTERNAL rows stand as written', a D3 head note (sharing rules apply, item 1 shipped, the ownerless-object limit, explain's guest -> EXTERNAL mapping), I3's dated exception (asked by the seat at 00:22Z), and I7 read through the floor. ADR-0090: a Re-amended line and a second D11 note (the population inside the member pool, by role; the D6 external-audience-column sentence retired). ADR-0135: a dated objectstack note after D4 (a third population marked by role on sys_member; cloud ADR-0024 not amended). Rider: the core external-owd-floor.ts docblock no longer lists controlled_by_parent, with a patch changeset on @objectstack/core (Clause-②: no, Metadata-protocol: none).",
"tests": "n/a, docs and one comment line",
"gates": "dispatch-gates --commands: 69 commands, each with </dev/null; dispatch-gates --ran: 69 derived, 69 run, 0 NOT-MEASURED, all exit 0, on e9bb1782d and again on f653505be. The red Lint & Repo Gates at e9bb1782 (job 120723994996) was the aggregator over six lanes cancelled by the f653505be push; nothing failed. CI on f653505be is green.",
"dogfood_battery": "n/a: docs only",
"line_budget": "6 files, under the 3000-line threshold",
"files_changed": "docs/adr/0090, 0095, 0099, 0135; packages/core/src/security/external-owd-floor.ts (docblock); .changeset/25233-external-floor-docblock.md",
"deviations": "The core docblock rider and its patch changeset came from the seat's carry note (23:17Z). The I3 note came from the seat's ask (00:22Z). The PR is a draft with no auto-merge, for the maintainer to merge.",
"mcp_calls": "read-only only: pull_request_read get (#25541), get_job_logs x1; 0 MCP writes",
"api_writes": "3: POST pulls (#25557, draft, fleet-write relay); PATCH issues/25557 body (the I3 row and note); POST issues/25233/comments (this report, post-stamped.mjs)",
"open_questions": [],
"out_of_scope_findings": [
"carrier: #25549 (awaiting a ruling) · its ADR-0111 D7 line rides a later Tier H PR"
],
"next": "Awaiting maintainer review and merge of #25557 (os-zhuang and hotlong requested). Push nothing unless a reviewer asks for changes."
}
Activity
objectstack-fleet commentedon Oct 22, 2026
Claim: PM loop round 8
Session:
session_01EjZr5fYfHhxeJA2EjdEEkn(the card thread; seat sessionsession_01B2EeooonSXrANpkpgUSfFZposts this claim)Account:
os-billBranch:
claude/project-thread-md96s8(the card thread's designated push branch, atmain42b60b9;claude/issue-N-slugis the convention, and this is the measured platform exception the pm-dispatch runbook records)Worktree:
objectstack-issue-25233Domain:
domain:specSeat:
domain:spec#2(seat post #18549)File surface:
packages/plugins/plugin-security/src/objects/default-permission-sets.ts,packages/platform-objects/src/identity/(thesys_user/sys_memberrow scopes),packages/plugins/plugin-auth/src/(the organization role config and the JIT population stamp), the spec contract the role or population is declared in if one is owed, their tests, one changeset; a separate docs-only PR for the dated ADR-0135 D2/D4 note (Part of #25233, Tier H, the maintainer's merge) (stop on breach; explain in the report)Container & model:
L,mode:thread,model: default(dispatch-gates --tier: no path-derived mandate),effort: xhigh(p1,security); a published-contract widening, if the diff makes one, owes oneCONTRACT_REVIEW_TIERreview of record before the queueClause-②: yes (expected widening: a new organisation role the invite and the population accept; the dev re-measures from the diff and the PR body states the measured value)
Responsibility: the baseline member sets'
sys_user_org_membersrow scope andsys_user'sdefaultRowScopeproduce the directory exposure | no platform path covers a signed-in external member (ADR-0138 covers the anonymous principal only) | reached by portal users invited as organisation members on hosted environments (hotsrm's supplier portal design, cloud#3149); measured use today: none in production per the cardThread-read: none
Serial constraints cleared: #25238 (open,
claude/issue-25112-magic-link-ttl) touchesplugin-auth/src/auth-manager.tsand a mail-locale test only, a different region; whoever lands second mergesmain. No other open PR touches the surface.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
os-dev-report
{ "issue": 25233, "status": "needs_decision", "branch": "claude/project-thread-md96s8", "pr": null, "session": "session_01EjZr5fYfHhxeJA2EjdEEkn", "premise_still_valid": true, "summary": "Stopped before code at a fork, per the dispatch order. The leak is real and measured at main 3e07389: sys_user_org_members sits on three shipped sets (organization_admin :447, member_default :858, viewer_readonly :1159), sys_user's defaultRowScope repeats it, sys_member carries no member-side row scope, RLS OR-combines, and better-auth's list-members and get-full-organization doors check membership only. The fork: the card's sub-claim 'a new population, not an unused capability' is false. ExecutionContext.audience 'external' (a published, zero-consumer spec key) and the EXTERNAL posture rung (a test-locked dead branch) are reserved for exactly this population, and three accepted records lock its semantics against the 2026-10-22 ruling: ADR-0095 D2 and ADR-0099 D3/I7 (explicitly shared rows only; sharing rules, OWD and held bits never apply; 'model EXTERNAL as a restricted MEMBER' rejected twice) and ADR-0090 D11 as amended by ruling of record 6159300530 (external principals come from an identity pool separate from members). The 2026-10-22 ruling puts portal users in the member pool and reaches them through positions, permission sets and sharing rules, and names none of those records. The full analysis (options with customer-visible effect and workload, the four axes, re-check commands) goes to the seat with this report for the decision card.", "tests": "none run: stopped at a fork before any code (os-dev, when to stop and not write code). Every reading is a file:line at objectstack main 3e07389, plus better-auth 1.7.12 dist (routes/crud-members.mjs:465, routes/crud-org.mjs:370).", "gates": "not run: no diff (the branch sits at origin/main 3e07389 with zero commits).", "dogfood_battery": "n/a: no diff", "mcp_calls": "0", "api_writes": "1: POST repos/objectstack-ai/objectstack/issues/25233/comments (this report, through scripts/pm/post-stamped.mjs). The empty-branch probe was a git push, not a REST write.", "open_questions": [ { "question": "Is the external organisation role the platform's reserved EXTERNAL principal (ExecutionContext.audience 'external', the ADR-0095/0099 rung), and if so which reach channels does that rung admit? The 2026-10-22 ruling (member pool; positions, permission sets, sharing rules) contradicts ADR-0095 D2, ADR-0099 D3/I7 and ADR-0090 D11 as amended on 2026-10-14 (separate pool; explicit shares, sharing sets and share links only).", "options": [ "A - two meanings: the role is a restricted member (posture MEMBER, audience unstamped), the EXTERNAL rung stays reserved for a separate pool; cross-notes on the three records. Smallest; leaves audience reporting a portal partner as internal and two 'external's for an AI to conflate. Spec + engine, 1 code PR + 1 Tier H docs PR, no measured precedent.", "B - one meaning, rung re-ruled: the role IS the EXTERNAL principal (audience 'external', derivePosture returns EXTERNAL), and the rung admits explicit grants only (shares, sharing rules, position and permission-set grants with their RLS), never the internal OWD baseline, the baseline or everyone, or the directory; Tier H amendments to ADR-0095 D2, ADR-0099 D3/I7, ADR-0090 D11. A plus the OWD seam and matrix cells, 1-2 code PRs + 1 Tier H docs PR, est. 1.5-2x A.", "C - the rung as locked: explicitly shared rows only, sharing rules and held bits never apply. Withdraws the ruling's channels, card acceptance item 2 fails as written, and a supplier portal needs per-record shares until sharing sets (unbuilt) exist." ], "recommendation": "B, because on long-term soundness it leaves one external concept whose reserved shapes get their first consumer, in the mainstream shape (Salesforce external users: permission-set object access plus sharing rules, sharing sets and manual shares; Power Pages table permissions); it serves hotsrm's supplier portal today as A does; it removes the AI trap A creates (wiring audience from the role silently empties every portal under the locked rung); and the workload is under 3x A. Confidence gap: cloud#3149's own wording was not read (out of this session's reach)." }, { "question": "Execution parameters to rule in the same stroke (they hold under A and B).", "options": [ "E1 the population marker is sys_member.role, per organisation, read at the resolver's one provisioning boundary (ADR-0095 D3), not a sys_user column (a stored tier ADR-0095 rejects; cannot say external in one org and member in another)", "E2 the better-auth directory doors (list-members, get-full-organization) are narrowed for an external caller in the same code PR (same leak, second door)", "E3 the invitation grant row for external is 'any' issuer that reaches invite-member (it confers less than member, by the table's own rule)", "E4 an external's self-service baseline is a new shipped anchor-safe set: member_default's own-row carve-outs without sys_user_org_members, plus sys_member own row; app isDefault sets and accepted package defaults do not reach it", "E5 external is exclusive: a membership role list carrying it with another role is refused at write" ], "recommendation": "E1 to E5 as stated." } ], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Triage: one lane,
domain:spec.domain:servicesremoved,needs-user-decisionkeptTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-22T15:53Z. ⛔ Not a claim.6344121515), belong to the spec seat that claimed it. The fork: the 2026-10-22 ruling puts portal users in the member pool, against the accepted ADR-0095 D2, ADR-0099 D3/I7 and ADR-0090 D11.area:access. Type: +enhancement.7 remaining items
objectstack-fleet commentedon Oct 23, 2026
Claim: PM loop round 2
Session:
session_01AZbN6PU5Xz4WTi6NKmyKhN(the card thread; seat sessionsession_013odhA26dvzy7pQmevdTTvyposts this claim)Account:
os-teslaBranch:
claude/project-thread-cmvqwfWorktree:
objectstack-issue-25233Domain:
domain:specSeat:
domain:spec#1(seat post #6017)File surface: stage 1 of ruling
6355206139only —packages/plugins/plugin-security/src/**(default sets, anchoring),packages/platform-objects/src/identity/**,packages/plugins/plugin-auth/src/**(role config, directory doors, invitation grant, exclusivity), the spec file declaring membership roles if one exists, tests, one changeset (stop on breach; explain in the report). ⛔ no audience /derivePosture/ OWD seam / matrix column (stage 2), no ADR edit, no services half.Container & model:
L,mode:thread,model: default tier (dispatch-gates --tier: "no path-derived mandate", floor sonnet · default opus · ceiling fable),effort: highClause-②: yes (widening; the dev measures for any narrowing of an existing holder)
Metadata-protocol: added
Responsibility:
n/a — not a defect cardThread-read: none
Serial constraints cleared: seat 2's design-round claim
6343811714released by this seat after seat 2's demotion on #18549. #25234 is dispatched in parallel; its PR ① is spec-only, and its plugin-security PR ② is serialised against this card's stage 2, not stage 1.The platform lets the card thread push only its designated thread branch, so
Branch:records that branch, notclaude/issue-25233-<slug>. Dispatch order: the project's shared folder,spec-seat-1/dispatch-25233.md. The seat runs aCONTRACT_REVIEW_TIERreview before the queue.Generated by Claude Code
objectstack-fleet commentedon Oct 23, 2026
os-dev-report
{ "issue": 25233, "status": "done", "branch": "claude/project-thread-cmvqwf", "pr": "https://github.com/objectstack-ai/objectstack/pull/25515", "session": "session_01AZbN6PU5Xz4WTi6NKmyKhN", "premise_still_valid": true, "summary": "Stage 1 of ruling 6355206139 letter B, one draft PR. Adds the external membership role (better-auth role with no statements; spec vocabulary, reserved identity org_external, invitation grant row 'any'). E1: the resolver projects sys_member.role=external in the active organization to org_external and withholds the everyone anchor (per organization, fail closed with no tenant). E4: new shipped set external_default (member_default's own-row carve-outs without sys_user_org_members, plus a sys_member own-row policy) bound by the org_external built-in position, and plugin-security's additive baseline is swapped to it on an external context, so neither an app isDefault set nor an accepted package default reaches it. E2: list-members and get-full-organization answer an external caller its own member row and own invitations only. E5: external with another role is refused at write on sys_member and sys_invitation (400 VALIDATION_FAILED, invalid_value). E6: an external is refused any business-unit placement (BU membership, BU-anchored position, invitation business_unit_id, role change while placed).", "tests": "New: core resolve-authz-context.external-population.test.ts 5/5 (4 red before the resolver change); plugin-security external-population.test.ts 11/11; plugin-auth external-membership.test.ts 16/16 and external-directory-door.test.ts 8/8; spec membership-role-external.test.ts; dogfood external-member-directory.dogfood.test.ts 6/6 on a real showcase boot with the showcase isDefault set wired (external reads only own sys_user/sys_member at the data door and both better-auth doors; one-object grant reads that object, 403 on the app-default object; member unchanged; update-member-role ['external','member'] -> 400, stored role unchanged). Suites after merging main 05cc3ee9: plugin-security Test Files 215/215 Tests 4478 passed; plugin-auth 191/191, 3607 passed; core src/security 53/53, 1092 passed; spec src/identity 11/11, 182 passed; role/identity pin tests in 13 other packages all pass; dogfood membership-role-vocabulary 6/6 (pin updated), security-catalog-showcase and org-admin-affordance-reach pass. typecheck exit 0 for spec, core, plugin-security, plugin-auth, platform-objects; dogfood tsc --noEmit exit 0 with an 8 GB heap (the default heap aborted with 134, environmental).", "gates": "dispatch-gates --commands re-derived after the changeset and the main merge: 109 commands, each run with </dev/null, exit codes landed in ran.txt before any pipe; dispatch-gates --ran: 109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0. Fixed on the way: check:where-matcher (two new test doubles now refuse combinators), check:engine-double-contract (ledger --write for the new pin). Prerequisite exits 3 cleared by building, not waived (spec dist rebuilt after merge; check:dual-build-cjs-loads after building seven unrelated unbuilt packages and rebuilding @objectstack/organizations with declarations). CI check-runs: pending on the draft PR.", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm; targeted dogfood files run, see tests", "line_budget": "45 files, +1650/-67 vs origin/main; non-test, non-generated source: 13 files, +649/-18", "files_changed": "spec (membership-role.ts, eval-user.zod.ts, invitation-role-grant.ts, index.ts, api-surface + export-origins regenerated); core (security/resolve-authz-context.ts); plugin-security (objects/default-permission-sets.ts, builtin-positions.ts, security-plugin-permission-sets.ts, translations regenerated); plugin-auth (auth-manager.ts, auth-plugin.ts, new external-membership.ts, new external-directory-door.ts); platform-objects (translations regenerated); qa/dogfood (new test, vocabulary pin); scripts/engine-double-contract.pinned.json; .changeset/25233-external-org-role.md", "deviations": "File surface: packages/core/src/security/resolve-authz-context.ts is outside the order's listed surface; it is the resolver's one provisioning boundary E1 names, so the stamp could not land elsewhere. Also outside the list: spec eval-user.zod.ts/index.ts (the reserved identity), generated i18n and spec surface files, the engine-double ledger, and dogfood tests. No platform-objects identity object file changed (only generated translations). Changeset levels are minor (Metadata-protocol: added) for spec, core, plugin-security, plugin-auth, platform-objects. Pins moved deliberately: built-in positions 6->7, reserved identity names 4->5, delegated_admin/org_admin invite offers include external, the closed better-auth role map includes external, and in the delegated posture test org_external meets the delegated-admin gate's 403 before the reserved-name 400 (both store nothing).", "mcp_calls": "0 — no MCP GitHub calls (subscribe_pr_activity on #25515 is a session subscription, not a GitHub MCP tool)", "api_writes": "2 — POST repos/objectstack-ai/objectstack/pulls (#25515, draft, via fleet-write relay run 39995653474); POST repos/objectstack-ai/objectstack/issues/25233/comments (this report, via post-stamped.mjs)", "open_questions": [], "out_of_scope_findings": [ "class: b · reach: public door — better-auth /organization/list-teams and /organization/list-team-members, and get-full-organization's teams member, check membership only, so an external still sees team names and team rosters · evidence: better-auth 1.7.12 crud-team routes, same membership-only shape as listMembers · dedupe words: external teams directory door", "class: b · reach: named producer — member_default grants read on sys_team, sys_sso_provider and the OAuth resource tables with no row scope (externals no longer hold it) · dedupe words: member_default unscoped team sso read", "class: c · reach: stage 2 — explain/describe surfaces still read baselinePermissionSets, not the external swap; externals have no owner_only OWD floor until the stage 2 OWD seam · dedupe words: external explainer baseline owd floor", "carrier: PR #25515 body · noted, not filed" ] }objectstack-fleet commentedon Oct 23, 2026
Stage 2 scope note ·
domain:specseat 1 · 2026-10-23T17:45ZFrom the contract review on PR #25515 (stage 1, verdict PASS). Two items the review judged out of stage 1 are folded into stage 2 of this card instead of a new card, because both are the same leak one door over from E2 and touch the response stage 1 already rewrites:
list-teamsand theteamsmember ofget-full-organizationare not narrowed. No member row leaks through them (list-team-membersneeds the caller on the team), but an external portal user can read the organization's team names, which is internal structure the ruling's "no member directory" rung means to hide. Stage 2 narrows both for an external holder./organization/list-invitationsis membership-only was not readable in the review container. If it is, an external reads other invitees' emails there. Stage 2 measures it first, and narrows it to the caller's own email if so (the ruleget-full-organization.invitationsalready follows).externalas external; the directory door requires exactlyexternal. They agree on every row the engine admits (E5 refuses mixed lists). Stage 2 adds one line saying so to the door's docblock.Business check: the customer is the app whose supplier/portal contacts sign in as externals; a portal contact seeing the tenant's team list or other invitees' emails is a confidentiality complaint the first time it is noticed.
Generated by Claude Code
objectstack-fleet commentedon Oct 23, 2026
Seat ACCEPT on PR #25515 (stage 1): comment
6372519428· 2026-10-23T19:00Z. Queue landing enabled; card stayspm:dispatchedfor stage 2 (scope note6371125645).Generated by Claude Code
feat(security): the external organization role is the EXTERNAL princi…
docs(permissions): the sharing-rules page names the external role's c…
docs(permissions): name the external membership without the reserved …
objectstack-fleet commentedon Oct 23, 2026
os-dev-report
{ "issue": 25233, "status": "done", "branch": "claude/project-thread-cmvqwf", "pr": "https://github.com/objectstack-ai/objectstack/pull/25541", "session": "session_01AZbN6PU5Xz4WTi6NKmyKhN", "premise_still_valid": true, "summary": "Stage 2 of ruling 6355206139 letter B (item 2 plus scope note 6371125645), one draft code PR, body Closes #25233, one changeset. The resolver stamps audience 'external' on a holder and derivePosture returns EXTERNAL (PLATFORM_ADMIN > EXTERNAL > TENANT_ADMIN > MEMBER); the assembled context carries the audience, agent envelopes included. OWD seam: core owdForAudience floors an external's effective model to private (owner + shares) in plugin-sharing buildReadFilter/buildWriteFilter/checkEdit/checkDelete; plugin-security lifts the floor per object and operation class when a held set's RLS using policy applies, via operation-private stamps (__externalReadGrant/__externalWriteGrant) that name the object; the object's declared default row scope and the platform ownership floor are not grants. external_default carries owner_only_writes/owner_only_deletes bound to org_external (same provenance key as the member floor), keeping EXTERNAL within MEMBER on writes. authz-matrix-gate gains the EXTERNAL column. Explain/describe resolve the baseline the principal actually gets (external_default, 'external baseline (org_external)') and explain's OWD layer reports the owner-only floor. list-teams and get-full-organization teams answer an external the teams it sits on; list-invitations (measured membership-only in better-auth 1.7.12 crud-invites listInvitations) answers it its own email's invitations. Door docblock notes includes('external') vs exactly 'external'. sharing-rules.mdx external section updated.", "tests": "New: core external-owd-floor.test.ts 6/6; plugin-sharing external-owd-floor.test.ts 15/15; plugin-security external-owd-grant-stamp.test.ts 9/9, explain-external-principal.test.ts 6/6, authz-matrix-gate 54/54 with the EXTERNAL column; plugin-auth door tests (teams, full-org teams, invitations) 37/37 with external-membership; core posture-ladder/resolver/assembly cells. Dogfood external-member-directory 10/10 on a real showcase boot: external reads its own + shared row of four on public_read showcase_announcement; an external with a select-RLS grant reads exactly the one matching row; list-teams/full-org teams and list-invitations narrowed, member controls armed. Full suites: plugin-security 4602, core 2806, plugin-sharing 1224, plugin-auth 3620, plugin-approvals 1233, rest 6608 passed (before the main merge, which touched only plugin-auth among these; plugin-auth tsc and the 37 door tests re-passed after it). tsc clean: core, plugin-security (incl. tsconfig.test.json), plugin-sharing, plugin-auth, dogfood (8 GB heap).", "gates": "dispatch-gates --commands: 121 commands, each with </dev/null, exit codes recorded before any pipe; dispatch-gates --ran: 121 derived, 121 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0. Fixed on the way: check:engine-double-contract (dropped unused update/delete from a read-only fake, no ledger change). CI on head 917632215: all 42 check-runs success or skipped (opt-in/path-scheduled); one interim red, Reserved-word ('role') docs ratchet on the docs edit, fixed by rewording.", "dogfood_battery": "n/a — no ! in the title and no (narrowing) arm; targeted dogfood file run, see tests", "line_budget": "36 files, +1556/-148 vs origin/main; under the 3000-line threshold, not split", "files_changed": "spec (kernel/execution-context.zod.ts, security/explain.zod.ts; generated reference mdx); core (security posture-ladder.ts, resolve-authz-context.ts, assemble-execution-context.ts, new external-owd-floor.ts, index.ts); plugin-sharing (sharing-service.ts); plugin-security (new external-baseline.ts, new external-owd-grant-stamp.ts, security-plugin.ts, security-plugin-sharing-verdicts.ts, security-plugin-permission-sets.ts, security-plugin-explain-access.ts, explain-engine.ts, objects/default-permission-sets.ts, platform-ownership-policies.ts docblock); plugin-auth (external-directory-door.ts, auth-manager.ts); qa/dogfood test; content/docs/permissions/sharing-rules.mdx; .changeset/25233-external-posture-owd-floor.md", "deviations": "Changeset: minor on spec, core, plugin-security, plugin-sharing, plugin-auth; Clause-②: yes (widening) for the new core exports, with the note that the narrowings apply only to the external role, which no release has shipped (stage 1 changeset still pending); Metadata-protocol: semantic. Write floor on external_default is an addition the order did not name: without it an external's write reach on public objects was org-wide, wider than a member's (I2). Pin moved: platform-ownership-policies.test.ts deduplicates the floor by identity key (two carriers) and pins the carriers. Outside the order's file list: platform-ownership-policies.ts docblock, sharing-rules.mdx.", "mcp_calls": "read-only only: issue_read (#25233), get_job_logs x3, pull_request_read get_check_runs x2; 0 MCP writes", "api_writes": "3 — POST pulls (#25541, draft, fleet-write relay run 40025862331); PATCH issues/25541 body (relay run 40025968100, corrected the verification sentence); POST issues/25233/comments (this report, post-stamped.mjs)", "open_questions": [ "'Access matrix' was read as the EXTERNAL role column of authz-matrix-gate; ADR-0090 D11 also names a D6 explain access-matrix external-audience column. If that was meant, it is a follow-up." ], "out_of_scope_findings": [ "class: b · reach: public door — creating a share on a public_read/public_read_write object is refused as inert (inertGrantReason), but such an object is now owner-only for an external, so the create gate closes the share channel to externals on public objects (dogfood writes its share row directly) · dedupe words: external share inert public object create gate", "class: b · reach: public door — better-auth /organization/list-team-members still checks membership only, so an external reads a team roster for a team id it knows (list-teams is narrowed, list-team-members is not; not in the stage 2 order) · dedupe words: external list-team-members roster door", "carrier: PR #25541 body · noted, not filed" ], "next": "Tier H docs PR (ADR-0095 D2, ADR-0099 D2 column/D3/I7, ADR-0090 D11, dated note on ADR-0135 D2/D4) drafted locally; opens from the thread branch restarted at main once #25541 merges." }fix(sharing): a controlled_by_parent detail follows its master for an…
objectstack-fleet commentedon Oct 24, 2026
os-dev-report
{ "issue": 25233, "status": "done", "branch": "claude/project-thread-cmvqwf", "pr": "https://github.com/objectstack-ai/objectstack/pull/25557", "session": "session_01AZbN6PU5Xz4WTi6NKmyKhN", "premise_still_valid": true, "summary": "Tier H docs PR for ruling 6355206139 letter B, item 2, opened as a draft from the thread branch restarted at main 45f996231 after #25541 merged. ADR-0095: a dated Amended line, TL;DR item 2, the D2 enum comment and EXTERNAL bullet (channel list: shares and share links, sharing rules, held position and permission-set grants with RLS; owner-only OWD and write floors; the controlled_by_parent exemption; the named ownerless-object limit; the original quoted), and a D3 bullet (EXTERNAL derives from the reserved org_external position projected from sys_member.role = external). ADR-0099: a dated header line, the D2 EXTERNAL column rewritten with a note superseding the 2026-07-18 'EXTERNAL rows stand as written', a D3 head note (sharing rules apply, item 1 shipped, the ownerless-object limit, explain's guest -> EXTERNAL mapping), I3's dated exception (asked by the seat at 00:22Z), and I7 read through the floor. ADR-0090: a Re-amended line and a second D11 note (the population inside the member pool, by role; the D6 external-audience-column sentence retired). ADR-0135: a dated objectstack note after D4 (a third population marked by role on sys_member; cloud ADR-0024 not amended). Rider: the core external-owd-floor.ts docblock no longer lists controlled_by_parent, with a patch changeset on @objectstack/core (Clause-②: no, Metadata-protocol: none).", "tests": "n/a, docs and one comment line", "gates": "dispatch-gates --commands: 69 commands, each with </dev/null; dispatch-gates --ran: 69 derived, 69 run, 0 NOT-MEASURED, all exit 0, on e9bb1782d and again on f653505be. The red Lint & Repo Gates at e9bb1782 (job 120723994996) was the aggregator over six lanes cancelled by the f653505be push; nothing failed. CI on f653505be is green.", "dogfood_battery": "n/a: docs only", "line_budget": "6 files, under the 3000-line threshold", "files_changed": "docs/adr/0090, 0095, 0099, 0135; packages/core/src/security/external-owd-floor.ts (docblock); .changeset/25233-external-floor-docblock.md", "deviations": "The core docblock rider and its patch changeset came from the seat's carry note (23:17Z). The I3 note came from the seat's ask (00:22Z). The PR is a draft with no auto-merge, for the maintainer to merge.", "mcp_calls": "read-only only: pull_request_read get (#25541), get_job_logs x1; 0 MCP writes", "api_writes": "3: POST pulls (#25557, draft, fleet-write relay); PATCH issues/25557 body (the I3 row and note); POST issues/25233/comments (this report, post-stamped.mjs)", "open_questions": [], "out_of_scope_findings": [ "carrier: #25549 (awaiting a ruling) · its ADR-0111 D7 line rides a later Tier H PR" ], "next": "Awaiting maintainer review and merge of #25557 (os-zhuang and hotlong requested). Push nothing unless a reviewer asks for changes." }