Repository navigation
feat(platform-objects,mcp): a member's agent finds a colleague through the read-only sys_user - #33180
Conversation
…h the read-only sys_user sys_user declares enable.aiReadable, so the MCP read tools admit it under the gates a console read already runs: the caller's own row and their organization's members, the Admin field group withheld. No write tool opens it. The query_records text and the served SKILL.md say how to find a colleague for a person field and to tell namesakes apart by email. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzL1RvtJP7jM7QZJaF
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibzL1RvtJP7jM7QZJaF
📓 Docs Drift CheckThis PR changes 2 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin dba2fbe992679f06c05a0064cd2a5430aa811c3c && git checkout dba2fbe992679f06c05a0064cd2a5430aa811c3c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9e235344f19c483b18c54fb063143dbef0de837b 1e1c156181938f70d797754ed6b940b6f7873574 && git checkout -B drift-repro 9e235344f19c483b18c54fb063143dbef0de837b && git merge --no-ff 1e1c156181938f70d797754ed6b940b6f7873574
node scripts/docs-audit/affected-docs.mjs --json 9e235344f19c483b18c54fb063143dbef0de837b
|
|
epic:#32897#1 · os-tesla · session_01L9qibzoApSyVPuia3NCrBY Contract reviewServed-tier: Owed by the ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
epic:#32897#1 · os-tesla · session_01L9qibzoApSyVPuia3NCrBY 维护者速读 · PR #33180 @
|
Requested by Tesla · project thread
epic:#32897#1 · os-tesla · session_01L9qibzL1RvtJP7jM7QZJaF
Fixes #32641
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
sys_user声明了enable.aiReadable: true。成员的 MCP 代理现在能用现有的query_records/get_record按邮箱或姓名找到同事、拿到用户 id,去填「负责人 / 审批人 / 员工」这类人员字段。没有新工具,任何写工具仍拒绝sys_user。query_records的说明和下发的 SKILL.md 教代理:按邮箱找唯一一个人;重名时用邮箱区分。Admin组:id、姓名、邮箱、邮箱已验证、两步验证开关、停用标记、头像、语言、上级、主业务单元、账号类型(人/AI)、责任人、来源、创建/更新时间。登录记录、锁定、封禁原因、登录手机号、AI 席位等Admin组字段不给,按它筛选也被拒;别的组织的用户查不到。这比 option B 原文「只有姓名/邮箱」宽,座位已就此问你(卡上 6867361507);若你要只留 id/姓名/邮箱,需要新加一条只针对代理的屏蔽规则,那是另一个决定。回滚即撤本 PR。packages/platform-objects/src/identity/sys-user.object.ts的aiReadable: true与注释;②packages/qa/dogfood/test/colleague-lookup-over-mcp.dogfood.test.ts(端到端:找人、跨组织不可见、Admin 组被屏蔽、写被拒、填人员字段);③packages/mcp/src/skill-md.ts新加的「People aresys_userrecords」一条。pnpm --filter @objectstack/dogfood exec vitest run test/colleague-lookup-over-mcp.dogfood.test.ts,预期 10 条全过。What changes
packages/platform-objects/src/identity/sys-user.object.ts:enable.aiReadable: true(the mechanism PR feat(approvals,mcp): an approver's agent lists the approvals waiting on them over MCP #33117 landed,3eac63a9). Both MCP bridges already attest it throughisAiReadExposed, so the five read tools (list_objects,describe_object,query_records,get_record,aggregate_records) admitsys_user; the write tools keep the plainsys_*guard. No code change in the admission.member_defaultalready grantssys_userallowRead(default-permission-sets.ts, thesys_userobject entry), scopes rows withsys_user_self+sys_user_org_members(id in current_user.org_user_ids), and withholds theAdminfield group withidentityAdminFieldSecurity('withhold'). Those are exactly the gates the console person picker (UserField→PeoplePicker→useRecordQuery, no field projection) is served through, so the agent's read and the picker's read are the same read.query_records' description: "To fill a person field, find the colleague's id: objectName "sys_user", where {"email":"..."} for exactly one person, or {"name":{"$contains":"..."}}; names repeat, so tell people who share a name apart by email." The servedSKILL.md(packages/mcp/src/skill-md.ts) gains a Conventions bullet saying the same, plus "ask your human which one if unsure", and itslist_objectsline no longer says every system object is hidden. The Console "Download skill" copy (ui/apps/console/src/pages/developer/objectstack-skill.ts) carries the same two edits, becauseskill-md.test.tsholds it byte-identical to the served render.content/docs/ai/connect-mcp.mdx(the default-exposure bullet namessys_user, what is served, that names repeat) andpackages/mcp/README.md(thelist_objectsline names both declared objects)..changeset/32641-sys-user-ai-readable.md:@objectstack/platform-objects,@objectstack/mcp,@object-ui/consolepatch, both declaration lines, the served field list.The served field set (measured, live row)
For a plain member, on a colleague's row, over MCP and at the HTTP data door alike:
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. That is every declared field outside theAdmingroup; the dogfood pins it as "declared minusAdmingroup", read off the declaration, so a field moved into or out of the group moves the pin with it.Tests
packages/qa/dogfood/test/colleague-lookup-over-mcp.dogfood.test.ts(new, 10 tests). RealSecurityPlugin, realMCPServerPlugin, the live/mcptransport; no permission set in the file namessys_user(members hold the shipped baseline and an app set naming the app's one object). Fixture: a member, two colleagues who share one name, and a third user of that name moved into another organization (membership and sessions); syntheticAdmingroup values on one colleague. Armed controls: the group is non-empty and covers the fixture fields; the values are at rest; the outsider is in another organization and the name matches three rows at rest.$containson a partial name, the same two; by email, exactly one id;get_recordrefused, no email leaked), by count (n: 2);Admingroup: not on the served row, not on a projection naming it, and a filter onfailed_login_countis refused; control: the platform admin'sget_recordis servedlast_login_ip;query_recordsand the member'sGET /data/sys_userreturn the same ids and the same field set, which equals the declared non-Adminfields;create_record,update_record(own row and a colleague's),delete_record,delete_recordsare each refused with the system-object sentence, and nothing changed at rest;query_recordstext namessys_userandemail;create_recordon the app object withassignee= the found id, the stored row holds it.packages/platform-objects/src/identity/sys-user-ai-read.test.ts(new, 2): the declaration andisAiReadExposed;apiMethodsunchanged.packages/mcp/src/skill-md.test.ts(+1 describe): the served skill's bullet namesquery_records,"email",$contains, "tell them apart by email", "read only"; the old "are hidden" claim is gone. The existing byte-identity pin holds the Console copy.packages/qa/dogfood/test/approval-read-over-mcp.dogfood.test.ts: itslist_objectspin saidsys_approval_requestis the only listedsys_*object; it now listssys_approval_requestandsys_user.Ablations
Each on a committed head with
scripts/ablation-replace.mjs --hold, the package rebuilt,scripts/ablation-dist-preflight.mjsproving the mutation indist/before the run and gone after the restore, restore provedblob == HEADandgit status --porcelainempty.aiReadable: trueline deleted (platform-objects rebuilt). This is also the measurement ofmainObject "sys_user" is a system object and is not exposed via MCP(the two still green: the write refusals and the tool text)sys_user_org_memberswidened totrue(plugin-security rebuilt; the anchor occurs three times, member, viewer and external sets, all mutated)member_defaultkeeps theAdmingroup (identityAdminFieldSecuritykeep, plugin-security rebuilt)update_recorduses the read guard (mcp rebuilt)update_recordonsys_useranswered a recordA4's first try used a comment as the marker, which the bundler strips, so the preflight refused it and nothing ran; the second used an identifier. A2's first try used an anchor that occurs three times without
--expect 3, so nothing was written and the run was void; it was rerun with--expect 3.Acceptance notes
sys_userfield is visible". The existing gates serve the console set above (15 fields). Narrowing the agent to id/name/email needs a new agent-only mask that the console does not use. This PR builds the console reading and contains no such mask; the seat has put the question to the maintainer (6867361507). If the answer is id/name/email only, this comes back as a fork.sys_useronmain(A1 is that state, measured); after this PR they serve it. Widening only: no tool, schema, export or answer shape narrows.enable.aiReadablealready exists (spec spec: a per-object, read-only AI exposure declaration so an agent can read a declared system object through the MCP record tools (#32442 read half) #32865); this declares a value on a platform object. No authorable key, value or type is added, narrowed or changed in meaning.ui/apps/console/src/pages/developer/objectstack-skill.ts(held byte-identical toskill-md.tsbyskill-md.test.ts) andpackages/qa/dogfood/test/approval-read-over-mcp.dogfood.test.ts(its pin listed thesys_*set this PR grows).aiReadExposedwith the sameisAiReadExposed, so a stdio host admitssys_usertoo; the row and field gates there are the sibling card's (mcp: stdio describe_object and the objects resource serve readonly: false for a stored expression field whose expression the caller's field mask hides, while the HTTP door answers true #33156 / MCP stdio get_record answers a missing or hidden id with uncoded error text, while the HTTP door answers RECORD_NOT_FOUND 404 #33030) scope and are not pinned here.skills/**(Tier H). The served MCP skill isskill-md.ts, which carries the ruling-6867150851 sentence.!in the title, no(narrowing)arm); the two MCP dogfood files that read this surface ran, 20/20.Verification
Local, on head
1e1c1561:pnpm --filter @objectstack/platform-objects test122 files / 1957 tests passed;pnpm --filter @objectstack/mcp test(after mergingmain) 64 / 1141 passed.pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 test/colleague-lookup-over-mcp.dogfood.test.ts test/approval-read-over-mcp.dogfood.test.ts2 files / 20 tests passed (after mergingmain).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, 100 commands, each with stdin closed) andpnpm lint: all exit 0. Three exited 3 (PREREQUISITE NOT MET, nothing measured) on the first pass and exit 0 on a rerun after the precondition was met:check:dual-build-cjs-loadsandcheck:skill-examples(packages withoutdist/, built),check-plugin-teardown-shape --self-test(its pinned fixture commit, fetched into this shallow clone).--ranreconciliation: 100 derived, 100 run, 0 NOT-MEASURED (a derived zero: every line carries its exit code);pnpm lintrecorded outside the derivation.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed. (not touched)content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code