Skip to content

mcp: an agent finds a colleague through a read-only sys_user (id, name, email) within the caller's organization visibility — ruled B, email visible #32641

Closed

Description

@os-bob

triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

The signature line above and the Blocked-by: line at the end were added by triage#1 when recording the ruling; the card text below is the filing seat's, unchanged.

Filing category: ② a decision only the maintainer can make.

维护者速读

  • 现象:MCP agent 无法把一个人(按姓名或邮箱)解析成环境里的用户 id。sys_user 是系统对象,query_records 直接拒绝("is a system object and is not exposed via MCP")。所以 agent 不能往「员工 / 审批人 / 负责人」这类 user 字段填任何别人,只能等当事人自己建记录、再从 owner_id 里抄 id。托管环境里 cloud 账号 id 和环境里的用户 id 还不是同一个,agent 手里的 id 也用不上。
  • 要你裁:MCP 是否提供「找人」的能力,以及形状。
  • 推荐:A:只读的「人员查找」,按姓名/邮箱前缀返回 id + 显示名(受调用者自己的可见范围约束),不开放 sys_user 整表。

One-sentence problem

"Create Zhang San's leave balance" or "assign this request to Li Si" cannot be done by an agent, because it has no way to find out who Zhang San is.

Evidence (reach)

Hosted tenant (pnpm dev:stack, Team plan; cloud ff373e02, framework d7c3d44e), MCP OAuth token of the organization owner:

  • query_records { objectName: 'sys_user', where: { email: '<member email>' } } → Object "sys_user" is a system object and is not exposed via MCP (same for a plain member).
  • create_record rtbw_leave_bal { employee: '<the member's cloud user id>' } → 员工: no sys_user record has id "…" — the id the control plane knows is not the environment's user id (the member signed in through OIDC and got an environment user MgyoX3…).
  • Workaround used in the test: the member created a request themselves; the owner read its owner_id.

Governing text

  • packages/mcp/README.md — "sys_* objects hidden by default" (list_objects), "sys_*-object actions are held back fail-closed".
  • docs/NORTH-STAR.md — agents through MCP are one of the two kinds of user; "approvals and automation", "sign-in and identity" are on the road.
  • Rule 14c (served by get_authoring_rules) — approvers by field name "a user lookup field on the record", which an agent cannot fill for anyone but itself.

Options

Option What it does What the customer sees Effort
A A read-only people lookup on the MCP data surface (e.g. find_people { query } → { id, name, email? }), scoped by what the caller may already see of members Agents fill person fields and route work by name mcp seat · 1 PR · no precedent
B Expose sys_user read-only through the record tools under FLS (name/email only) Same, through query_records mcp + security seats · 1–2 PRs · no precedent
C Keep people invisible to agents; document that person fields are filled by people Agents leave every person field empty none

Business reading: A = agents can say "this is for Zhang San" like a colleague would; B = the same, by opening a system table; C = agents only ever act on their own records.

Recommendation

A. Two years out an agent on a business app routes and assigns work by name (Salesforce/ServiceNow agents resolve users through a scoped lookup, not by opening the user table). A keeps the system object closed and adds one narrow, read-only tool. Confidence gap: whether member email should be visible to every member (privacy) — the tool can return name + id only by default.
Self-check: only ① picks A; ②③④ do not flip it.

After the ruling

A: packages/mcp adds the tool under data:read, scoped through the same identity read the console's people picker uses; a test asserting it never returns a member outside the caller's organization.

os-decision-facets
① 项目长远合理性: A 用一个窄的只读工具补上 agent 的「找人」,不打开 sys_user 整表,特例最小。
② 实际业务拉动: 任何带人员字段的流程(请假余额归属、审批人、负责人分配)今天都卡在这一步(本次实测)。
③ 防 AI 犯错: 现状 agent 只能猜 id 或把字段留空;A 让它拿到真实 id,写错时被响亮拒绝而不是静默留空。
④ 创业阶段不扩散: A 新增一个工具(永久义务);C 零成本但让人员字段对 agent 不可用。
Prior rulings read: "mcp sys_user lookup person user field" → 65 hits (top: #3146, #31350, #24952; none on MCP people lookup); ADR-0101; thread: none

Reader who acts: the packages/mcp seat, after the ruling.
Dedupe: repo:objectstack-ai/objectstack is:issue mcp sys_user lookup person user field → 65 hits, none proposes an MCP people lookup.

Blocked-by: #32442

Activity

  1. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 12, 2026
  2. objectstack-fleet commented on Nov 12, 2026

    @objectstack-fleet
    Contributor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Path: none

    分诊首触 + 决策分析(fable 档独立评估)· 2026-11-12T16:54Z

    维护者速读

    • agent 无法把「张三」变成用户 id,所以填不了员工、审批人、负责人这类人员字段。
    • 界面里的人员选择器本来就能看到同组织成员,agent 缺的就是同一把钥匙。
    • 推荐加一个窄的只读「找人」工具,不开放用户整表。这涉及「谁能枚举通讯录」,属权限边界,需您亲自定。
    • 请裁:A 加 find_people 窄查找 / B 对 sys_user 开放 query_records / C 不做。推荐 A。

    是否已有裁决: 未找到直接裁决。可见范围已由 ADR-0092 D5 定死:sys_user_org_members 是只读的同组织可见范围。

    前提核实:

    • MCP 默认隐藏 sys_*,且 fail-closed(packages/mcp/src/mcp-http-tools.ts:2116-2118、:3195)。
    • 界面的人员字段走通用数据路径读 sys_user。

    拉动: 六张卡里最实:HotCRM 有 22 处 sys_user 引用,审批和指派都卡在这一步。

    选项 客户看到 工作量
    A find_people{query} → {id,name} agent 可以按名字指派 mcp + runtime · 1–2 个 PR + 契约评审(放宽);先例 #31009
    B 对 sys_user 开放 query_records 同上,但字段级权限允许的列全部可读 1 个 PR;对 fail-closed 开单对象例外,无先例
    C 不做 人员字段对 agent 永远为空 0

    四轴:

    • ① 长远:Slack users.lookupByEmail、Linear MCP list_users、Agentforce 都是窄查找,不是开整表。
    • ② 拉动:人员字段普遍。
    • ③ 防 AI:A 的返回形状闭合,agent 拿到真实 id;B 是自由查询面。
    • ④ 不扩散:A 多一个工具,但复用既有可见范围,不加新权限概念。

    推荐 A: 默认只返回 id 和显示名;邮箱只支持精确匹配,不支持前缀枚举;结果设上限。置信缺口:成员对同事行的列级可见集合未核实。自检: 只看①选 A;④单看会选 C,不足以翻转。人工底线项:通讯录枚举与邮箱可见性。

    os-decision-facets

    • ① 长远:窄只读查找是主流 agent 平台的形状,系统表保持关闭
    • ② 拉动:人员字段普遍(HotCRM 22 处 sys_user 引用),审批和指派卡在这一步
    • ③ 防 AI:返回形状闭合,写错会被响亮拒绝
    • ④ 不扩散:新增一个工具,复用 ADR-0092 D5 既有可见范围

    Prior rulings read: ADR-0092 D5; packages/mcp README (sys_* fail-closed); thread: none


    Generated by Claude Code

  3. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 12, 2026
  4. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Brief-claimed-by: session_01Q4EjjMnyLqJdzuktEmRPXH · batch #1 · 2026-11-13T04:18Z

    Decision batch #1 (the MCP agent-capability family, under epic #32897), taken on the maintainer's 「5张一批按总监决裁格式给我」.

  5. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Ruling recorded · #32641 · B · 2026-11-13T04:39Z

    Maintainer, verbatim (triage#1 session): 「以上原则同意」, answering decision batch #1 (32641 B / 32591 A / 32592 C / 32686 A / 32747 A). The batch was analysed independently at the fable tier, with prior rulings checked first; the full analysis was presented in the session.


    Generated by Claude Code

  6. added
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 13, 2026
  7. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Path: none

    Triage: unblocked → pm:queue · 2026-11-13T22:59Z

  8. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Nov 13, 2026
  9. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Path: none

    Ruling recorded · #32641 · the email gap is closed: member email is visible to agents · 2026-11-13T23:05Z

    Maintainer, verbatim (triage#1 session): 「成员的邮箱可以对 AI 可见,姓名容易重复吧?」. This answered triage's question about the confidence gap under ruling B: should a member's email be visible to every member's agent?

    What it settles for the builder:

    • sys_user is exposed read-only to the MCP record tools under field masking with id, name and email, as option B states. Email stays in, because names repeat and email is the reliable key to resolve a person.
    • Unchanged: the read stays within the caller's organization and member visibility, nothing writes, and no other sys_user field is visible. Pin that a non-member is invisible and that every masked field stays hidden.
    • Where a name matches more than one member, the agent should be able to tell them apart by email. Say so in the skill text the PR updates.
  10. changed the title [-][Decision] How should an MCP agent find a colleague (by name or email) to fill a person field or route work, when sys_user is not exposed over MCP?[/-] [+]mcp: an agent finds a colleague through a read-only sys_user (id, name, email) within the caller's organization visibility — ruled B, email visible[/+] on Nov 13, 2026
  11. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    epic:#32897#1 · os-tesla · session_01L9qibzoApSyVPuia3NCrBY

    Claim: PM loop round 1 · epic seat epic:#32897#1 (registration 6867120489) · 2026-11-13T23:11Z
    Session: session_01L9qibzL1RvtJP7jM7QZJaF (card thread; seat session_01L9qibzoApSyVPuia3NCrBY)
    Account: os-tesla
    Branch: claude/project-thread-83mt0o (the card thread's harness-pinned branch; the thread can push only this name)
    Worktree: objectstack-issue-32641
    Domain: domain:server
    Seat: epic:#32897#1
    File surface: packages/platform-objects/src/identity/sys-user.object.ts, the baseline permission file PR #33117 touched (if needed), packages/mcp/src/** (tests; code only if admission needs it; the MCP-served skill text packages/mcp/src/skill-md.ts for the ruling-6867150851 sentence), content/docs/ai/connect-mcp.mdx, packages/mcp/README.md, root .changeset/ (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default (dispatch-gates --tier: no path-derived mandate), effort: xhigh (permission boundary)
    Clause-②: yes (widening) — expected; the dev derives and cites
    Metadata-protocol: none
    Responsibility: platform code in objectstack (sys_user in packages/platform-objects/src/identity/sys-user.object.ts declares no AI read exposure); the platform path that already covers it is enable.aiReadable + isAiReadExposed (PR #33117, 3eac63a9); reached by every agent filling a person field — measured on a hosted tenant in the card body
    Thread-read: 6867150851
    Premise: holds (rulings 6844187236 B and 6867150851 email visible; Blocked-by: #32442 closed; main c04aa295)
    Serial constraints cleared: none in flight on these files

    Order: /mnt/project-files/epic-32897-1/r2026-11-13/dispatch-32641.md under that folder's FRAME.md. Contract review: this seat, at CONTRACT_REVIEW_TIER.


    Generated by Claude Code

  12. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 13, 2026
  13. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    epic:#32897#1 · os-tesla · session_01L9qibzoApSyVPuia3NCrBY

    Seat note · the agent's sys_user field set is put to the maintainer · 2026-11-13T23:17Z

    ⛔ Not a ruling. The card thread measured (static read at origin/main) that the console person picker reads the whole member-readable sys_user row: self + org peers, every field except the Admin group (identityAdminFieldSecurity('withhold') on member_default). Declaring enable.aiReadable lands an agent on exactly that set through the existing gates.

    Two recorded texts differ on the field set: ruling record 6844187236 ("exactly what that member sees in the console's person picker … directory fields only (#21237)") and option B's own text plus triage's 6867150851 ("name/email only", "no other sys_user field is visible"). A narrower set needs a new agent-only mask, a privacy boundary this seat does not decide.

    • Asked the maintainer in this seat's thread: console set (recommended, no new mask) or id/name/email only.
    • The card thread builds on the console-set reading and lists the served fields in the PR; the PR does not land until the maintainer answers. An id/name/email-only answer comes back as a fork, not a silent build.

    Generated by Claude Code

  14. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    Contributor

    epic:#32897#1 · os-tesla · session_01L9qibzL1RvtJP7jM7QZJaF

    os-dev-report

    2026-11-14T00:16Z · PR #33180 · head 1e1c1561

    {
      "issue": 32641,
      "status": "done",
      "branch": "claude/project-thread-83mt0o",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/33180",
      "session": "session_01L9qibzL1RvtJP7jM7QZJaF",
      "premise_still_valid": true,
      "summary": "sys_user declares enable.aiReadable: true (the PR #33117 mechanism); no admission or permission code changes. A member's agent reads sys_user with the existing MCP read tools under the gates the console person picker is served through: own row + active-organization members (member_default sys_user_self / sys_user_org_members), every field except the Admin group (identityAdminFieldSecurity withhold), on results and filters. Measured served set for a member: accountable_user_id, banned, created_at, email, email_verified, id, image, locale, manager_id, name, primary_business_unit_id, principal_kind, source, two_factor_enabled, updated_at. query_records' text and the served SKILL.md (plus its byte-identical Console copy) say how to find a colleague for a person field and to tell namesakes apart by email (ruling 6867150851). The field-set question (console set vs id/name/email only) is with the maintainer via the seat (6867361507); this PR builds the console reading and contains no agent-only mask.",
      "tests": "head 1e1c1561 (after merging main 58443466a). dogfood colleague-lookup-over-mcp (new, 10) + approval-read-over-mcp (pin updated): 2 files / 20 tests passed. platform-objects test 122 files / 1957 passed (incl. new sys-user-ai-read.test.ts, 2). mcp test 64 / 1141 passed (incl. new skill-md describe). typecheck platform-objects 0, mcp 0, dogfood 0 (8 GiB heap; 4 GiB OOM exit 134). Ablations, each committed head, ablation-replace --hold, package rebuilt, dist preflight present before / absent after, restore blob == HEAD and porcelain empty: A1 aiReadable line deleted (= main state) -> declaration pin 1 failed, dogfood 8 failed / 2 passed, reads answer 'Object \"sys_user\" is a system object and is not exposed via MCP'; A2 sys_user_org_members widened to true (x3 anchor, --expect 3) -> 2 failed (outsider appears); A3 member_default keeps the Admin group -> 2 failed (13 group fields served); A4 update_record on readGuard -> 1 failed (update answered a record). Two first tries were void and rerun: A2 anchor ambiguous without --expect (nothing written), A4 comment marker stripped by the bundler (preflight refused, nothing ran).",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 1e1c1561: 100 commands, each run with stdin closed, exit captured before any pipe: 97 exit 0 first pass; check:dual-build-cjs-loads and spec check:skill-examples exit 3 PREREQUISITE NOT MET (packages without dist/) -> built -> exit 0; check-plugin-teardown-shape --self-test exit 3 (pinned fixture commit missing from shallow clone) -> fetched -> exit 0. pnpm lint exit 0. --ran with exit codes: 100 derived, 100 run, 0 NOT-MEASURED (derived zero); pnpm lint outside the derivation. Record: /mnt/project-files/epic-32897-1/r2026-11-13/32641-gates-ran-1e1c1561.list. CI check-runs: in_progress at report time.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "line_budget": "+~420 / -4: 2 new test files, skill text +7 lines in skill-md.ts and its Console mirror, docs +9, changeset; no skills/** touched",
      "deviations": [
        "Outside the claim's file surface, changed as consequences: ui/apps/console/src/pages/developer/objectstack-skill.ts (skill-md.test.ts holds it byte-identical to the served render) and packages/qa/dogfood/test/approval-read-over-mcp.dogfood.test.ts (its list_objects pin said sys_approval_request is the only listed sys_* object).",
        "New dogfood file packages/qa/dogfood/test/colleague-lookup-over-mcp.dogfood.test.ts (dogfood is the lane's real-client run; the order's step 4 asked for an end-to-end MCP pin).",
        "PR body attribution second line links this card thread (session-context instruction), not the seat thread FRAME names.",
        "Dogfood typecheck run at --max-old-space-size=8192: the 4096 default OOMs (exit 134)."
      ],
      "files_changed": [
        ".changeset/32641-sys-user-ai-readable.md",
        "content/docs/ai/connect-mcp.mdx",
        "packages/mcp/README.md",
        "packages/mcp/src/mcp-http-tools.ts",
        "packages/mcp/src/skill-md.ts",
        "packages/mcp/src/skill-md.test.ts",
        "packages/platform-objects/src/identity/sys-user.object.ts",
        "packages/platform-objects/src/identity/sys-user-ai-read.test.ts",
        "packages/qa/dogfood/test/colleague-lookup-over-mcp.dogfood.test.ts",
        "packages/qa/dogfood/test/approval-read-over-mcp.dogfood.test.ts",
        "ui/apps/console/src/pages/developer/objectstack-skill.ts"
      ],
      "mcp_calls": "4 — reads only: issue_read x3 (get, get_comments x2 on #32641), pull_request_read x1 (get_files #33117); 0 write tools",
      "api_writes": "2 relay writes — pr_create (#33180, with assignee os-tesla) and this os-dev-report comment via post-stamped",
      "open_questions": [
        { "question": "The agent's sys_user field set: the console member set (15 fields, Admin group withheld, what this PR builds) or id/name/email only?", "options": ["A console set: no new mask; agent = console (ruling record 6844187236 wording)", "B id/name/email only: needs a new agent-only field mask the console does not use (option B text, triage 6867150851 wording)"], "recommendation": "A, because it adds no second security boundary and keeps the agent's answer equal to the HTTP twin; already with the maintainer via the seat (6867361507)" }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: #33156 / #33030 · the stdio bridge admits sys_user through the same isAiReadExposed; its row and field gates are that card's scope and are not pinned here · noted, not filed"
      ]
    }
  15. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    Contributor

    epic:#32897#1 · os-tesla · session_01L9qibzoApSyVPuia3NCrBY

    Ruling recorded · #32641 · the agent's sys_user field set = the console person picker's set · 2026-11-14T00:49Z

    Maintainer, verbatim (decision card in this seat's project thread, question 「#32641 让 agent 读同事时,能看到哪些用户字段?」): tapped 「和界面选人一样」 — "agent 看到的同事字段与成员在界面选人时一致(姓名、邮箱、头像、语言、上级等,管理员字段照旧隐藏),不加新规则。" The other option, 「只给 id、姓名、邮箱」 (a new agent-only mask), was not chosen.

    This settles the question in seat note 6867361507:

    • An agent reading sys_user through the MCP record tools is served exactly what the member is served by the console's person picker: their own row and their organization's members, every field except the Admin group (identityAdminFieldSecurity('withhold') on member_default). No agent-only mask.
    • Unchanged from ruling B (6844187236) and 6867150851: email is visible, nothing writes, other organizations' users are invisible.
    • Execution: PR feat(platform-objects,mcp): a member's agent finds a colleague through the read-only sys_user #33180, as built, is the shape ruled. Its served field list stays in the PR body.

    Generated by Claude Code

  16. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    Contributor

    epic:#32897#1 · os-tesla · session_01L9qibzoApSyVPuia3NCrBY

    Claim amendment to this seat's claim 6867271061 on #32641 · 2026-11-14T00:52Z: the file surface adds two files the card thread touched outside it, both accepted:

    • ui/apps/console/src/pages/developer/objectstack-skill.ts: the Console "Download skill" copy of packages/mcp/src/skill-md.ts, held byte-identical by skill-md.test.ts; the ruling-6867150851 sentence has to land in both or that test is red. Same text, no other change. (domain:ui file; domain:ui#4 is this account's seat and has no open PR on it.)
    • packages/qa/dogfood/test/approval-read-over-mcp.dogfood.test.ts: it pins the listed sys_* set, which now includes sys_user.

    The field set was ruled 6868810528 (the console person picker's set); PR #33180 is that shape.


    Generated by Claude Code

  17. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    Contributor

    epic:#32897#1 · os-tesla · session_01L9qibzoApSyVPuia3NCrBY

    Landed · #32641 · 2026-11-14T02:18Z — PR #33180 merged through the queue as 6a36b79c; Fixes #32641 closed this card. pm:dispatched off in this stroke.

    Delivered: sys_user declares enable.aiReadable: true, so a member's agent finds a colleague with query_records / get_record (by email for exactly one person, by name with email to tell namesakes apart) and is served the console person picker's set: own organization only, Admin group withheld, every write refused (ruling 6868810528). Left: the stdio host's row and field gates for sys_user belong to the stdio parity work (PR #33175, merged) and are not pinned by this card's dogfood.


    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 guardsdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions