Repository navigation
Description
Ruled: 5978653398 · question 1 letter A — host handlers keep the projected read; ruling B covers sandboxed bodies only · 2026-10-04T09:48Z
Ruled: 5974479930 · letter B · 2026-10-03T23:14Z
Filing gate: ② a decision only the maintainer can make. It is a security boundary, and the existing rules do not decide it. Filed by the triage seat (objectstack-wide, seat post #6015, session_01AavokzJ5DndAwitDXvKy4U). It carries option C of #21454 (5963137791, 5963161540). Triage named C as the maintainer's option and did not rule it (5963299937). #21454 closed completed on disposition A, so C had no carrier. ⛔ Not a claim, ⛔ not a dispatch. ⛔ Classes, doors and roles only.
Reader who acts: the maintainer, or the director seat. If the letter is B, the domain:cli seat dispatches it on this card.
维护者速读
- 问题: 应用自己写的代码(动作、自动化的代码体)现在还能读那组存放元数据的系统表,只是读出来的敏感内容已经遮蔽。写入和挂钩子已经按 [Decision] security(runtime): may an app-authored body touch the stored-metadata family's tables at all — a hook bound to them, or an elevated body writing them directly (#21454 items 3 and 4) #21520 的 A 彻底禁止。要不要把「读」也一并禁止,让应用代码完全碰不到这组表?
- 选项:
- A: 维持现状,读仍然允许。每次读都先遮蔽,危险的查询形状一律拒绝,这已在 PR fix(runtime): a sandboxed body or an action handler reading the stored-metadata tables is served the data door's form (#21454) #21513 和 PR fix(runtime)!: refuse the stored-metadata family evaluate shapes and serve write returns at the reader-context seams #21539 落地。不新增门。
- B: 读也直接拒绝,并给出提示:「元数据请走元数据接口」。拒绝放在已落地的同一接缝处。
- 分诊推荐: B,理由只看①。回退项为 A。
- 你要做的: 回一个字母。
Measured, read now (origin/main 901e7cf13a)
- What has landed: the reader-context seam (
packages/runtime/src/stored-metadata-reader-seam.ts).serveStoredMetadataReadsThroughserves the family's reads projected and keyed, at the three author contexts: a body's object API, a handler's scoped API and a handler's engine handle.- The evaluate shapes are refused through the door's own predicates.
refuseStoredMetadataBodyWritescarries [Decision] security(runtime): may an app-authored body touch the stored-metadata family's tables at all — a hook bound to them, or an elevated body writing them directly (#21454 items 3 and 4) #21520's A, which refuses writes and hook binding.
- Pull for reads from app bodies: zero.
- objectstack
examples/**at901e7cf13a: the family's table names appear in changelog prose and comments only, never in a body. - hotcrm at
94668373: no hit insrc/**orpackages/**. - Every engine reader of the family in
packages/**is a platform internal, which [finding] [security] An action/automation body's object API and an action handler's engine handle read the stored-metadata family outside its body projection and keyed serve (reach NOT MEASURED) #21454's dev measured (5963137791).
- objectstack
- No leak remains under A. Every read is served projected and keyed. So B is boundary hardening, not a leak fix. That is why this card is p2, not the family's p1.
Governing text
- The family's rule ([security] Family closeout: stored metadata bodies (stored datasource credential material included) are still reachable outside the redacting doors at several further positions — detail withheld pending maintainer #21120, [security] Stored-metadata-body family: two exits #21120 did not reach. An engine-only door serves an administrator credential material in cleartext, and the data door serves a content hash computed over the withheld credentials #21207 ruling B, and [finding] [security] An action/automation body's object API and an action handler's engine handle read the stored-metadata family outside its body projection and keyed serve (reach NOT MEASURED) #21454's A): every door that serves, copies or evaluates the family's stored content projects it and serves it keyed. It governs what a reader is served. It does not say whether an app body may read at all.
- [Decision] security(runtime): may an app-authored body touch the stored-metadata family's tables at all — a hook bound to them, or an elevated body writing them directly (#21454 items 3 and 4) #21520's ruling A (
5965059068, maintainer 「同意」): app bodies may not bind to the family or write it. Metadata changes go through the metadata API. It ruled writes and hooks only, not reads. - AGENTS.md Prime Directive 12: "fix it at the producer and reject it at authoring/publish".
- No ADR names the metadata API as the family's only reader. Searched
docs/adrforonly reader,sole readerandmetadata APItogether withbody: 0 hits.
一句话问题
应用代码现在能读到「元数据存储表」的遮蔽版内容。要保留这个读法,还是让应用代码彻底碰不到这组表、只能走元数据接口?
选项 × 真实代价
| 选项 | 做什么 | 客户可感知的后果 |
|---|---|---|
| A 维持 | 什么都不做。读照常遮蔽,危险查询照常拒绝 | 无变化。平台继续维护接缝上的遮蔽、按列判定和搜索收窄,作为长期义务 |
| B 读也拒绝 | 在同一接缝处,应用代码读这组表直接返回明确的拒绝,提示走元数据接口 | 实测零个应用代码读这组表,所以今天谁也不受影响。以后有人这么写,会立刻收到清楚的报错,而不是拿到半遮蔽的数据 |
业务含义直译
- A 等于允许外包装修队进机房看配线图,但敏感的地方都打了马赛克。马赛克要一直有人维护。
- B 等于机房不对外包开放,要看图纸去档案室(元数据接口)申请。和 [Decision] security(runtime): may an app-authored body touch the stored-metadata family's tables at all — a hook bound to them, or an elevated body writing them directly (#21454 items 3 and 4) #21520 已定的「不许动机房」是同一条规矩。
四轴(业务立场)
- 实际业务需求: 实测零拉动,示例应用和 HotCRM 里都没有应用代码读这组表。A 维护的是一个没人用的能力;B 拒绝的也是没人用的读法,今天不伤任何人。
- 项目长远合理性: B 让「应用代码与元数据存储表」只剩一条规矩:完全不碰,元数据走元数据接口。它和 [Decision] security(runtime): may an app-authored body touch the stored-metadata family's tables at all — a hook bound to them, or an elevated body writing them directly (#21454 items 3 and 4) #21520 的 A 对齐。A 留下「写不能、读可以但要遮蔽」的双轨,每新增一种查询形状,都要再判一次。
- 防 AI 犯错: 出错时谁看到什么?A 下,AI 写的代码去读这组表,会拿到遮蔽后的数据并继续运行,作者以为拿到了真实内容,这是静默的。B 下,作者立刻看到一条点名元数据接口的拒绝,这是响亮的。
- 创业阶段不扩散: B 是移除一个能力,A 是声明并长期维护一个能力。按「移除优于声明并维护」,B 更省。
os-decision-facets
- ① 项目长远合理性:B 收缩特例。应用代码与元数据存储表只剩「不碰」一条规矩,与 [Decision] security(runtime): may an app-authored body touch the stored-metadata family's tables at all — a hook bound to them, or an elevated body writing them directly (#21454 items 3 and 4) #21520 A 同形。A 保留读侧的遮蔽和查询判定特例。
- ② 实际业务拉动:今天零个应用代码读这组表(examples、hotcrm 实测)。零拉动默认 remove,偏 B。
- ③ 防 AI 犯错:B 响亮拒绝,并点名元数据接口;A 静默给出遮蔽数据。偏 B。
- ④ 创业阶段不扩散:B 移除一个读能力;A 是声明并长期维护。偏 B。
Prior rulings read: 21454 option C, outright, refuse family reads, [Decision] stored-metadata → 2 hits (#21454 5963299937, which named C the maintainer's; #21520 5965059068, letter A, writes and hooks only); ADR none; thread: #21454.
推荐:B。 两年后的样子:应用代码通过元数据接口读到有类型、已遮蔽的定义,从不直接查原始存储表。参照 Salesforce:定义经 Schema describe 和 Metadata API 提供。但它的 Tooling 对象也能被查询,所以这只是方向参照,不是全封闭的先例。
回退:A。 已落地的遮蔽接缝原样保留,不加门,读侧零风险。
自检: 只看①选 B;②③④ 是否翻转:否。三轴同向,只影响时序:B 可以在 #21520 的认领之后单独一个 PR。
置信缺口: 我看不到私有客户的应用包,零拉动只覆盖 objectstack examples 和 hotcrm。常设规则「新门默认否」偏向 A。B 是新增的拒绝,所以维护者不回字时按 A 处理。
裁后执行
- A: 本卡以
not planned关闭,并在 [finding] [security] An action/automation body's object API and an action handler's engine handle read the stored-metadata family outside its body projection and keyed serve (reach NOT MEASURED) #21454 记录一行。已落地的接缝不动。 - B: 本卡转
pm:queue(domain:cli)。认领先对 examples、hotcrm 和 cloud 的应用代码体做一次读侧普查,再动手。- 在
serveStoredMetadataReadsThrough处,把读改为拒绝,提示点名元数据接口。 - 已失效的读侧投影与收窄代码随之删除,不保留两套。
- 平台内部读者不受影响,并钉住。
- 按收窄发版(
!、Clause-②: yes (narrowing)、ADR-0087 标记、minor),与 PR fix(runtime)!: refuse the stored-metadata family evaluate shapes and serve write returns at the reader-context seams #21539 同形。 - ⛔ 不碰 [Decision] security(runtime): may an app-authored body touch the stored-metadata family's tables at all — a hook bound to them, or an elevated body writing them directly (#21454 items 3 and 4) #21520 的写与钩子拒绝。
- 在
Activity
objectstack-fleet commentedon Oct 3, 2026
Ruling: batch #275 item 2 · letter B · maintainer 「同意」 2026-10-03T23:11Z
Director seat, summon #32,
session_016tKoy8NJa35Yih1FdzrVmn. Written asobjectstack-fleet[bot]through the relay. ⛔ Classes, doors and roles only.The ruling
B: an app-authored body may not READ the stored-metadata family's tables either.
serveStoredMetadataReadsThrough) is refused, with a prescription naming the metadata API.四棱(本裁决新记录)
Execution parameters (ruled here; no further decision card)
needs-user-decision→pm:queuein this act,domain:cli, p2,security.examples/**, hotcrm and cloud. A real reader found there goes back to the maintainer before the refusal lands.!,Clause-②: yes (narrowing), the ADR-0087 marker,minor), in PR fix(runtime)!: refuse the stored-metadata family evaluate shapes and serve write returns at the reader-context seams #21539's shape, with an at-tier contract review.Generated by Claude Code
objectstack-fleet commentedon Oct 4, 2026
Claim: PM loop round 1 (ruled B by the maintainer, record
5974479930)Session:
session_016GiHYRmLSNWTfbX9gVQkpzAccount:
os-bill(the seat's linked user asget_meanswers it; the card's assignee)Branch:
claude/issue-21594-body-read-refusalWorktree:
objectstack-issue-21594Domain:
domain:cliSeat:
domain:cli#1File surface, per the ruling's execution parameters (an app-authored body may not READ the stored-metadata family; ⛔ #21520's write and hook refusals untouched):
examples/**and hotcrm, measured in this claim;objectstack-ai/cloud) is private and unreachable from this seat's container. Its leg is reportedNOT MEASUREDand routed to the maintainer. The refusal does not land until that leg is answered.packages/runtime/src/stored-metadata-reader-seam.tsandstored-metadata-body-boundary.ts: the seam refuses the family's reads from app bodies with a prescription naming the metadata API. The now-dead read-side projection and narrowing for bodies is deleted, ⛔ not kept beside the refusal.packages/runtime/src/sandbox/body-runner.ts, plusaction-execution.tsonly as the context census decides..changeset/.⛔ No
packages/metadata-protocoledit: the door's own collector and predicates stay, and they serve the data door. A symbol there left with no consumer is measured and reported, ⛔ not deleted from this lane. ⛔ Nopackages/spec, and ⛔ no write-side or hook-binding change. (stop on breach; explain in the report)Container & model:
L,mode:subagent,model: default (opus)build. The path globs mandate nothing;dispatch-gates --tiersays so, derived. The contract review is owed at the ceiling,CONTRACT_REVIEW_TIER. reason:Clause-②: yes (narrowing)(a released read door refuses what it served).Clause-②: yes (narrowing). This is a narrowing: the
!, the ADR-0087 marker andminor, in PR #21539's shape, as the ruling sets it.Thread-read: 5974479930
Serial constraints, other open PRs read 2026-10-04T00:08Z on
origin/main15fe567c9c:packages/runtime/src/stored-metadata-*,sandbox/body-runner.ts,action-execution.tsorpackages/metadata-protocol/src/index.ts.packages/services/service-automation).--allow-destructivedrops them (the coupling #21571 names) #21573 (PR feat(cli): os migrate unmapped-columns reads a retired field's columns, keyed by record id, for conversion before the destructive drop #21643, auto-merge armed), touchespackages/clionly.domain:cliseat ·session_016GiHYRmLSNWTfbX9gVQkpz· 2026-10-04T00:08ZGenerated by Claude Code
objectstack-fleet commentedon Oct 4, 2026
os-dev-report
{ "issue": 21594, "status": "needs_decision", "branch": "claude/issue-21594-body-read-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/21660", "session": "session_016GiHYRmLSNWTfbX9gVQkpz (the dispatching PM session; this run is its subagent)", "premise_still_valid": true, "summary": "Ruling B is implemented for the sandboxed body context. The PR is draft #21660, `Part of #21594`, HEAD d5b890226c. A new body READ layer (`refuseStoredMetadataBodyReads`, stored-metadata-reader-seam.ts) and `storedMetadataBodyReadRefusal` (stored-metadata-body-boundary.ts) refuse every read verb the seam serves (find, findOne, count, aggregate), and with them every evaluate and search shape. They refuse on both family tables and through every derived context, with #21520's own PERMISSION_DENIED / 403 envelope and a prescription naming the metadata API's read route; no new error code. `buildSandboxApi` (body-runner.ts) is now `refuseStoredMetadataBodyWrites(refuseStoredMetadataBodyReads(source))`, so the hook, action and job faces are all refused. The now-dead `serveStoredMetadataReadsThrough` wrap around a body's API is deleted. #21520's write and hook-binding code is untouched (comments only). status is needs_decision on ONE leg, A2: the action-handler contexts (`ctx.api` via buildActionApi, `ctx.engine.find` via buildActionEngineFacade) carry app-authored host code (examples/app-todo registers 8 host handlers) that #21520's write refusal does not reach. Per the dispatch they were not widened and stay served. That is also why the seam's projection, evaluate-refusal and narrowing code is still live and was not deleted. A1 census found 0 readers in examples and hotcrm; the cloud leg is NOT MEASURED.", "tests": "All at final HEAD d5b890226c unless stated. (1) `pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2` over the six family files (stored-metadata-body-reads.test.ts NEW, stored-metadata-body-writes.test.ts, stored-metadata-body-boundary.test.ts, stored-metadata-reader-contexts.pin.test.ts, stored-metadata-body-boundary.pin.test.ts, stored-metadata-reader-seam.test.ts): 'Test Files 6 passed (6) / Tests 104 passed (104)', lock VERDICT command-exit 0. (2) New unit file: 15/15. (3) Public-door integration pin: reader-contexts.pin.test.ts, 21/21. It covers REST /actions action bodies (find, findOne, count, aggregate on both tables; inside ctx.api.transaction; all 7 evaluate and search shapes) and the data-door hook body (administrator and member; nothing lands). The /meta runtime-authored body: refused 403 PERMISSION_DENIED naming GET /api/v1/meta/:type/:name. Handler ctx.engine.find and ctx.api: still projected and keyed. Handler engine body-filter: still INVALID_FIELD / 400. Controls: data door unchanged, metadata API answers the item projected, the engine's own read still returns the stored form. Run on the built dependency closure (`pnpm --workspace-concurrency=2 --filter '@objectstack/runtime^...' build`, exit 0), later a full `pnpm build --concurrency=2` (72/72). (4) Full runtime suite on 9c87884191: --project local 'Test Files 319 passed (319) / Tests 4540 passed | 19 skipped (4559)', --project repo 'Test Files 3 passed (3) / Tests 751 passed (751)', LOCAL_EXIT=0 REPO_EXIT=0. The delta to d5b890226c is the new unit file's double plus one ledger row, rerun in (1). (5) `pnpm --filter @objectstack/runtime typecheck`: exit 0, check:test-typecheck OK. (6) Full `pnpm lint`: LINT_EXIT=0, no findings, on d5b890226c. (7) `pnpm --filter @objectstack/spec exec vitest run --project repo scripts/liveness/evidence.test.ts`: 42/42. (8) ABLATION A5 via `node scripts/ablation-replace.mjs` (WRAP, under os-verify-lock) on packages/runtime/src/stored-metadata-reader-seam.ts at committed 9c87884191. Anchor 'if (BODY_FAMILY_READS.has(prop)) {' x1 → x0, replacement 'if (false && BODY_FAMILY_READS.has(prop)) {' x0 → x1, blob 76eabe5afe1a → c56dca3ae3e6. Expected direction red; observed red: 'Tests 18 failed | 86 passed (104)'. Red = 10 new unit refusal pins + 8 integration body cases (the bodies fell through to the handler-served read; the hook body's insert landed carrying the stored form). Green = all #21520 write and hook-binding pins (body-writes 10/10, body-boundary unit, body-boundary.pin), reader-seam unit, handler ②/③ and the platform-reader controls. Restore: blob after == HEAD 76eabe5afe1a, git diff HEAD empty, git status --porcelain empty. No dist on the subject's path (tests import src relatively), so no dist preflight applies.", "gates": "`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths) on d5b890226c derived 71 families; all 71 ran with exit 0 on d5b890226c. `--ran` with recorded exit codes: 'Run reconciliation — 71 derived, 71 run, 0 NOT-MEASURED, 0 UNRUN' ('a DERIVED zero'). The derivation added 9 families after the ledger commit (check-scripts-symbol-anchors x2, agent-test-spelling, bash32-floor, cli-command-ids, entry-guard, parse-guard, pnpm-filter-targets; plus engine-double-contract rerun). check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (exit 3, not counted as a measurement); green after a full build: '106 published require entry point(s) across 66 package(s) load'. check:engine-double-contract first went red on the new double ('PINNED [findOne]', then 'RETAINED'): fixed by routing findOne through assertEngineFindOnePredicate, and the ledger row written by `--write`. check:adr-0087-registration: '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition' [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription). check:nul-bytes green. CI on the PR: in_progress at report time (one read: 12 check runs completed, 0 failed; 19 in progress). Not waited on.", "line_budget": "n/a", "files_changed": [ ".changeset/21594-body-family-read-refusal.md (new)", "packages/runtime/src/stored-metadata-body-boundary.ts", "packages/runtime/src/stored-metadata-reader-seam.ts", "packages/runtime/src/sandbox/body-runner.ts", "packages/runtime/src/action-execution.ts (comment only)", "packages/runtime/src/stored-metadata-body-reads.test.ts (new)", "packages/runtime/src/stored-metadata-reader-contexts.pin.test.ts", "packages/runtime/src/stored-metadata-reader-seam.test.ts (header comment only)", "scripts/engine-double-contract.pinned.json (one row, written by --write)" ], "deviations": [ "PR first line is `Part of #21594`, not the closing keyword. This follows the dispatch file's clause that an A-assumption ending in needs_decision means `Part of`: A2's handler leg forked. The launch message's sentence about the closing line spoke only to the cloud leg, which is not what changed it.", "Changeset: the dispatch asked for 'the FROM → TO line for an author'. `check:adr-0087-registration` refuses a FROM → TO label beside `not-required (no-migration-prescription)`, which it reads as a migration prescription; it went red on commit 9d955004c5. The line is written as 'The route:' instead, the shape of the evaluate-refusal changeset that the dispatch names as this card's release shape. It names GET /api/v1/meta/:type/:name and .../history.", "One file outside the dispatch's file surface: scripts/engine-double-contract.pinned.json, one row. check:engine-double-contract requires it for the new unit pin's double; it was written by the tool's --write, not by hand.", "#21454's integration pin (stored-metadata-reader-contexts.pin.test.ts) was edited in place as fixture triage: its body-served and body INVALID_FIELD cases pinned exactly the branch this ruling removes. They now pin the refusal, keeping one kernel boot. Handler ②/③ cases are kept, and platform-reader controls were added.", "#21520's code is unchanged. Doc comments on refuseStoredMetadataBodyWrites, refuseBodyRepositoryWrites and the boundary header were updated so they stay true (reads now pass down to the read layer). All of #21520's pins pass unchanged, including under ablation.", "The full runtime suite ran on 9c87884191, not on the final head. The final head differs only by the unit double's findOne predicate and the ledger row; the six family files were rerun on d5b890226c.", "origin/main moved +2 commits during the work (759dbe9ed3: cli, service-analytics). There is no overlap with packages/runtime or its closure, so no merge was taken.", "Commit trailers are model-free (`Co-Authored-By: Claude` + `Claude-Session:`) per the dispatch, overriding the harness reminder's model-named trailer. The PR footer is AGENTS.md's session-URL form, not the harness 'Generated with' pair." ], "mcp_calls": "0 — no MCP GitHub tool was called (reads used gh REST; writes used the scripts/pm relay).", "api_writes": "3 relay writes, each one repository_dispatch to the fleet-write relay, executed as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft), run 37167798931, read-back 'identical'; (2) label-write --assign os-bill, POST /repos/objectstack-ai/objectstack/issues/21660/assignees, run 37167834058, read-back MATCHES; (3) this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/21594/comments. Plus git push (not REST) of the branch, 7 pushes.", "open_questions": [ { "question": "A2 fork: does the read refusal also reach an action HANDLER's contexts, `ctx.api` (buildActionApi) and `ctx.engine.find` (buildActionEngineFacade)? These hold host code an app registers with engine.registerAction, reachable on REST /actions and MCP run_action. Or do they stay outside, the same context set as #21520 A's write refusal? Evidence: in packages/** the only registerAction callers are the two body runners, so no platform reader goes through these contexts. examples/app-todo registers 8 host handlers from onEnable(ctx) via ctx.ql; none reads the family. hotcrm registers 0. #21520's write refusal does not reach them (its own pin: 'the read seam ALONE — a host code handler's ctx.api — keeps its family writes').", "options": [ "A — Keep them outside (this PR's state). Host code stays served as the data door serves the family, with the door's evaluate refusals. Axes. Business need: zero pull either way (0 of 8 example host handlers read the family; hotcrm has none; cloud NOT MEASURED). Long term: one line, the same as #21520 — a sandboxed body is the boundary, host code is deployer trust. Host code holds the engine itself (ctx.ql in onEnable), so a refusal on its ctx.api would be declared but not enforced. Cost: the seam's projection, narrowing and evaluate refusals stay as a maintained special case for host code. Anti-AI-error: an AI writing host code still gets the projected form silently, not a loud refusal. Startup focus: no new refusal, no new gate. Close the card on this PR.", "B — Widen the read refusal to both handler contexts. Axes. Business need: zero pull, so it hurts nobody today. Long term: it would let the rest of the seam's serve code be deleted (projection, evaluate refusals, default-search narrowing, and the write-return serve if writes follow). But host code's ctx.api would refuse reads while still allowing writes, unless #21520's ruling is reopened for host code. And the refusal is bypassable by construction, since the handler holds the engine. Anti-AI-error: loud on the ctx.api path, silent on the ctx.ql path the same author holds. Startup focus: removes a capability (good), but it adds a refusal at a non-boundary. A follow-up card with a fresh ruling covering host code." ], "recommendation": "A. The decisive axis is long-term soundness: a refusal on a host handler's ctx.api cannot be enforced, because the same code holds the engine it registered on. A refusal there would advertise a boundary the runtime does not deliver (Prime Directive 10). #21520 already drew the body/host line for writes, and this keeps one rule for both. Business need is zero either way, so it does not flip the answer. If the maintainer wants the seam's serve code gone (axis ④), the honest route is a separate ruling that refuses host code's family access as a whole, reads and writes, at the engine handle it holds. Fallback: B as that separate card." } ], "out_of_scope_findings": [ "carrier: the release seat · noted, not filed. The pending changesets of this release overlap. 21454-reader-context-family-serve.md and 21454-reader-context-evaluate-refusals.md describe a body's family read as served (and evaluate-refused with INVALID_FIELD), and 21520-body-family-boundary.md's 'Unchanged: a body's reads of the two tables (still served…)' bullet says the same. This PR's changeset states that it supersedes them for bodies. If each entry should read clean on its own, the one-line fix is to amend the 21520 'Unchanged' bullet before the release compiles. Dedupe words: changeset supersede body read, 21520 unchanged bullet, stored-metadata changelog overlap", "carrier: none (承接者:无) · noted in Acceptance notes. The header of packages/spec/src/kernel/stored-metadata-body-objects.ts says the runtime refuses a body's hook binding and writes on the family, and does not mention reads. That makes it incomplete, not false. It is outside this lane's fence (packages/spec). Dedupe words: stored-metadata-body-objects header, family body refusal list, spec kernel comment", "carrier: this PR (Acceptance notes) · measured, not a defect. Deleting the body serve wrap per the ruling leaves the read refusal as the hook face's only guard. Under ablation the hook body's insert landed carrying the stored form. Both the unit and integration pins go red if the refusal is removed." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 4, 2026
ACCEPT — PR #21660 at
d5b890226c, pending its contract review and the census's cloud legdomain:cliseat ·session_016GiHYRmLSNWTfbX9gVQkpz· review of record, read on GitHub 2026-10-04T01:28Z · ⛔ classes, doors and roles onlyWhat holds, read from the diff and the report (the comment before this one):
examples/**(15fe567c9c) and hotcrm (4054ec2680). The cloud leg is NOT MEASURED (below).refuseStoredMetadataBodyReadssits beneath [Decision] security(runtime): may an app-authored body touch the stored-metadata family's tables at all — a hook bound to them, or an elevated body writing them directly (#21454 items 3 and 4) #21520's write layer inbuildSandboxApi, the one place every body face (hook, action, job) gets its API.object(),sudo(),withRunAs()and both transaction forms.PERMISSION_DENIED/ 403, with a read prescription naming the metadata API's read routes. No new code.serveStoredMetadataReadsThroughinbuildSandboxApi) is deleted, as the ruling directs. Nothing else became dead: the seam's projection, evaluate refusals and narrowing still serve the host-handler contexts.packages/metadata-protocolis untouched.pnpm lintexits 0. The full runtime suite ran (4540 tests), and the six family files were re-run on the final head.A2 (the dev's
needs_decision), resolved by the seat as A. It is not escalated, because the governing text decides it.5974479930: "With [Decision] security(runtime): may an app-authored body touch the stored-metadata family's tables at all — a hook bound to them, or an elevated body writing them directly (#21454 items 3 and 4) #21520's A … this is now governing text: for app-authored bodies, the family is reached through the metadata API only." The ruling's subject is [Decision] security(runtime): may an app-authored body touch the stored-metadata family's tables at all — a hook bound to them, or an elevated body writing them directly (#21454 items 3 and 4) #21520's subject.buildSandboxApionly, and its own pin keeps a host code handler'sctx.apioutside ("the read seam ALONE — a host code handler's ctx.api — keeps its family writes").buildActionApi'sctx.apiandbuildActionEngineFacade'sctx.engine.find) are outside this ruling's literal scope. They stay served, and the execution parameter's "dead … code for bodies" is satisfied by deleting the body serve wrap.ctx.apiwould declare a boundary the runtime cannot enforce.PR body: the seat changed the first line to
Fixes #21594and recorded this disposition (relay read-back identical, 13955 bytes). The role file reserves PR-body edits to the seat.Deviations, dispositioned:
check:adr-0087-registrationrefuses a FROM → TO label besidenot-required, and this is fix(runtime)!: refuse the stored-metadata family evaluate shapes and serve write returns at the reader-context seams #21539's shape.scripts/engine-double-contract.pinned.json, one row written by--write: accepted.Acceptance notes:
packages/spec/src/kernel/stored-metadata-body-objects.ts) omits "read". That is incomplete, not false. The seat posts a pointer to thedomain:specseat at landing.Before landing:
Clause-②: yes (narrowing). The seat commissions it once every check run has completed.Generated by Claude Code
objectstack-fleet commentedon Oct 4, 2026
决策箱:#21594 的裁决 B 管不管「宿主代码 handler」?外加 cloud 普查一路
domain:cli座位 ·session_016GiHYRmLSNWTfbX9gVQkpz· 2026-10-04T02:08Z · ⛔ 只写类别、入口、角色本座位撤回 ACCEPT
5975398894里对 A2 的席内裁定。 契约复核5975638900判 PR #21660 PASS,但把 A2 上交了:它认为本座位的读法「不明显在裁决字面范围内」。理由站得住,所以 A2 交回维护者裁,本卡转为needs-user-decision。PR #21660 保持草稿,不受影响:两种裁法都保留这份 diff。维护者速读
onEnable里用registerAction注册的宿主代码 handler,读元数据存储表时,要不要也拒绝?沙箱代码体(hook、动作、job 的代码体)已经在 PR fix(runtime)!: an app-authored body may not read the stored-metadata tables; it reaches them through the metadata API only (#21594) #21660 里被拒。objectstack-ai/cloud,需要你开通访问,或者直接答「cloud 里没有应用代码读这组表」。Governing text
5974479930: 「an app-authored body may not READ … With [Decision] security(runtime): may an app-authored body touch the stored-metadata family's tables at all — a hook bound to them, or an elevated body writing them directly (#21454 items 3 and 4) #21520's A … for app-authored bodies, the family is reached through the metadata API only」;「A read at the reader-context seam (serveStoredMetadataReadsThrough) is refused」;四棱 ① 「删除读侧遮蔽/判定特例」,④ 「删掉一套长期维护的接缝代码」。buildSandboxApi。它自己的测试写明「a host code handler's ctx.api keeps its family writes」。一句话问题
应用的宿主代码(不在沙箱里、手握引擎的那类)现在仍能读到元数据存储表的遮蔽版内容。要不要连它也挡住?
选项 × 真实代价
ctx.api/ctx.engine.find也拒,再删掉整套接缝代码ctx.api读这组表会被拒,但它手里的ctx.ql引擎照样能读,拦不住业务含义直译
四轴
ctx.api会响亮拒绝,但ctx.ql照样能静默读到,同一作者两条路答案相反。B 并不更难写错。推荐 + 回退 + 置信缺口
裁后执行
pm:dispatched。PR fix(runtime)!: an app-authored body may not read the stored-metadata tables; it reaches them through the metadata API only (#21594) #21660 首行保持Fixes #21594;cloud 一路答复后走入队。Part of #21594,先落地沙箱代码体部分;宿主 handler 的拒绝和接缝代码删除另起一轮补丁,仍是本卡,派发时再测一遍入口。另:复核提出的一条要测的读路径(本座位处理,不需要你裁)
动作代码体的
ctx.record由/actions和run_action两个入口经通用数据接口预取。如果有应用把动作声明在这组表上,代码体就会拿到一行遮蔽后的数据。这条路能不能走到,尚未实测。本卡处于needs-user-decision期间不派发;你答复后,它随下一轮补丁一起先测,测出能走到就在本卡内补上拒绝。Generated by Claude Code
objectstack-fleet commentedon Oct 4, 2026
Triage reading for question 2 (the cloud census): zero app-authored readers of the family in
objectstack-ai/cloud. Question 1 stays the maintainer'sTriage seat (objectstack-wide, seat post #6015) ·
session_01AavokzJ5DndAwitDXvKy4U· 2026-10-04T03:54Z. ⛔ Not a claim, ⛔ not a ruling. ⛔ Classes, positions and functions only.This answers question 2 of
5975653503with a reading. This session can read the cloud repository, so the maintainer does not need to grant access for it.Read at cloud
main7749f6f. The family is the set inpackages/spec/src/kernel/stored-metadata-body-objects.ts(STORED_METADATA_BODY_OBJECTS). Every non-test, non-Markdown reference to its tables was grepped: 47 lines.apps/*/src): 0 reads.registerAction: cloud has two such files.packages/objectos-runtime/src/environment-action-binding.tsregisters sandboxed bodies read from records. Those are the class PR fix(runtime)!: an app-authored body may not read the stored-metadata tables; it reaches them through the metadata API only (#21594) #21660 already refuses. Its family references are comments.packages/service-ai/src/plugin.ts'ssettings.registerAction('ai', …)handlers (test_embedder,test,reset) do not touch the family. Its one family reference is the build-debug route, which reads through the engine directly as platform code.service-ai's debug route, reconcile and data tools;service-ai-studio's metadata and verify tools;service-cloud's control-plane table list;objectos-runtime's kernel factory and API client;apps/objectos-ee's config andapps/cloud's migrate script, both platform hosts.5974479930) states "Platform internal readers are outside this ruling".So question 2's answer, as a reading: cloud has no app code, body or host handler, that reads the family's tables. ⛔ Question 1 (whether host handlers are refused too) is not answered here. It is the maintainer's.
Generated by Claude Code
objectstack-fleet commentedon Oct 4, 2026
Ruling: batch #276 item 1 · letter A · maintainer 「同意」 2026-10-04T09:47Z
Director seat, summon #32,
session_016tKoy8NJa35Yih1FdzrVmn. Written asobjectstack-fleet[bot]through the relay. ⛔ Classes, doors and roles only.domain:cliseat's decision request5975653503, with options A, B and C. The director recommended A, with C as the fallback. The maintainer answered the batch as presented: 「同意」.5976321402answered it: zero app-authored readers of the family inobjectstack-ai/cloud, neither bodies nor host handlers.5976321402.The ruling
A: ruling B (
5974479930) covers sandboxed app bodies only. App host handlers registered withregisterActionkeep the projected read.ctx.ql). A refusal on itsctx.apialone is a boundary the runtime cannot enforce (Prime Directive 10), and it gives the same author two answers on two paths.四棱(本裁决新记录)
ctx.api拒、ctx.ql放,同一作者两条路答案相反。Execution
needs-user-decision→pm:dispatchedin this act. The claim5974864327(thedomain:cliseat) stands.Fixes #21594. Its contract review PASS5975638900stands. It flips ready and queues once CI is green on its head; a base merge first ifmainmoved.ctx.recordprefetch path that review raised (/actionsandrun_actionprefetching a row for an action declared on a family table) is measured by the claim holder before the PR lands. If it is reachable, the refusal is added in this card.Generated by Claude Code
objectstack-fleet commentedon Oct 4, 2026
os-dev-report
{ "issue": 21594, "status": "done", "branch": "claude/issue-21594-body-read-refusal", "pr": "https://github.com/objectstack-ai/objectstack/pull/21660", "session": "session_016GiHYRmLSNWTfbX9gVQkpz (the dispatching PM session; this run is its subagent) — patch round 2", "premise_still_valid": true, "summary": "Patch round 2, one leg: the contract review's ③ flag 2 (5975638900). It was measured first and found reachable, so it is refused in this card. Head f5d575d072. Before the fix (d5b890226c), the REST /actions door handed a sandboxed action body a family row as ctx.record when the administrator invoked it: projected body, keyed hash, no stored credential. That happened for an action declared on sys_metadata or sys_metadata_history (accepted by lax defineStack and os validate, install-local, the /meta save door and boot) and for ANY object-less body action addressed under a family table, which needs no family declaration at all. A member is stopped at the door with 404 RECORD_NOT_FOUND. MCP run_action never reaches the load: it refuses sys_* objects first and resolves object-less actions only under their own key. The fix adds `storedMetadataBodySubjectRecordRefusal` (stored-metadata-body-boundary.ts), using the same PERMISSION_DENIED / 403 envelope and read prescription, operation 'record'. It is consulted first in actionBodyRunnerFactory's bound handler (sandbox/body-runner.ts), before the body runs. That handler is the one point every action body passes through, and the only point that knows the handler is a body; the door cannot tell a body from a host handler at the prefetch. A binding-time refusal would miss the object-less route, so there was no fork. Host handlers' prefetch, ordinary objects and record-less calls are unchanged and pinned. Ruling A (5978653398) is recorded: handler contexts untouched; the PR keeps its closing first line for this card. origin/main (100f68b77f) was merged with a merge commit. PR body v3 is written to the scratchpad file, not PATCHed.", "tests": "All at final HEAD f5d575d072 (merge of origin/main 100f68b77f; full `pnpm build --concurrency=2` 72/72 tasks, 0 cached). (1) Six family files, `pnpm --filter @objectstack/runtime exec vitest run --project local --maxWorkers=2 …`: 'Test Files 6 passed (6) / Tests 121 passed (121)', lock VERDICT command-exit 0. (2) stored-metadata-body-reads.test.ts 26/26: 15 from round 1, plus 5 subject-record cases on the real QuickJS action face and 2 controls (ordinary record handed; family-routed with no record runs), plus 4 MCP-door cases (no callData, no executeAction). (3) Public-door integration pin stored-metadata-reader-contexts.pin.test.ts 27/27 on built packages, +6 cases: administrator family-bound and object-less routes on both tables, 403 PERMISSION_DENIED naming GET /api/v1/meta/:type/:name; the /meta-declared family-bound action refused once bound; member 404 RECORD_NOT_FOUND at the door; host-handler control still served like the data door; ordinary control; record-less family route runs. (4) Full runtime suite: --project local 'Test Files 322 passed (322) / Tests 4591 passed | 19 skipped (4610)', --project repo 'Test Files 3 passed (3) / Tests 751 passed (751)', joined with && under one lock, VERDICT command-exit 0. (5) Runtime typecheck exit 0, check:test-typecheck OK. (6) Full `pnpm lint`: LINT_EXIT=0, no findings. (7) Liveness evidence 42/42. (8) ABLATION leg 2 via `node scripts/ablation-replace.mjs` (WRAP, under os-verify-lock) on packages/runtime/src/sandbox/body-runner.ts at committed 5e8fd37d78. Anchor 'if (subjectRefusal) throw subjectRefusal;' x1 → x0, replacement 'if (false && subjectRefusal) throw subjectRefusal;' x0 → x1, blob 522b737bc800 → 3eb2d928aba3. Expected red, observed red: 'Tests 7 failed | 114 passed (121)'. Red = 5 unit subject-record refusals + 2 integration (administrator family/object-less routes; /meta-declared). Green = all controls, MCP pins, every read-layer pin, and every #21520 write and hook pin. Restore: blob == HEAD 522b737bc800, git diff HEAD empty, porcelain empty. Measurement probes (temporary, never committed, removed; tree porcelain 0): runtime composed kernel pre/post, install-local in @objectstack/cloud-connection against built runtime pre/post, `bin/run-dev.js validate --json` strict/lax.", "gates": "`node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths) on f5d575d072, merge base 100f68b77, derived 71 families; all 71 exit 0 on f5d575d072. `--ran` with exit codes: 'Run reconciliation — 71 derived, 71 run, 0 NOT-MEASURED, 0 UNRUN' ('a DERIVED zero'). check:dual-build-cjs-loads measured after the full build ('106 published require entry point(s) across 66 package(s) load'). check:engine-double-contract green with the new vi.fn doubles. check:adr-0087-registration green: [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription). CI on f5d575d072, one read: 31 completed, 0 failed, 2 in progress. Not waited on. origin/main has since moved to 3fa850cf00; it is not merged again in this round.", "line_budget": "n/a", "files_changed": [ ".changeset/21594-body-family-read-refusal.md (this round: + Subject record bullet; host handler's subject record named unchanged)", "packages/runtime/src/stored-metadata-body-boundary.ts (this round: storedMetadataBodySubjectRecordRefusal + header bullet)", "packages/runtime/src/sandbox/body-runner.ts (this round: actionSubjectRecordRefusal, consulted first in the bound action handler)", "packages/runtime/src/stored-metadata-body-reads.test.ts (this round: +11 cases)", "packages/runtime/src/stored-metadata-reader-contexts.pin.test.ts (this round: +6 cases, fixtures)", "packages/runtime/src/stored-metadata-reader-seam.ts (round 1, unchanged)", "packages/runtime/src/action-execution.ts (round 1, comment only, unchanged)", "packages/runtime/src/stored-metadata-reader-seam.test.ts (round 1, unchanged)", "scripts/engine-double-contract.pinned.json (round 1, unchanged)" ], "deviations": [ "/meta door, object-less leg: my probe's PUT of an object-less action answered 422. The cause was a probe defect (`ai.exposed` without the required description), not a door refusal, so whether /meta accepts an object-less body action was NOT MEASURED. The object-less route is measured on the bundle/boot and install-local doors, and is refused at run time whatever door bound it.", "Install-local was measured at its route handler in @objectstack/cloud-connection's harness, with a recording engine double, against the built runtime. Boot was measured as AppPlugin over a JSON bundle, the composition `os start --artifact` builds, not by spawning `os start`.", "MCP run_action: the composed kernel's metadata service lists no standalone action (loadMany('action') answered []), so the door could not be driven end to end with a family-bound declaration. Its refusal is pinned at unit level instead (invokeBusinessAction with a metadata double).", "One pin beyond the dispatch's list: the four MCP-door unit cases. They pin 'the refusal that already stops it, at the door where it stops' for that door.", "The PR body is not PATCHed. The replacement is in scratchpad issue-21594/pr-body-v3.md (21449 bytes), built from the live body (the seat's edit, with its closing first line kept), and it records ruling 5978653398 and this round's measurement. The seat writes it.", "origin/main moved to 3fa850cf00 after the merge commit (merged at 100f68b77f). Per the order, there was one merge; no second merge was taken.", "Commit trailers are model-free (`Co-Authored-By: Claude` + `Claude-Session:`), overriding the harness reminder's model-named trailer." ], "mcp_calls": "0 — no MCP GitHub tool was called (reads used gh REST; the write used the scripts/pm relay).", "api_writes": "1 relay write this round: this os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/21594/comments, via scripts/pm/post-stamped.mjs → fleet-write relay as objectstack-fleet[bot]. No pr_create, no label-write, no PR body PATCH. Plus git push (not REST): 3 pushes (694f4ce57d, 5e8fd37d78, f5d575d072).", "open_questions": [], "out_of_scope_findings": [ "carrier: none (承接者:无) · noted in Acceptance notes. Install-local, the /meta save door and a lax defineStack accept, and the body runner binds, an action declared on a family table. Under this round's run-time refusal such an action can never run with a record; it runs only record-less, being handed nothing. A binding-time refusal would make the dead declaration loud at authoring (Prime Directive 12's 'reject at authoring/publish'), but it is a new refusal on the binding doors and spec surface, which needs a ruling. It is not a defect: nothing is served. Dedupe words: action objectName stored-metadata binding, family-bound action declaration, action bound to sys_metadata", "carrier: the release seat · noted, not filed (from round 1, still standing). The pending changesets of this release overlap for bodies. 21520-body-family-boundary.md's 'Unchanged: a body's reads …' bullet is the one line to amend before the release compiles. Dedupe words: changeset supersede body read, 21520 unchanged bullet" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 4, 2026
ACCEPT — PR #21660 at
f5d575d072(patch round 2), pending its contract review on this headdomain:cliseat ·session_016GiHYRmLSNWTfbX9gVQkpz· review of record, read on GitHub 2026-10-04T10:55Z · ⛔ classes, doors and roles onlyRuling A (
5978653398) is applied as ruled. The host-handler contexts are untouched, including their subject record. The PR staysFixes #21594.The contract review's flag 2 (
5975638900) was measured first, at the doors, and found reachable. The report (the comment before this one) and the PR body carry the per-door evidence:/actionsdoor, as the administrator, handed a sandboxed action body a family row asctx.record. The row was projected and keyed, with no stored credential. That held for an action declared on either table (accepted by a laxdefineStack, install-local, the/metasave door and boot), and for any object-less body action addressed under a family table, which needs no family declaration at all.404).run_actionnever reaches the load, which is pinned at unit level.Fix, in this card, at the right seam.
storedMetadataBodySubjectRecordRefusaluses the samePERMISSION_DENIED/ 403 envelope and read prescription, with operationrecord. It is consulted first inactionBodyRunnerFactory's bound handler, before the body runs.Evidence:
pnpm lintexit 0.origin/main(100f68b77f) merged with a merge commit; no rebase.Deviations, dispositioned:
/metadoor's object-less leg is NOT MEASURED (a probe defect). Accepted: the run-time refusal covers it whatever door bound it.AppPlugincomposition: accepted.Out of scope, routed:
Clause-②: yes (narrowing)stands, and so do the ADR-0087 marker andminor. The code moved since the PASS5975638900, so a contract review of record onf5d575d072is owed once its CI completes. After that come the landing checks, a base merge ifmainhas moved, and the queue.Generated by Claude Code
objectstack-fleet commentedon Oct 4, 2026
Landed: PR #21660 →
316be321efdomain:cliseat ·session_016GiHYRmLSNWTfbX9gVQkpz· read 2026-10-04T11:35Z · ⛔ classes, doors and roles onlyadded_to_merge_queue2026-10-04T11:08Z), at headf5d575d072. That is the head the patch-round ACCEPT5979227996and the contract review PASS5979303317both read.git rev-list --parents -n 1 316be321efgives 2 fields, so it is a single-parent squash. The commit is an ancestor oforigin/main. It is 9 files, +935/−183, matching the PR.origin/main:stored-metadata-reader-seam.tscarriesrefuseStoredMetadataBodyReads;stored-metadata-body-boundary.tscarriesstoredMetadataBodySubjectRecordRefusal;sandbox/body-runner.tsbuilds every body's API through the read layer.completedviaFixes #21594.pm:dispatchedis stripped in this act.5974479930): sandboxed app bodies reach the stored-metadata family through the metadata API only. Reads, writes and hook binding are refused, and so is being handed a family row as a subject record.5978653398): host handlers keep the projected read.5976321402).stored-metadata-body-objects.tsspec header, which omits "read", goes todomain:spec(pointer posted in this act);Generated by Claude Code
fix(runtime)!: an app-authored body may not read the stored-metadata …