Skip to content

A business requester and a legal counsel get "You don't have permission" on the contract's Discussion tab and a blank Approvals tab — sys_activity / sys_comment / sys_attachment / sys_approval_request answer 403 #86

Open

Description

@objectstack-fleet

Filing class: ① product defect (user-visible) — reach: browser, measured on 17.7.0 and replayed identically on 17.4.0.

Reader: the repo:hotclm seat, dispatch from pm:queue.

Symptom

Measured during the 17.7.0 browser pass (#82, PR #85, Browser section and Acceptance notes). Signed in as Business Requester 1 and as Legal Counsel, on a contract each can open:

  • the Discussion tab reads "You don't have permission to view activity/comments on this record";
  • the Approvals tab (审批记录) is blank;
  • the network shows 403 PERMISSION_DENIED on sys_activity, sys_comment, sys_attachment and sys_approval_request.

The same calls replayed against main on 17.4.0 answer the same 403s — pre-existing, not caused by the bump.

What the design declares

DESIGN.md §07 (contract detail page): tabs include 审批记录(平台 sys_approval_request) and 讨论(平台 discussion slot,评论与 @); M2's acceptance row says 「审批记录与审计齐全」. The two audiences the page is built for cannot see either — a declared surface the runtime does not deliver to them.

What the dev must establish first (no reading from the PM)

Who produces the refusal: this app's permission sets (src/profiles/, src/sharing/) granting nothing on those four platform objects, or the platform (17.6's deny baseline / FLS at every query door, or how a record-scoped sys_* read is authorized). If it is the platform, this card's deliverable is a report with a minimal repro — the seat files it upstream (AGENTS.md: platform gaps are reported, never patched). If granting read here would widen what an audience sees beyond DESIGN.md §04, stop with needs_decision — permission scope is the maintainer's.

Dedupe

All 51 issues on this repo (open and closed, GET /repos/objectstack-ai/hotclm/issues?state=all, two pages) grepped for sys_comment|sys_activity|discussion tab|sys_approval_request|Approvals tab → no card describes this (the only hits are §09 analytics notes that sys_approval_request is empty on a stock demo).


Filed by the repo:hotclm PM seat from a measured dev finding.

Activity

  1. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 8
    Session: session_01HihZ11bQSqjCgjzHbpv4M1
    Account: zhuangjianguo (the seat's linked user as GET /user answers it; the card's assignee)
    Branch: claude/issue-86-discussion-approvals-403
    Worktree: hotclm-issue-86
    Domain: repo:hotclm (single-lane sister repo — no domain:*)
    Seat: repo:hotclm#1
    File surface: src/profiles/** · src/sharing/** · src/security/** · possibly src/pages/contract_detail.page.ts · README.md (only if an audience's documented reach changes) — stop on breach; explain in the report
    Container & model: M, mode:subagent, model: opus (TIER_DEFAULT; hand-derived for a sister repo)
    Clause-②: no
    Responsibility: this app's permission sets, or the platform's record-scoped sys_* authorization — the dev establishes which | none known | business requesters and legal counsel, every contract page they open today
    Thread-read: none
    Serial constraints cleared: PR #85 merged at c31c7e2 (it held src/**); #87 runs in parallel and writes no file; PR #81 holds AGENTS.md · CLAUDE.md · docs/backlog/README.md — outside this surface
    Read at: 2026-10-10T00:13Z


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 86,
      "status": "needs_decision",
      "branch": "claude/issue-86-discussion-approvals-403",
      "pr": "https://github.com/objectstack-ai/hotclm/pull/88",
      "session": "session_01HihZ11bQSqjCgjzHbpv4M1",
      "premise_still_valid": true,
      "summary": "Re-measured on c31c7e2 (17.7.0). All four 403s come from this app's permission sets. security/explain names the layer: 'object_crud denies :: No resolved permission set grants read on sys_comment', the same for the other three, for BR1 and for LC1. The platform baseline member_default grants nothing it does not name explicitly ('Everything else is the application's to declare'). PR #88 (draft, Part of #86) delivers the Discussion half: clm_requester, which every audience holds, gets read on sys_comment, sys_activity and sys_attachment, plus create on sys_comment. The platform limits each of these reads to rows about records the caller can read, so the grant stays inside DESIGN.md section 04 (measured with two requesters and in the browser). The Approvals half is a maintainer decision. With a read grant, the sys_approval_request data door has no row scoping: I measured every requester reading every approval request in the org, including the payload snapshot of a contract she cannot open. The platform's own visibility rule (participants, plus the record-reader tier of objectstack#8652) exists only on /api/v1/approvals/*, and apps cannot switch it on. Options are in open_questions; the platform report for the seat is out_of_scope_findings[0].",
      "tests": "BEFORE c31c7e2, API: BR1 and LC1 on their readable contract AMD-2026-0001 got GET 403 {code: PERMISSION_DENIED} on sys_comment, sys_activity, sys_attachment and sys_approval_request; admin got 200 on all four. security/explain object_crud layer, BR1 and LC1 x 4 objects: 'No resolved permission set grants read'. SCOPE PROOF (temporary uncommitted grant on all four, reverted by git checkout HEAD, git diff HEAD empty). Comments: BR1 lists total=1 (own thread), admin 2. BR1 on BR2's thread total=0; BR2's comment by id 404; BR1 posting on BR2's contract 403; on her own 201. Attachments: BR1 1 of 2; LC1 2. Activity: all 640 rows served to BR1 (359 distinct parents, 14 objects) re-read as BR1, 0 unreadable. Approval requests: BR1, BR2 and BR3 each list both requests (one on BR1's ICA-2026-0004, one on BR2's SUP-2026-0001). BR1 GET by id of BR2's request returns 200 with a payload containing contract_number SUP-2026-0001, amount 447000, risk_level, liability_cap, although BR1 GET of that contract returns 404. Approvals door /api/v1/approvals/requests?object=clm_contract&recordId=: owner BR1 gets total=0 for her own contract's request; LC2 (submitter) gets 1. AFTER b970d10, browser (Chromium 1194, port 3486). BR1 Discussion: GET 200 comments total=1, activity, attachments total=1; Ctrl+Enter gives POST 201 /api/v1/data/sys_comment and the comment shows as 'Business Requester 1 just now'. LC1 sees total=2 and posts 201, visible. Negative: BR1 on BR2's AMD-2026-0002 shows 'Record not found'; sys_comment total=0 and sys_attachment total=0 although BR2's comment and c2-secret.txt exist. Approvals tab still 403, blank (expected, pending decision). Baseline browser (c31c7e2) for BR1 and LC1: 'You don't have permission to view activity/comments on this record ... You don't have access to these attachments' with the four 403s. Upload probe: upload/complete 200 then POST sys_attachment 403, toast 'You don't have permission to do that.' GATES at HEAD b970d10, exit codes captured to file before any pipe: validate 0 ('Validation passed'), lint 0 (6 pre-existing approval-approvers-may-resolve-empty suggestions), typecheck 0, lint:i18n-gate 0 ('0 missing keys across 2 locale(s)'). No ablation file: the before/after is the reverse proof, because the only diff is the grant.",
      "gates": {
        "head": "b970d10",
        "validate": 0,
        "lint": 0,
        "typecheck": 0,
        "lint:i18n-gate": 0
      },
      "line_budget": "n/a",
      "files_changed": [
        "src/profiles/requester.profile.ts (+29, 0 deleted)"
      ],
      "mcp_calls": "0",
      "api_writes": "3 REST writes through the fleet-write relay. Each is one repository_dispatch to objectstack-ai/objectstack, executed as objectstack-fleet[bot] and read back: (1) POST /repos/objectstack-ai/hotclm/pulls, pr_create #88, draft, 8682 bytes sent = stored; (2) POST /repos/objectstack-ai/hotclm/issues/88/assignees, via label-write.mjs --assign zhuangjianguo, read back MATCHES; (3) POST /repos/objectstack-ai/hotclm/issues/86/comments, this os-dev-report. Plus git push of the branch, which is not REST. Reads only otherwise: GET issue 86 and its comments, GET PR 85 body, GET PR 88.",
      "deviations": [
        "Commit b970d10 carries the harness's model-named 'Co-Authored-By: Claude Opus 5.5' trailer, which I copied. The os-dev contract wants a model-free trailer pair and forbids rewriting history, so I left it as is and am reporting it.",
        "PR body: 'Part of #86' is both the first line (the os-dev contract) and the last line (the dispatch said the body should end with it). Neither is a closing keyword.",
        "The dispatch and the card cite DESIGN.md section 07 for the detail-page tabs. The declaration is in section 05 (line 218); section 07 is AI. No DESIGN.md text was touched.",
        "To prove the row scope, I compiled a temporary, uncommitted grant on all four objects (sys_approval_request included) into the local server. I reverted it with git checkout HEAD -- (verified git diff HEAD empty) before the baseline browser pass. It never reached a commit.",
        "The two seed errors on my reboots ('818 ok / 2 errors') come from my own approval fixtures: two contracts I sent to in_approval, which the demo upsert cannot move back to in_review. Explained in the PR body.",
        "I did the README operator setup through admin/create-user plus sys_user_permission_set / sys_user_position rows (the README's own scripted path), not by clicking in Setup. The persona accounts live only in the removed local database."
      ],
      "open_questions": [
        {
          "question": "Approvals tab (DESIGN.md section 05 审批记录, section 11 M2 审批记录与审计齐全): how should requesters (and the other non-admin audiences) see a contract's approval history? The tab is record:related_list on sys_approval_request, which reads the generic data door. That door has no parent-record scoping: a read granted there serves every approval request in the organization, payload snapshot included (measured). The platform's own rule (participants, plus the read-only record-reader tier the platform maintainer ruled on 2026-08-15, objectstack#8652) lives only on /api/v1/approvals/*. Its switch, ApprovalsServicePlugin recordReaderVisibleObjects, cannot be reached from app metadata: os serve constructs the plugin with no arguments, and it is still unwired on objectstack main 4638625. No page component reads the approvals door.",
          "options": [
            "A — Grant allowRead on sys_approval_request in clm_requester (variant A2: in clm_legal only). Business need: real for both audiences. Cost: every employee reads every approval request in the tenant, across every app sharing it, including snapshots of contracts they cannot open. That is wider than section 04 ('requester: own contracts'). A2 limits the exposure to legal, who already read every contract, but still exposes other apps' requests in a shared tenant and leaves requesters blank. Long term: a standing over-grant. AI-error axis: worst of the four, because the declaration reads narrow and enforces org-wide, and nothing catches it. Startup axis: one line, but it buys a data exposure.",
            "B — Grant read and add app-side RLS on sys_approval_request (for example submitter_id == current_user.id). Business need: not met. RLS cannot reach the contract (no cross-object predicates, ADR-0055), and the owner is not the submitter: legal submits via send_for_approval, and the measured submitter of BR1's request is LC2. Long term: a second copy of the platform's participant rule inside the app, which DESIGN.md section 09 forbids in kind. AI-error axis: a hand-maintained predicate that drifts. Startup axis: new app surface for a partial result.",
            "C — Report a platform gap; no app change now. Ask the platform to make the objectstack#8652 record-reader visibility reachable declaratively and on the door a record page reads: either gate the sys_approval_request data door by the caller's read of (object_name, record_id), as sys_activity and sys_audit_log already are (objectstack#21069 / objectstack#21194), or add a record-page component over /api/v1/approvals/requests?object&recordId together with a stack-level opt-in. Until then the tab stays blank for non-admins. Optionally (a section 05 sub-decision) hide it per 'Honest capabilities'. Business need: met once, in one place, for the ruled semantics. Long term: contract-first, with one rule in the platform. AI-error axis: best, because the declaration and the enforcement agree. Startup axis: zero new app surface; the cost is a visible gap until the platform lands it.",
            "D — App instantiates ApprovalsServicePlugin({ recordReaderVisibleObjects: ['clm_contract'] }) in objectstack.config.ts plugins (the CLI skips its own copy by identity), with @objectstack/plugin-approvals as a direct dependency. Business need: only half met, because record:related_list still reads the data door and no metadata component reads the approvals door, so the tab would still need a custom component. Long term: pins a platform internal in app config. AI-error axis: a constructor option outside the spec. Startup axis: a new runtime dependency and plugin, which AGENTS.md 'Capability expansion: Tight' refuses without a card."
          ],
          "recommendation": "C, on the four axes. (1) The business need is real and measured (the owner sees 0 requests for her own contract on both doors), and the platform has already ruled the exact semantics (objectstack#8652); what is missing is reach. (2) Contract-first and no workaround: the fix belongs in the one place that owns approval visibility. (3) A and A2 are precisely the 'declared narrow, enforced wide' trap; C keeps declaration and enforcement equal. (4) Zero app surface. Sub-question for the maintainer: hide the Approvals tab until the platform lands it, or keep it blank. My recommendation is to keep it: it is a declared section 05 tab, and admins still see it."
        },
        {
          "question": "The Discussion tab's attachments panel now shows an Upload button. PR #88 grants no sys_attachment create, so a requester or legal upload stores the bytes and then the attach is refused (POST /api/v1/data/sys_attachment 403, toast 'You don't have permission to do that.'). Should Upload work?",
          "options": [
            "A — Grant allowCreate on sys_attachment in clm_requester. The platform gates attaching by the caller's edit of the parent record, so a requester could attach only while her contract is draft or submitted, and legal anywhere; that matches the section 04 edit windows. It does open a free-form file channel beside clm_contract_version.file and clm_signature.executed_file. Business need: unmeasured (nobody asked for it). Long term: two places for contract files. AI-error axis: neutral. Startup axis: capability expansion without pull.",
            "B — Keep it refused. Section 05 describes the tab as comments and @; files belong to versions and signatures. Report upstream that the console renders Upload to a caller with no create bit, and that the file is committed before the refusal (out_of_scope_findings[2])."
          ],
          "recommendation": "B. Startup axis (no pull for a third file channel) and long-term axis (contracts are data: files live on versions and signatures). The refusal is honest, and the misleading button is the platform's to hide."
        }
      ],
      "out_of_scope_findings": [
        "class: b · reach: public door, HTTP measured on 17.7.0. With an app read grant, GET /api/v1/data/sys_approval_request serves every request in the org to every holder (BR1, BR2, BR3 each total=2). GET by id serves a snapshot of a record the caller cannot read (contract GET 404, request GET 200 with amount 447000). Meanwhile the ruled record-reader tier (objectstack#8652) is unreachable: recordReaderVisibleObjects is a constructor option, and cli serve.js constructs the approvals plugin with no argument (only automation, analytics, email, sms and storage get one); still absent on objectstack main 4638625. This is the platform report the lane ruling asks for; the seat files it upstream. Minimal repro: two owners of private records, one approval flow, grant one of them read on sys_approval_request, list. Seam: spec:ApprovalsServicePlugin.recordReaderVisibleObjects → runtime:cli serve.js capability constructor (no arg) | renderer:record:related_list over the sys_approval_request data door · dedupe words: sys_approval_request data door row visibility, recordReaderVisibleObjects unreachable, approval history record page, record-reader approval visibility",
        "class: b · reach: public door, HTTP measured as admin. GET /api/v1/data/sys_approval_request and GET /api/v1/approvals/requests both answer pending_approvers 'manager:undefined' when the owner has no manager_id (no product surface writes manager_id; 0 of 7 users have one). The contract, content/docs/automation/approvals.mdx, says: 'An entry that resolves to nobody is not an error: the request opens with an empty pending_approvers'. Platform side: plugin-approvals expandApprovers falls back to the literal type:value, and a manager approver carries no value. Seam: spec:approval approver {type:'manager'} → runtime:plugin-approvals ApprovalService.expandApprovers literal fallback · dedupe words: manager:undefined, pending_approvers literal, manager approver empty slate, expandApprovers fallback",
        "class: a (user-visible misleading affordance) · reach: browser measured (BR1 and LC1, Discussion tab). The console renders the attachments Upload button to a caller holding no sys_attachment create. Upload stores and commits the bytes (POST /api/v1/storage/upload/complete 200) before POST /api/v1/data/sys_attachment answers 403, leaving an unreferenced committed file. Seam: renderer:console record:discussion attachments panel, runtime:service-storage upload-then-attach order · dedupe words: discussion upload button no permission, sys_attachment create 403 after upload, orphan committed sys_file",
        "carrier: the PM seat / maintainer (card #11, Q2). The three Discussion grants follow clm_requester; if Q2 changes which audiences hold that set, the grants must move with it. Noted in the PR Acceptance notes, not filed."
      ]
    }

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #88, head b970d10 · Discussion half (Part of #86) · 2026-10-10T00:48Z

    Reviewed by the repo:hotclm seat against GitHub.

    check reading
    CI on the head both Validate check-runs success on b970d10 = PR head.sha
    scope 1 file, src/profiles/requester.profile.ts +29 / −0; no governed surface
    closing keywords none (Part of #86, first and last line)
    the grant clm_requester (every audience holds it): read sys_comment · sys_activity · sys_attachment, create sys_comment; sys_approval_request deliberately absent
    inside DESIGN.md §04? yes, as measured: the platform scopes each read by the caller's read of the record the row is about — two requesters each saw only their own contract's comment and attachment, the other's comment answered 404, posting on the other's contract 403, all 640 activity rows served to one requester were about records she can read
    producer this app's permission sets — security/explain names the layer (object_crud denies :: No resolved permission set grants read on sys_comment)

    Seat correction: this card and its dispatch cited DESIGN.md §07 for the detail-page tabs; the declaration is §05 (line 218). The dev caught it; recorded here.

    Deviations accepted: the commit carries a model-named co-author trailer (no history rewrite on a pushed branch — noted, not reworked); a temporary uncommitted four-object grant was compiled locally to prove scope, reverted, never committed.

    Landing: permission-model change — left for the maintainer to merge (it is also the change this session's permission layer will not let the seat merge on its own). Not flipped out of draft.

    Open on this card: the Approvals half and the Upload sub-question — the decision below.

    Out-of-scope findings


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    决策:合同页「审批记录」页签怎么对业务承办与法务可见?另附「附件上传」一个子问题 · 2026-10-10T00:49Z

    Governing text: DESIGN.md §05(详情页 tab「审批记录(平台 sys_approval_request)」)与 §11 M2「审批记录与审计齐全」· §04 权限矩阵(业务承办:本人合同)· AGENTS.md「Platform gaps: Report, never patch」与「Capability expansion: Tight」

    维护者速读

    事情:「讨论」页签已由 PR #88 修好(复核通过,待你合并)。「审批记录」页签读的是平台对象 sys_approval_request 的通用数据接口。实测:一旦在应用里给它读权限,这个接口不按合同过滤——每个业务承办都能看到全组织的所有审批单,连看不了的合同的金额、风险等级快照也能看到。平台有正确的规则(审批参与人 + 「能读该记录的人可只读看到」,objectstack#8652 已裁),但只在 /api/v1/approvals/* 上,应用无法开启,页面组件也不读那个接口。

    你要做的:回一个字母;子问题回 A 或 B(可省略,默认按推荐)。

    做法 代价 / 收益
    A 在 clm_requester(或只在 clm_legal)上直接开 sys_approval_request 读权限 一行改动;但等于全员可读全组织审批单含快照,越过 §04「本人合同」——⛔ 不推荐
    B 开读权限 + 应用侧行级规则(如只看自己提交的) 达不到目标:送审人是法务而非承办人,跨对象条件写不了;还在应用里复制平台规则
    C(推荐) 不改应用,作为平台缺口上报(已立 objectstack#22559:让该数据接口按「能否读所属记录」过滤,与 sys_comment/sys_activity 同形);平台修好前该页签对非管理员保持空白 零应用改动、规则只在平台一处;代价是平台修好前有一个看得见的空白
    D 应用里自己实例化审批插件并传 recordReaderVisibleObjects 新增运行时依赖与插件(违反「能力扩张从紧」),且页面组件仍读不到那个接口——只解决一半

    子问题(附件上传):讨论页签的附件区现在会显示「上传」按钮,但 PR #88 没给 sys_attachment 创建权限,点了会先存文件再被拒(403)。推荐 B:保持拒绝——合同文件归「版本」与「签署」管,不再开第三个文件入口;误导性的按钮属平台问题,下一轮上报。A 是开放上传(受合同编辑窗口约束)。

    另一个小问题:页签在平台修好前,是保持可见但空白,还是隐藏?推荐保持可见(§05 声明的页签,管理员能看到内容)。

    四棱分析

    • 实际业务需求:真实且已测——承办人对自己合同的审批单在两个接口上都看到 0 条;审批历史是 M2 验收的一部分。
    • 项目长远合理性:可见性规则只应在平台一处(#8652 已裁语义),C 是契约优先、不在应用里绕路。
    • 防 AI 写错:A 是「声明看起来很窄、实际执行全组织」的典型陷阱,任何门禁都抓不到;C 让声明与执行一致。
    • 创业阶段不扩散:C 零新增;D 新增依赖与插件;A 一行但买来数据暴露。

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Release: PR #88 merged at 46e65f0 · 2026-10-10T02:21Z

    Merged with the maintainer's explicit approval, verbatim, 2026-10-10: 「#88 同意合并;你遇到的平台问题应该提交issue;然后继续测试」.


    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

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions