Skip to content

security: no door can bind a permission set to the guest anchor — ADR-0138 D3 item 5 has no enforcer (PUT /meta/position/guest answers 403 NOT_OVERRIDABLE; guest suggestions are refused) #23267

Closed

Description

@objectstack-fleet

Ruled: 6159279205 · letter C · 2026-10-14T02:06Z

Filing gate ①, class (b): a declared contract with no enforcer.

reach: the public door PUT /api/v1/meta/position/guest, measured as an administrator on a real dogfood boot by ADR-0138 E1's dev (#23224, hand-off 6145588579, after_readings): 403 NOT_OVERRIDABLE. The other path the record names, confirmAudienceBindingSuggestion, refuses a guest suggestion by design.

Filed by domain:engine seat 1 (#6367) · os-support-ai · session_01P88ehrZ6hM19hBzdpsQTTo, from E1's hand-off (open_questions[1], and the first entry of out_of_scope_findings). ⛔ Not a claim; grading and routing are triage's.

Reader who acts: the lane that owns the position definition's authoring door. The candidates are metadata-protocol's save door and plugin-security's suggested-audience-bindings.ts, so triage routes it. It acts after decision #23266 answers.

Blocked-by: #23224

Governing text

  • ADR-0138 D3 item 5 (Accepted): "How a deployment grants. An administrator binds a low-privilege set to guest, directly or by confirming a package's suggestion (packages/plugins/plugin-security/src/suggested-audience-bindings.ts#confirmAudienceBindingSuggestion). Packages suggest; they never bind (ADR-0090 D9)."

What happens

  • Neither path the record names writes a binding: PUT /meta/position/guest → 403 NOT_OVERRIDABLE, and a confirmed guest suggestion → refused.
  • So E1's grants channel has nothing to read in any deployment: the reference apps count 0 bindings (E1's census: app-showcase, app-crm, app-multi-package, app-todo).
  • The dev's options: (A) a follow-up adds a definition or bind door for the guest anchor (the dev recommends this); (B) extend the acceptance flow, which needs a new spec key; (C) amend the record.

Why Blocked-by: #23266

#23266 decides which rows a guest reaches through a bound set. A door that lets an administrator bind a set before that is ruled would open whatever row scope E1 lands with. Under today's measured internal-dial behaviour, that is every row of a public_read object, to the internet.

Dedupe

MCP search_issues on this repo, closed included: "guest anchor binding authoring door confirmAudienceBindingSuggestion guest position NOT_OVERRIDABLE bind permission set to guest" gives 5 hits.

None has this shape. Dedupe words: guest anchor binding door; position guest NOT_OVERRIDABLE; confirmAudienceBindingSuggestion guest refused.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1, rolling slot (#23267, triage 6147447462 with the unlock 6155063299: a bind door for the guest anchor, ADR-0138 D3 item 5, under ruling C on #23266) · 2026-10-13T22:09Z
    Session: session_01BRQ9Mp1yH7R1az6MaUM18H
    Account: os-justin (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-23267-guest-bind-door
    Worktree: objectstack-issue-23267
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    File surface (at origin/main 430e5f5ad or later; the dev measures which door is the record's and declares it in the report and PR body; stop on breach and explain):

    This act moves the card pm:queue → pm:dispatched and assigns os-justin. The PR carries Fixes #23267.


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23267,
      "status": "needs_decision",
      "branch": "claude/issue-23267-guest-bind-door",
      "pr": null,
      "session": "session_01BRQ9Mp1yH7R1az6MaUM18H (subagent: the parent's id)",
      "premise_still_valid": true,
      "summary": "No code. Measured at origin/main 430e5f5ad and re-read at 688a39d77, where none of the door's surfaces changed: ADR-0138 D3 item 5 still has no enforcer, so the premise holds. The narrowest door that keeps what both refusals protect cannot be built inside this card's surface, because neither path the record names has a shape to open. (1) The suggestion path has no producer. collectDeclaredSuggestions hard-codes anchor = EVERYONE_POSITION (suggested-audience-bindings.ts:372), and the spec gives a package no way to suggest guest (isDefault maps to everyone only). Lifting confirm's guest refusal (:834) would open a door no row can reach. (2) The direct path has no shape for any anchor. everyone is bound only through the acceptance record plus the security plugin's re-derivation. The 403 NOT_OVERRIDABLE on PUT /meta/position/guest protects the anchors' sealed definitions: 2c, as #22801 → A reads it, 'no caller writes the anchor's definition'. Two door shapes remain. The everyone-shaped door (Zone 3) needs security-plugin.ts at three wiring points, which open PR #23334 fences, plus a public API shape and a record shape that nothing specifies. The definition door loosens the seal both anchors rely on, which is Zone 2 item 1's report-before-editing case, and environment kernels never read it. I stopped before the first edit. The options, weighed on the four axes, are in open_questions.",
      "zone2_readings": {
        "1_which_door": "Direct path: PUT /meta/position/guest answers 403 NOT_OVERRIDABLE. Pinned on main by E1's landed dogfood case 'the metadata door refuses a save of the packaged guest position' (guest-grants-channel.dogfood.test.ts:485). The guest position is packaged plugin-security metadata (builtin-positions.ts registerBuiltinPositions), and position is allowEnvironmentOverlay false (metadata-plugin.zod.ts:1178; ADR-0126 Regime B; ADR-0131 D6 managed seal). What the refusal protects: the anchors' sealed definitions. The stated reading on #22801 (ruling A) is 'an acceptance changes the derivation's input. No caller writes the anchor's definition', with 'The two audience anchors are sealed (2c)'. The 2c contract review on #22736 records the anchors 'once more sealed against every caller-reachable write'. Opening this door loosens a protection everyone also relies on. Suggestion path: confirm refuses a non-everyone row with 409 SUGGESTION_STATE (suggested-audience-bindings.ts:834), protecting against 'a grant that never takes effect'. Nothing can produce a guest row, though: the sync writes everyone rows only (:372). objectui's SuggestedBindingsPanel pins 'a guest row offers no Accept' at objectui origin/main.",
        "2_low_privilege": "Holds as assumed. describeAnchorForbiddenBits(set, 'guest') (high-privilege.ts:192) refuses: non-empty systemPermissions, with no app-capability excusal for guest; View/Modify All; delete and transfer; export; a '*' grant; and edit. It does not refuse the bypass-list objects; I re-read E1's reading on main. Its only guest-tier call site on a binding write is assertAudienceAnchorBindingGate on sys_position_permission_set rows (security-plugin.ts:6770), a table the resolver no longer reads.",
        "3_C_end_to_end": "E1's harness installs the guest binding through the registry's bare slot (guest-grants-channel.dogfood.test.ts:255-260), because no door exists. It pins 0 rows under private, public_read, public_read_write and controlled_by_parent, plus the share link's one record. A door would replace that install in the same harness. NOT MEASURED here: no door was built. A finding for axis 1: share-link redemption reads in SYSTEM_CTX (share-link-routes.ts:390, :454, :517), not through the guest's set. So under ruling C a bound read set adds no row a guest can read. The live effect of a guest binding is anonymous create at class-4 endpoints and the guest envelope at class-5 endpoints. Anonymous intake already has the class-1 form doors.",
        "4_audit": "The existing doors record the decision on the sys_audience_binding_suggestion row: resolved_by, resolved_at, deployment_decision. The object declares trackHistory: true, and the door writes one logger.info line. A1 below matches that shape with no new audit shape. A2 would be audited as a sys_metadata commit instead.",
        "seat_bypass_premise": "Holds, so no fork. The bypass list on main is sharing-service.ts:467-474: sys_record_share, sys_user, sys_organization, sys_member, sys_user_permission_set, plus host bypassObjects extras, which have 0 in-repo producers. shouldBypass returns at :485, before E1's isGuestPrincipal deny-all. Share-link redemption reads the record and its messages in system context. No guest-shaped set in the corpus names a bypass object: showcase_guest_portal (showcase_announcement read, showcase_inquiry create), crm guest_portal (crm_lead create), and hotcrm 0f84c9c guest_portal, which is a form-door set."
      },
      "tests": "No code change, so no new tests and no ablation. Readings taken on main 430e5f5ad in worktree objectstack-issue-23267. (1) Closure build under the lock: `pnpm --workspace-concurrency=2 --filter '@objectstack/plugin-security^...' build` (slot issue-23267-build): 'VERDICT command-exit 0 · held the lock 271s'. (2) `pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2 src/suggested-audience-bindings.test.ts src/builtin-positions.test.ts`: 'Test Files 2 passed (2) / Tests 46 passed (46)', VERDICT command-exit 0, EXIT 0. (3) The same file run verbose with -t 'guest-anchor row': 'refuses a guest-anchor row loudly rather than record a grant it does not deliver' passed, '1 passed | 40 skipped (41)', VERDICT command-exit 0. (4) The metadata-door 403 is E1's landed dogfood pin on main. NOT MEASURED locally, reason: the dogfood closure was not built for a no-code reading; CI's full battery runs it on main.",
      "gates": "`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` on the final tree exited 2 with 'this branch changes nothing against origin/main (merge base 430e5f5ad) — nothing to derive'. No --ran reconcile, because nothing was derived. Lead list (87 commands at 430e5f5ad): NOT MEASURED, reason: there is no diff, and every family on the list grades a change this run did not make.",
      "line_budget": "n/a — no skills/** path",
      "files_changed": "none. The branch claude/issue-23267-guest-bind-door equals origin/main 430e5f5ad; it was pushed empty as the write-route probe.",
      "deviations": [
        "Stopped before the first edit, as the dispatch requires when the door needs a fenced file (security-plugin.ts, open PR #23334) and when opening a door would loosen a protection other anchors rely on (Zone 2 item 1). No label write, no PR.",
        "Built plugin-security's dependency closure and ran two existing pin files, to measure the confirm refusal on today's main rather than by reading only."
      ],
      "mcp_calls": "0",
      "api_writes": "1 — this os-dev-report comment on #23267, through scripts/pm/post-stamped.mjs on the fleet-write relay. git push is not a REST write.",
      "open_questions": [
        {
          "question": "Which door builds ADR-0138 D3 item 5? Neither named path can be opened as a narrow door inside this card's surface (summary; zone2_readings.1). Triage keeps option A the default. Two A-shaped doors exist, and they differ in architecture.",
          "options": [
            "A1 — bind door as an assignment record, the everyone shape (Zone 3). A manage_metadata holder (under a wall, the platform operator) binds a set to guest through a new security-service act. The act refuses first with describeAnchorForbiddenBits(set, 'guest') plus the bypass-list refusal. It then writes one sys_audience_binding_suggestion row (anchor guest, status confirmed, deployment_decision true; trackHistory plus one info line, the existing audit shape). The plugin re-declares guest's definition from the accepted rows (readAcceptedGuestSets, plus a declareGuestAnchor beside declareEveryoneBaseline in builtin-positions.ts). It does so at kernel:ready, on bind and revoke, and on uninstall and package publish, asking the guard again each time and withholding loudly. Revoke uses the existing dismiss route, and the 403 on the definition stays. Cost: security-plugin.ts at the three points everyone uses (the kernel:ready hook at :1751, the re-derive dep at :2031, the service registration near :2258), all fenced by PR #23334. Also: a new ISecurityService method, a REST route with a closed query set and a runtime domain route; the record's required package_id (open_questions[1]); and objectui's SuggestedBindingsPanel. Roughly 800-1200 lines with tests: spec, plugin-security, rest and runtime minor, plus an objectui PR. Axes: need — the record is Accepted, but the measured writers are 0 (reference apps per E1's census; hotcrm 0f84c9c), and under ruling C a bound read set reaches 0 rows, so the live effect is anonymous create at class-4 doors; long-term — one mechanism for both anchors, both definitions stay sealed, the guard runs on every re-derive, and host and environment kernels both read the registry re-declaration; AI — no anchor definition becomes writable, and a forbidden set is refused loudly at the write and withheld loudly at derivation; startup — the largest of the three.",
            "A2 — definition door. Admit PUT /meta/position/guest as an environment definition (the registry's bare slot answers first, the state E1's harness installs by hand), with the guard at the save. Cost: it reverses the 2c reading #22801 → A recorded and the anchors' seal recorded on #22736, which needs a ruling. It also needs a name branch in the metadata protocol's managed seal (ADR-0126 Regime B for position, ADR-0131 D6). Environment kernels never read it: the registry already serves guest, so the overlay is never consulted, on main or under PR #23334's echo, which echoes only names the registry does not serve. Its audit shape is a sys_metadata commit, not everyone's acceptance record. Roughly 400-600 lines, plus the ruling and the record text. Axes: need — as A1; long-term — two mechanisms for two anchors, and an environment-kernel gap; AI — the anchor definition opens to Studio and AI edits, guarded only at the save; startup — less code, but it costs a ruling reversal.",
            "C — for the spec seat to raise only, since triage keeps A the default and A holds as A1. Amend D3 item 5 implementation-first: the bind door stays unbuilt until a named deployment needs it, and the amendment records A1 as its shape. Cost: one Tier H sentence by the maintainer's hand, about 20 lines. E1's reader stays at the declared empty state (D3 item 2, deny-all). Axes: need — 0 writers measured, the read half reaches 0 rows under C, and class-1 form doors already serve anonymous intake; long-term — no workaround, and the shape is on record; AI — nothing new to misuse; startup — the smallest, and it matches how ruling C item 2 handled the guest sharing rule."
          ],
          "recommendation": "A1, after PR #23334 lands, or with the fence on security-plugin.ts lifted for its three wiring hunks. Need: the record is Accepted and triage graded the gap p1. Zero measured pull is the seat's input for C, not a reason to pick A2. Long-term: A1 is #22801 → A's mechanism, so both anchors keep one sealed-definition rule; A2 reverses that rule and misses environment kernels. AI: A1 keeps every anchor definition unwritable, and the guard runs on the write and on every derivation. Startup: A1 is the largest of the three. If the seat weighs zero pull above the record, C is the honest smaller answer, never A2."
        },
        {
          "question": "If A1 is chosen, the direct bind needs a public shape the card does not specify. (i) The act: a new ISecurityService method and REST route, or an administrator-created pending guest row confirmed through the existing confirm? (ii) The record: package_id is required and is part of the unique key (package_id, permission_set_name, anchor) (sys-audience-binding-suggestion.object.ts), but an environment-authored set has no package.",
          "options": [
            "R1+b1 — One act: POST /api/v1/security/audience-bindings/guest with body {permissionSet} and a closed query set, answering the bound row. Revoke through the existing suggested-bindings/:id/dismiss. package_id is optional on a guest row: the set's declaring package when it has one, else empty. Uniqueness is held by a read-before-write 409 and by the derivation's dedupe by name.",
            "R2 — Two acts: an administrator creates a pending guest row through a new route, then confirms it through the existing confirm, whose guest refusal opens. The record question is the same as R1.",
            "b2 — Bind package-declared sets only: package_id is the set's declaring package, and an environment-authored set is refused 409 with a located reason. No schema change, but a deployment's own Studio set can never reach the guest.",
            "b3 — A dedicated object for anchor bindings an administrator makes: a clean key, but a new system object with its own audit shape, which Zone 2 item 4 rules out."
          ],
          "recommendation": "R1+b1. It is one administrator act per binding, it reuses the existing revoke, and it keeps the existing record and audit shape. b2 blocks the most likely guest set, a deployment's own, and b3 adds an object and an audit shape."
        },
        {
          "question": "The seat's bypass ruling holds with no fork (zone2_readings.seat_bypass_premise). Where does the guard read the list? Today it lives only in plugin-sharing's SharingService constructor (sharing-service.ts:467-474), and @objectstack/spec cannot import plugin-sharing.",
          "options": [
            "L1 — One declaration in @objectstack/spec/security. SharingService seeds its set from it, a one-line edit to E1's file with no behaviour change. describeAnchorForbiddenBits(set, 'guest') refuses a set naming an object on it, with a prescription. Host bypassObjects extras (0 in-repo producers) are asked of the sharing service at the bind door.",
            "L2 — A spec copy of the list, pinned equal to plugin-sharing's by a cross-package test: two lists, kept equal by a test."
          ],
          "recommendation": "L1: one list, and the guard reads the same fact the row pipeline skips by. L2 is a second spelling of the same list."
        }
      ],
      "out_of_scope_findings": [
        "carrier: the build card that follows this decision (A1) · ADR-0138 D3 item 4's 'Enforced by' names SecurityPlugin.assertAudienceAnchorBindingGate, whose guest tier guards sys_position_permission_set rows, a table the resolver no longer reads, so no live binding write runs the guest tier today · noted, not filed (dormant: nothing binds guest)",
        "carrier: the A1 build's objectui sibling PR · SuggestedBindingsPanel (objectui packages/app-shell) pins 'a guest row offers no Accept, says why' · noted, not filed"
      ]
    }

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 13, 2026

    @objectstack-fleet
    ContributorAuthor

    决策:给访客(guest)绑权限集的门,现在建(A1)、改开定义口(A2),还是先只改记录、等有部署要用再建(C)?

    domain:spec 席位 2 · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H · 2026-10-13T22:30Z。dev 报告 6156571003(无代码,停在第一处修改之前)。本卡在本评论同一笔转入 needs-user-decision,释放认领(见文末 Release: 行)。

    一句话问题: 已接受的 ADR-0138 写明"管理员可以给访客绑一个低权限的权限集",但今天没有任何一扇门能做到;而能做到的两种建法,一种要新开公开接口和改记录格式,另一种要推翻一条已有裁决。

    背景(dev 实测,origin/main 430e5f5ad,复读于 688a39d77)

    • "直接绑"这条路: PUT /meta/position/guest 答 403 NOT_OVERRIDABLE(E1 已落地的 dogfood 用例钉着)。它保护的是两个受众锚点(everyone、guest)被封存的定义:[Decision] 装包时包里声明的「默认权限集」怎么生效:管理员整部署接受一次、装包即自动生效,还是取消这条路(安全边界) #22801 裁 A 的记录写明"接受只改派生的输入,没有调用方写锚点的定义",feat(runtime): activation door for positions and permission sets on sys_metadata_activation (stage 2c) #22736 的 2c 契约复审记录"锚点对每一个调用方可达的写再次封存"。
    • "确认包的建议"这条路: 没有生产者。包的建议只会产出 everyone 锚点的行(suggested-audience-bindings.ts 写死 EVERYONE_POSITION),规范也没有让包建议 guest 的写法;打开确认处对 guest 的拒绝,等于开一扇没有行能走到的门。
    • 在裁决 C 下,一个绑定的读权限集读不到任何行(访客只经分享链接读记录,分享链接的兑现在系统上下文里读,不经访客的权限集)。所以绑定的实际效果只剩 class-4 端点上的匿名创建和 class-5 端点上的访客信封;匿名收集今天已有 class-1 的表单门。
    • 实测拉动为零: 参考应用里的访客绑定数为 0(E1 普查),hotcrm 0f84c9c 的 guest 权限集是表单门用的。
    • 席位关于绕行名单的前提成立,无分叉: 名单在 plugin-sharing 的 sharing-service.ts,语料里没有任何访客权限集点名它们。

    Governing text

    选项 × 真实代价 × 工作量

    选项 做什么 客户看得见的后果 工作量
    A1 赋权记录(everyone 同款) 新增一个安全服务动作和 REST 路由:管理员(墙下为平台运营者)给 guest 绑一个权限集,先过 describeAnchorForbiddenBits(set, 'guest') 和绕行名单两道拒绝,再写一条 sys_audience_binding_suggestion 行(已确认)。安全插件在启动、绑定、撤销、卸载时按已接受的行重新派生 guest 的定义,每次再过一遍守卫,拒绝时大声说明。撤销复用现有的 dismiss。定义口的 403 保留。 部署管理员能给访客开放匿名创建等能力;所有锚点定义仍然不可写。 spec 车道 · 1 个 PR(spec、plugin-security、rest、runtime 均 minor,约 800–1200 行含测试)加 1 个 objectui PR · 先例 PR #22854(everyone 的接受机制,+1224/−1599)· 要等 PR #23334(另一席位,改同一个 security-plugin.ts)落地后再派
    A2 开放定义口 让 PUT /meta/position/guest 以环境定义的身份通过,在保存时过守卫。 能绑,但锚点定义从此可被 Studio 和 AI 编辑,只靠保存时一道守卫;环境内核读不到这条覆盖(注册表已经提供 guest)。 约 400–600 行,外加推翻 #22801 裁 A 和记录改文 · 无先例
    C 先改记录、按需再建 ADR-0138 D3 item 5 改为实现优先:绑定门在有具名部署需要之前不建,记录写下 A1 作为将来的形状。E1 的读取方保持声明的空态(D3 item 2,全拒)。 今天零变化:没有部署能给访客绑权限集(今天本来也做不到)。 1 句 Tier H 改文,由您亲笔,约 20 行 · 先例:裁决 C 第 2 条对"访客共享规则"的同样处理

    业务上等于什么: A1 像"给访客专门做一张门禁卡,发卡记录可查、随时吊销,访客的岗位说明书本身依旧锁在柜子里";A2 像"把访客的岗位说明书本身改成可编辑";C 像"在规章里写明访客卡将来怎么发,今天先不印卡"。

    推荐:A1(在 PR #23334 落地之后派),回退 C

    • 只看①(两年后的样子): 主流平台都让管理员给匿名角色授权(Salesforce 的 guest user profile、Supabase 的 anon 角色),平台的终局会有这扇门。A1 和 everyone 共用一套"封存定义、记录赋权、派生时再验"的机制,两个锚点一条规则;A2 推翻这条规则,还漏掉环境内核;C 不建,但把形状记下来。
    • 防 AI 犯错: A1 让任何锚点定义都不可写,禁止的权限集在写入时和每次派生时都被响亮拒绝;A2 把锚点定义暴露给 Studio 和 AI 编辑;C 没有新东西可误用。
    • 业务拉动与不扩散(②④)指向 C: 实测零拉动,读这一半在裁决 C 下读不到任何行。按规则②③④只调时序、不翻字母,所以推荐 A1,但时序上排在 PR fix(plugin-security): an environment kernel's security catalog serves metadata-door permission sets and positions again #23334 之后,不抢在它前面。
    • 回退 C: 如果您认为零拉动比"记录已接受"更重,C 是诚实的小答案(只改一句记录),⛔ 不选 A2。
    • 置信缺口: 看不到云端或客户是否已经在等匿名门户;A1 的 objectui 一侧只数了组件,没有测过先例。
    • 自检:只看①选 A1;②③④是否翻转:是(零拉动指向 C),按规则不改字母,只把时序排在 fix(plugin-security): an environment kernel's security catalog serves metadata-door permission sets and positions again #23334 之后。

    若裁 A1,同笔需定的两个子问题(席位推荐)

    1. 公开形状: R1+b1:一个动作 POST /api/v1/security/audience-bindings/guest(body {permissionSet},封闭的查询参数集),撤销复用现有的 dismiss;package_id 在 guest 行上可选(有声明包就填,没有就空),唯一性靠写前读的 409 和派生时按名去重。不选 R2(两步:先建待定行再确认);不选 b2(只许包声明的权限集,会挡住部署自己在 Studio 建的那个,恰恰是最可能的访客权限集);不选 b3(新对象,会多出一种审计形状)。
    2. 绕行名单放哪: L1:一份声明放进 @objectstack/spec/security,SharingService 从它播种(不改行为),守卫读同一份事实。不选 L2(两份清单靠测试保持相等)。

    裁后执行

    os-decision-facets

    • ① 项目长远合理性:A1 与 everyone 共用一套封存定义加记录赋权的机制,不新增特例;A2 推翻已裁的封存规则;C 不建但记下形状。
    • ② 实际业务拉动:实测零拉动(参考应用 0 个绑定,hotcrm 的访客权限集是表单门用的),裁决 C 下绑定的读集读不到任何行。
    • ③ 防 AI 犯错:A1 锚点定义永不可写,禁止的权限集写入与派生时都响亮拒绝;A2 把定义暴露给 AI 编辑。
    • ④ 创业阶段不扩散:C 最小(一句记录);A1 新增一个公开路由、一个服务方法和记录格式的一处放宽,是永久义务。

    Prior rulings read: guest,anchor,audience binding,sealed,bind door,suggestion → 25 hits; ADR-0090 D9, ADR-0131 D3, ADR-0056 D5, ADR-0058 D3, ADR-0068 D1, ADR-0090 D10; thread: none; plus ADR-0138 D3 item 5 (the governing text), #23266 ruling C 6148183570, #22801 ruling A

    Release: session session_01BRQ9Mp1yH7R1az6MaUM18H (domain:spec#2) · cause: the dev's measured report 6156571003 needs a decision (a security boundary, an accepted record and a new public contract shape) · to: needs-user-decision, unassigned. Branch claude/issue-23267-guest-bind-door was pushed empty at 430e5f5ad; the next claim starts from main.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    席位意见 · director seat (summon #37, session_01LqNgdPZB2Jnr5HpGbaqLeB), read on the maintainer's 「开始决裁」, 2026-10-14T00:34Z

    Read before writing: the dev's no-code report (6156571003), seat 2's decision analysis (6156613206), triage's grade, re-block and unlock (6147447462, 6148511128, 6155063299), E1's bypass-list pointer (6153215332); ruling C on #23266 (6148183570, items 1, 2 and 4). One fact moved since 6156613206 was written: PR #23334 merged at 2026-10-14T00:01Z, so the fence on security-plugin.ts is lifted and the seat's reason to order A1 after it is gone. What is left is the choice itself.

    推荐:C(先改记录,形状记下,按需再建),回退 A1;⛔ 不选 A2。 这里我和席位 2 的字母不同,理由如下。

    四维分析(业务立场)

    • ① 项目长远合理性: 终局里有这扇门(Salesforce 的 guest user profile、Supabase 的 anon 角色),席位 2 说得对。但 ① 问的是「两年后的样子」,不是「今天就建」:C 把 A1 的形状(everyone 同款的赋权记录、R1+b1 的公开形状、L1 的一份绕行名单声明)写进 ADR-0138 D3 item 5,将来建的时候一次建对;两个锚点「定义封存、记录赋权、派生再验」的规则在 C 下原样保留([Decision] 装包时包里声明的「默认权限集」怎么生效:管理员整部署接受一次、装包即自动生效,还是取消这条路(安全边界) #22801 裁 A),只有 A2 会推翻它。所以 ① 在 C 和 A1 之间不分高下,分高下的是 ②③④。
    • ② 实际业务拉动:零,而且被另一扇门覆盖。 裁决 C 之后,绑给访客的读权限集读不到任何一行(分享链接在系统上下文里兑现,不经访客的权限集);这扇门剩下的实际效果只有 class-4 端点的匿名创建和 class-5 端点的访客信封,而匿名收集今天已有 class-1 的表单门。实测写入方为 0(四个参考应用、hotcrm 的访客权限集是表单门用的)。
    • ③ 防 AI 犯错: C 下没有新东西可误用,guest 仍然绑不上,E1 的读取方保持全拒的声明空态。A1 虽然守卫严密,但在 v18 发版压力下新增约 1000 行、一条公开路由、一个服务方法、一处记录格式放宽和一个 objectui 面板,每一处都是以后要防的面。
    • ④ 创业阶段不扩散: 您的常设判据是「多花钱的我不要」「让车队提速」。为零个调用方建一条永久公开契约,是最该不做的事。先例就是 [Decision] 访客(guest)的行可见范围:沿用对象的内部共享模型(A,现状,公开对象全部行可见)、按 externalSharingModel 判(B)、还是只经分享链接(C) #23266 裁决 C 第 2 条:公开列表「现在只点名、不建」。

    四条口径(随 C 一起记)

    1. ADR-0138 D3 item 5 改为实现优先(Tier H,您亲笔): 「绑定门在有具名部署需要给访客授权之前不建;建时的形状是 A1:经一个新的安全服务动作写一条已确认的 sys_audience_binding_suggestion 行(anchor guest,package_id 在 guest 行上可选),插件按已接受的行重新派生 guest 的定义并每次过 describeAnchorForbiddenBits(set, 'guest');绕行名单只声明一份,放在 @objectstack/spec/security;锚点定义始终不可写。」
    2. Restart-when: 出现具名部署需要给访客绑权限集,且 class-1 表单门不能满足它。
    3. 两条已记未立的 finding 跟着这张卡关: D3 item 4 点名的 assertAudienceAnchorBindingGate guest 档今天没有活的写入会跑到它(休眠);objectui SuggestedBindingsPanel 钉着「guest 行不提供接受」。都在 A1 建时一并处理。
    4. ⛔ 不选 A2: 推翻锚点封存、漏掉环境内核、把定义暴露给 Studio 和 AI 编辑,三条都是反向。

    回退 A1 的唯一条件:您知道 cloud 或某个客户正在等匿名门户。那时按席位 2 的两个子答案(R1+b1、L1)立即派发,围栏已经不在了。

    置信缺口:cloud 或客户是否在等匿名门户,席位看不到;class-4/5 端点的行为是 dev 的读数,本席未复测。

    裁后执行:席位 2 起草 D3 item 5 的改句 Tier H PR 交您批准;本卡按「实现优先、形状已记录」关闭,Restart-when 如上;E1 保持全拒的声明空态(D3 item 2)。

  5. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #1 item 4 · letter C · maintainer 「23267 我们的系统 和 salesforce 类似,暂时没有 没有公开站点或门户的产品计划」 2026-10-14T02:06Z

    Director seat, summon #37, session_01LqNgdPZB2Jnr5HpGbaqLeB, on the seat's reading 6158189687 (batch #1 item 4: recommendation C, fallback A1, ⛔ A2) and the long-term design discussion in chat, where the seat posed the fork "a public site or portal planned for v18–v19 → A1 with a surface scope now; none → C" and the maintainer answered as quoted. The same words answer the other two forks: the system is Salesforce-like, so the guest's grants are scoped per public surface (Salesforce: a guest user profile per site) and bound by the environment's administrator (Salesforce: the site's administrator).

    What the ruling binds (letter C):

    1. ADR-0138 D3 item 5 is amended implementation-first (Tier H, the maintainer's hand): the bind door is built when a named deployment needs to grant the guest on a public surface (a site or portal); until then no door binds a set to guest, E1's reader stays at the declared empty state (D3 item 2, deny-all), and the record carries the door's shape.
    2. The recorded shape: A1's assignment record (an administrator act that writes one confirmed sys_audience_binding_suggestion row for anchor guest, package_id optional on a guest row; the plugin re-derives the guest definition from the accepted rows and runs describeAnchorForbiddenBits(set, 'guest') and the bypass-list refusal on every write and every derivation; anchor definitions stay sealed, [Decision] 装包时包里声明的「默认权限集」怎么生效:管理员整部署接受一次、装包即自动生效,还是取消这条路(安全边界) #22801 A), scoped to the public surface that publishes the door (the binding carries the app or site it serves; the anchor stays one identity), bound by the environment's administrator, never by a package (ADR-0090 D9). The bypass list is declared once in @objectstack/spec/security (L1).
    3. The row half stays ruling C on [Decision] 访客(guest)的行可见范围:沿用对象的内部共享模型(A,现状,公开对象全部行可见)、按 externalSharingModel 判(B)、还是只经分享链接(C) #23266: share links, and, when built, an explicit guest sharing rule: read-only, criteria-based, the strictest lint tier (Salesforce's guest user sharing rules), the retired guest recipient (D8) returning only in that stricter shape; not built now.
    4. ⛔ A2 (opening the definition door) is refused: it reverses the anchors' seal ([Decision] 装包时包里声明的「默认权限集」怎么生效:管理员整部署接受一次、装包即自动生效,还是取消这条路(安全边界) #22801 A) and misses environment kernels.
    5. Restart-when: a named deployment needs to grant the guest on a public surface, and the class-1 form doors cannot serve it. The two carried findings (D3 item 4's dormant guest tier of assertAudienceAnchorBindingGate; objectui SuggestedBindingsPanel's "a guest row offers no Accept" pin) close with this card and are re-read when the door is built.

    Workload of the letter (the card's column): one Tier H sentence on ADR-0138 D3 item 5 (about 20 lines), drafted by domain:spec seat 2 and approved by the maintainer's hand · precedent: ruling C on #23266 item 2 (named, not built) and its amendment PR #23326 (+28/−11, merged by the maintainer).

    Execution: seat 2 (domain:spec#2, #18549) reclaims from pm:queue, drafts the Tier H PR carrying items 1–3 and the Restart-when, and closes this card "implementation-first, shape recorded" when it merges. needs-user-decision removed and pm:queue added in this act; the body's first line carries Ruled:.

  6. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1, serial slot (#23267, ruling C 6159279205: ADR-0138 D3 item 5 amended implementation-first, with the bind door's shape recorded) · 2026-10-14T02:11Z
    Session: session_01BRQ9Mp1yH7R1az6MaUM18H
    Account: os-justin (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-23267-adr0138-guest-door-shape
    Worktree: objectstack-issue-23267
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    Ruling-ref: the director seat's ruling 6159279205 (batch #1 item 4, letter C, on the maintainer's words 「23267 我们的系统 和 salesforce 类似,暂时没有 没有公开站点或门户的产品计划」), on its reading 6158189687; read in this act. Its execution line names this seat: draft the Tier H PR carrying items 1–3 and the Restart-when, and close the card "implementation-first, shape recorded" when it merges.
    File surface (at origin/main dc393266f or later; stop on a breach and explain it in the report):

    This act moves the card pm:queue → pm:dispatched and assigns os-justin. The PR body's first line is Fixes #23267, as the ruling's close condition reads.

  7. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 23267,
      "status": "done",
      "branch": "claude/issue-23267-adr0138-guest-door-shape",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/23370",
      "session": "session_01BRQ9Mp1yH7R1az6MaUM18H (subagent: the parent's id; the relay envelope resolved the same id from the container)",
      "premise_still_valid": true,
      "summary": "Draft PR #23370 (Tier H, head cc9a40576, one file, +41/-8) amends ADR-0138 under ruling C on #23267 (6159279205). D3 item 5 now reads implementation-first. The door is built when a named deployment needs to grant the guest on a public surface and the class-1 form doors cannot serve it. Until then E1's reader stays at the declared empty state, because no door binds a set to guest. The item names the three refusals that hold this, each measured at dc393266f: the metadata door's 403 NOT_OVERRIDABLE on the packaged guest position, the package door's built-in-name refusal, and confirmAudienceBindingSuggestion's refusal of a guest row, which the sync never produces. The item records the door's shape (ruling items 1-3, the L1 bypass list, the surface scope, never a package), A2's refusal with its reason (item 4), and the Restart-when with the two carried findings as re-read items (item 5). One '· **Amended** (2026-10-14, ruling C on #23267, ...)' line continues the status paragraph; the status stays Accepted. Three consistency edits in the same file: the Consequences bullet that named the two paths as live, one Enforcement map row for the amended item's declaration, and follow-up F4. No other decision is edited; D8, D3 item 4 and the Context reading are left as they are and named in Acceptance notes.",
      "zone2_readings": {
        "1_only_live_sentence": "Disproved in part. D3 item 5 (dc393266f :368-371) is the item that states the door as live, but three more passages in the same ADR lean on it. (a) The Consequences bullet 'directly or through a confirmed suggestion': edited. (b) D8's row for the suggestion anchor's guest value, 'enforce - live once D3 lands': not edited, because D8 is a separate decision; item 5 now says that value goes live with the door. (c) The Context reading at 7b926f7600, 'A package may suggest a guest binding': a dated reading, not edited. D3 item 2 (the empty state) is consistent and unchanged. D3 item 4 and the Enforcement map's 'anchor binding tier ... enforced' row are unchanged; item 5 names the dormant guest tier as a re-read item, per ruling item 5. D3's own 'Enforced by' paragraph concerns E1 and is unchanged. The Enforcement map gains one row, because the record's preamble requires an enforcer for every declared thing.",
        "2_status": "Holds. The status stays Accepted, and one Amended line follows #23266's line, in PR #23326's form.",
        "3_re_read_placement": "Holds. The two re-read items sit in D3 item 5, where the door's builder reads the shape. Follow-up F4 points there, in the form F2 uses for D5's site binding.",
        "4_size": "Measured +41/-8 against the precedent's +28/-11. Item 5 alone is +28/-4 against the ~20-line estimate: it carries five ruling items, three measured refusals and two re-read pointers, and three citations with full links take six of its lines. The three consistency edits add a net 5 lines."
      },
      "tests": "Docs only: no package touched, so no build, test or typecheck target of its own, and no ablation. Prerequisite build for check:doc-formula-expressions, under the lock (slot issue-23267-lint-build): `turbo run build --filter=@objectstack/lint --concurrency=2` gave 'Tasks: 4 successful, 4 total' and 'VERDICT command-exit 0 · held the lock 162s'. Gates are in `gates`. Extra control: OBJECTUI_CHECKOUT=/home/user/objectui node scripts/check-adr-symbol-anchors.mjs exited 0 with '2335 anchors across 142 records resolve'. This judges the new cross-repo anchor objectui:packages/app-shell/src/components/SuggestedBindingsPanel.tsx, which the plain run reports as cross-repo-skipped. Control-byte self-scan (grep -naP) of the edited file and the PR body: none. Quote bytes: the maintainer quotation in the Amended line matches the ruling comment's first line byte for byte (grep -c -F = 1, full-width comma U+FF0C included).",
      "gates": "On final head cc9a40576, `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths; merge base dc393266f, 1 path) exited 0 with 19 commands, identical to the PM's lead list. Each was run with its exit code captured before any pipe; all 19 exit 0: check-adr-links (+ --self-test), check-adr-symbol-anchors (+ --self-test), check-ci-filter-parity, check-closing-keyword-parity (+ --self-test), check-comment-mask-corpus, lint check:doc-formula-expressions, check:adr-anchors, check:cross-package-test-inputs, check:doc-authoring, check:driver-memory-census, check:gitlink-declared, check:nul-bytes, check:pm-governed-merges, check:pm-prior-rulings, check:refd-timer-probe, check:watch-hint-literal. The `--ran` reconcile exited 0: '19 derived famil(ies) accounted for — 19 run, 0 NOT-MEASURED (a DERIVED zero — all 19 recorded an exit code and none of them is 3)'. Verdict lines: 'check-adr-links: 730 relative link destination(s) under docs/adr/ resolve'; 'check-adr-symbol-anchors: 2335 anchors across 142 records resolve'; 'check-adr-anchors: OK (68 anchored file(s) ... 45873 citation(s) across 6019 file(s) resolve ...)'; 'check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords ...)'; 'check-nul-bytes: OK (scanned 11352 text file(s) ... no raw ASCII control bytes)'. NOT MEASURED locally, outside the derived set: the CI-only families the derivation lists (the Test Core shard job, the type-check lanes, the WIDE and artifact-roster families). Those are CI's.",
      "line_budget": "n/a: no skills/** path",
      "files_changed": "docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md only (+41/-8)",
      "labels": "skip-changeset added through scripts/pm/label-write.mjs (relay run 38501801529); read-back: documentation, size/s, skip-changeset; assignee os-justin. documentation and size/s were applied by the labelers, not by this run",
      "pr_state": "Draft, head cc9a40576, assignee os-justin. The body was read back byte-identical (18533 bytes): line 1 the closing-keyword line naming this card, line 2 'Clause-②: no', the ## 维护者速读(草稿) section with 席位意见 left empty, and the session-URL footer. Not readied, not queued, no reviews requested.",
      "deviations": [
        "Beyond the ruled item, three consistency edits in the same file: the Consequences bullet, one Enforcement map row and follow-up F4. Zone 2 item 1 asks for this; the PR body and 维护者速读 flag each for the maintainer.",
        "Item 5 adds four facts beyond the ruling's words, flagged for review in the PR body: the three measured refusals, 'as it does everyone's', '(D8's guest value, live with this door)', and where the bypass list lives today.",
        "Attribution: the harness reminder asked for a model-named Co-Authored-By trailer and an emoji-led PR footer. The commit carries AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), and the PR body ends with the AGENTS.md session-URL footer. The reminder itself gives repo instructions precedence.",
        "Cleanup runs AFTER this comment, as the run's last step: rm -rf node_modules and git worktree remove for /home/user/objectstack-issue-23267. The order is forced because the comment tool imports typescript from that worktree's node_modules. The two background processes this run started (pnpm install PID 1029, gate runner PID 2626) have exited. No server or monitor was started."
      ],
      "mcp_calls": "0",
      "api_writes": "4 REST writes in 3 relay strokes (each stroke is one POST /repos/objectstack-ai/objectstack/dispatches; fleet-write.yml executed them as objectstack-fleet[bot]). (1) pr_create, run 38501706438: POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos/objectstack-ai/objectstack/issues/23370/assignees. (2) labels_add, run 38501801529: POST /repos/objectstack-ai/objectstack/issues/23370/labels. (3) This comment: POST /repos/objectstack-ai/objectstack/issues/23267/comments. git push (2: the empty probe, then cc9a40576) is not a REST write.",
      "open_questions": [],
      "out_of_scope_findings": [
        "carrier: domain:spec seat 2 (the seat may fold all three into one Tier H ADR-0090 status-line pointer to ADR-0138 D3 item 5, or into the F4 build card) · one family: texts outside ADR-0138 that still state the guest suggestion or bind path as live; not edited, per the dispatch's one-file surface. (1) docs/adr/0090-permission-model-v2-concept-convergence.md:356-357: 'a package may suggest bindings to `everyone` **or** `guest`; the admin confirms each individually'. (2) packages/lint/src/validate-security-posture.ts:33-37 and :773-775: 'a set an operator binds to `guest` at install time ...' / 'judged by the bind-time gate alone'. (3) packages/spec/src/identity/position.zod.ts:164-165: 'Packages SUGGEST bindings to these anchors at install time'. Evidence at dc393266f: collectDeclaredSuggestions hard-codes EVERYONE_POSITION, confirmAudienceBindingSuggestion refuses a non-everyone row (409 SUGGESTION_STATE), and the package door refuses a built-in position name. No public-door reading of the suggestion half was taken, so this is noted, not filed · dedupe words: guest suggestion live text; ADR-0090 D9 guest suggest; operator binds guest install docblock",
        "carrier: the F4 bind-door build (D3 item 5's Restart-when) · E1's changeset .changeset/23224-guest-grants-channel.md:13 says a set starts granting 'only where a deployment holds an environment definition of the `guest` position that names it'. That is true today as a necessary condition. Under the recorded shape, the grant arrives through the plugin's re-derived definition instead, so the builder's changeset should say so · noted, not filed"
      ]
    }

    Generated by Claude Code

  8. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ ACCEPT: PR #23370 at cc9a40576 (Fixes #23267), Tier H. It waits for an authorized approval, then the seat lands it

    domain:spec seat 2 (#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H · 2026-10-14T02:41Z · holder of claim 6159342669. Report: 6159642131. Thread-read: the report.

    Read on GitHub:

    • A draft against main, assigned to os-justin, labelled skip-changeset. Line 1 Fixes #23267, line 2 Clause-②: no.
    • One file, docs/adr/0138-guest-model-anonymous-principal-doors-grants-and-organization.md, +41 / −8.
    • check-governed-merges --pr 23370: 1 of 1 path hits the governed register (docs/adr/**), so Tier H; 49 changed lines.
    • CI on the head: 32 success and 11 skipped, all completed, 0 failures.

    Read against ruling C (6159279205), item by item, on the diff:

    The additions beyond the ruling's words, accepted:

    Gates: 19 derived, all 19 exit 0 on cc9a40576, reconciled by --ran. check-adr-symbol-anchors with the objectui checkout resolves the new cross-repo anchor.

    Report items:

    Landing (Tier H): needs-user-decision is added to PR #23370, the final 维护者速读 is posted there, and os-zhuang and hotlong are requested as reviewers, all in this act. The PR stays a draft. With an approval from either account, the seat runs the landing checks, readies it and arms the queue. The card stays pm:dispatched with os-justin until MERGED.


    Generated by Claude Code

  9. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    ✅ Authorized approval in; PR #23370 at cc9a40576 is in the merge queue (Fixes #23267, Tier H)

    domain:spec seat 2 (#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H · 2026-10-14T03:09Z · holder of claim 6159342669. Thread-read: 6159686556.

    • Approval: os-zhuang, review 5512598183, APPROVED at 03:08:32Z on cc9a40576 (the current head). The account is in GOVERNED_APPROVERS. The approver readied the PR and added it to the merge queue in the same minute (03:08:35Z and 03:08:37Z).
    • Landing checks on cc9a40576:
      • check-governed-merges --pr 23370: governed, landing tier H, "an authorized APPROVED review (GOVERNED_APPROVERS) and then the owning seat lands it";
      • CI: 32 success, 15 skipped, 0 failures; Governed Surface Queue Guard is re-running on the new review, and auto-merge waits for it.
    • In this act, through the relay: needs-user-decision removed from PR docs(adr-0138): the guest's bind door is built implementation-first, with its shape recorded (D3 item 5, ruling C) #23370 (the maintainer's review is done). The seat's automerge_enable found the PR already queued by the approver.

    Next: confirm MERGED, the mis-close scan, and the landing record. The card then closes "implementation-first, shape recorded", as the ruling's execution line reads.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 14, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23370 → 6eb6f7d65 (Fixes #23267), Tier H on os-zhuang's approval. The card closed completed: implementation-first, shape recorded

    domain:spec seat 2 (#18549) · os-justin · session session_01BRQ9Mp1yH7R1az6MaUM18H · 2026-10-14T03:35Z · holder of claim 6159342669. Thread-read: 6160003145.


    Generated by Claude Code

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 guardsbugSomething isn't workingdomain:specpriority:p1High: required for production / M2securitytarget:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions