Skip to content

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

Closed

Description

@objectstack-fleet

Ruled: 6355206139 · letter B · 2026-10-23T01:26:35Z

Filing gate ②: from the maintainer's ruling on objectstack-ai/cloud#3149 (batch #386 item 5, 「3149 1 2 5 且第 1 项加 external 角色」, 2026-10-22T13:42Z). Filed by repo:cloud#1 (seat post #6026, session session_01FSg7JRUiTPo8GrgT38tAWP). ⛔ Not a claim. A design card: domain:spec + domain:services.

Reader who acts: the objectstack lane. objectstack-ai/cloud#3149 (the tracker) is Blocked-by: this card.

The gap (cloud#3149 item 1, found while designing objectstack-ai/hotsrm#1)

  • 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).

Activity

  1. added
    priority:p1High: required for production / M2
    pm:queueReady for the PM dispatch loop
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    domain:specDomain lane — seat card indexed by label:pm:seat
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 22, 2026
  2. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    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 main 42b60b9; 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.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  4. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Oct 22, 2026
  5. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    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.
    • Area: + area:access. Type: + enhancement.
  6. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    enhancementNew feature or request
    on Oct 22, 2026
  7. 7 remaining items

  8. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 23, 2026
  9. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    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.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    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"
      ]
    }
  11. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Stage 2 scope note · domain:spec seat 1 · 2026-10-23T17:45Z

    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:

    1. 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.
    2. 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).
    3. 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.


    Generated by Claude Code

  12. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Seat ACCEPT on PR #25515 (stage 1): comment 6372519428 · 2026-10-23T19:00Z. Queue landing enabled; card stays pm:dispatched for stage 2 (scope note 6371125645).


    Generated by Claude Code

  13. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    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."
    }
  14. objectstack-fleet commented on Oct 24, 2026

    @objectstack-fleet
    ContributorAuthor

    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."
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsdomain:specDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpriority:p1High: required for production / M2security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions