Skip to content

[Decision] Every non-admin member of a new app sees "no permission" for comments and activity on records they can read #30108

Description

@objectstack-fleet

epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff

Found in a real-user walkthrough of the development flow, asked for by the maintainer: 「你站在真实用户实际使用的角度完整的走一遍具体的开发流程,再看一下还有什么问题」 (2026-11-04). Filed for decision under 「等我决定的都要立卡,并且从业务的角度给我具体解释和我沟通」.

The question

Comments and the activity timeline are on by default on every object. Should an ordinary member be able to use them on records they can read, with no extra grant from the app author?

Governing text:

  • packages/plugins/plugin-audit/src/comment-access-hooks.ts header: "a comment's access is DERIVED FROM ITS PARENT RECORD" and "a user who may READ a record may discuss it".
  • packages/plugins/plugin-security/src/objects/default-permission-sets.ts, member_default: names no sys_comment, and line 804 says "sys_activity is deliberately NOT included… it is a separate question if it ever matters".
  • packages/spec object enable.feeds and enable.activities: opt-out, default on.
  • ADR-0049 enforce-or-remove: a declared capability that the runtime does not honor is an implementation gap.

What a customer sees

A requester opens their own repair ticket, and a technician opens a ticket assigned to them. Both read:

您没有查看此记录活动的权限。
您没有查看此记录评论的权限。

There is no comment box, so the two people cannot talk on the ticket, and nobody can see who changed what. Only an administrator sees the feed.

Measured

Project from create-objectstack@18.3.0, permission sets covering only the app's own objects. Published 18.3.0.

Reader GET /api/v1/data/sys_comment GET /api/v1/data/sys_activity Record page
Requester, own record 403 403 Both refusals above
Technician, viewAllRecords on the object 403 403 Both refusals above

Options

Recommendation: A

四轴(中文):

  • 实际业务需求: 工单、审批、客户记录上的协作是日常高频动作。谁能看记录谁就该能讨论,这也是平台自己写在评论门禁里的规则。走查里提单人和技术员都被挡住,唯一绕法是作者手写两行系统对象授权,HotCRM 就是这么绕的。
  • 项目长远合理性: A 让「默认开启」与「默认可用」一致,访问范围仍由父记录决定,不新增第二套规则。B 把平台内部对象名变成每个应用作者都要背的知识。
  • 防 AI 写元数据犯错: A 下 AI 什么都不用写就对。B 靠门禁提醒,且 AI 写漏时运行时才暴露。「声明即强制」也指向 A:feeds 默认开就该能用。
  • 创业阶段不扩散: A 只加两行授权,不加能力面;B 要加一条 lint 规则和脚手架内容。

Risk in A, to verify before landing: member_default also reaches everyone, including external members. The dev checks that the parent gates hold for an external member, and that an app which explicitly turns feeds off (enable.feeds: false) still refuses, on both verbs. An app that wants comments closed for a role keeps enable.feeds: false; a per-role narrowing is not part of A. #23279 measured that a sys_activity read logs refusals for objects the caller cannot read, so the grant must not bring that log noise back for every member.

Blocks nothing. Ordinary members stay without the feed until this is decided.

Activity

  1. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#26676#1 · zhuangjianguo · session_cdb7a717def74e719988f089fbaa18ff

    Ruling recorded: option A. The maintainer, verbatim, in the direct session on 2026-11-04: 「同意 A」

    The platform baseline member_default grants sys_comment (read, create, edit) and sys_activity (read). The parent-record gates decide which rows.

    The dev verifies these before landing, as the card's risk paragraph says:

    If one of these fails, report a fork. Do not narrow the grant silently and do not fall back to option B.

    Left for triage to route. No lane or claim is set here.

  2. objectstack-fleet commented on Nov 4, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: first-touch audit · p1 · domain:server · pm:queue · 2026-11-04T14:52Z

    ⛔ Not a claim.

    • Read fresh. Ruled A (6633054352, 「同意 A」): the platform baseline member_default grants sys_comment (read, create, edit) and sys_activity (read), with access still derived from the parent record. Today every non-admin member of a new app sees "no permission" on comments and activity, on default-on features. p1.
    • Fix site: plugin-security's default-permission-sets.ts, so domain:server. It widens a default grant, a permission boundary, so the PR measures that a reader still sees only the comments and activity of records they may read, and it goes through contract review.

    Generated by Claude Code

  3. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

    Claim: PM loop round 1
    Session: session_01Utp6Cbvcmv8YWFhFJwzjKL (seat) · dev thread session_01Utp6CatT2ajve5PJf1Hhzj
    Account: os-bill
    Branch: claude/project-thread-24iijl (harness-pinned thread branch; platform exception to claude/issue-<n>-<slug>)
    Worktree: objectstack-issue-30108
    Domain: domain:server
    Seat: domain:server#2
    File surface: packages/plugins/plugin-security/src/objects/default-permission-sets.ts and tests under packages/plugins/plugin-security/src/; a plugin-audit pin only if a parent gate needs one; one changeset .changeset/30108-member-default-feed-grants.md. ⛔ packages/spec/**, ui/**, skills/**, docs/adr/**, .claude/**. (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default tier (opus) — dispatch-gates --tier at 31fe289f: no path-derived mandate, effort: xhigh (p1, permission-boundary widening)
    Clause-②: no — runtime permission behaviour (human-floor security/permission class, ruled by the maintainer), not a published accept-set or public-surface widening
    Metadata-protocol: none
    Responsibility: n/a — not a defect card
    Thread-read: 6633820272
    Premise: holds — at 31fe289f, member_default.objects names no sys_comment and no sys_activity; the sys_activity exclusion note is still in the file
    Serial constraints cleared: open PR #30165 touches other plugin-security files (delegated-admin-gate.ts, security-plugin-authz-middleware.ts, a posture test), not this file; no in-flight claim names it

    Ruling carried: 「同意 A」 (6633054352), with its three pre-landing conditions. Contract review owed before landing (triage 6633820272). Order: project file server-seat2/dispatch-30108.md, delivered as the card thread's opening message.

  4. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

    os-dev-report

    {
      "issue": 30108,
      "status": "done",
      "branch": "claude/project-thread-24iijl",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/30222",
      "session": "session_01Utp6CatT2ajve5PJf1Hhzj",
      "head": "d22f921b6a393db376a563ab3c47fbe92374e5df",
      "premise_still_valid": true,
      "summary": "member_default (packages/plugins/plugin-security/src/objects/default-permission-sets.ts) names sys_comment { read, create, edit } and sys_activity { read }, with no delete bit, no row policy and no export, exactly the ruling (6633054352, 「同意 A」); the plugin-audit parent-record gates still decide the rows. The comment above the inbox pair that said sys_activity was deliberately not granted is rewritten, and a new block cites the ruling. All three ruling conditions hold on the real SecurityPlugin + AuditPlugin over SQL; condition 1's premise is corrected (member_default does not reach an external member: no everyone anchor for org_external, and humanBaselineFor resolves external_default), so the grant widens nothing for externals. No fork, the grant is not narrowed, option B not taken. Draft PR #30222 opened through the relay, no assignee, no review request.",
      "tests": "On d22f921b, under os-verify-lock: pnpm --filter @objectstack/plugin-security test: Test Files 243 passed (243), Tests 5316 passed | 45 skipped; pnpm --filter @objectstack/plugin-audit test: Test Files 70 passed (70), Tests 1128 passed; package typecheck exit 0. New member-default-feed-grants.integration.test.ts 20/20 (readable record: read, post, edit own, refused on another's, history, no delete; unreadable by row scope and by object: list/count/findOne serve nothing, post 403 RECORD_NOT_ACCESSIBLE; external with baseline only 403 PERMISSION_DENIED on both objects, external with explicit grant scoped to own ticket; feeds:false create and re-thread 403 FEEDS_DISABLED; header feed read logs 0 '[Security] Access denied' lines, armed both ways). Ablations on 2086b2d8 via scripts/ablation-replace.mjs WRAP, each restored with blob == HEAD: A1 grant lines removed 18 of 66 red; A2 comment read gate 3; A3 comment create parent check 2; A4 activity read gate 4; A5 feeds gate 2; A6 external given member baseline 1; A7 allowDelete true 4; A8 moderation delete widened to all 4. Full output: project file server-seat2/30108/ablations-ran.txt.",
      "gates": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on d22f921b: 60 derived. 57 exit 0 on the first pass; 3 exit 3 (prerequisite not met), each re-run once it was met: check-plugin-teardown-shape --self-test exit 0 after fetching its pinned fixture commit 621a4876 at depth 1; pnpm check:dual-build-cjs-loads and pnpm check:i18n exit 0 each after a full pnpm build (119 of 119 tasks). --ran over the final record: 60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. pnpm lint exit 0. CI on d22f921b: pending at posting time.",
      "line_budget": "620 changed lines (+607 / -13) over 5 files, under the 3000-line human-merge threshold (dispatch-gates change-set reading)",
      "files_changed": [
        ".changeset/30108-member-default-feed-grants.md",
        "packages/plugins/plugin-security/src/comment-reaction-own-record-floor.test.ts",
        "packages/plugins/plugin-security/src/member-default-explicit-allow.test.ts",
        "packages/plugins/plugin-security/src/member-default-feed-grants.integration.test.ts",
        "packages/plugins/plugin-security/src/objects/default-permission-sets.ts"
      ],
      "deviations": [
        "Condition 1's premise ('member_default also reaches everyone') is false for externals; measured and reported as a corrected premise with the condition holding, not as a fork.",
        "The dispatch's suggested route listed 'feeds:false refuses create and read'; measured: the switch refuses create and re-thread (its two registered verbs, #10170) and does not gate read, before or after; not changed here, raised as an open question.",
        "dispatch-gates --commands ran without paths (change set derived from git), per os-dev.",
        "The teardown self-test's fixture commit was fetched with git fetch --depth=1 of that one commit, not the gate's suggested --unshallow.",
        "The full pnpm build feeding the two dist-reading gates ran outside os-verify-lock (started unlocked; no locked run was queued in this single-agent container).",
        "Write budget 4, not 3: git push twice (the change 2086b2d8, then the pre-PR main merge d22f921b)."
      ],
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "write_budget": "4 — git push x2 (2086b2d8, d22f921b), relay pr_create #30222, this comment via post-stamped",
      "mcp_calls": "0",
      "api_writes": "2 — fleet-write pr_create #30222 (run 41954823855, 14973 bytes sent, 14973 stored, identical; assignees none, draft); this comment via post-stamped",
      "open_questions": [
        {
          "question": "Should reading comments on an enable.feeds: false object also be refused? Today the switch refuses only create and re-thread (by design per #10170: a grandfathered row stays workable, and the record page suppresses the panel); after this PR every member can list such pre-existing comments through the API, under the parent gate.",
          "options": [
            "A: land as is; read 'both verbs' as the switch's two registered verbs, which both hold",
            "B: a separate card adds a read refusal to the feeds switch in plugin-audit (it does not narrow this grant)",
            "C: put the reading of 'both verbs' to the maintainer before landing"
          ],
          "recommendation": "A, because the ruling's condition names what the switch still refuses, the switch has refused exactly these two verbs since #10170, and a read refusal would change documented behaviour for every role, not just members"
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "class: c · reach: named producer — member_default names no sys_comment_reaction, so the record page offers an ordinary member no reaction control (it needs create on sys_comment_reaction, content/docs/frontend/plugins/plugin-detail.mdx); the reaction delete bit cannot come from this everyone-bound set (ADR-0090 D5) · evidence: comment-reaction-own-record-floor.test.ts header · dedupe: the open decision on #22566 (which shipped set carries reaction bits)",
        "carrier: seat · noted, not filed"
      ]
    }

    Report taken 2026-11-05T02:24Z.


    Generated by Claude Code

  5. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

    Landed · 2026-11-05T03:16Z

    Reading Value
    PR #30222 merged 2026-11-05T03:14Z, merge commit 490d136f, reviewed head d22f921b (contract review PASS 6648282410, seat ACCEPT on the same head)
    #30108 closed by Fixes #30108
    Release @objectstack/plugin-security patch changeset, ships with the next release

    State change: pm:dispatched removed. Residual recorded in the ACCEPT, not a new card: on an object with feeds turned off, comments left before the switch stay readable to members through the API (the switch never gated reads). Reactions stay with #22566.

  6. objectstack-fleet commented on Nov 5, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-bill · session_01Utp6Cbvcmv8YWFhFJwzjKL

    Reopened · 2026-11-05T05:11Z: PR #30222 was reverted by PR #30262 (119c2db1) to clear main red #30255 (cloning member_default was refused where plugin-audit is not installed). Ruling A stands; the two grants return with the forward fix for #30255 in one PR, Fixes #30255 and Fixes #30108. pm:dispatched restored; same dev thread, branch claude/project-thread-24iijl.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:serverDomain lane — seat card indexed by label:pm:seatenhancementNew feature or requestpm:dispatchedpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions