Repository navigation
docs(skills,docs): teach "the person is a record", a membership key with valueField 'id' - #31266
Conversation
…ith valueField 'id' The data skill's security rule and the RLS guide now teach a membership key whose valueField is the roster's own `id`: an `employee` roster's `my_employee_ids` read by a `leave_request` policy as `employee in current_user.my_employee_ids`. They say it grants nothing by itself and that a policy testing it against a column not referencing the roster, or another object's `id`, is refused (rls-predicate-membership-key-target). The skill's false "nor `id`" clause on valueField is corrected, as is the same claim in an unreleased changeset. A new eval case exercises it. The security rule pays with three Source pointer lines (paths kept in references/_index.md) and its ratchet row rises by the measured remainder under the standing ruling. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8VVAmphavoevUax9bUVkY
|
domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7 Contract reviewServed-tier: ① Derived judgmentsInputs: card #31176 (body; triage 6710238236; claim 6712646214), blocker #31129 with ruling A (6708428544), PR #31174's merged diff, the PR body (the live body equals /mnt/project-files/card-31176/body-v1.md apart from one trailing blank line), the order at /mnt/project-files/skills-seat3/order-31176.md, the gate record gates-b9f4ec22.txt (87 commands, 86 exit 0,
② Semver level
③ Boundary flags
④ Design judgment
Verdict: PASS |
- security.md: the refusal code on valueField no longer reads as the same-column refusal (the schema refuses that case itself); same bytes. - eval: the prompt names the key the assertions require, and the must_not_contain matches only an owner-scoped policy, not a mention. - rls.mdx: leave_request is public_read so the policy is what scopes its rows, with the reason; the valueField sentence reads cleanly. - changeset: the platform change ships in the same release. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8VVAmphavoevUax9bUVkY
|
domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7 Contract reviewServed-tier: ① Derived judgments
② Semver levelUnchanged: ③ Boundary flagsThe 4 files are all inside the claim and the seat's rulings. Nothing new is added. The ratchet row stays at 3768, equal to the measurement. ④ Design judgmentThe delta does only what the first record asked for. N1–N3 stay as they are. Verdict: PASS |
|
domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7 维护者速读
席位核对 — ACCEPT · domain:skills#3 · head
|
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8VVAmphavoevUax9bUVkY
|
domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7 Contract reviewServed-tier: ① Derived judgments
② Semver levelUnchanged: ③ Boundary flagsThe merge adds no path and no new claim. The ratchet row stays at 3768, equal to the measurement. ④ Design judgmentThe merge does only what the conflict needed. The previous record's findings stand. Implemented-by: VERDICT: PASS |
|
domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7 席位核对 — 增量 ACCEPT · domain:skills#3 · head
|
Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J8VVAmphavoevUax9bUVkY
|
domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7 Contract-review-claim: session_01J8VVAj7CvjkGXvgXzd6RZ7 · head 1d88c9b · 2026-11-08T07:53Z Delta contract review of PR #31266 (#31176) from |
|
domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7 Contract reviewServed-tier: ① Derived judgments
② Semver levelUnchanged: ③ Boundary flagsThe merge adds no path and no new claim. The ratchet row stays at 3768, equal to the measurement. Main's eight new ratchet rows are main's own, untouched by this PR. ④ Design judgmentThe merge does only what the conflict needed. The previous records' findings stand. Nit (not blocking): the body's "What it teaches" table still says "eval 12" in the roster/policy row. It is now case 17. The same stale label survived v3. Gates: CI check-runs on Implemented-by: VERDICT: PASS |
|
domain:skills#3 · huangyiirene · session_01J8VVAj7CvjkGXvgXzd6RZ7 席位核对 — 增量 ACCEPT · domain:skills#3 · head
|
Requested by Irene · project thread
Fixes #31176
Clause-②: no
Metadata-protocol: none
A
skills/**diff ships in@objectstack/skills, so this PR adds.changeset/data-skill-person-is-a-record.md('@objectstack/skills': patch, both declaration lines), notskip-changeset.dispatch-gatesmandates the contract-review model tier forskills/**(clause ①); that mandate is waived by the maintainer's standing order in this project (2026-11-03), and this card ran on the project's default model. The dispatching seat (domain:skills#3) runs the contract review. The docs pagecontent/docs/permissions/rls.mdxis in scope by the card itself (triage 6710238236 folded it into thedomain:skillslane).维护者速读
rules/security.md的成员键一节、它的 eval(新增 case 17)和 RLS 指南rls.mdx教会「人即记录」:employee名册声明my_employee_ids: { userField: 'user', valueField: 'id' },leave_request的策略写employee in current_user.my_employee_ids;并纠正技能里「valueField不能是id」的旧说法(PR feat(spec,lint,plugin-security): a membership key may yield the caller's own roster rows (valueField: 'id') #31174 后已不成立)。id, so an employee, agent or customer roster cannot give its usercurrent_user.my_employee_ids#31129 裁定 A 已在 PR feat(spec,lint,plugin-security): a membership key may yield the caller's own roster rows (valueField: 'id') #31174 落地,但技能和指南仍说id被拒,AI 作者会继续给每行加owner_id或建联接表。skills/objectstack-data/rules/security.md§ Membership keys 的 diff(策略块两行注释 + 一条 policy、valueField行、新 bullet、Choose-by 改写)。node scripts/check-skills-token-ratchet.mjs(退出 0,security.md 3768 / 3768);pnpm --filter @objectstack/spec run check:skill-examples(需先构建,见下方门禁)。check-governed-merges判为 GOVERNED,Tier H(skills/**×2):需要os-zhuang或hotlong的批准(或亲手合并),席位契约复审 ACCEPT 后再由席位落地。What it teaches, and where each sentence comes from
mainvalueFieldis a reference column, orid(security.md § Membership keys; rls.mdx)packages/spec/src/security/rls.zod.tsL740-L741 ("F is a reference column on X … orid, the roster row's own primary key (#31129 ruling A)");packages/lint/src/validate-membership-key-declarations.tsL245isRosterOwnId; explanationmembership-key-value-field-invalid(packages/lint/src/rule-explanations.tsL1783-L1794: "F may also beid, X's own primary key")userField(stated beside the code, not attributed to it)valueFieldnaming the same column asuserField" — refused by the schema's own superRefine, somembership-key-value-field-invalidnever fires for it; rule-explanations L1793 "F may not be U's column: the schema refuses that"employee,my_employee_ids: { userField: 'user', valueField: 'id' }, policyemployee in current_user.my_employee_ids(security.md policy block; rls.mdx os:check block; eval 12).changeset/31129-membership-key-own-rows.mdexampleidthe set is the ids of the caller's own roster rowsidthe set is the caller's own rows of X"; rule-explanations L1789-L1790id, the policy is refused (rls-predicate-membership-key-target)packages/lint/src/validate-rls-predicate-enforceability.tsL1868-L1872 (anidkey's target is the declaring object), L1486-L1488 (idof another object faults), L1492-L1496 (non-reference / other-reference faults), L1667severity: 'error'; rule-explanations L1830-L1834id(rls.mdx)The diff, file by file
skills/objectstack-data/rules/security.md§ Membership keys: a two-line variant in the existing policy block (comment naming theemployeeroster and its key, then anown_leavepolicy onleave_request); the policy block's comment says "in a permission set" (it now holds a non-portal policy); thevalueFieldline says "orid(membership-key-value-field-invalid; never the same column)"; one new bullet on what anidkey holds and does not do; the Choose-by bullet re-scoped. The os:check object block above is byte-identical.valueField"(never the same column, norid:membership-key-value-field-invalid)" was false after PR feat(spec,lint,plugin-security): a membership key may yield the caller's own roster rows (valueField: 'id') #31174; the "norid" is gone.skills/objectstack-data/evals/hooks-security-seeds-datasources.json: case 17 (below). Swept the file: no case rewards the old "norid" claim (case 6 is the account roster, unchanged).content/docs/permissions/rls.mdx§ Context variables: thevalueFieldsentence names "a reference column, orid"; a paragraph When the person is a record, anos:checkblock (employeeroster withmy_employee_ids,leave_request(public_read) with anemployeelookup, anemployee_self_servicepermission set withown_leave), and a paragraph on what the key compares with, the refusal code and "grants nothing by itself", and whyleave_requestispublic_read: RLS only narrows what the org-wide default already allowed (the same page, § How policies compose), so underprivatethe policy would narrow the employee's own rows and a request someone else filed for them would stay hidden. Pickedpublic_read(review S3) so the policy is what scopes the rows..changeset/data-skill-person-is-a-record.md: new,'@objectstack/skills': patch..changeset/data-skill-optional-master.md(deliberate touch, seat ruling): this unreleased note from this lane's PR docs(skills): teach controlled_by_parent with an optional master in the data skill, permission docs and ADR-0055 #31092 said "the membership-key notes say avalueFieldcannot be the roster's ownid(membership-key-value-field-invalid)"; that clause is deleted so the next release notes do not print both claims. Nothing else in the file changes.scripts/check-skills-token-ratchet.mjs: security.md's row only, 3698 → 3768 (below).Token budget
skills/objectstack-data/rules/security.md,ceil(bytes / 4):origin/main(bdefd49): 14783 bytes → 3696 tokens, ceiling 3698 (headroom 2).- Source: \node_modules/@objectstack/spec/src/security/permission.zod.ts`(§ RBAC). Fact held: that path only. Home:skills/objectstack-data/references/_index.md` L18 (same path, with its exports).- Source: \node_modules/@objectstack/spec/src/security/rls.zod.ts`.(§ RLS). Fact held: that path only. Home:references/_index.md` L47.- Source: \node_modules/@objectstack/spec/src/data/field.zod.ts`and its blank line (§ Sensitive fields). Fact held: that path only. Homes:references/_index.mdL13,rules/field-types.md` L31.Eval case
hooks-security-seeds-datasources.jsoncase 17:employeerecord has auserlookup to the person who signs in. An employee may read only their ownleave_requestrows (each has anemployeelookup), and we don't want an owner stamp or a junction table for it. Call the setmy_employee_ids."membershipKeys,my_employee_ids,valueField: 'id',employee in current_user.my_employee_idsusing: 'owner_id(matches an owner-scoped policy, not an answer that names owner scoping as the thing to avoid)Other sites
content/docs/releases/v18/18-1.mdxL285 says avalueField"that is not a reference" is refused. It is the v18.1 release history, true when written; left alone (seat ruling).membershipKeysorvalueFieldfor a membership key. The generated reference pages (content/docs/references/security/rls.mdxL174,references/data/object.mdxL461) already say "orid".Gates
Head
ebb6bce9= the card commitb9f4ec22, a merge oforigin/main(42252a5a, reviewed: contract review PASS, no must-fix) and one review delta commit (S1, S2, S3, N4, N6, N7).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived the same 87 commands atb9f4ec22and atebb6bce9, after a fullpnpm build(exit 0). Each ran undertimeout 900withNODE_OPTIONS=--max-old-space-size=8192, exit captured before any pipe. Atebb6bce9: 86 exit 0, 1 exit 1 (check-empty-changeset, the deliberate correction below; it stays red by design until the seat's record confirms it). The same split held atb9f4ec22.origin/mainhas moved since (07559ce); none of its new paths is in this diff, so the branch was not re-merged.Also named by the order:
check:skill-examplesexit 0 andcheck:skill-identifier-livenessexit 0 (both in the list above; liveness Leg 1: 490 citations over 71 files),check:skill-refsexit 0 (not derived, run separately: 9 generated files in sync),check-skills-token-ratchet.mjsexit 0 and--self-testexit 0.Review delta (
42252a5a→ebb6bce9)valueFieldline reads "orid(membership-key-value-field-invalid; never the same column)", so the code is no longer pinned on the same-column case (the schema refuses that itself). Same bytes: 15069 → 3768 tokens, ratchet row unchanged.my_employee_ids;must_not_containis nowusing: 'owner_id, which an answer naming owner scoping only as the thing to avoid does not match.leave_requestispublic_read, with the reason stated (RLS only narrows the org-wide default).valueFieldsentence reads "(valueField: a reference column, orid)". N6: table row cites L1667. N7: the new changeset says the platform change is in this same release.Merge of
origin/main(ebb6bce9→0a8ff459)PR #31201 added eval cases 12 and 13 to the same fixture, so this card's case became case 14 (now 17, below), content unchanged; the merge (plain merge commit, parents
ebb6bce9and85c01247) resolves only that file. At0a8ff459: JSON parses, ids 1–14 unique;check-skills-token-ratchetexit 0 (security.md 3768 / 3768);check:skill-examplesexit 0 (286 examples, after rebuilding spec/client/client-react on merged main);check:skill-identifier-livenessexit 0;check-commit-card-trailersexit 0.Merge of
origin/main(0a8ff459→1d88c9b8)PR #31223 added eval cases 14–16 to the same fixture, so this card's case is now case 17, content unchanged; the merge (plain merge commit, parents
0a8ff459andae816c40) resolves only that file, keeping main's cases 1–16 byte for byte. At1d88c9b8: JSON parses, ids 1–17 unique;check-skills-token-ratchetexit 0 (security.md 3768 / 3768);check:skill-examplesexit 0 (286 examples, after rebuilding spec/client/client-react on merged main);check:skill-identifier-livenessexit 0;check-commit-card-trailersexit 0.DELIBERATE CORRECTION of a pending release note
check-empty-changesetnames.changeset/data-skill-optional-master.md(present on the merge base, changed here). This is not a collision: that note, from this lane's PR #31092, is unreleased and says a thing this PR's teaching makes false. Corrected sentence, before:After:
Nothing else in that file changes. Per the gate, the dispatching seat confirms this by its own record (ACCEPT / same-head contract-review PASS); it is not restored from the base, which would republish the false clause.
Commit trailers:
node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD→ exit 0 before every push.🤖 Generated with Claude Code
https://claude.ai/code/session_01J8VVAmphavoevUax9bUVkY