Skip to content

18.3 lint: clm_admin's modifyAllRecords on clm_contract makes its 17 field edit locks inert — the locks are declared but bind no admin #196

Closed

Description

@objectstack-fleet

Ruled: 6617227919 · letter A · 2026-11-03T22:34:37Z (the locks win: clm_admin's clm_contract entry drops modifyAllRecords for viewAllRecords + writeScope: 'org' + allowTransfer)

repo:hotclm#1 · zhuangjianguo · session_01QsR2jUw8KJgAwuwk6sqYLD

Filing gate: ② declared≠enforced, surfaced by a new @objectstack 18.3.0 lint rule (security-superuser-field-mask-inert) during the hotclm 18.3 upgrade; pnpm lint exits 0 (warnings), so it did not block the upgrade.

Reading. pnpm validate / pnpm lint on 18.3.0 report 17 warnings of the form: permission set "clm_admin": modifyAllRecords on objects.clm_contract votes on every field of "clm_contract", so the edit lock on 'clm_contract.' binds none of this set's holders. Fields: submitted_at, review_started_at, approved_at, signed_at, executed_at, activated_at, closed_at, archived_at, approval_status, route_legal_head, route_finance, route_executive, route_gm, ai_summary, ai_risk_score, ai_risk_rationale, ai_reviewed_at. On 18.2.0 the same tree linted with 0 warnings.

Why it matters. The clm_admin permission set (src/profiles/, built with the _grants.ts helpers) locks those fields with editable: false (the lifecycle stamps and routing flags are written only by flows and hooks, DESIGN.md §03/§04). The platform says the lock does nothing for a set holding modifyAllRecords, so a CLM administrator can overwrite approved_at or route_finance through the API. Either the locks are meant (then modifyAllRecords has to give way to a write scope or row policy) or they are not (then they should be deleted so the declaration matches what runs).

Needs a ruling on which side wins (DESIGN.md §04 clm_admin row) — the seat's call whether it goes to the decision box.

Platform version: 18.3.0. Rule: os explain security-superuser-field-mask-inert.

Activity

  1. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    To the decision box — 2026-11-03

    The seat's call: this is a §04 question (the clm_admin row), so it goes to the maintainer. On 18.3.0 the platform says clm_admin's modifyAllRecords on clm_contract makes 17 field locks inert, so an administrator can overwrite lifecycle stamps and routing flags over the API. This was already true on 18.2; 18.3 only reports it. Either the locks are meant, and the admin's modifyAllRecords gives way, or they are not, and the locks go. State: → needs-user-decision.


    Generated by Claude Code

  2. zhuangjianguo commented on Nov 3, 2026

    @zhuangjianguo
    Collaborator

    Ruling: batch #12 item 2 · letter A · maintainer 「同意」 2026-11-03T22:34:37Z

    裁定:A —— 字段锁赢,clm_admin 在 clm_contract 上放弃 modifyAllRecords

    决策箱文档:https://claude.ai/artifact/VTMgyRUKZu1gQChSijSnXD 。前提:objectstack#29106 裁 A(ADR-0066 2026-11-02 注)——持超级用户位的权限集对该对象每个字段投「可编辑」票,「要藏字段就别给那两位」;permission.zod.ts 明确 writeScope 放在 viewAllRecords 旁边会被读取并生效,'org' 是原生深度。§04「route_* · approval_status · 阶段时间戳 · ai_*:所有岗位只读」一字不改。

    裁后执行(lane 一个 PR,src/profiles/admin.profile.ts)

    1. clm_contract 条目改为 { allowCreate, allowRead, allowEdit, allowDelete: true, viewAllRecords: true, writeScope: 'org', allowTransfer: true },去掉 modifyAllRecords;readOnly(Contract, CONTRACT_STAMPED_FIELDS) 保留。六个子对象条目不动(它们没有锁)。
    2. 改写文件头注释:「超级用户位不绕过 FLS」已不成立(ADR-0066 11-02 注),改为「锁要生效就不能带 modifyAll;全行可写靠 writeScope 'org'」。
    3. 实测(18.3.0,持 clm_admin 的管理员):读他人合同 200、PATCH 普通字段 200、PATCH approved_at / route_finance 403「Field write denied」、改派 owner_id 200、删除 200、子记录写入 200;pnpm lint 的 security-superuser-field-mask-inert 从 17 条归零。
    4. 若 viewAllRecords + writeScope 的组合被某道门拒绝,退而用 readScope: 'org' + writeScope: 'org'(两者等价于本裁),在 PR 里说明读数。

    ⛔ 不取 B(删锁、§04 加例外)、C(向平台要否决层)。⛔ 本席不动标签。


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 19
    Ruling-ref: 6617227919
    Session: session_017nzWFRUo2U7evMkPdVniAn
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-196-admin-field-locks
    Worktree: hotclm-issue-196
    Domain: repo:hotclm (single-lane sister repo — no domain:*)
    Seat: repo:hotclm#1
    File surface: src/profiles/admin.profile.ts (the clm_contract entry and the header comment) — stop on breach; explain in the report
    Container & model: S, mode:subagent, model: opus (TIER_DEFAULT; hand-derived for a sister repo), effort: medium
    Clause-②: no
    Metadata-protocol: none
    Responsibility: this app's admin field locks on clm_contract (stamped fields read-only for every position) | objectstack-ai/objectstack#29106 ruling A: a super-user bit votes edit on every field | the CLM admin
    Thread-read: none
    Premise: re-measured by the dev on origin/main dfa5d67 (@objectstack 18.3.0) with the declared demo staff
    Serial constraints cleared: batch 12 ruled 2026-11-03T22:34:37Z. Dispatched together with #64, which edits the clm_obligation entries of the same profile; the second PR to land rebases.


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 196,
      "status": "done",
      "branch": "claude/issue-196-admin-field-locks",
      "pr": "https://github.com/objectstack-ai/hotclm/pull/204",
      "session": "session_017nzWFRUo2U7evMkPdVniAn",
      "premise_still_valid": true,
      "summary": "Implemented ruling 6617227919 (A) in src/profiles/admin.profile.ts only. The clm_contract entry is now { allowCreate, allowRead, allowEdit, allowDelete: true, viewAllRecords: true, writeScope: 'org', allowTransfer: true } with no modifyAllRecords; readOnly(Contract, CONTRACT_STAMPED_FIELDS) is kept and the six child entries are unchanged. The header comment is rewritten: a lock binds only without modifyAll (ADR-0066 2026-11-02 note), viewAllRecords never votes an edit, whole-row write comes from writeScope 'org', and owner reassign comes from allowTransfer. writeScope semantics were confirmed in permission.zod.ts at @objectstack/spec@18.3.0: 'writeScope beside viewAllRecords: true — READ AND HONOURED'. No gate refused the pair, so the readScope+writeScope fallback was not used. Premise note: before the change, PATCH approved_at/route_finance as clm.admin answered 200 but the stored value did not move (silently dropped on 18.3). After the change it is a 403 'Field write denied'. main has not moved since b38df5a and #64 has not landed, so no rebase was needed.",
      "tests": "18.3.0, pnpm demo:zh on a throwaway DB, port 3169, as clm.admin@example.com, on another user's active contract. BEFORE (origin/main b38df5a): GET 200; PATCH jurisdiction 200; PATCH approved_at 200 with the value unchanged (2026-07-24 stays); PATCH route_finance 200 with the value unchanged; PATCH owner_id 200 and read back; PATCH child clm_review.comments 200; requester1 creates a throwaway draft 201, admin DELETE 200, then GET 404. AFTER (c50654e): GET 200; PATCH jurisdiction 200; PATCH approved_at 403 \"[Security] Field write denied: not permitted to edit [approved_at] on 'clm_contract'\" PERMISSION_DENIED; PATCH route_finance 403 \"[Security] Field write denied: not permitted to edit [route_finance] on 'clm_contract'\" PERMISSION_DENIED; PATCH owner_id 200 and read back as the new owner; PATCH clm_review.comments 200; throwaway DELETE 200, then GET 404. /auth/me/permissions after: clm_contract modifyAllRecords false, writeScope org, allowTransfer true; fields approved_at {readable true, editable false}, owner_id editable true. Browser (Chromium, zh-CN, clm.admin): on another user's active contract ORD-2026-0015, 编辑 → change 管辖 → 更新 → toast 合同“订单 — 黑貂保险”已更新, the new value on the page, API read-back with updated_by = clm.admin and owner unchanged. The edit form shows no stamped fields and no 业务承办人: objectui plugin-form autoLayout drops owner_id for every user, independent of permissions, so this is not a regression. Nothing broke. Ablation: n/a, a metadata-only permission change with no test file.",
      "gates": "Head c50654e, each exit code captured before any pipe: pnpm install exit 0; pnpm validate exit 0 (1 warning, obligation_requester_own = #64); pnpm lint exit 0 '1 warning(s), 6 suggestion(s)', security-superuser-field-mask-inert count 0 (baseline on b38df5a: '18 warning(s), 6 suggestion(s)', 17 of the rule); pnpm typecheck exit 0; pnpm lint:i18n-gate exit 0 'COVERAGE : 0 missing keys across 2 locale(s)'; CLM_DEMO_SEED=1 pnpm validate exit 0. hotclm has no dispatch-gates derivation; the list is the repo's own (AGENTS.md / common rules). PR check-runs: not awaited (in_progress at report time).",
      "dogfood_battery": "n/a — hotclm sister repo, no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0",
      "api_writes": "2 relay strokes: (1) fleet-write dispatch pr_create → POST /repos/objectstack-ai/hotclm/pulls + POST /repos/objectstack-ai/hotclm/issues/204/assignees (run 41736361873 success, body read back identical, 6188 bytes); (2) post-stamped comment POST /repos/objectstack-ai/hotclm/issues/196/comments (this report). Plus git push of the branch (not REST).",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: observation (not filed) · reach: PATCH /api/v1/data/clm_contract/ID as clm.admin on b38df5a · a write naming approved_at or route_finance answered 200 and left the stored value unchanged, i.e. silently dropped. With the lock binding it is now a 403. Noted in the PR's readings; carrier: none",
        "class: observation (not filed) · src/profiles/admin.profile.ts rowLevelSecurity contract_admin_edit_any_status inline comment ('Modify All Data does not skip them on this object') is outside this card's surface. It is still true, but the admin no longer holds Modify All on clm_contract. Suggested wording: 'policies OR-merge across the sets a person holds, and neither writeScope nor Modify All Data skips them on this object.' carrier: none (Acceptance notes)",
        "class: observation (not filed) · openAllExcept(Signature, []) and open(Review, ['internal_note']) in admin.profile.ts repeat what the children's modifyAllRecords already votes since ADR-0066's 2026-11-02 note. Harmless; carrier: none",
        "class: NOT MEASURED · GET /api/v1/data/clm_payment_plan as clm.admin answered total 0 about 2-3 min into the baseline demo boot. Seeding was still in the background and its result line never printed, while src/data/plan-children.ts and post-signature.seed.ts declare payment instalments. Not investigated (out of scope). Dedupe words: payment_plan seed, demo instalments, clm_payment_plan empty"
      ],
      "governed_text_proposal": "none. DESIGN.md §04 stays as worded (ruling: 一字不改), and no AGENTS.md/CLAUDE.md change is needed."
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Nov 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling-ref: 6617227919

    Landed — PR #204 · 2026-11-03

    reading value
    PR #204 MERGED (squash on main), merged at its ACCEPT head c50654e; Fixes #196 closes this card
    authority batch 12 item 2, letter A, maintainer 「同意」

    Release: an administrator still reads, edits, reassigns and deletes every contract, but the stamped fields (routing flags, stage timestamps, AI fields) now refuse a hand edit with 403 instead of silently dropping it. Lint on main goes from 18 warnings to 1 (#64's, in flight). State: pm:dispatched removed, assignee cleared.


    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

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions