Repository navigation
fix(platform-objects): withhold the sign-in phone fields from org peers in the user directory - #27431
Conversation
…rs in the user directory phone_number and phone_number_verified move into the sys_user Admin field group, which the shipped non-admin permission sets mark readable: false and the admin sets keep. An org member reading the user directory is served a colleague's name and email, no longer their sign-in phone number. Claude-Session: https://claude.ai/code/session_017xRBcUoS2G4jgqRskpExS7 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xRBcUoS2G4jgqRskpExS7 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xRBcUoS2G4jgqRskpExS7 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check1 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 18 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 251ffe279303a17ee452f01e49ea9c2ae339cd9c && git checkout 251ffe279303a17ee452f01e49ea9c2ae339cd9c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a552e8699255db754446256aabac02c81ebd73a8 a77f981d835a424b30360744bb07b49973d84fe8 && git checkout -B drift-repro a552e8699255db754446256aabac02c81ebd73a8 && git merge --no-ff a77f981d835a424b30360744bb07b49973d84fe8
node scripts/docs-audit/affected-docs.mjs --json a552e8699255db754446256aabac02c81ebd73a8 |
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk Ruled: maintainer, in the PM session session_017xRBcVy4VXmkQjn2MGeaWk, 2026-10-29 · 2026-10-29T13:06Z Question (open decision in the PR body): accept that, because field-level read is row-blind (ADR-0066), a member also loses their OWN phone fields on the generic data API. Answer, verbatim option chosen: 「接受」 — ship as is; the own number stays reachable through the auth session. A row-aware read vote is not pursued. Generated by Claude Code |
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk Contract reviewServed-tier: Face: narrowing. The diff touches no Security-hardening ruling: this record names positions and refusals; it carries no reproduction. ① Derived judgmentsThe accept set and the served surface the diff changes, read off the code on this head:
② Semver levelDeclared: The arm is missing. A query shape every non-admin member could write and have evaluated on
Two consequences of the bare What is right and stays: ③ Boundary flags
④ Design judgmentOn the narrowing face, against the published spec on
Must-fix
Should-fix
Implemented-by: VERDICT: FAIL The withholding is complete at every position the PR claims, the own-row cost is ruled, and the mechanism is the settled one; the FAIL is the declaration alone (must-fix 1). A head that carries the corrected declaration and nothing else owes a delta review by this reviewer, not a full round. |
…arrowing Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk Co-authored-by: Claude <noreply@anthropic.com>
…ne narrowing Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk Co-authored-by: Claude <noreply@anthropic.com>
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk {
"issue": null,
"status": "done",
"branch": "claude/harden-user-directory-fields",
"pr": "https://github.com/objectstack-ai/objectstack/pull/27431",
"session": "session_017xRBcVy4VXmkQjn2MGeaWk",
"premise_still_valid": true,
"summary": "Rework of contract review 6501154665 (FAIL on the declaration alone). Changeset now declares `Clause-②: no (narrowing)`, keeps `Metadata-protocol: none` and `patch`, carries the `**BREAKING** (an accept-set narrowing)` banner with a FROM / TO that names every narrowed position (row withheld incl. own row; filter, sort, group-by, aggregate refused 403 PERMISSION_DENIED; default `$search` set narrowed silently; writes unchanged) and exactly one ADR-0087 marker `not-required (no-migration-prescription)`. Should-fix: three dogfood cases at the HTTP door (member `$search` by colleague name -> 200 with colleague; by colleague synthetic phone -> 200, no row; platform admin same phone search -> colleague, the control). Full replacement PR body staged, not PATCHed: /tmp/claude-0/-home-user-objectstack/bdda939c-7424-5594-af38-0e6fe5d7eeed/scratchpad/pr-27431/pr-body.md (declaration lines + `Skills-docs: none — REASON`, 维护者速读 kept with 需要你做的 = 无,席位落地 citing ruling 6497499302 「接受」, verification readings on a77f981d). The PM's `Lint & Repo Gates` red on 99bfe6d4 was the lint lanes concluding `cancelled` (run 40904302735 conclusion cancelled) because the push of a77f981d superseded that run; no fix was needed, and on a77f981d the same check is success. Report comment: post-stamped requires the lane/account/session provenance line as the first non-empty line, so `os-dev-report` sits alone on the line right after it (a conflict between the dev contract's first-line rule and the write tool, named here, not chosen silently).",
"tests": "Head a77f981d. Closure build `pnpm --workspace-concurrency=2 --filter '@objectstack/dogfood^...' build` lock VERDICT command-exit 0. `pnpm --filter @objectstack/dogfood exec vitest run test/identity-admin-fields-org-peer.dogfood.test.ts`: Test Files 1 passed, Tests 18 passed (was 15), VERDICT command-exit 0. `pnpm --filter @objectstack/dogfood typecheck` exit 0. platform-objects / plugin-security sources unchanged this round (previous round's suites on a195fe983 stand; CI Test Core green on a77f981d). Ablation (one-shot, not kept): ablation-replace on phone_number `group: 'Admin'` -> marker group, anchor 1->0, blob 67491e481b64->c9c848e94377, platform-objects rebuilt, preflight marker present in 4 dist files; file 2 failed / 16 passed (member projection read; member phone search found the colleague); restore leg blob == HEAD 67491e481b64, git diff HEAD empty, whole-tree status clean, rebuilt, preflight --absent over 66 files, file 18/18.",
"gates": "Local: `pnpm check:adr-0087-registration` exit 0 (1 declared-breaking changeset, [BREAKING+clause-②-narrowing] not-required (no-migration-prescription); self-test 705). `node scripts/check-changeset-no-major.mjs` exit 0 (no major; level axis NOT APPLICABLE locally). `--event` from the staged body: exit 0, LEVEL AXIS declares `no (narrowing)` / `none`, patch; control over the old body reads `direction arm: none declared`. dispatch-gates `--repo objectstack-ai/objectstack --commands` on a77f981d: 65 commands, 64 exit 0, 1 exit 3; `--ran`: 65 derived, 64 run, 1 NOT-MEASURED, 0 UNRUN. NOT MEASURED: check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (7 packages outside the closure have no dist/); CI carries it in Lint & Repo Gates (success). check-nul-bytes exit 0. CI check-runs on a77f981d: every non-skipped run success (Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard, Test UI, Check Changeset, Dogfood Regression Gate, PR-body guards, Spec property liveness and all lint/type-check lanes); Lint UI, Type Check UI, Build & E2E UI, Live E2E UI and the Dogfood shards skipped by selector. Check Changeset will re-read the body on the seat's `edited` write.",
"dogfood_battery": "a77f981d · os-verify-lock VERDICT command-exit 0 · Test Files 348 passed / 3 skipped (351) · Tests 2812 passed / 20 skipped (2832) · no failures",
"mcp_calls": "0",
"api_writes": "1 — POST /repos/objectstack-ai/objectstack/issues/27431/comments (this os-dev-report, via scripts/pm/post-stamped.mjs); plus 3 git pushes to claude/harden-user-directory-fields (99bfe6d4, a77f981d; not REST). PR body not PATCHed (staged).",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none · noted, not filed: `check-changeset-no-major.mjs` prints `direction arm: narrowing — a BREAKING change; during the launch window it ships minor` on a passing patch-level narrowing, which reads against ruling A (a non-protocol narrowing ships patch); prose only, the verdict is right. Dedupe words: changeset-no-major direction arm launch window minor wording"
]
} |
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk Contract reviewServed-tier: Face: narrowing, now declared. Delta review of record 6501154665 (FAIL on the declaration alone, head Security-hardening ruling: this record names positions and refusals; it carries no reproduction. ① Derived judgments
② Semver levelDeclared on the changeset and the PR body, both line-initial: ③ Boundary flags
④ Design judgmentUnchanged from 6501154665 on the narrowing itself: the change takes no declared capability away at the consumer and makes an existing posture (ADR-0066 ⑧, the #21237 Implemented-by: VERDICT: PASS Delta review complete: the declaration is corrected in the #22646 shape, the |
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk ACCEPT → head 维护者速读
席位意见:收。在本 head 上,增量契约复核 PASS(6502933103,判的是 6501154665 之后的增量);dogfood 全套 348 个文件、0 失败。 席位核对
Generated by Claude Code |
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no file under
skills/**teaches thesys_userphone fields or the user directory's field security (git grep -i -c 'phone_number\|phoneNumber' -- 'skills/**': zero hits, exit 1; controlgit grep -c sys_user -- 'skills/**': hits), so no skill text describes the served set this narrows.维护者速读
sys_user.phone_number/phone_number_verified从Account字段组移入Admin字段组;普通成员读用户目录时不再拿到其他用户的登录手机号。Admin组已由内置权限集做字段级只读屏蔽(成员屏蔽、管理员保留),沿用同一机制,不新增规则。patch发布):成员按这两个字段过滤、排序、分组、聚合会被 403 拒绝,$search静默不再扫手机号列,成员读自己那行也看不到手机号(本人手机号从登录会话读取);回滚 = 把两个字段的 group 改回Account。packages/platform-objects/src/identity/sys-user.object.ts(两个字段的group);.changeset/user-directory-sign-in-phone-fields.md(BREAKING 横幅与 FROM/TO);packages/qa/dogfood/test/identity-admin-fields-org-peer.dogfood.test.ts(成员按姓名搜同事命中、按手机号搜无结果)。pnpm --filter @objectstack/plugin-security exec vitest run src/identity-admin-field-group.test.ts,预期 19 passed。What changes
The user directory no longer returns another user's sign-in phone number to non-admin readers. Security hardening, declared as an accept-set narrowing.
sys_user.phone_numberandsys_user.phone_number_verified(the better-auth phone sign-in identifier and its verified flag) move from theAccountfield group to theAdminfield group.Admingroup is already the declaration the shipped permission sets read (identityAdminFieldSecurityinplugin-security'sdefault-permission-sets.ts):member_default,viewer_readonlyandexternal_defaultmark every group fieldreadable: false, editable: false;admin_full_access,organization_adminand its no-bypass variant keep it. No new rule, list or field attribute: the server's existing field-level security (field masker, predicate guard, activity redaction, metadata-plane masking) does the work, so the Console needs no change.**BREAKING** (an accept-set narrowing)banner):sys_userrow served, the member's own row included (field-level read is row-blind, ADR-0066);403 PERMISSION_DENIED;$search: the default search set is narrowed silently, so a user picker keeps answering onnameandemailand a search for a colleague's number returns no row.banned/ban_reason/ban_expires: reviewed, no change.bannedstays served to org peers by an earlier ruling (directory status every user picker filters on);ban_reasonandban_expiresare already in theAdmingroup and already withheld.@objectstack/platform-objectspatch(a non-protocol narrowing shipspatchand keeps its BREAKING banner); exactly one ADR-0087 disposition,not-required (no-migration-prescription): no metadata key, spelling, export or stored shape moves.Acceptance notes
readablestays row-blind;whenbesidereadable: falseis refused at parse). So a member's OWN row on the generic data API is withheld the phone fields too, exactly as for every otherAdmin-group field. The maintainer ruled this accepted in comment 6497499302 (「接受」): ship as is, the own number stays reachable through the auth session, no row-aware read vote. better-auth's phone-number plugin declaresphoneNumber/phoneNumberVerifiedwithreturned: true, so the session user carries them when the plugin is enabled.readable: true(field grants merge most-permissively).(narrowing)arm, the BREAKING banner with FROM / TO and the ADR-0087 marker to the changeset, theSkills-docs:line here, and the should-fix$searchcases at the HTTP door. No package source changed in this round. Rework run dispatched from PM session session_017xRBcVy4VXmkQjn2MGeaWk.Tests
packages/plugins/plugin-security/src/identity-admin-field-group.test.ts: the two phone fields are declared inside the group; a member (member_default) is withheld both and the field masker strips them from a colleague row; each admin set keeps both and the masker serves them.packages/qa/dogfood/test/identity-admin-fields-org-peer.dogfood.test.ts(real showcase boot, HTTP door): the colleague fixture carries a synthetic phone; a member's directory read projecting the phone fields is served the colleague's id, name and email without them; the org owner and the platform admin read the stored values. New this round: a member$searchonsys_userby the colleague's name is served200with the colleague listed; a member$searchby the colleague's synthetic number is served200with no row; the platform admin's$searchby the same number lists the colleague (the control that the term matches when the column is scanned). The existing generic group pins (member reads colleague, list door, activity door, member own row, filter refusal) cover the phone fields too.Verification
All readings below were taken on head
a77f981d(the final head;git rev-parse --short HEAD=a77f981d). No package source changed in this round; the package-suite readings of the previous round ona195fe983(unchanged sources) stand forplatform-objectsandplugin-security.Declaration gates (this round's must-fix)
pnpm check:adr-0087-registration: exit 0.✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.—.changeset/user-directory-sign-in-phone-fields.md [BREAKING+clause-②-narrowing] not-required (no-migration-prescription). Self-test 705 assertions.node scripts/check-changeset-no-major.mjs(local, no event): exit 0, nomajor; level axis NOT APPLICABLE (no payload, by construction).node scripts/check-changeset-no-major.mjs --event EVENTwith apull_requestpayload built from this staged body: exit 0.✓ LEVEL AXIS: this PR declares clause-② no (narrowing) and metadata-protocol none ... a non-protocol change at patch. Control: the same run over a payload built from the previous body readsdirection arm: none declared— the arm is read from the body, not assumed.Dogfood (the narrowing owes the battery)
pnpm --workspace-concurrency=2 --filter '@objectstack/dogfood^...' build: lock VERDICT command-exit 0.identity-admin-fields-org-peer.dogfood.test.ts: Test Files 1 passed, Tests 18 passed (15 before this round, plus the three$searchcases). Lock VERDICT command-exit 0.pnpm --filter @objectstack/dogfood typecheck: exit 0.pnpm --filter @objectstack/dogfood test(no shard, no path) ona77f981d: Test Files 348 passed, 3 skipped (351); Tests 2812 passed, 20 skipped (2832). Lock VERDICT command-exit 0. The CIDogfood Regression Gateshards were skipped by their selector on this head, so this local run is the battery's evidence.Ablation of the new search pins (one-shot, not kept)
Mutation via
scripts/ablation-replace.mjs:phone_number'sgroup: 'Admin'replaced with a marker group (anchor 1 → 0, blob67491e481b64→c9c848e94377),platform-objectsrebuilt, marker present in 4 built files perablation-dist-preflight.mjs.67491e481b64),git diff HEADempty, whole-treegit status --porcelainclean, rebuilt, marker absent from all 66 built files (--absent); file green again, 18/18.Derived gate families
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsona77f981d: 65 commands; 64 exit 0, 1 exit 3.--ranreconciliation: 65 derived, 64 run, 1 NOT-MEASURED, 0 UNRUN. NOT MEASURED:check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (seven packages outside this diff's closure have nodist/in this worktree); CI carries it inLint & Repo Gates(success on this head).node scripts/check-nul-bytes.mjsexit 0. Repo-wide scans (pnpm lint,test:repo) are left to CI.CI on `a77f981d`
Every non-skipped check-run concluded
success, the ten required contexts among them:Lint & Repo Gates,TypeScript Type Check,Test Core,Build Core,Governed Surface Queue Guard,Test UI;Lint UI,Type Check UI,Build & E2E UIandLive E2E UIwere skipped by their path filter. The earlierLint & Repo Gatesred on99bfe6d4was its lint lanes concludingcancelledwhen the push ofa77f981dsuperseded that run (run 40904302735 concludedcancelled), not a gate verdict.Generated by Claude Code
Body refreshed by the PM seat 2026-10-29T16:49Z from os-dev report 6502824437.
🤖 Generated with Claude Code
https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk