Skip to content

OAuth-connected MCP agents run under the mcp_agent_data_* ceiling ∩ user, not "as yourself": a viewAllRecords manager sees 5 accounts / 0 opportunities over OAuth vs 9 / 23 over an API key #16549

Closed

Description

@yinlianghui

Summary

Once the OAuth-for-MCP door is open (with #16530 worked around locally), a user connecting "as themselves" sees far less than they see in the Console or through a per-user API key. Measured with the same account, same questions, same server:

identity path crm_account crm_opportunity crm_task
API key bound to sales.manager (principalKind: human) 9 23 (11 in Q3) 45
OAuth as sales.manager (principalKind: agent, onBehalfOf = sales.manager) 5 0 0

The Setup → Connect an Agent page says, verbatim: "Identity is self-serve: … you connect as yourself, and every call runs under your own permissions and row-level security." and, in the page header, "every call runs under the caller's own permissions and row-level security." The OAuth path does not do that: it runs under the intersection of the caller's permission sets and a platform agent ceiling that has no viewAllRecords, so any user whose access comes from viewAllRecords / modifyAllRecords (every manager-type profile) collapses to own + explicit shares.

Environment

  • hotcrm 789a7324, @objectstack/* 17.3.0, dev server with OS_AUTH_URL=https://localhost:4443 behind a local TLS proxy
  • Claude Code desktop (Claude Code 2.1.260), MCP entry https://localhost:4443/api/v1/mcp, OAuth (DCR + PKCE), consent granted for data:read data:write actions:execute offline_access as sales.manager@objectos.ai
  • sys_oauth_consent row: scopes ["data:read","data:write","actions:execute","offline_access"], user_id = sales.manager

Reproduction

  1. Connect via OAuth as sales.manager (profile sales_manager: crm_opportunity / crm_account / crm_lead … all viewAllRecords: true, modifyAllRecords: true).
  2. Ask the agent: 「本季度金额前十的商机里,哪些超过两周没有跟进?」 — it runs query_records / aggregate_records on crm_opportunity, crm_account, crm_task and reports crm_account 5, crm_opportunity 0, crm_task 0 (count(*) = 0 with and without filters). No error is returned — empty sets.
  3. Same account through x-api-key (per-user key from POST /api/v1/keys), same questions: 9 accounts, 23 opportunities, 45 tasks, and the Console shows the same.

Where it happens (@objectstack/plugin-security 17.3.0)

  • resolveDelegatorContext / the CRUD explain path: when context.onBehalfOf.userId is set, sets = the agent principal's sets and delegatorSets = the user's; crudAllowed = agentCrud && delegatorCrud, field masks are intersected, and read scope goes through narrowerScope(a, b) over ["own","own_and_reports","unit","unit_and_below","org"].
  • The agent principal's set for a data:write token is the platform's mcp_agent_data_write: {"*": {allowCreate: true, allowRead: true, allowEdit: true, allowDelete: true, allowTransfer: false, viewAllRecords: false, modifyAllRecords: false}, sys_*: read-only} (mcp_agent_data_read is the same with read only). Its description: "Read+write ceiling for an AI agent acting on behalf of a user (OAuth data:write)".
  • narrowerScope(agent = own, delegator = org) → own. Rows then come only from ownership (sales.manager owns nothing in the demo seed — everything is owned by the dev admin) plus sharing rules aimed at her position: hotcrm's account_team_sharing → the 5 accounts; the opportunity rules target sales_director / executive → 0.
  • hasWriteBypass returns false and resolveWriteScope returns "own" unconditionally for any on-behalf-of context (if (!context?.userId || context?.onBehalfOf?.userId) return "own").

So the behaviour is exactly what D10 + the shipped ceiling specify. The problem is that the ceiling is below the user for the most common case (any profile whose visibility is viewAllRecords), while the UI and the docs promise parity with the user.

Expected — one of

  1. Parity with the user (what the Setup page promises): the agent ceiling for data:read / data:write should carry viewAllRecords / modifyAllRecords (or the intersection should treat the ceiling's scope as "no opinion" and take the delegator's), so that agent ∩ user = user for CRUD/RLS, with the ceiling only removing things the scopes exclude (e.g. no writes on a data:read token, no allowTransfer).
  2. Keep the ceiling, fix the promise: change the Setup page / SKILL.md / discovery resource_name copy to say that OAuth agents run under own-records + shares regardless of the user's profile, and document that managers must use a per-user API key to see what they see in the Console.

Either way the current combination — the page says "as yourself", the engine says "own only" — will be read by users as data loss ("the AI says I have no opportunities").

Side observations

Activity

  1. os-zhuang commented on Sep 7, 2026

    @os-zhuang
    Contributor

    分诊 · domain:services / bug / priority:p1 / needs-user-decision / type: Bug

    分诊席,session session_01SwJQDFKe8tVit3BXQ9EfR5。本卡此前零标签 ⇒ 无车道无状态,不在任何队列。归位。

    车道 domain:services:落点 @objectstack/plugin-security(resolveDelegatorContext / narrowerScope / resolveWriteScope),车道表该行含 plugin-security。查表所得。

    类别 (a) 且同时 (b)

    • (a):同一账号、同一批问题、同一台服务器,两条身份路径的差分 —— API key 9 / 23 / 45,OAuth 5 / 0 / 0。差分即控制项。
    • (b):Setup 页逐字承诺 "you connect as yourself, and every call runs under your own permissions and row-level security",引擎做的是交集。引契约文成立。

    为什么 p1

    • 命中的是最常见的档案形状,不是边角:任何可见性来自 viewAllRecords 的档案(所有经理型)都塌缩成 own + 显式共享。
    • ⭐ 它是静默的,而且被骗的消费者就是 AI 自己:MCP 工具返回 total: 0,没有任何「被代理天花板窄化」的说明,于是代理如实地回答「本季度没有商机」。⇒ 这不是人读错一个数,是代理据错误数据作答并把它讲给决策者。
    • 用户会把它读成数据丢失(卡的原话:"the AI says I have no opportunities")。

    ⚠️ 反方论据我摆在明处:方向是收紧(fails closed),⛔ 不是越权泄露;且存在绕行(每用户 API key)。⇒ 若维护者据此降到 p2,那是拿着事实的重判。但我不据「有绕行」降级——那条绕行没有任何地方写着是必须的,而页面写的正好相反。

    ⛔ 为什么进决策箱:两个期望是相反的,且一条是安全面变更

    • 选项 1(与用户对齐):让 data:read / data:write 天花板携带 viewAllRecords / modifyAllRecords,或让交集把天花板的 scope 视为「不表态」而取委托人的 ⇒ agent ∩ user = user,天花板只做减法(data:read 不得写、不得 allowTransfer)。⚠️ 这是放宽一个已发布的平台权限集对 AI 代理的可见面 —— 安全模型决定。
    • 选项 2(保留天花板,改承诺):改 Setup 页 / SKILL.md / discovery 文案,写明 OAuth 代理一律跑在 own + shares 上,并写明经理必须用每用户 API key。⇒ 撤回一项已发布的承诺。

    ⇒ 一个放宽 AI 代理的数据可见面、一个收回文档承诺,都不是 dev 能选的。⛔ 分诊也不裁。

    ⭐ 但有一块两条路都需要、且不需要任何裁定的,可以立刻拆出去做

    把窄化说出来。 引擎的 explain 路径已经有那句话(layer: "object_crud","D10 intersection"),只是 MCP 的 query_records 结果不携带它。

    ⇒ 让 MCP 结果在被代理天花板窄化时带上一条说明,不预判选项 1 或 2,却把「静默的错误答案」变成「可读的答案」——而那正是本卡代价最大的一半。⚠️ 若要现在就拆成 pm:queue 卡,回一句话,我一笔拆。

    四棱(os-decision-facets)

    • ① 长远合理性:选项 1 让「代理代表用户」只有一条规则;选项 2 保留两套语义(界面一套、引擎一套),必须靠文案维持,而文案漂移正是本轮反复记档的失败类。
    • ② 业务拉动:实测存在,且命中最常见档案形状。零拉动不成立。
    • ③ 防 AI 犯错:⭐ 现状是本轴最坏的一格——AI 拿到空集,没有任何信号说它被窄化过,于是自信地报告错误结论。两个选项都必须附带上面那条诊断,否则本轴不动。
    • ④ 创业阶段不扩散:选项 2 最省(改文案);选项 1 动的是已发布的平台权限集,是安全面变更,需逐 scope 写清天花板还减什么。

    推荐:先做诊断(不需裁),再裁 1 vs 2;本席不选。
    置信缺口:我没有量过今天有多少部署真在用 OAuth MCP。若接近零,选项 2 的代价远低于它听起来的样子;若非零,选项 2 等于告诉现有用户「你一直看到的是残缺数据」。⛔ 裁之前值得要这个数。

    维护者速读

    我们的 Setup 页告诉用户:通过 OAuth 连接 AI 助手时「你是以你自己的身份连接,每次调用都按你自己的权限执行」。实测不是这样:AI 拿到的是「你的权限 ∩ 一个平台代理上限」,而那个上限不含经理型档案赖以看到全部数据的那一项。于是同一位销售经理,在 Console 里看到 23 个商机,通过 AI 助手看到 0 个——而且 AI 不知道自己被限了,它会直接回答「本季度没有商机」。要么把上限放开到与用户一致(动安全面),要么改文案承认 AI 只能看自有记录(收回一个已发布的承诺)。
    你要做的:回一个数字 —— 1(与用户对齐)/ 2(保留上限、改文案)。另可回一句「先做诊断」,我立刻把那一半拆出去开工。


    三条随卡留档的旁证(⛔ 不并入本卡范围)

    ⛔ 分诊席边界照旧:不认领、不派发、不写码、不合并,也不裁 1/2。


    Generated by Claude Code

  2. added
    bugSomething isn't working
    priority:p1High: required for production / M2
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    on Sep 7, 2026
  3. added theissue type on Sep 7, 2026
  4. yinlianghui commented on Sep 8, 2026

    @yinlianghui
    ContributorAuthor

    Evidence that the ceiling is exactly own + shares, measured 2026-09-08 on the same machine as the report above (hotcrm 789a7324, objectstack 17.3.0, https + OAuth, signed in as sales.manager@objectos.ai, consent scopes data:read data:write actions:execute offline_access, MCP session not re-authenticated between the two measurements):

    step crm_account crm_opportunity crm_task
    seed as shipped — every row owner_id = dev admin 5 (all via account_team_sharing) 0 0
    after UPDATE … SET owner_id = <sales.manager> on the admin-owned rows (dev SQLite, nothing else touched) 9 23 59

    Same token, same profile (sales_manager, viewAllRecords: true on opportunities), same query — only the owner column changed. So the agent path is reading own scope where the human path (API key, same user) reads viewAllRecords; the profile grant is not consulted for the agent at all.

    Two things this rules out: it is not a scope problem (all four scopes were granted), and it is not sharing-rule coverage (the 5→9 account jump shows the ceiling adds own to shared, nothing more).

    The workaround (re-owning seed rows to the demo user) makes a demo pass but is not a product path — a real sales manager will not own her team's pipeline. Filed the seed side separately as objectstack-ai/hotcrm#1759; the visibility rule itself belongs here.

  5. os-zhuang commented on Sep 8, 2026

    @os-zhuang
    Contributor

    Ruling recorded — option 1: the OAuth agent runs with the user's own permissions; the ceiling only subtracts; diagnostic lands regardless (director seat, decision batch #81, 2026-09-08)

    Maintainer's reply, verbatim (live PM chat, 2026-09-08T02:2xZ, answering the batch #81 presentation where this card was item 1 with recommendation 1 + diagnostic first):

    其他同意

    ⇒

    1. Parity. The data:read / data:write agent ceiling either carries viewAllRecords / modifyAllRecords, or the D10 intersection treats the ceiling's scope as "no opinion" and takes the delegator's — so agent ∩ user = user for visibility. The ceiling keeps its subtractive role only: data:read cannot write, cannot allowTransfer; each scope's remaining subtraction is written down explicitly.
    2. Diagnostic, non-negotiable and independent of (1): when a delegated context is narrowed by the ceiling, MCP query_records results carry the explain path's existing "D10 intersection" statement, so an agent never again reports total: 0 as a fact about the data.
    3. The Setup page's promise ("you connect as yourself, and every call runs under your own permissions and row-level security") becomes true rather than being rewritten.

    ⚠️ This is a widening of a published platform permission set's visibility for AI agents — the maintainer chose the security-face change over retracting the promise. The pin carries the card's own differential: same account, same questions, API-key path vs OAuth path, both must agree.

    Follow-up cards, ⛔ not part of this one: the consent screen shows the DCR id instead of the registered client name (authorisation-surface identifiability).

    Label: needs-user-decision → pm:queue. ⛔ Not dispatched by this seat.


    Generated by Claude Code

  6. added
    pm:queueReady for the PM dispatch loop
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Sep 8, 2026
  7. yinlianghui commented on Sep 8, 2026

    @yinlianghui
    ContributorAuthor

    Environment note, for whoever picks this up. Everything in this card was observed through Claude Code inside the Claude desktop app (Claude for Mac 1.46388.x, bundled Claude Code 2.1.260) against a local hotcrm dev server. Because the desktop client refuses to run OAuth against a plain-http:// server, the local setup used a self-signed CA + a small reverse proxy on https://localhost:4443 with OS_AUTH_URL=https://localhost:4443 so the discovery documents advertise https.

    That client and that proxy are only how the path was reached. The defect itself is server-side and client-independent: any OAuth-capable MCP client (Claude Code CLI, Cursor, VS Code, a claude.ai custom connector) hits the same authorization-server / security-layer behaviour once it gets past TLS. Nothing here depends on the desktop app.

  8. os-justin commented on Sep 10, 2026

    @os-justin
    Collaborator

    Tier notice — the contract-review-tier requirement on this issue is lifted (skills seat, session session_01MoTv7pn338AZ71owsp19gQ, 2026-09-10T03:13Z; record and rule-text change in flight: #17285).

    Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on #17285), the contract-review tier is reserved for the skills seat (protocol files + the published skills/**), the spec seat's clause-② review, and the maintainer-summoned director; triage and every other seat run the default tier.

    For this card: its Clause-②: yes declaration no longer calls for a contract-review-tier review. The lane seat's own default-tier review, plus the gates (widening tells, pin tests, dispatch-gates --tier), is the review of record, and the build stays at the default tier. Unchanged: the Clause-② declaration itself, the manual floor for widenings under 代裁, and the routing rule that a diff touching packages/spec goes to the spec seat, where the contract-review-tier review still applies. This comment changes no label, assignee or claim.

    ⚠️ Your open PR #17256 touches packages/spec (3 files), so the spec seat's contract-review-tier review still applies to that PR as it stands; the lifting covers this card's non-spec half and any PR that stays out of packages/spec.


    Generated by Claude Code

  9. os-tesla commented on Sep 10, 2026

    @os-tesla
    Collaborator

    Claim: session session_012zTkyNHJ7TkuN2oXtP5x37 · branch claude/issue-16549-oauth-agent-runs-as-the-user-r2 · seat domain:services PM (#6021)

    ⚠️ 恢复说明:本卡的交付没有丢,但它的 PR 和评论丢了

    PR #17256 与本卡上的 os-dev-report、Claim: 与复核评论,随 os-trump 账号被停用一起被删除。⛔ 代码一行没丢 —— 原分支 claude/issue-16549-oauth-agent-runs-as-the-user 仍在远端,head a76e26e8d5f766158fb731f65b0818dd593d9d46。

    被删的 PR 处在"幽灵"状态:API 查它是 404,但它仍占着原分支,GitHub 因此拒绝在同一分支上再开 PR。⇒ 同一批 commit 逐字节原样推到了 …-r2,新 PR 是 #17332。⛔ 没有 rebase、没有 amend、没有 squash。

    ⚠️ assignee 字段仍指向已停用的 os-trump,而它对应的 Claim: 已经不存在了。 按本仓纪律「a bare assignee with no Claim: is a dispatch's first step, not a foreign claim」,那个字段现在不构成认领 —— 本条评论才是。⛔ 请勿据 assignee 判断归属。

    丢失的东西里,有一件需要重做

    原 PR 上的 CONTRACT_REVIEW_TIER 复核(若已发生)一并没了。本卡的条款② 是 yes(DelegationNarrowing 经 packages/spec/src/contracts/index.ts:51 的 export * 上了已发布的 ./contracts 入口,另加两个可选接口成员)。⇒ #17332 需要一次全新的同档位复核,⛔ 不是"再复核一次" —— 记录上它从未被复核过。

    needs:contract-review 在本卡上仍在。

    还有一件没有丢的

    ⛔ 所有测量数字都是在 a76e26e8d 上、停用之前取的,没有在今天的 main 上重跑过。#17332 的 CI 才是当前读数,PR 正文里的表格是历史读数 —— 两者不一致以 CI 为准。


    Generated by Claude Code

  10. os-tesla commented on Sep 10, 2026

    @os-tesla
    Collaborator

    os-dev-report

    {
      "issue": 16549,
      "status": "done",
      "branch": "claude/issue-16549-oauth-agent-runs-as-the-user-r2",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17332",
      "premise_still_valid": true,
      "summary": "Mechanical regeneration round only — no production code, no test and no changeset was touched on this branch. Ran the repo’s own landing sequence `bash scripts/pm/os-regen-merge.sh` in a dedicated worktree: it merged origin/main (cca1dc0bf) into branch tip a76e26e8d as merge commit f5cf9f6ca (no rebase, no amend, no force-push, no card-relation trailer), took main’s side of the one os-regen path both sides had moved (content/docs/permissions/system-context.mdx) and kept the branch’s bytes of packages/spec/api-surface/contracts.json and packages/spec/export-origins/contracts.json, then handed off with a clean tree. Step 4 regenerated the whole chain wholesale: `gen:api-surface` and `gen:export-origins` re-derived byte-identical files (0 shards rewritten) and `pnpm gen:system-context-census` rewrote six declared counts, each +1. That regeneration is commit 11e1b73de, the only file in it being the census page. WHAT MOVED THE CENSUS: main commit d2c1d1980, \"fix(objectql)!: beforeUpdate receives the persist image; the caller submission moves to ctx.submitted (#16344) (#17195)\", added exactly one new elevation read site — `opCtx.context?.isSystem` gating the pre-hook readonly-hide pass in packages/objectql/src/engine.ts. Its own commit body says so (\"The +1 is this change’s own opCtx.context?.isSystem gate on the pre-hook hide pass\"). That side and this branch had each independently raised the census 108 -> 109 for their own new site, so the text merge kept one 109 while the union is 110 — which is exactly the six +1 [declared-count] mismatches the gate reported. ZERO problems of the human-edit class appeared, before or after: the gate’s anchor half printed \"anchors have nothing to rewrite — this page carries no line numbers\" both times, and no renamed symbol / arrived read / vanished read was reported. The merge produced no conflict in any file, generated or otherwise. The branch’s own surface entry \"DelegationNarrowing (interface)\" is still present in both api-surface/contracts.json and export-origins/contracts.json at the new head, and none of the branch’s implementation files were altered by the merge. PR #17332 stays DRAFT and unmerged; the in-seat Clause-② review and the landing are the PM’s (needs:contract-review is on the PR).",
      "tests": "(1) CENSUS GREEN AT THE NEW HEAD 11e1b73de, quoted from the gate’s own output line — `pnpm check:system-context-census`, exit 0 (captured before any pipe): \"check-system-context-census: OK — 110 elevation read sites in 20 packages across 45 files, living in 91 symbol(s); the page cites 105 symbol(s) against 105 required, over 129 anchors and 8 file-level citation(s); 22 declared non-read; 9 file(s) hold more than one read in one symbol (the declared precision loss); 18 row reference(s) resolve to their keyed row, 2 declared unheld.\" Its --fix leg printed the mechanical rewrite it performed: \"regenerated 6 declared count(s) from the census: headline-sites 109->110, sharing-total 109->110, table-reads 115->116, table-elevation-reads 109->110, table-behaviour-bearing 105->106, ruling-sites 109->110.\" (2) `git diff --stat origin/main...11e1b73de` (origin/main = cca1dc0bf) — 14 files, 864 insertions(+), 22 deletions(-): the identical file set and the identical line counts the branch had before the merge, so nothing outside the merge and the six-line regeneration entered the diff. Files: .changeset/oauth-agent-runs-as-the-user.md 31+, content/docs/permissions/system-context.mdx 12 (6+/6-), packages/mcp/src/mcp-http-tools.ts 57, packages/mcp/src/query-records-delegation-diagnostic.test.ts 115, packages/plugins/plugin-security/src/explain-engine.ts 108, .../objects/default-permission-sets.ts 51, .../permission-evaluator.ts 41, .../security-plugin.test.ts 197, .../security-plugin.ts 91, packages/qa/dogfood/test/showcase-agent-scope-ceiling.dogfood.test.ts 82, packages/runtime/src/domains/mcp.ts 31, packages/spec/api-surface/contracts.json 1, packages/spec/export-origins/contracts.json 1, packages/spec/src/contracts/security-service.ts 68. (3) SUITES RE-RUN AT 11e1b73de — first honest reading against today’s main, NOT a confirmation of the pre-suspension numbers. Dependency closure rebuilt first (`pnpm --workspace-concurrency=2 --filter ...` build, VERDICT command-exit 0). Every run went through `bash scripts/pm/os-verify-lock.sh`; verdicts read from its VERDICT line, never a bare $?. plugin-security: 109 test files / 2114 tests passed — VERDICT command-exit 0 (held 157s). plugin-sharing: 37 files / 910 tests passed — VERDICT command-exit 0 (73s). spec: 471 files / 13251 tests passed — VERDICT command-exit 0 (743s). runtime: 251 files / 3532 tests passed — VERDICT command-exit 0 (461s). mcp: 27 files / 297 tests passed — VERDICT command-exit 0 (44s). showcase dogfood ceiling file (`vitest run --maxWorkers=2 test/showcase-agent-scope-ceiling.dogfood.test.ts`): 1 file / 7 tests passed — VERDICT command-exit 0 (45s). (4) GATE RECONCILIATION at the final head, exit codes captured before any pipe and recorded per family: `node scripts/pm/dispatch-gates.mjs --ran ... --repo objectstack-ai/objectstack` says \"110 derived famil(ies) accounted for — 109 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)\". 109 of 110 exited 0. The one NOT MEASURED is `pnpm check:dual-build-cjs-loads`, exit 3: \"PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ ... Run `pnpm build` first. This is NOT a pass: nothing was measured.\" It names 8 packages outside my build closure (studio, client-react, embedder-openai, knowledge-memory, knowledge-ragflow, organizations, service-cluster-redis, service-knowledge); CI’s Build Core job is what satisfies it. Recorded as NOT MEASURED, never as a pass. One further family needed its prerequisite built before it could measure anything: `pnpm --filter @objectstack/spec run check:skill-examples` first exited 1 with its own refusal text (\"packages/client-react/dist holds no .d.ts declarations — the package is not built ... A verdict now would be computed against a build that no longer matches src\"). I built that closure and re-ran it: exit 0, \"258 prose examples type-check across 3 surface(s) — every marked block parsed, so tsc ran the SEMANTIC pass on all of them\". The 0 recorded for it is that second, genuinely measured run. Outside the derived total and outside these numbers, as the tool itself prints: 45 artifact-roster families, 10 declared wide-population families, the unreachable listing, 6 path-scheduled CI jobs and the always-runs tail. Repo-wide `pnpm lint` was not run locally — it is CI’s. (5) NO ABLATION THIS ROUND — nothing was implemented, so there is no new guard to prove can fail; the pre-suspension ablation evidence for the implementation itself stands in the PR body unchanged.",
      "mcp_calls": "0 — this run used git plus repo-scoped REST only (probe `GET /repos/.../pulls/17332` returned 200); no MCP GitHub call was made.",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: `pnpm check:dual-build-cjs-loads` cannot be measured in any worktree that has not run a full `pnpm build` — it exits 3 by design. A condition of local measurement, not a defect on this branch.",
        "noted, not filed: `check:skill-examples` reads packages/client-react/dist and refuses (exit 1, not 3) when that package is unbuilt. Same class as the exit-3 above but a different exit code, so a sweep that classifies NOT-MEASURED by \"exit 3\" alone will silently score it as a red. Mentioned only so the PM reads my recorded 0 as the post-build re-run it is."
      ]
    }

    Generated by Claude Code

  11. os-tesla commented on Sep 10, 2026

    @os-tesla
    Collaborator

    席内条款②复核 — PR #17332 @ 11e1b73de

    Implemented-by: claude/issue-16549-oauth-agent-runs-as-the-user (mode:subagent dev, no own session) + claude/issue-16549-oauth-agent-runs-as-the-user-r2 (regen round)
    Reviewed-by: session_012zTkyNHJ7TkuN2oXtP5x37 · domain:services PM (#6021)
    

    ⚠️ 独立性对如实记录:实施与复核同属本席。按 SKILL.md:641「余席默认档自审加门禁」,自审是本车道的规定形状(达档独立复核只在 spec 席),⇒ ⛔ 这不是缺陷,但读者应当知道这份裁决没有跨席独立性,只有门禁做补偿控制。


    ① Derived judgments — 接受集与公开面变化逐条

    # 变化 判
    1 DelegationNarrowing (interface) 新增于 packages/spec/src/contracts/security-service.ts ⚠️ 公开面 —— packages/spec/src/contracts/index.ts:51 是 export * from './security-service.js',⇒ 它落在已发布的 ./contracts 入口上,没有新增一行具名导出语句。⭐ 这是桶测试最易漏判的一格。判:正确,条款② yes 的承重点。
    2 ISecurityService.describeDelegationNarrowing 新增成员 可选(?)⇒ 不是必需成员,既有实现不被破坏。判:加法,正确。
    3 McpDataBridge.diagnoseDelegation 新增成员 可选;且 packages/mcp/src/index.ts 具名再导出 McpDataBridge ⇒ 已发布面变化、零新名字。判:加法,正确。
    4 intersectDelegatedScope / d10NarrowingStatement 新增导出 packages/plugins/plugin-security/src/index.ts 第 10、11 行是具名再导出(SecurityPlugin、PermissionEvaluator),无 export * ⇒ 这两个未发布。判:不构成公开面变化,正确。
    5 行为面:agent ∩ user 由「取窄」改为「静默即无意见」⇒ 委派读放宽 ⚠️ 这是放宽,但它是裁决(批次 #81 item 1,option 1)明令的方向;且负边界规则「运行时权限/安全行为变更不是条款②,归人工地板」⇒ 该放宽的授权来自裁决本身,⛔ 不由本复核给出。判:在裁决射程内,正确。
    6 生成基线:api-surface/contracts.json +1、export-origins/contracts.json +1 由仓库自己的工具报出(check:api-surface → './contracts + DelegationNarrowing (interface)',0 breaking / 1 added)⇒ ⛔ 不是断言。重生成轮再次确认两文件在合并后逐字节存活。判:一致,正确。

    ⇒ ①:全部判对,无一条被漏报或误报为 no。

    ② Semver 定级

    changeset 声明 minor on plugin-security / spec / mcp / runtime。核:无成员被删、无必需成员新增、无 wire shape 退役 ⇒ ⛔ 不是 major、⛔ 不是 protocol:breaking。三个新增点全部是可选成员或纯新增 interface。check:api-surface 独立给出 0 breaking。

    ⇒ ②:级别与 diff 一致,PASS。

    ③ 边界旗与 open_questions 逐旗处置

    交付席报告 "open_questions": [](零条),四条 out_of_scope_findings 逐条处置:

    1. OAuth agent 仍读不到 delegator 能读的 private 姿态对象 —— 那是响亮拒绝,不是本卡的静默收缩,且扩宽它正是裁决围起来的面。⇒ 按裁决自己那句「a subtraction nobody wrote down is the next card」写进减法表。处置:接受,⛔ 不另立。
    2. 共享管理权完全未被委派(hasWriteBypass 恒 false、resolveWriteScope 恒 'own')—— 裁决点名的是可见性。⇒ 同样写进减法表。处置:接受,⛔ 不另立。
    3. 派单把 resolveWriteScope / hasWriteBypass 点名为落点是错的 —— 它们的消费者只有 canManageShares 与附件/评论钩子,是共享管理权不是数据可见性。处置:错在派单席(本席),已认下;交付席没去动它们是对的。
    4. 重生成轮独立再撞一次 check:skill-examples 用 exit 1 表达前提缺失(应为 exit 3)—— 这与 A notify node that enqueued NOTHING reports the same as one that delivered: unmeasured=0 makes a zero-delivery run indistinguishable from a successful one #17123 那轮报的是同一条,两轮独立命中。⇒ ⛔ 它不属于本 PR,需要另立卡(原卡随账号删除,尚未重建)。处置:升级为待立卡,记在此处以免再丢一次。

    ⇒ ③:零 open_questions,四条 out-of-scope 全部处置,其中一条升级。

    门禁(自审的补偿控制半边,退出码在任何管道之前捕获)

    门禁 读数
    CI 全部 check @ 11e1b73de 38 个,0 失败(Lint & Repo Gates success,census 已修)
    check-system-context-census exit 0 —— 110 elevation read sites … 129 anchors,零 human-edit 类问题
    check-clause2-carriers --pair 17332 ⛔ exit 4 —— 见下
    重生成后本 PR 自身改动面 git diff --stat origin/main...11e1b73de = 14 文件 / 864+ / 22−,与合并前同一张清单 ⇒ 合并与重生成之外没有东西进入 diff

    ⭐ 重生成轮把普查差 1 的成因查实了,⛔ 不是猜:main 的 d2c1d1980(#17195)新增了一个 elevation read 站点,两侧各自独立把普查从 108 抬到 109 且两个 diff 逐字节相同,于是文本合并只保留了一个 109,而并集是 110。


    裁决:⛔ PASS 暂缓 —— ①②③ 全部 PASS,但载体不可读,⛔ 不放行

    技术面三项全部 PASS,CI 全绿,改动面干净。唯一未满足的是放行前检第②条:check-clause2-carriers --pair 17332 仍是 exit 4,因为本卡的 Claim: 评论缺少固定拼写那一行(本席当初写成了散文)。

    ⇒ 需要在本卡 Claim: 评论(#issuecomment-5614981374)的首行之后加一行,逐字:

    Clause-②: yes
    

    ⛔ 本席无法自行补:MCP 无编辑既有评论的工具,而 ⛔ 补发第二条 Claim: 违反认领纪律。已请维护者代点。

    该行到位后 --pair 17332 应转 0,本席即同笔剥双载体(卡侧 + PR #17332 侧的 needs:contract-review)、留 provenance 评论引本 PASS、转 ready、挂 auto-merge。


    Generated by Claude Code

  12. removed
    needs:contract-reviewMarker only — PR awaits its at-tier Contract review PASS; no check reads it, the record decides
    on Sep 10, 2026
  13. os-tesla commented on Sep 10, 2026

    @os-tesla
    Collaborator

    闸门清标 — ⚠️ 维护者豁免,⛔ 不是复核通过

    needs:contract-review 已从双载体同笔剥除:卡 #16549 与 PR #17332。

    ⛔ 这条记录存在的理由,请勿读错

    references/contract-review.md 说得很直白:第二个载体是**「唯一机器可读的、区分闸门被 CLEARED 还是被 STRIPPED 的证据」**——「一次合法的清标留下两处相隔数秒的移除,一次剥标只留一处」,而「⛔ 闸门被剥不是红灯是放行」。⇒ 光看标签消失,分不出是复核通过还是被豁免。所以这条评论就是那个区分。

    本次是 STRIPPED,⛔ 不是 CLEARED。

    依据

    维护者裁定(2026-09-10,实时 PM 会话):本车道(domain:services)已不需要契约复审;并明示 #16549 的 Claim: 评论不必补那行固定拼写的声明。

    ⇒ 这覆盖了本席上一条提出的两个拦截理由:

    1. check-clause2-carriers --pair 17332 的 C2「卡侧声明肢无读数」—— 维护者明示不补;
    2. 03:13Z 那条档位通知里的路由保留项——「你的 PR 触及 packages/spec(3 个文件),所以 spec 席的契约复审档复核对该 PR 仍然适用」,以及 references/lanes/spec.md:12「packages/spec 恒归本席,不论谁需要它」。

    ⛔ 本席没有推翻那两条规则,也没有代替 spec 席作出裁决。 是维护者在其权限内免除了它们。规则文本本身没有变——⇒ 若后续有人重读本卡,看到的应当是「豁免」,⛔ 不是「spec 席审过了」。

    在案的是什么

    • 技术面:本席的席内条款②复核(#issuecomment-5616116189)—— ①接受集与公开面逐条、②semver 定级、③边界旗逐旗,三项全 PASS,并如实记录了独立性对为同席自审。
    • ⛔ 不在案的:spec 席的达档复核。它从未发生,本卡后续任何读者都不应假定它发生过。
    • CI @ 11e1b73de:38 个 check,0 失败。

    遗留(⛔ 不因本次豁免而消失)

    本卡的 Claim: 评论(#issuecomment-5614981374)仍然缺固定拼写的声明行 ⇒ check-clause2-carriers --pair 17332 在本地仍会判 exit 4。⚠️ 这条不是本 PR 的问题,是本席无法自解的那个状态——MCP 无编辑评论的工具,而 ⛔ 补发第二条 Claim: 违反认领纪律。⇒ 该缺陷本身已立卡 #17366。


    Generated by Claude Code

  14. os-tesla commented on Sep 10, 2026

    @os-tesla
    Collaborator

    os-dev-report

    {
      "issue": 16549,
      "status": "done",
      "branch": "claude/issue-16549-oauth-agent-runs-as-the-user-r2",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/17332",
      "premise_still_valid": true,
      "summary": "Bounded patch round on the domain:spec seat's shape review (PR comment 5617462625). (1) Its one REQUIRED change is done: DelegationNarrowing in packages/spec/src/contracts/security-service.ts is now a discriminated union on `narrowed` instead of one shape with three optional fields, so the invariant the docblock only asserted is now the compiler's. The `?`-optional METHOD with feature detection was left exactly as written, as the seat asked. (2) The scope-alias advisory was TAKEN but NOT as a new export, and this is the one reportable deviation: the dispatch's premise that there is nothing to reuse is FALSIFIED. packages/spec/src/security/permission.zod.ts already declares and already publishes ObjectAccessScope (ADR-0057 D1) on the ./security entry, with the identical five members in the same order (machine-compared; the seat's dark control, a fabricated member, reads 0 repo-wide). It is NOT the HierarchyScope trap the seat flagged, which is a different three-member set. Minting a second name beside the Zod enum would have created two declarations of one ladder against Prime Directive #1, so I named the existing one at all three sites instead. That delivers the advisory's whole point (one named thing, a future member as a one-site edit at the enum) at ZERO new permanent exports. The CARD's premise is untouched and the PR stands; only the dispatch's item-2 sub-premise was wrong. Branch is append-only on the reviewed head (11e1b73de is an ancestor of 63a63e459; both pushes were fast-forwards), origin/main was MERGED in via scripts/pm/os-regen-merge.sh, never rebased. Baselines regenerated wholesale with check:generated --fix; nothing hand-edited.",
      "tests": "All re-run at the final head after the origin/main merge, each through scripts/pm/os-verify-lock.sh with VERDICT command-exit 0: plugin-security 109 files / 2114 tests passed; mcp 27 / 297; spec 469 / 13210; runtime 251 / 3532; showcase dogfood ceiling file 1 / 7. Typecheck over spec + plugin-security + mcp + runtime + dogfood: 'Scope: 5 of 81 workspace projects', all five Done, 0 'error TS'; check:test-typecheck reports plugin-security's test layer at 0 files / 0 errors. PUBLIC SURFACE, the repo's own reading: check:api-surface exit 0, '@objectstack/spec public API surface + factory signatures unchanged'. Whole-PR delta vs origin/main is ONE added line in each baseline (api-surface: + 'DelegationNarrowing (type)'; export-origins: the matching row), zero removed, zero narrowed - unchanged in COUNT from the pre-review PR, because reusing ObjectAccessScope minted no new symbol. check:generated: 15 of 15 artifacts current. NO ABLATION IS OWED - this round adds no new guard, so none was staged. REVERSE VERIFICATION owed for a cross-package type change: two legs, direction predicted first (both red), each mutate then prove on disk (marker count AND git hash-object differing from the HEAD blob) then typecheck then restore via 'git checkout HEAD -- ABSOLUTE_PATH' then prove restored (blob hash equal AND git diff HEAD empty), under trap EXIT INT TERM. Leg A (producer drops a required field): TS2353, and its message prints the resolved arm with all five members, independently re-proving the alias is not a narrowing. Leg B (consumer forgets the discriminant, i.e. the card's own failure mode): TS2339 Property 'statement' does not exist on type '{ narrowed: false; }'. Both restored byte-exact to 57b8a8e2c20751e734b9b13880efb2dfc17fe3f2; tree clean. NO NON-NULL ASSERTION was added anywhere - none in this round's commits, none in the PR's four security-touching files vs origin/main. GATES: dispatch-gates re-derived on the merged tree (no STALE TREE warning) = 110 commands, all run with the exit code captured BEFORE any pipe. Tool verdict verbatim: 'dispatch-gates --ran: 110 derived famil(ies) accounted for - 109 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3)'. The 1 NOT MEASURED is check:dual-build-cjs-loads, exit 3 PREREQUISITE NOT MET (8 packages unbuilt, none touched by this PR) - NOT a pass; CI's Build Core measures it. check:skill-examples first exited 1, which is a PREREQUISITE REFUSAL and not a red about this diff; I built client-react and re-ran it to a real green (258 prose examples type-check across 3 surfaces). ONE RED THIS ROUND PRODUCED AND FIXED: CI 'Check Changeset' was green at 11e1b73de and red at 0f0ff0bc8, so this round caused it. The changeset paragraph I added spelled one direction's cost as a bold-emphasised word, and bold followed by that word is byte-identical to this repo's breaking-change DECLARATION marker, so check-adr-0087-registration correctly read the changeset as declaring a breaking change. There is no disposition to give - the PR is minor, the surface is unchanged, and the sentence described a hypothetical FUTURE direction - so asserting 'not-required' would have written a false answer into that ledger. The content is now a table, which mints no marker: gate exits 0, 'this PR adds no declared-breaking changeset'. This is NOT the token-dropping evasion that gate warns about, which targets a changeset that genuinely breaks consumers. It also exposed a stale reading: that family ran at position 3 of the sweep, BEFORE the changeset edit existed, so every family whose gate source includes .changeset was re-derived and re-run at the final head 63a63e459 - 11 families, all exit 0 - plus check:changeset-fixed, check:nul-bytes and check:closing-keyword-parity. CI at 63a63e459 at report time: 0 failing, Check Changeset success, remainder still in progress; CI convergence is the PM's.",
      "mcp_calls": "1 - a single mcp__github__get_job_logs to read the Check Changeset failure (the Actions log blob is on a host the egress proxy denies). Everything else went over repo-scoped REST and git.",
      "open_questions": [
        {
          "question": "The scope-alias advisory: keep the reuse of the already-published ObjectAccessScope, or still mint a contracts-local name as the dispatch originally specified?",
          "options": [
            "A - keep the reuse (shipped). Zero new exported names; one Zod enum stays the single source of the five-member ladder; a future member is a one-site edit at the enum that both the permission-set authoring schema and the three contract sites inherit. Consumers that want to NAME the type import it from @objectstack/spec/security, exactly as ./contracts already relies on PermissionSet from that module.",
            "B - additionally export a contracts-local alias (for example ScopeDepth) that resolves to ObjectAccessScope, so ./contracts is self-naming. Costs one more permanent exported name; risks a second declaration drifting from the Zod enum if anyone ever spells it by hand instead of aliasing.",
            "C - revert to the three inlined copies. Rejected: it is the status quo the seat's advisory was against."
          ],
          "recommendation": "A, because it delivers everything the advisory wanted at zero cost on the startup-focus axis the PM raised, and because a hand-written union beside ObjectAccessScopeSchema would be a second declaration of one ladder (Prime Directive #1, Zod First). B remains available later as a one-line, non-breaking addition if the spec seat wants ./contracts self-naming - it forecloses nothing."
        }
      ],
      "out_of_scope_findings": [
        "noted, not filed: a changeset that merely DISCUSSES breaking-ness trips check-adr-0087-registration if the word is bold-emphasised, because bold-plus-the-word IS the declaration marker. Working as designed and the gate's header already reasons about token erosion, so this is prose guidance rather than a defect - but it cost this round a CI red and the fix is non-obvious (reword, do not assert a false disposition).",
        "noted, not filed: dispatch-gates' derived sweep is a point-in-time reading. A commit made DURING the sweep leaves earlier families answering about a tree that no longer exists, silently, with the tally still green. Re-running the families whose gate source matches the changed paths is the repair; nothing in the tooling flags it.",
        "noted, not filed: packages/plugins/plugin-security/src/security-plugin.test.ts types its service handle as `any`, so the describeDelegationNarrowing pins do not type-check against the union at all. Not touched (out of this round's scope, and the dispatch forbade widening test assertions); the producer's own typed return plus reverse-verification leg B carry the compile-time evidence instead."
      ]
    }

    Generated by Claude Code

  15. os-tesla commented on Sep 10, 2026

    @os-tesla
    Collaborator

    落地 — PR #17332 MERGED,main 上是 331a1a2b0

    pm:dispatched → pm:done。本卡的交付历经账号停用、PR 重建、spec 席形状复审与一轮定界补丁,⛔ 一行代码没丢。

    落地读数(⛔ 从 origin/main 的对象库读的,不是转述 PR 自报)

    项 读数
    commit 331a1a2b09caf3ea89f3dacbbf8f8de717d3c1cc,2026-09-10 14:45:22Z,squash(本仓 main 是线性 squash 历史)
    变更面 14 文件 / 891+ / 24− —— 与 PR 自报逐字一致
    形状 DelegationNarrowing 在 main 上确为判别联合:narrowed: false 臂不携带任何字段;narrowed: true 臂三字段必需
    梯子 复用已发布的 ObjectAccessScope(ADR-0057 D1),⇒ 0 个新的永久导出;HierarchyScope 全 diff 0 命中
    CI 63a63e459 上 46 项 check 全 completed、0 红,commit status 亦 success

    ⚠️ 入队方式:本仓强制 merge queue —— 我先试的直接 squash 被 GitHub 以 405 Changes must be made through the merge queue 挡回,⇒ 挂 auto-merge 不是可选项而是入队的唯一动作。记在这里,免得下一个席位重复撞。

    ⭐ 判据用的是 origin/main 上真出现 (#17332) 那条 commit,⛔ 不是 auto-merge 接口的成功回报 —— armed ≠ landed。

    本卡最终交付了什么

    1. 平价:D10 交集把「权限集对深度沉默」读作无意见而不再贡献 owner 收窄 ⇒ agent ∩ user = user(可见性)。天花板只保留减法角色。
    2. 诊断:MCP query_records 在被代理天花板窄化时携带 delegationNarrowed: true 与点名 ADR-0090 D10 的 warning ⇒ ⛔ 代理不会再把 total: 0 当成关于数据的事实来回答。⭐ 这一条是本卡代价最大的那一半。
    3. 形状:statement 现在是必需字段。反向验证 leg B 实测 TS2339: Property 'statement' does not exist on type '{ narrowed: false; }' —— 那条会渲染出 undefined 的路径,现在在编译期写不出来。

    ⛔ 未随本卡解决的(已在案,不因合并而消失)

    • OAuth 代理仍读不到委托人能读的 private 姿态对象 —— 那是响亮拒绝而非静默收缩,扩宽它正是裁决围起来的面。已写入减法表。
    • 共享管理权完全未被委派(hasWriteBypass 恒 false、resolveWriteScope 恒 'own')。裁决点名的是可见性。已写入减法表。
    • 三条 noted, not filed:changeset 里加粗某词会误触 ADR-0087 声明标记;dispatch-gates 的扫描是时点读数,扫描期间的提交会留下静默的陈旧行;security-plugin.test.ts 把 service 句柄类型写成 any,⇒ 那几条 describeDelegationNarrowing 的钉子根本没有对着联合做类型检查(编译期证据由生产端返回类型 + 反向验证 leg B 承担)。⚠️ 最后一条是真缺口,本席下班前未立卡,留给下一位读到此处的席位。
    • 本卡 Claim: 评论缺固定拼写声明行 ⇒ check-clause2-carriers --pair 17332 本地仍判 exit 4。维护者已明示豁免(#issuecomment-5617317245),缺陷本身立在 条款②声明载体是一扇单向门:席位能把自己写进一个自己出不去的状态 —— 一个会话里同一个坑被踩了 5 次 #17366。

    Generated by Claude Code

  16. added and removed
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    on Sep 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpm:donepriority:p1High: required for production / M2

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions