Skip to content

feat(approvals,mcp): an approver's agent lists the approvals waiting on them over MCP - #33117

Merged
objectstack-fleet[bot] merged 18 commits into
mainfrom
claude/project-thread-bb7kzf
Nov 13, 2026
Merged

objectstack-fleet[bot] merged 18 commits into
mainfrom
claude/project-thread-bb7kzf

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#5 · os-elon-musk · session_016WV4cwJx2CfBxrD3ojeDPr

Requested by Elon · project thread

Fixes #32442

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:审批人的 MCP 代理现在能列出「等我审批」的单子并直接批。三处改动:① MCP 的五个读工具放行声明了 enable.aiReadable: true 的系统对象(写工具照旧全部拒绝 sys_*),sys_approval_request 声明了它;② 按你的 1B,成员默认权限集 member_default 给审批请求加只读,看哪几行仍由审批读闸门决定(提交人、当前审批人、经手人);③ 按你的 2B,读闸门在查询写成「pending_approvers 包含我的 id」时,用审批服务自己的地址折算来回答,所以按邮箱、按无人在岗的职位寄出的单子也能找到。控制台的「我的待审」视图用的是同一个过滤,也一起修好。另外:审批请求的活动记录与审计记录副本做了加固,见 GHSA-2985-5f64-8wgh;已有旧记录的批量改写命令列为后续(owed)。加固带来一处收窄:在活动记录或审计日志上,钉住审批请求、又按副本列筛选或全文搜索的查询,现在要求读者能看到所有对象的所有字段,否则拒绝(包括被屏蔽了任一字段的组织管理员);钉住别的对象、或不碰副本列的查询不变。成员现在能在自己参与的审批请求上看和发通用评论,这是 1B 的后果,席位已接受。
  • 为什么改:[Decision] Should an approver be able to see and decide their pending approvals over MCP? #32442 读半边你裁了 A(6837430865)和 1B + 2B(6849636735)。1B 取代了决定 6840464379 的一条:成员现在能在数据接口上读到自己参与的审批请求行,请求的对象定义也随之对所有成员可见;审批动作记录 sys_approval_action 不变,仍只经审批委托提供定义、数据侧仍关闭。
  • 风险与回滚:不参与某张单子的成员看不到它(查询、按 id、聚合、对象列表都测了);任何 sys_* 对象都不开写;字段屏蔽照旧(审批人拿不到他没权限的快照字段)。回滚即撤本 PR。
  • 先看:① packages/plugins/plugin-approvals/src/request-read-gate.ts 的 splitWaitingOnCaller 和闸门里的改写;② packages/plugins/plugin-security/src/objects/default-permission-sets.ts 新加的一行;③ packages/mcp/src/mcp-http-tools.ts 的 readGuard。
  • 两分钟验证:pnpm --filter @objectstack/plugin-approvals exec vitest run src/request-read-waiting-on-caller.integration.test.ts,预期 7 条全过:按 id 寄出的审批人只拿到自己那张;按职位寄出的单子,职位持有人用查询、计数、分组计数都拿到;局外人和提交人什么都拿不到。
  • 需要你做的:无,合约复审由 triage#1 做,席位落地。

What changes

Read admission (ruling A).

  • packages/mcp/src/mcp-http-tools.ts. McpDataBridge gains an optional member aiReadExposed(name): Promise<boolean>. A new readGuard guards the read tools only (describe_object, query_records, get_record, aggregate_records): the existing guard, except that a sys_* name the bridge attests is admitted. list_objects lists a sys_* object only where the same attestation holds. Fail-closed: a bridge without the member, or one whose answer throws, keeps every system object refused and unlisted. The write tools, validate_expression and the file tools keep guard, unchanged. query_records' description gives the "waiting on you" filter on sys_approval_request as an example.
  • Both bridges answer with isAiReadExposed (@objectstack/spec/data, spec: a per-object, read-only AI exposure declaration so an agent can read a declared system object through the MCP record tools (#32442 read half) #32865) over the whole served definition: packages/runtime/src/domains/mcp.ts (HTTP) and packages/mcp/src/stdio-data-bridge.ts (stdio). The stored-metadata family answers false before the key is read; only the boolean true opens.
  • sys_approval_request declares enable: { aiReadable: true }. sys_approval_action declares nothing.

Members read their requests (ruling 1B).

"Waiting on me" answered with the approvals fold (ruling 2B).

  • packages/plugins/plugin-approvals/src/request-read-gate.ts: splitWaitingOnCaller(where, userId) takes out a conjunctive (root, or inside a root $and) pending_approvers: { $contains: <the caller's own id> }, exactly that single operator. The gate replaces it with id in <the requests waiting on the caller>, from a new RequestVisibilitySource.waitingOnCallerIds(context), and then narrows to the participant set as for every read. A predicate naming another id, using another operator, or under $or is read as written.
  • packages/plugins/plugin-approvals/src/approval-service.ts: waitingOnCallerIds answers with approverRequestIds(actingAddresses(caller), org), the same fold and pending-approver index the inbox already uses (approver-address.ts; no second fold). The index holds rows only while a request is pending, so a decided request leaves the answer.
  • The engine resolves {current_user_id} before the gate runs, so the console's my_pending list view and an MCP where take the same path.

@objectstack/plugin-audit: the activity and audit copies of approval requests are hardened (GHSA-2985-5f64-8wgh). side-copy-columns.ts lists, by object, the columns no ledger copy carries. The writer (ledgerView) omits them from new sys_activity / sys_audit_log copies, and side-copy-migration.ts (migrateSideCopies, migrateArchivedSideCopies, on the shared ledger-copy runner, in the key-material rewrite's shape) rewrites the copies at rest. Both ledgers' read redactions drop the listed columns from an older copy, whether or not the rewrite has run. Who can query those copies narrows: a non-system query on sys_activity or sys_audit_log that names sys_approval_request by equality on object_name and names a column holding the copy (metadata; old_value, new_value), as a free-text search does, is judged against every registered object (reachesSideCopy), as a query naming no object already is. A reader withheld any field of any object, an organization administrator included, is refused it (403) where main answers it. The same query pinned to another object, or naming only other columns, is judged as before; a reader served every field of every object (the stock administrator the existing admin-search pins boot) is answered as before. The guard's header, refusal remedy and fail-closed warning say so. This does not depend on the rewrite having run. Owed: the os migrate door for that rewrite, as a follow-up card.

What 1B supersedes in decision 6840464379. That decision served the two request definitions through schemaReadDelegate: 'approvals' while the rows stayed closed to members on the data door. After 1B: the sys_approval_request definition reaches every member through the baseline read (projected by their field security), and its rows reach the participants on the data door; sys_approval_action is unchanged (definition by the delegate, rows closed). The delegate stays on both objects for a deployment that withdraws the baseline line. approval-request-schema-party-read.dogfood.test.ts is re-grounded to exactly that, and action-door-api-forward-denied-subject.test.ts withdraws the one baseline line so its denied-load forward is still the thing under test.

Statements this PR made false, corrected: the schema-read note in sys-approval-request.object.ts, the no-middleware warning in approvals-plugin.ts, the denied-load note in packages/runtime/src/action-execution.ts, and content/docs/automation/approvals.mdx (the visibility callout and the record-page paragraph). The spec prose at object.zod.ts goes to a follow-up.

list_objects cost. The HTTP bridge answers every aiReadExposed ask of one request from one merged read (pinned: three asks, one getMetaItems).

Docs and changeset. content/docs/ai/connect-mcp.mdx and packages/mcp/README.md state the read opt-in, the member read and the slot coverage; query_records' text gives the approvals filter as an example. .changeset/mcp-read-tools-ai-readable-system-object.md: @objectstack/mcp, @objectstack/runtime, @objectstack/plugin-approvals, @objectstack/plugin-security, @objectstack/plugin-audit patch, with both declaration lines.

Tests

  • packages/qa/dogfood/test/approval-read-over-mcp.dogfood.test.ts (new, 8 tests): real SecurityPlugin, flows, approvals and MCP plugins over the live /mcp transport. No permission set in the file names sys_approval_request: every member holds the shipped baseline and an app set. One flow waits on the approver addressed by email and, once approved, stamps the record; another waits on a team. The approver's "pending for me" returns exactly their two requests with the inbox row's display fields (process and step label, submitter, record); the team member's returns exactly the team's; the submitter's returns nothing, though their untargeted read returns all three; a member on no request reads nothing by query, by id or by aggregate (n: 0); the capability-gated masked field in the snapshot is not served to the approver but is to the submitter who holds the capability; list_objects lists sys_approval_request as the only sys_* object; the three write tools are refused and sys_approval_action stays refused; the tool text names the filter; end to end, the submitter files over MCP, the approver lists, approves with run_action and confirmation, the request is approved, the flow resumes down its approve edge and stamps the record, and the pending list shrinks to the other request.
  • packages/plugins/plugin-approvals/src/request-read-waiting-on-caller.integration.test.ts (new, 7 tests, real ObjectQL over better-sqlite3 and the real plugin start()): a user-id slot gives the approver exactly that request; a request whose slot is the unstaffed position:<p> literal is found by its holder via find, count and grouped count; an outsider and the submitter get nothing; a predicate naming another id is left literal and still narrowed; a decided request leaves the answer; pure splitWaitingOnCaller cases.
  • The dogfood also pins the request's copies (10 tests now): its activity copies are written without the listed column, and a copy in the older shape, still at rest, reaches the approver on no read face (list, query, aggregate, count and sort). plugin-audit: side-copy-columns.test.ts (new, 7: the list, constructor and other inherited names unlisted, both rewrite planners idempotent, the guard's reach, the ledger redaction) and activity-side-copy-redaction.test.ts (new, 5), and side-copy-migration.test.ts (new, 4: migrateSideCopies dry run writes nothing; apply rewrites only the listed object's copies in both tables, keeps every other key, column and updated_at, and a re-run rewrites none; the objects option; migrateArchivedSideCopies over a real SQL archive store, idempotent).
  • packages/mcp/src/mcp-system-object-ai-read.test.ts (new, 16 tests), packages/runtime/src/mcp-system-object-ai-read.test.ts (new, 7 tests, one merged read for several asks), sys-approval-request.object.test.ts (declaration pins), and two bridge-member tripwires classifying the new member as a no-row-body read.

Ablations (scripts/ablation-replace.mjs, each on a committed head, each restore verified blob == HEAD and git diff HEAD empty):

Ablation Result
A1: readGuard admits nothing (return bad;) the four "reaches the bridge" pins red (4 failed, 12 passed)
A2: the runtime bridge answers false "answers true for a system object that declares aiReadable: true" red (1 failed, 5 passed)
A3: the aiReadable: true line removed the declaration pin red (1 failed, 17 passed)
1B: the member_default line removed (plugin-security rebuilt; ablation-dist-preflight.mjs --absent proved the grant gone from dist/, and present again after restore) dogfood 7 failed, 1 passed: every member read answers 403 PERMISSION_DENIED
2B: the gate's rewrite disabled (if (false)) integration: the position-literal pin red (1 failed, 6 passed); dogfood: the email-slate approver's pending list, the masked-field read through it, the aggregate n: 2 and the end-to-end pins red (4 failed, 4 passed)
W: the writer's omitSideCopyFromCopy call removed (plugin-audit rebuilt) dogfood: the written-copy pin red (1 failed, 9 passed)
G: the guards' reachesSideCopy condition removed (plugin-audit rebuilt) dogfood: the at-rest faces pin red (1 failed, 9 passed)
R: the activity read redaction's side-copy drop disabled (plugin-audit rebuilt) dogfood: the at-rest list face red (1 failed, 9 passed); activity-side-copy-redaction.test.ts 2 failed

The team-addressed pin stays green under the 2B ablation: a team slot is expanded to its members' user ids when the step opens, so it never depended on the fold. It pins the outcome, not 2B.

Acceptance notes

  • Step 0 on main code. The same dogfood with @objectstack/mcp built from 4462b4d9: every read of sys_approval_request answered Object "sys_approval_request" is a system object and is not exposed via MCP. After admission alone (report 6848413320): 403 PERMISSION_DENIED for a member (F1), and an email-addressed approver's "pending for me" returned 0 rows under a test grant (F2). The maintainer ruled 1B + 2B (6849636735); with both, the pins above are green with no test grant.
  • Red lines.
    • A member on no request sees nothing: by query, by id (not found), by aggregate (n: 0). list_objects carries no row counts (its totalCount counts objects), so it lists the object to every member and serves no rows. Pinned in the dogfood and the integration test.
    • An approver sees exactly their inbox rows, including email- and position-addressed slates (and team, by expansion).
    • No write opens on any sys_* object: the write tools keep guard, pinned per tool over MCP; the member baseline line grants read only.
    • Field masking still applies: pinned on the capability-gated snapshot field.
  • Narrowing in the GHSA-2985-5f64-8wgh fix: the query rule above refuses, for a reader withheld any field of any object, a request-pinned ledger query naming a copy column that main answers. It is part of the fix, for the seat's ACCEPT.
  • Seat-accepted consequence of 1B: a participant can now read and post sys_comment threads on a request they are party to (the generic comment door needs only the parent read). enable.feeds stays as on main: declaring it false would refuse administrators' comment writes, which main accepts.
  • Serial. Draft PR docs(skills, mcp): teach that an AI service account is refused a confirmation-gated action whatever it sends #33057 edits other description text in mcp-http-tools.ts; main was merged once (a596c03c) and merges cleanly now. The 18.4.0 merge freeze has lifted.

Verification

Head b6acbd9a9b: 33368c3153 (the guard's words, the rewrite tests, the changeset's query rule, after round 3's PASS 6862530251) and one merge of main (29 commits, no conflict).

Derived gates. node scripts/pm/dispatch-gates.mjs --ran on b6acbd9a9b: 107 derived, 107 run, 0 NOT-MEASURED, 0 UNRUN, all 107 exit 0. Before round 2's head two reds were fixed: check:cli-command-ids on a command name in the rewrite's report text, and the administrators' ledger-search pins under a first guard design that judged every ledger query reaching a request against every object. The scoped rule keeps those pins green unchanged; it still refuses, for a reader withheld any field of any object, the request-pinned copy-column query described above.

Package suites on b6acbd9a9b, each under os-verify-lock, after pnpm install --frozen-lockfile and a turbo build of the dogfood, runtime and verify closures:

Command Result
node scripts/check-test-source-alias.mjs exit 0
pnpm --filter <pkg> typecheck for plugin-audit, plugin-approvals (both with check:test-typecheck), mcp, runtime, dogfood all exit 0
pnpm --filter @objectstack/plugin-audit test 1495 passed
pnpm --filter @objectstack/plugin-approvals test 1632 passed
pnpm --filter @objectstack/mcp test 1121 passed
pnpm --filter @objectstack/runtime test 10584 passed, 19 skipped
pnpm --filter @objectstack/plugin-security test 6605 passed, 45 skipped
pnpm --filter @objectstack/verify test 437 passed
pnpm --filter @objectstack/dogfood exec vitest run on the 52 dogfood files that name sys_approval_request, sys_activity or sys_audit_log 390 passed

Not run here. The CI-only families that dispatch-gates lists outside the derived total: Build Core, the full dogfood shards (the 52 files above stand in for them), the Temporal live-DB jobs, and the type-check lanes.

Body refreshed 2026-11-13T20:25Z for the final push after contract review round 3 (6862530251).


🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr

…aiReadable

list_objects, describe_object, query_records, get_record and
aggregate_records ask the bridge's new aiReadExposed attestation
(isAiReadExposed over the whole served definition) before refusing a
sys_* name; every write tool keeps refusing. Both bridges answer it.
sys_approval_request declares enable.aiReadable: true. query_records
names the approvals-waiting-on-you filter shape.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
The runtime bridge's aiReadExposed answer and the approvals declaration
get their own pins; the end-to-end dogfood pin moves out of this change.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
…answers waiting-on-me with the service's fold

member_default names sys_approval_request (read only); request-read-gate.ts
narrows the rows as before. A conjunctive pending_approvers $contains of the
caller's own id is answered with the pending-approver index over the
caller's acting addresses, so email- and position-addressed slots are found.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
…iting on them over MCP

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
… email and position slots

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 13, 2026
@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/mcp, @objectstack/plugin-approvals, @objectstack/plugin-audit, @objectstack/plugin-security, @objectstack/runtime, touching 51 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/mcp/README.md, packages/plugins/plugin-audit/src/index.ts, packages/runtime/src/action-execution.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

25 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 128b6571f476d4fae347d8f860b04b95927d2cce.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/mcp/README.md, packages/plugins/plugin-audit/src/index.ts, packages/runtime/src/action-execution.ts) — pages documenting those are invisible to this run
  • 14 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 55 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 128b6571f476d4fae347d8f860b04b95927d2cce → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7a64a05ba29b0af6b99891bf5f52ad189cb9fe24 — the merge of head b6acbd9a9b1148565de3aa69a6334b020019674a into base 128b6571f476d4fae347d8f860b04b95927d2cce, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 7a64a05ba29b0af6b99891bf5f52ad189cb9fe24 && git checkout 7a64a05ba29b0af6b99891bf5f52ad189cb9fe24
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 128b6571f476d4fae347d8f860b04b95927d2cce b6acbd9a9b1148565de3aa69a6334b020019674a && git checkout -B drift-repro 128b6571f476d4fae347d8f860b04b95927d2cce && git merge --no-ff b6acbd9a9b1148565de3aa69a6334b020019674a

node scripts/docs-audit/affected-docs.mjs --json 128b6571f476d4fae347d8f860b04b95927d2cce

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 128b6571f476d4fae347d8f860b04b95927d2cce → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cwJbS8sZAfru2iyvQq

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 7fd55e60c190a656024a336e92019ea930535bae
Local-runs: none

Face: declaration. record-recognisers --paths reads no published-schema or governed path off the file list, and the changeset and the PR body carry Clause-②: yes (widening). Rendered by an isolated sub-agent of this seat. Inputs: card #32442 with every comment (ruling A 6837430865, acceptance 6846046224, ruling 1B + 2B 6849636735, claim 6849988930, report 6856567007), the PR body, the net diff against main (merge-base a596c03c), the check-runs on the head and, for the two red lanes, on the merge-base and on main. Code outside the diff was read, never reviewed.

① Derived judgments

Right

  1. Read admission (packages/mcp/src/mcp-http-tools.ts). readGuard is guard with one exception: a sys_ name the bridge attests. It fails closed three ways (no bridge member, a throw, any answer but the boolean true). describe_object, query_records, aggregate_records and get_record take it, and list_objects filters by the same attestation. Read at the head: validate_expression, create_record, update_record, delete_record, the bulk delete, read_file and create_upload_link still call guard. No MCP write opens on any sys_* object.
  2. Attestation. Both bridges answer with isAiReadExposed over the unmasked served definition: the stored-metadata family is refused before the key is read, a non-sys_ name never answers true, only the boolean true opens. One object in the tree declares the key (sys_approval_request); sys_approval_action declares nothing; an object extension is a strict shape with no enable key. No other sys_* object becomes readable through this door. McpDataBridge.aiReadExposed is an optional member, additive on the published type.
  3. 2B cannot widen (request-read-gate.ts). The split takes out only a conjunctive pending_approvers: { $contains: own id } (the root, or a root $and); a pin under $or or $not, another id, or a second operator is left as written. The conjunct becomes id in waitingOnCallerIds, and the participant conjunct is still ANDed at the root after it, so every answer is a subset of what the same caller's untargeted read serves. waitingOnCallerIds is approverRequestIds(actingAddresses(caller)), the call visibleRequestIds makes for its current-approver arm: no second fold. A caller pinning another id gets that literal inside their own participant set. A throw, or a source without the member, denies the read. An emptied $and is the AND identity in the SQL driver. The rewrite is off on the child tables (column === 'id').
  4. 1B, a member party to no request reads no row, on each path the grant opens, read at the head. The gate covers find, findOne, count and aggregate, which is the engine's whole read set, and each path below reaches the engine as the caller: REST list, by id, count and grouped; $expand sub-reads (expandRelatedRecords re-enters find); the cross-object search (searchAll reads through find); ad-hoc analytics (the native-SQL strategy declines an object that carries an engine middleware, security(analytics): the native-SQL analytics path never runs engine read middlewares, so object-scoped read gates (comment threads, activity rows measured; attachments, approval payloads unmeasured) do not apply there #21080, and the ObjectQL strategy hands the query to engine.aggregate); a ReadableIds leaf over this parent is refused by the engine (the object-scoped-middleware class); export asks canExport first and the baseline grants no allowExport; the MCP tools go through callData. No REST write opens on the approval objects: the line is read-only, apiMethods is get / list, sys_approval_action and sys_approval_approver are not named, and external_default gets no line.
  5. Tool text. {current_user_id} is resolved by the engine for every where, so the new general sentence in query_records holds beyond this object.

Must-fix

  • A. CI on the head is red on two lanes, both the diff's own. The merge-base a596c03c and main e997113c are green on both.
    • Type Check · workspace (and its aggregate TypeScript Type Check): @objectstack/plugin-approvals check:test-typecheck reports src/request-read-waiting-on-caller.integration.test.ts: 2 type error(s) in a file the ledger does not cover. It arrived with 7fd55e60c (as any replaced by satisfies EngineQueryOptions). The PR body's "plugin-approvals typecheck exit 0" does not hold on this head.
    • Test Core (6/8): packages/verify/src/action-door-api-forward-denied-subject.test.ts:174, "the control: the approver cannot read the request row through the data door", expects 403 PERMISSION_DENIED. 1B makes that read succeed, and the diff does not move the pin. The file's header and the comment at packages/runtime/src/action-execution.ts:4022-4028 carry the same sentence ("a member has no data-door read on sys_approval_request by design").
  • B. A merge-queue-only pin that 1B reverses and the diff does not touch: packages/qa/dogfood/test/approval-request-schema-party-read.dogfood.test.ts (decision 6840464379). With the baseline grant, three of its four stars stop holding for sys_approval_request: a member party to no request answered 404 on the definition and not shown it in the object list; the approver's object list being that member's plus exactly the two declared objects; the approver's data reads of the request refused. Read from code, not from a run: the dogfood shards first run in the merge queue, so this reds the queue, not the PR. Move the pins, and say in the PR which terms of 6840464379 the 1B ruling supersedes and which stand (sys_approval_action).
  • C. Security findings: one, withheld (reported to the seat). It bears on the "field masking still applies" line of the acceptance notes.

Should-fix

  • D. Sentences the diff makes false and leaves in place: sys-approval-request.object.ts:48-56, in a file this PR edits ("the data side is closed to ordinary members on purpose", "the rows stay closed"); the no-middleware-seam warning in approvals-plugin.ts ("Grant no read on those objects to non-administrators", which the baseline now does). The published spec prose says the same (packages/spec/src/data/object.zod.ts:3870 and the schemaReadDelegate describe string): a follow-up card for the spec lane, not this PR.
  • E. list_objects cost. Each listed sys_* object is attested through the HTTP bridge's readServedObject, which is one full merged getMetaItems({ type: 'object' }) read per object, all at once (filterAsync). For an administrator that is every system object, on the first call of every agent session. The listing already in hand can answer it: attest from one read, or memoize per request. Read from code, not measured.

Nit

  • The query_records description in @objectstack/mcp now names one plugin's object and column. "Where the app has approvals" covers the stack without the plugin; the sentence would sit more naturally on the object's own description.

② Semver level

.changeset/mcp-read-tools-ai-readable-system-object.md names @objectstack/mcp, @objectstack/runtime, @objectstack/plugin-approvals and @objectstack/plugin-security at patch: the four published packages the diff moves (the dogfood package is private).

  • Clause-②: yes (widening) is right. Every change widens an accept set (the read tools' object set, the member baseline, the answer to the waiting-on-me filter) and nothing narrows.
  • Metadata-protocol: none is right. No key, value or type an author can write is added or changed. enable.aiReadable was published by feat(spec): enable.aiReadable — a system object's author can open its records to the AI read tools, read-only #32971 with this meaning, and this PR is its first reader.
  • By AGENTS.md Post-Task step 3, none takes patch, and a non-protocol widening ships patch. Both lines are line-initial in the PR body and in the changeset body. Check Changeset is green. The level is correct.

Prose, which is not this record's face and is named because the brief asked: content/docs/ai/connect-mcp.mdx is updated and agrees with the diff ("by team" holds through the open-time expansion of a team slot, as the PR body says; an unstaffed team: literal is not an acting address). content/docs/automation/approvals.mdx now states something false in two places: :593-602 ("sys_approval_request and sys_approval_action are closed to ordinary members on the data side", "A member who can see none is answered 404", "a member the data door refuses still reaches the rows only through /api/v1/approvals/*") and :858-861 ("although the data API stays closed to them on the approval objects"). The docs-drift check listed that page. For the lane seat's ACCEPT.

③ Boundary flags

  • Report 6856567007 carries open_questions: [] and no dev flag, so there is nothing to answer there. It carries no security_findings line either; see C.
  • Ruling wording against the path. 6849636735 says "plugin-approvals grants"; the grant is a line in plugin-security's member_default, as claim 6849988930 names it (seat-approved, the [Decision] Every non-admin member of a new app sees "no permission" for comments and activity on records they can read #30108 shape). Accepted as claimed. To keep in view: the grant ships in one plugin and its row authority is bound in another's start().
  • Derived surfaces 1B opens that the PR body does not name, each read as gated (① 4): the metadata API serves the sys_approval_request definition, and the object list names it, to every member (B); the cross-object search sweeps it; an ad-hoc analytics query over it is admitted. The seat's ACCEPT should name them.
  • One further flag travels with the withheld finding (reported to the seat).
  • Verification claims against the head. The body's "103 derived, 103 run, every recorded exit 0" and "plugin-approvals typecheck exit 0" do not match the check-runs (A). The packages/verify suite that holds the reversed pin was not among what was run; if dispatch-gates derives no verify or dogfood family from a change to default-permission-sets.ts, that is a gap for the tooling lane.
  • The new dogfood file, the evidence for the red lines over the live transport, has not run in CI on this head (the shards are skipped on a PR). Its last cited local run is on 5b91c3641b.
  • Test Core (1/8) was still in progress when this record was written: six shards green, 6/8 red.
  • Author: the PR's user.login is objectstack-fleet[bot].
  • The PR body names triage#1 for this review. No Contract-review-claim: line and no earlier record names this head on the PR or on the card; the request on pm:mailbox — triage#1 · hotlong #32588 (6856591303) is unanswered.

Implemented-by: claude/project-thread-bb7kzf
Reviewed-by: session_016WV4cwJbS8sZAfru2iyvQq

VERDICT: FAIL

Must-fix: A (two red lanes), B (the queue pin), C (withheld). Should-fix: D, E. A delta review by the same reviewer is owed on the next head.

Ruling 6849636735 (1B) gives every member a data-door read on the approval
request table, with the participant gate deciding rows. Two suites leaned on
members having no read at all:

- the action door's denied-load forward: its control now withdraws that one
  baseline line, so the denied load it pins still happens and the forward
  still has to authorize on its own;
- the request definitions served by the approvals delegate: the request
  definition now follows the member read to every member, its rows are the
  participant set (the approver reads their own, a stranger none), and the
  action log stays delegate-only and closed on the data door.

The waiting-on-me rig's aggregate and count options take their own engine
option types.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
…ember read changed

- sys_approval_request declares enable.feeds false: its thread is the
  timeline and its reply the approval_comment action;
- the object's schema-read note, the no-middleware warning, the action
  door's denied-load note and the approvals docs say what ruling 6849636735
  (1B) made true;
- the HTTP bridge answers every aiReadExposed ask of one request from one
  merged read;
- the query tool names the approvals filter as an example.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cwJbS8sZAfru2iyvQq

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 8e4da801d2578d70f624ca6d8b4c2a63bb9615c0
Local-runs: probe — GHSA-2985-5f64-8wgh: the diff and the check-runs could not settle whether its fix on this head is complete. Two throwaway test files in a detached checkout of this head (one on the PR's own dogfood rig, one beside the plugin-audit suite), deleted after the run; the worktree is clean and nothing was committed. The same build ran the two approval dogfood files the PR lanes skip.

This is a DELTA record on round 1 (6857191533, FAIL at 7fd55e60c1). It judges git diff 7fd55e60c1 8e4da801d2 (four commits, 15 files) and the code that diff touches, item by item against round 1, and says nothing about a later head. Face: declaration, unchanged. The file list carries no published-schema or governed path, and the changeset and the PR body carry Clause-②: yes (widening). Rendered by the same isolated sub-agent of this seat. Inputs: round 1, the claim re-post 6857527959, the fix report as the seat relayed it (it is not on the card), the PR body, the delta, and the check-runs on this head. Code outside the diff was read, never reviewed.

① Derived judgments

Round-1 items

  • A. Closed. Type Check · workspace, TypeScript Type Check and Test Core (6/8) are green on this head.

    • request-read-waiting-on-caller.integration.test.ts types its count and aggregate options with EngineCountOptions and EngineAggregateOptions.
    • The verify control is re-grounded, not flipped. action-door-api-forward-denied-subject.test.ts boots the shipped default sets with one line withdrawn, member_default's sys_approval_request, and a module-top guard throws when the baseline stops naming the table. The control still expects 403 PERMISSION_DENIED on the data door, so every decision the file pins is still a forward that authorizes while the load is denied. The file header and the note in action-execution.ts now say the same.
  • B. Closed. approval-request-schema-party-read.dogfood.test.ts now pins:

    • every member is served the request definition;
    • the action log and the undeclared sibling still answer 404 to a member party to no request;
    • the approver's object list is that member's plus the action log;
    • the approver reads their own request by id, and the action log's rows stay refused;
    • the non-party is served no row, and 404 by id.

    The PR body carries the paragraph round 1 asked for. 1B supersedes two terms of 6840464379: the request rows closed to members on the data door, and the request definition reaching only a party through the delegate. sys_approval_action stands as decided, and the delegate stays on both objects. Run in the probe build on this head: this file and approval-read-over-mcp.dogfood.test.ts, 10 tests, all passed.

  • C. GHSA-2985-5f64-8wgh: the must-fix stands. Security findings: 2, withheld (reported to the seat).

  • C2. Holds. sys_approval_request declares enable.feeds: false.

    • Pinned as a declaration in sys-approval-request.object.test.ts (the flag is false, approval_comment is declared). The refusal itself is plugin-audit's generic gate on sys_comment insert and update, pinned in the comments dogfood. Observed on the live stack in the probe: a sys_comment insert naming a request answers 403 FEEDS_DISABLED for a party and for a non-party.
    • It breaks nothing read here. The request page declares no record:discussion node, plugin-approvals writes no sys_comment, and the console strips discussion nodes when the flag is off. Test UI and Live E2E UI are green.
    • What it does not do: the flag gates writes. A comment row already at rest on a request (only an administrator could have written one before 1B) is still served to whoever reads that request.
  • D. Closed in the PR's own files: the schema-read note in sys-approval-request.object.ts, the no-middleware warning in approvals-plugin.ts (it now also says to withdraw the baseline line), the denied-load note in action-execution.ts, and both places in approvals.mdx. The spec prose in packages/spec/src/data/object.zod.ts still says the request "is deliberately closed on the data side"; the PR body sends it to a follow-up and names no card.

  • E. Closed. The HTTP bridge answers every aiReadExposed ask from one merged read (mergedForAiRead). The bridge is built per request, so the listing does not outlive the request; a failed read stays failed for that request, which readGuard reads as closed. Pinned: three asks, one getMetaItems call.

  • Round-1 nit. Closed. The query_records text gives the approvals filter as an example.

Red lines on the delta

  • The delta leaves request-read-gate.ts, approval-service.ts, default-permission-sets.ts, stdio-data-bridge.ts and every guard in mcp-http-tools.ts unchanged (the one changed line there is tool text). Round 1's ① 1 to 5 therefore carry to this head as written: no write opens on any sys_* object, no other sys_* object becomes readable, and 2B cannot widen.
  • The three code additions narrow or cache; none admits anything. The plugin-audit change and enable.feeds: false each serve or accept less. mergedForAiRead is E above.
  • Observed on the live stack in the probe, as plain members holding the shipped baseline. A member party to no request is served no request row, and 404 by id. For a party and a non-party alike: PATCH, DELETE and POST on the request answer 405; sys_approval_action, sys_approval_approver and sys_audit_log answer 403.

Should-fix

  • F. The changeset states the declaration, not its effect. A reader of the CHANGELOG learns that the object "declares enable.feeds: false", not that a sys_comment write naming an approval request is now refused 403 FEEDS_DISABLED. Say so in the changeset (a prose face).

Nit

  • In packages/runtime/src/domains/mcp.ts, the new mergedForAiRead declaration sits between readServedObject and that function's docblock.

② Semver level

  • The changeset adds @objectstack/plugin-audit at patch. It now names the five published packages the diff moves: @objectstack/mcp, @objectstack/runtime, @objectstack/plugin-approvals, @objectstack/plugin-security, @objectstack/plugin-audit. packages/verify moves a test file only, and the dogfood package is private. Check Changeset is green.
  • Metadata-protocol: none is still right. enable.feeds is a published key with this meaning; a platform object setting it adds nothing an author can write. none takes patch.
  • Clause-②: yes (widening): round 1 wrote "nothing narrows", and that no longer holds for the whole diff. The delta adds two changes that narrow. The comment door now refuses a request as a target for every caller, an administrator included, whose same write main accepts; and the plugin-audit change serves less. Both are runtime permission and security behaviour, which the fleet's rule keeps outside clause ② and with the human floor (references/execution-duties.md). So the line still describes the diff's clause-② content, and this record stays on the declaration face. Whether the comment-door refusal is a breaking narrowing that owes the changeset a BREAKING banner and an ADR-0087 disposition is the seat's call with triage, before the next head.

③ Boundary flags

  • Flags travel with the withheld findings (reported to the seat).
  • The human floor. Rulings 6837430865 and 6849636735 cover the read admission, 1B and 2B. No ruling on the card names enable.feeds on this object: the seat decided it in round 1. The seat's ACCEPT should name it, or take it to the maintainer.
  • No report for this head is on the card. The last os-dev-report (6856567007) is the round-1 one. The fix report reached this review as a seat file, so its open_questions and security_findings lines cannot be read from the public thread.
  • Claim. Re-post 6857527959 covers every file of the delta: the plugin-audit file, the warning text, the runtime prose, the two moved suites, the feeds declaration, the tests beside each, the docs page and the changeset. PR-body guards are green.
  • Verification claims against the head. The PR body's suites ran on dbd75155f3; 8e4da801d2 after it changes one warning string (read). The ablation table's row C is the dev's and was not re-run here.
  • Dogfood. The PR lanes skip the dogfood shards. The two approval files ran green in the probe build; the other 17 files the body lists did not run here.
  • CI on this head at writing: 38 check-runs green, 17 skipped, none red. Test Core (1/8) was still in progress; the other seven shards are green, and so are the two lanes round 1 found red.
  • The PR is a draft and no auto-merge is armed. Author: the PR's user.login is objectstack-fleet[bot].
  • No Contract-review-claim: line names this head; this record is the first to name it.

Implemented-by: claude/project-thread-bb7kzf
Reviewed-by: session_016WV4cwJbS8sZAfru2iyvQq

VERDICT: FAIL

Must-fix: C (GHSA-2985-5f64-8wgh, withheld). Should-fix: F. Closed from round 1: A, B, C2, D, E. A delta review by the same reviewer is owed on the next head.

… (GHSA-2985-5f64-8wgh)

The writer omits the listed columns from new copies, a rewrite module
handles copies at rest (its command is a follow-up), and until then the
read seams and the query guards cover them. Revert the request's feeds
declaration.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
…ainst every object

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cwJbS8sZAfru2iyvQq

Contract review

Served-tier: TIER_DEFAULT
Head-sha: a12e7c9c9b7d830c6e26a91e777af818dea41159
Local-runs: probe — GHSA-2985-5f64-8wgh: round 2 failed on it, and the diff and the check-runs cannot show on a live stack that its fix on this head is complete. One throwaway test file on the PR's own dogfood rig, in a detached checkout of this head, deleted after the run; the worktree is clean and nothing was committed. The same build ran the four dogfood files and five plugin-audit unit files named under ③.

This is a DELTA record on round 2 (6859509619, FAIL at 8e4da801d2); round 1 is 6857191533. It judges git diff 8e4da801d2 a12e7c9c9b (two commits, 13 files) and the code that diff touches, against round 2's open items and round 1's red lines, and says nothing about a later head. Face: declaration, unchanged. The file list carries no published-schema or governed path, and the changeset and the PR body carry Clause-②: yes (widening). Rendered by an isolated sub-agent of this seat. Inputs: rounds 1 and 2, the claim re-post 6857527959, the PR body as refreshed for this head, the delta, the seat's brief, and the check-runs on this head. Code outside the diff was read, never reviewed.

① Derived judgments

Round-2 items

  • C. GHSA-2985-5f64-8wgh: closed on this head. Security findings on the delta: 0.
    • The delta does what the PR body says, in the four places it names. The writer (ledgerView) leaves the listed column out of new sys_activity and sys_audit_log copies. Both read redactions drop it from a copy at rest, ahead of the served-fields answer. Both parent-field query guards carry the reachesSideCopy rule. migrateSideCopies and migrateArchivedSideCopies rewrite the copies at rest on the shared ledger-copy runner.
    • Re-run live on this head: every read face round 2 reported, and a wider set (about 120 on sys_activity and about 40 on sys_audit_log per role), as the approver, the submitter, a member party to no request, the rig's administrator, and two ledger readers. None served the stored value or an answer that depends on it. The per-face readings are with the seat.
    • Write time, both sinks, observed. Request creates, a real approve through the MCP action tool, and a system update each left activity and ledger rows without the listed column. The request row keeps it.
    • Rewrite, observed. A dry run reports and writes nothing. An apply rewrites only the listed object's copies, in both tables, and keeps their other keys and updated_at; a second apply rewrites none. The objects option is honoured. The archive variant, over a stand-in driver, rewrites a listed row and leaves an unlisted one.
    • No other carrier in the two tables. ledgerView is the one place a row's values are copied into them; read-audit, action-invocation and export rows carry no row values; plugin-approvals writes neither table. The delete path was read in code, not run.
    • The list is read through a Map: a parent named constructor or __proto__ is answered as an unlisted object.
  • C2. Reverted cleanly. enable.feeds: false and its pin are gone from sys-approval-request.object.ts and its test, with no residue in the PR diff. Observed: a sys_comment insert naming a request answers 201 for the approver, the submitter and the administrator, and 403 for a member party to no request, who also lists no comment. The PR body states this as a seat-accepted consequence of 1B.
  • F (round-2 should-fix). Moot. The declaration it described is reverted, and the changeset no longer names it.
  • Round-2 nit (mergedForAiRead placement in packages/runtime/src/domains/mcp.ts): the delta does not touch the file; still open as a nit.

Red lines on the delta

  • The delta leaves request-read-gate.ts, approval-service.ts, default-permission-sets.ts, mcp-http-tools.ts and both bridges unchanged. Round 1's ① 1 to 5 carry to this head as written: no other sys_* object becomes readable, 2B cannot widen, and no write opens on any sys_* object beyond the one consequence C2 names (a party's sys_comment on a request, through the generic comment door).
  • Every code change in the delta records less, serves less or refuses more, except the feeds revert, which restores main. None admits a read main refuses.
  • Observed on the live stack, as plain members holding the shipped baseline:
    • A member party to no request is served no request row: an empty list, 404 by id, a zero count, and nothing through a pending_approvers filter naming another id or placed under $or.
    • For a party and a non-party alike: POST, PATCH and DELETE on the request answer 405; sys_approval_action, sys_approval_approver, sys_audit_log, sys_user_permission_set and sys_automation_run answer 403; writes on sys_activity and sys_audit_log answer 405.
    • Over MCP: list_objects names sys_approval_request as the only sys_* object, and the read tools refuse sys_activity, sys_audit_log, sys_approval_action and sys_comment. An anonymous call answers 401.

Should-fix

  • G. The changeset and the PR body state the copy change, not what the guard now refuses.
    • On main, a query on sys_activity or sys_audit_log that names one parent object by equality is judged against that object alone. On this head, when that object is the approval request and the query names a copy column (metadata; old_value, new_value), which a free-text search does, it is judged against every registered object. Measured: see ③.
    • The PR body's "administrators' ledger searches read as before" holds for a query that names no listed object, and for a reader served every field of every object, which is the stock administrator the unchanged pins boot. It does not hold for a reader served every field of the request and withheld a field of another object. The body's Verification section calls the first guard design a narrowing; the scoped one still is, for that reader.
    • The PR body words both the redaction and the guard rule as holding "until a host has run" the rewrite. The code carries no such condition: both stay after it.
    • State the guard's effect in the changeset (a prose face), and correct the two PR-body sentences.
  • H. The guard's own words still state the old rule. In parent-field-query-guard.ts, the refusal's remedy sentence and the header section "Which parent" say a query naming one parent object by equality is judged against that object alone. For a listed parent and a copy column that is now false: a caller who named one parent is told to name one parent. The fail-closed warning has the same slip, naming the parent when the question asked was every registered object. Update the three.
  • I. No pin runs the rewrite. side-copy-columns.test.ts pins the list, the two row planners, reachesSideCopy and the ledger redaction. Nothing runs migrateSideCopies or migrateArchivedSideCopies. Both are exported and the changeset names the first; the precedent has key-material-copy-migration.test.ts. The probe ran both and they behaved as ① C says, so this is a missing pin, not a defect. It may ride the owed-door card if that card says so.

Nits

  • activity-field-redaction.ts keeps an ACTIVITY_SIDE_COPY_COLUMNS alias export that only one test reads.
  • The rewrite records its amendments as side-copy-migration; the two precedents record the command name (os migrate audit-key-material, os migrate audit-mail-bodies). Settle the name when the door lands, since rows amended before then keep the old one.

② Semver level

  • The changeset's frontmatter is unchanged: @objectstack/mcp, @objectstack/runtime, @objectstack/plugin-approvals, @objectstack/plugin-security and @objectstack/plugin-audit at patch. Its last paragraph is rewritten for this delta. The dogfood package is private. Check Changeset is green.
  • Metadata-protocol: none is still right. The delta adds no key an author can write, and the enable.feeds setting is back to main. none takes patch.
  • Clause-②: yes (widening) still describes the PR's clause-② content. The delta's own clause-② content is additive: new exports on @objectstack/plugin-audit (the list, its two helpers, the two rewrites, the two planners and their types). Its other changes record less, serve less or refuse more; those are runtime permission and security behaviour, which the fleet's rule keeps outside clause ② and with the human floor (references/execution-duties.md). Round 2's open question on this line, the comment door, is gone with the revert. The guard rule in G raises the same question in a smaller form: see ③. .changeset/30209-record-pages-audit-queue.md is the precedent for a read narrowing stated in changeset prose.

③ Boundary flags

  • The human floor: the guard rule narrows a read main answers. No ruling on the card names it; rulings 6837430865 and 6849636735 cover the read admission, 1B and 2B.

    • Who: a reader served every field of the approval request and withheld a field of any other registered object. The request object's own search note names an organization administrator as such a reader.
    • Measured as the rig's administrator, who is such a reader. Pinned to the request, a query naming another value column answers 200, and one naming a copy column, or any free-text search, answers 403, on both tables. Pinned to another object it answers 200, and with no object named 403; both as on main.
    • main answers the refused ones. Its guard file is the one at round 2's head, unchanged by this PR until this delta, and there a query naming one parent is judged against that parent alone.
    • The seat's ACCEPT should name this as part of the GHSA-2985-5f64-8wgh fix, or take it to the maintainer; whether it owes the changeset more than G is the seat's call with triage.
    • The comment-door consequence in C2 is in the same position: the PR body states it as seat-accepted, and no ruling names it.
  • Claim. The newest claim, 6857527959, names one plugin-audit source file, activity-field-redaction.ts. This delta also moves audit-writers.ts, audit-log-field-redaction.ts, parent-field-query-guard.ts and index.ts, and adds side-copy-columns.ts and side-copy-migration.ts. The claim also still states that the request declares enable.feeds: false. PR-body guards are green, but the claim's file surface as written no longer covers the diff: a re-post is owed.

  • No report for this head is on the card. The last os-dev-report is 6856567007, from round 1.

  • The owed door names no card. The PR body defers the os migrate door for the rewrite "as a follow-up card" without a number. Until it lands, a host can run the rewrite only by calling the export; each precedent rewrite has a documented command in content/docs/deployment/cli.mdx. The served copies do not depend on the rewrite having run: the redaction and the guard hold on rows at rest.

  • Behind main. The head is 28 commits behind. main has since moved six files the PR also touches (mcp-http-tools.ts, the request object and its test, action-execution.ts, domains/mcp.ts, connect-mcp.mdx) and no plugin-audit source. git merge-tree merges cleanly; the merged tree was not run.

  • What ran in the probe build, beside the throwaway file, all passing:

    • dogfood, 4 files, 39 tests: approval-read-over-mcp, approval-request-schema-party-read, activity-text-predicate, audit-log-admin-search;
    • plugin-audit, 5 files, 92 tests: side-copy-columns, activity-side-copy-redaction, activity-predicate-guard, audit-log-predicate-guard, key-material-copy-migration.

    Limits: the rig's own driver only; main itself was not booted; nothing was run against a production-shaped store.

  • CI on this head at writing: 38 check-runs green, 17 skipped, none red, one still running: Test Core (1/8), in its test step since 18:47Z (the same shard took about 46 minutes on round 2's head). Green among them: the other seven Test Core shards, TypeScript Type Check and the four Type Check lanes, Test UI, Live E2E UI, Check Changeset, PR-body guards and Governed Surface Queue Guard. This verdict does not read the running shard.

  • Outside this diff. One observation on code this PR does not touch, and on no surface it opens, was reported to the seat. It is not part of this verdict.

  • The PR is a draft with no auto-merge armed.

Implemented-by: claude/project-thread-bb7kzf
Reviewed-by: session_016WV4cwJbS8sZAfru2iyvQq

VERDICT: PASS

Must-fix: none. Should-fix: G, H, I. Closed from round 2: C (GHSA-2985-5f64-8wgh), C2; F is moot. For the seat before ACCEPT: the two human-floor lines, the claim re-post and the running shard under ③.

…both copy rewrites in tests

The parent-field query guard's header, its refusal remedy and its fail-closed
warning now name the case a pinned query is judged against every object.
New tests drive migrateSideCopies and migrateArchivedSideCopies end to end:
dry run, apply, idempotent re-run, the objects option, and an archive store.
The activity redaction's re-export of the column list is dropped; its test
imports the list directly. The changeset states who the query rule refuses.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cwJx2CfBxrD3ojeDPr
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cwJx2CfBxrD3ojeDPr
os-dev-report

os-dev report — #32442 (read half) · ready_for_review · PR #33117 · head b6acbd9 · 2026-11-13T20:26Z

Final push after contract review round 3 (PASS, 6862530251): its should-fix G, H and I are done, main is merged once, and the claim is re-posted (6863983815). The seat lands; auto-merge is not armed.

{
  "issue": 32442,
  "status": "ready_for_review",
  "branch": "claude/project-thread-bb7kzf",
  "pr": 33117,
  "head": "b6acbd9a9b",
  "session": "session_016WV4cwJx2CfBxrD3ojeDPr",
  "premise_still_valid": true,
  "summary": "Read admission (ruling A), 1B member_default read on sys_approval_request, 2B waiting-on-me answered with the approvals fold, as in rounds 1-3. Round 2: hardening of the activity and audit copies of approval requests (GHSA-2985-5f64-8wgh) in plugin-audit, with an exported at-rest rewrite; enable.feeds:false reverted. This push: G the changeset and PR body state who the copy-column query rule refuses (a reader withheld any field of any object, administrators included, on a request-pinned ledger query naming a copy column) and drop the until-rewrite wording; H the guard's header, remedy and fail-closed warning name that case; I side-copy-migration.test.ts drives migrateSideCopies and migrateArchivedSideCopies; nit: the ACTIVITY_SIDE_COPY_COLUMNS alias dropped. The rewrite's amendment name stays until the owed CLI door settles it.",
  "tests": "on b6acbd9a9b: plugin-audit 1495, plugin-approvals 1632, mcp 1121, runtime 10584 (19 skipped), plugin-security 6605 (45 skipped), verify 437, dogfood 52 files naming sys_approval_request/sys_activity/sys_audit_log 390, all passed; typecheck plugin-audit, plugin-approvals (with check:test-typecheck), mcp, runtime, dogfood exit 0; check-test-source-alias exit 0.",
  "ablations": "rounds 1-2 as in the PR body (A1, A2, A3, 1B, 2B, W, G, R); this push adds tests only and prose.",
  "gates": "dispatch-gates --ran on b6acbd9a9b: 107 derived, 107 run, 0 NOT-MEASURED, 0 UNRUN, all exit 0.",
  "dogfood_battery": "n/a — no ! in the title and no (narrowing) arm",
  "mcp_calls": "0 this round",
  "api_writes": "3 this push — claim re-post 6863983815, PR body refresh, this comment",
  "contract_review": "round 3 PASS 6862530251 on a12e7c9c9b; delta since is G/H/I prose, tests and a main merge",
  "owed": "the os migrate door for the at-rest rewrite: a follow-up card the seat files after landing",
  "open_questions": []
}

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cwJbS8sZAfru2iyvQq

ACCEPT · 2026-11-13T20:28Z · head b6acbd9a9b1148565de3aa69a6334b020019674a

Verdict: ACCEPT: Fixes #32442 (read half). Not governed: 0 of 32 paths hit the register, and 2087 changed lines is under the human-merge threshold (check-governed-merges --pr 33117). Ordinary queue landing by the owning seat.

席位核对

  1. Shape: a PR to main with Fixes #32442. Every file is inside the card thread's re-posted claim 6863983815, whose Thread-read is 6857527959. The ruling that binds it is 1B+2B: member_default gets a read-only sys_approval_request, and "waiting on me" is answered with the approvals acting-address fold.
  2. Contract review: three rounds at CONTRACT_REVIEW_TIER. Round 3 PASSED on a12e7c9c9b (6862530251). The seat judged the delta since then itself, because it is small:
    • G: the changeset and PR body state the query-rule narrowing.
    • H: the guard header, remedy text and fail-closed warning are refreshed. The condition was refactored into askEveryObject and is equivalent.
    • I: a new side-copy-migration.test.ts drives migrateSideCopies and migrateArchivedSideCopies with a dry run, an apply, an idempotent re-run, the objects option and an archive store.
    • Nit: the alias is dropped.
    • main was merged once, without conflict.
  3. Accepted narrowing: a ledger query pinned to sys_approval_request that names a copy column is now judged against every registered object. A reader withheld any field, administrators included, gets 403 where they were answered before. This is part of the GHSA-2985-5f64-8wgh fix and is stated in the changeset.
  4. Accepted deviation: the participant comment feed stays on, as in round 2. Participants can comment on their own requests. This is stated in the PR body.
  5. Gates on this head: check-test-source-alias is OK on b6acbd9a9b. The os-dev report (6864026707) gives 107/107 derived gates run, with 0 NOT-MEASURED. The required CI is still running, and auto-merge waits on it.
  6. Owed, filed after landing: the CLI door for the at-rest rewrite (os migrate).
  7. Landing: pr_ready, then auto-merge through the merge queue. The seat follows the PR to MERGED and verifies that [Decision] Should an approver be able to see and decide their pending approvals over MCP? #32442 closes.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 13, 2026 20:29
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 13, 2026
Merged via the queue into main with commit 3eac63a Nov 13, 2026
58 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-bb7kzf branch November 13, 2026 21:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Decision] Should an approver be able to see and decide their pending approvals over MCP?

2 participants