Repository navigation
Commit 704efef
feat(mcp): read_file and create_upload_link read and attach a record's files (#31009)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} -->
_Requested by **Elon** · [project
thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9DRxqCFSpBBvnTxKi4CsWK3E)_
domain:server#4 · os-elon-musk · session_016WV4cyqd3A7fbCWrqM3KCz
Fixes #30892
Clause-②: yes (widening) — two MCP tools join the HTTP surface where the
storage service is installed (`read_file` under `data:read`,
`create_upload_link` under `data:write`), and two signed-in routes join
the storage family (`GET` and `PUT
/api/v1/storage/upload-links/:token`). Nothing that answered before
answers differently.
Metadata-protocol: none
Claim: 6691779259 (`domain:server#2`, take-over on handoff 6690566522;
seat 4's claim 6685332126 released) · Branch:
`claude/project-thread-s2vcdk` · Rulings: option B, sign-in required
(6691366983); D1 / D2 / D3 from the seat (D1 below) · Triage: 6685125943
· Contract review: 6691738005 (PASS, 0 must-fix; should-fix 1 and 2 done
on this head)
## 维护者速读
- 改了什么:MCP 新增两个工具。`read_file` 读一条记录上的文件(文件字段或附件),返回文本(PDF、Word
在服务端抽取,图片作图片块)、短时下载链接、大小和读取上限;`create_upload_link`
给一条可编辑记录的某个文件字段(或附件列表)发一个 10 分钟内只能用一次、只限本人登录后使用的上传链接,用户在已登录的浏览器里打开选文件即可。
- 为什么改:卡片 #30892,agent 以前既读不到记录上的文件,也没法把文件挂到记录上。权限完全跟随记录:读不到记录就答
`get_record` 同样的 404,下载门会拒的文件答下载门同样的 403。
- 风险与回滚:新增两条需要登录的路由(上传链接页和 `PUT`),未登录答 401,不是匿名入口。按你的裁决(方案
B,6691366983),链接只限发链接的本人登录后使用。令牌 256 位随机、库里只存 SHA-256、条件更新保证只用一次、过期 10
分钟;已用/过期/不存在/别人的链接答同一个 404。链接行里记的使用人必须等于该行只读的上传人 `owner_id`,有 `sys_file`
编辑权的人改写链接行也拿不到链接。上传以发链接的用户身份重新解析权限后落库,走记录自己的更新门。新依赖 `unpdf` 1.9.1 只在
`@objectstack/mcp`、按需加载。回滚即撤 PR。
- 先看:①
`packages/services/service-storage/src/record-file-access.ts`(`openLink`
的各项检查,以及 `consumeUploadLink` 的顺序:开链、校验、大小门、重解析身份、条件更新花掉链接、落库);②
`packages/services/service-storage/src/storage-routes.ts` 的
`upload-links` 两条路由;③ `packages/mcp/src/file-content.ts`(PDF/Word
抽取与各上限常量)。
- 两分钟验证:`pnpm --filter @objectstack/service-storage exec vitest run
src/record-file-access.integration.test.ts`,预期 22
条全绿:访问跟随记录、大小上限、单次使用(并发两次只成一次)、过期、绑定一条记录一个字段、未登录 401 与他人 404
都不花掉链接、改写过使用人的链接行答 404 且不花掉、超限 413 不花掉链接。
- 需要你做的:本 PR 改动 4,000 多行,超过 3,000 行的人工审核门槛,需你经 os-zhuang 批准一次,之后由 server
席位 2 落地。
## Why
#30892: an agent connected over MCP could read a record's fields but not
the files on it (a receipt, a signed contract, a photo), and had no way
to put a file on a record. The card's ruling is two tools, access
following the record, a named size cap, and a short-lived single-use
upload link bound to one record and field; direct protocol upload stays
out of scope.
## What changes
**`@objectstack/service-storage`** — the record's file doors, as the
caller.
- `record-file-access.ts` (new) holds three caller-scoped verbs on the
`file-attachments` service: `listRecordFilesAs`, `readRecordFileAs`,
`createUploadLinkAs`, plus the link door the routes call
(`openUploadLink`, `consumeUploadLink`).
- The record is read as the caller first (`findOne` under the caller's
context). Unreadable, missing or refused answers `404 RECORD_NOT_FOUND`,
the sentence `get_record` answers.
- A file field's id is read off the record; an attachment is found by a
caller-scoped `sys_attachment` read bound to that record, so an
attachment id from another record answers `404`. A field holding several
files asks for `fileId` and lists the ids.
- The file's read is judged by the download door's own verdict
(`judgeFileRead`, the plugin's, unchanged and bound in; ADR-0104
public-read, attachments parent read, field owner). A refusal answers
that door's `403 ATTACHMENT_DOWNLOAD_DENIED` / `FILE_DOWNLOAD_DENIED`. A
file that is not committed (or deleted and not held) answers `404
FILE_NOT_FOUND`. No second access judgment.
- Bytes are read only when the size is within the caller's cap; the
download link is the doors' own `mintDownloadUrl` with the doors' TTL
(300 s).
- **The upload link.** `createUploadLinkAs` asks the record's edit door
before minting: attachments use `assertMayAttach` (the attach door's
verdict); a field uses `mayEditAttachmentParent` (`403
PERMISSION_DENIED`) and the caller's writable fields (`403
FIELD_NOT_ACCESSIBLE`). The link is one pending `sys_file` row whose
`name` holds its state, whose `metadata` holds the binding (object,
record, field or attachments, user, organization, expiry, secret
digest), and whose `owner_id` (readonly) is the minting user.
- **Spending a link**, in this order: the request's own session (none:
`401 AUTH_REQUIRED`); open (token shape, row, state, digest, expiry, the
session's user is the binding's user, and the binding's user is the
row's `owner_id`); validate the body; the size gate
(`uploadSizeRefusal`, `413 PAYLOAD_TOO_LARGE`) before anything is spent;
re-resolve the minting user (`resolveUserAuthzGrants` with the grants
cache bypassed, and `vetOrganizationClaim` on the bound organization;
unbacked answers `403 PERMISSION_DENIED`); the conditional update that
spends the link; then the write as that user. A field upload is written
through `engine.update` as the user, so the record's write RLS, the
field's `accept` / `maxSize` and the file-reference hooks apply exactly
as for any edit; an attachment goes through `attachFileAs`. Any refusal
after the spend discards the bytes and the row. The link row is deleted
after use.
- `metadata-store.ts`: `updateFileIfUnchanged(store, id, seen, patch)` —
a conditional `update` on `{ id, status, name }` with `multi: true`,
answering how many rows moved. (The JSON `metadata` cannot be the guard:
a `{…}`-shaped string where-value is read as a filter placeholder.)
- `storage-routes.ts`: `GET /api/v1/storage/upload-links/:token` (the
page) and `PUT …/:token` (the upload), mounted through
`mountFamilyRoute` (the ADR-0069 session gate applies) and ledgered
`server-only` (`storage-route-ledger.ts`). Both read the request's
session first and answer `401 AUTH_REQUIRED` without one, failing closed
when no session resolver is wired, so neither is an anonymous door
(ADR-0138 D2). `mintDownloadUrl` and `DEFAULT_DOWNLOAD_TTL` are now
exported so the doors and the verbs share them. `upload-link-page.ts`
renders the page; `upload-link-wire.ts` holds the one unusable-link
status, code and sentence.
- `storage-service-plugin.ts` binds the verbs to the plugin's own
`judgeFileRead`, servability check and the kernel's authz resolution
(`bindRecordFileAccess`).
**`@objectstack/mcp`**
- `read_file` (`objectName`, `recordId`, and `field` with optional
`fileId`, or `attachmentId`; neither lists the record's files) answers a
facts block (id, name, type, size, field or attachment, `maxBytes`,
download link) and then the content: text for text types, PDF and
`.docx`; an MCP image block for an image up to 5 MiB. Over the 10 MiB
cap it answers the facts and the link without content.
- `create_upload_link` (`objectName`, `recordId`, and `field` or
`attachments: true`) answers `url`, `expiresAt`, `singleUse: true`, the
binding, and one line of instructions (open it in a browser where the
user is signed in to this deployment).
- Both register only where the bridge carries the members, under the
OAuth scope gates (`data:read`, `data:write`), refuse `sys_*` objects as
the other data tools do, and render refusals with the ADR-0112 envelope.
- `file-content.ts` (new): the extractors and the named bounds —
`READ_FILE_MAX_BYTES` 10 MiB, `READ_FILE_TEXT_MAX_CHARS` 100 000,
`READ_FILE_PDF_MAX_PAGES` 200, `READ_FILE_DOCX_XML_MAX_BYTES` 32 MiB,
`READ_FILE_IMAGE_MAX_BYTES` 5 MiB. Every extractor answers a sentence on
a bad file (encrypted, damaged, no text layer, not a Word document); a
net around them keeps a file from ever failing the tool.
- `skill-md.ts`: the served `SKILL.md` teaches each tool only where it
is registered (`fileTools` option, scope-gated).
**`@objectstack/runtime`** — `domains/mcp.ts`: the bridge's file members
read the record through `get_record`'s door (`callData('get')`) FIRST,
so an unreadable record is that door's refusal and storage is never
asked; then call the storage verbs as the request's principal. A local
download path and the upload link's path come back absolute on the
deployment's public origin (the auth service's MCP resource URL, else
the request's host and forwarded proto); an adapter's absolute URL is
left as it is. Without the storage service the members are absent and
neither tool is listed.
**Docs** — `content/docs/ai/connect-mcp.mdx` (two tool rows and a
"Files" section), `content/docs/api/plugin-endpoints.mdx` (the two
routes). Two censuses the gates hold to the tree:
`content/docs/permissions/tenant-audit-census.mdx` and its counts ledger
(268 engine write call sites on the merged tree: `main`'s 266 plus the
link's field write and the conditional spend; regenerated by
`tenant-audit-census.mjs --write`, the page's hand-stated figures moved
to match), and `content/docs/permissions/system-context.mdx` row 47e,
which now names the record-file verbs beside the attach verbs (both
refuse the system context through a `requireUserContext`, so the row's
key is pinned per file).
### D1 — the PDF text extractor (seat ruling)
`unpdf` at exactly `1.9.1` (pinned, no caret), a dependency of
`@objectstack/mcp` only, loaded by dynamic `import()` on the first PDF
read. Reasons, as ruled: it is pdf.js packaged for server runtimes with
no native dependency (its `@napi-rs/canvas` peer is optional and not
installed), so the text layer is pdf.js's; 1.9.1 rather than 2.x keeps
the engines floor the workspace already has; the dynamic import keeps
the cost off every MCP request that never reads a PDF. It runs with
`isEvalSupported: false` (pdf.js compiles no font code with `eval`),
`disableFontFace: true`, `useSystemFonts: false`, only on bytes that
already passed the 10 MiB cap, and stops at 200 pages or 100 000
characters. A malformed or encrypted PDF answers a sentence, pinned.
`.docx` is read with `node:zlib` (`inflateRawSync` with
`maxOutputLength` at 32 MiB) over a minimal ZIP reader that opens only
`word/document.xml`; a decompression bomb answers a sentence, pinned.
`pnpm-lock.yaml` carries only the three `unpdf` hunks.
### D2 and D3 (seat: as recommended)
- **D2**: the link record is a pending `sys_file` row plus its
`metadata`, so no new object and no spec change.
- **D3**: the upload is judged twice. At mint, as the MCP caller (the
agent's own ceiling included): the attach verdict, or the record read,
the record-update verdict and field writability. At upload, as the
minting user with grants re-resolved, through the existing caller verbs
(`attachFileAs`, or the stored file plus the record's own update as that
user), so field security, RLS and validation judge it. The alternative,
judged at mint and written as the system (the signed-URL precedent), was
not taken.
### The upload-link security surface
- **Token**: `UUID.SECRET`, where the UUID is the link row's id and the
secret is 32 random bytes (`randomBytes`, 256 bits) in base64url.
Shape-checked before any read.
- **At rest**: only the SHA-256 of the secret is stored, in the row's
`metadata`; it is compared with `timingSafeEqual`. The secret is in no
log and no answer after the mint.
- **Single use**: a conditional update moves the row from `name:
'upload-link'` to `'upload-link (used)'` where `id`, `status: 'pending'`
and the open name still hold, and the spend counts only when exactly one
row moved. Two racing uploads: one `200`, one `404` (pinned in both
suites).
- **Expiry**: 600 s from the mint, checked when the page opens and again
when the upload arrives. An expired, never-used link row stays `pending`
and is reaped by the existing pending-upload retention
(`attachment-lifecycle.ts`, 7 days).
- **Whose link it is**: only the user it was minted for, signed in
(ruling 6691366983). No session answers `401 AUTH_REQUIRED` on both
routes and spends nothing; a session naming another user gets the
unusable-link answer below and spends nothing (both pinned).
- **The binding cannot be moved to another user.** The row's `name` and
`metadata` are not `readonly` columns, so the link opens only when the
binding's user is the row's `owner_id`, the readonly uploader column
written at mint. A row whose binding was rewritten to name another user
answers the unusable-link `404` to that user and to the minting user
alike, and is not spent (pinned, ablated). The binding's other columns
(record, field, organization) rest on `sys_file` edit being an
administrative grant, as the module doc states.
- **One answer for used, expired, unknown, malformed and another
user's**: page `404` with one page; upload `404 RESOURCE_NOT_FOUND`,
"This upload link cannot be used: it was used already, it has expired,
it was made for someone else, or it does not exist. Ask for a new link."
- **The page**: `Content-Security-Policy` with a per-response nonce
(`default-src 'none'; script-src 'nonce-…'; style-src 'nonce-…';
connect-src 'self'; form-action 'none'; frame-ancestors 'none'; base-uri
'none'`), `Cache-Control: no-store`, `Referrer-Policy: no-referrer`,
`X-Robots-Tag: noindex`, `X-Content-Type-Options: nosniff`,
`X-Frame-Options: DENY`. It names no record and no user. Its script
`PUT`s to its own address with `credentials: 'same-origin'`, carrying
the session cookie the route asks for.
- **Whose upload it is**: the minting user's, re-resolved at use, so
access revoked between mint and use refuses the upload (`403`, nothing
lands; pinned). The binding comes from the row only; headers and query
strings naming another record or field are ignored (pinned).
## Tests
-
`packages/services/service-storage/src/record-file-access.integration.test.ts`
(new, 22): a real ObjectQL on SQLite with the plugin's own wiring and a
write-side RLS fixture — read and its cap, access on read (unreadable
record, download-door refusal), multi-file fields, a non-file field,
attachments and the listing; the mint (token shape, TTL, secret not
stored), access on mint (`404`, `403 PERMISSION_DENIED`, attachments
parent), the page's headers, the binding (headers ignored), the
attachments link, no session (`401`, not spent), another signed-in user
(`404`, not spent), a row whose binding was rewritten to another user
(`404` for both users, not spent), single use (forged, unknown,
malformed: one `404`), the race, expiry, the `413` that does not spend
the link, revoked access (`403`, nothing lands), and a field's `maxSize`
refused with the bytes removed.
- `packages/mcp/src/mcp-file-tools.test.ts` (new, 21): both tools over
the HTTP transport, argument refusals, the listing, a real PDF's text
through `unpdf`, a damaged PDF, a `.docx`'s text, a `.docx` bomb, an
image block, the over-cap answer, and the skill lines per scope.
- `packages/runtime/src/mcp-record-file-bridge.test.ts` (new, 5): the
record is read through `get_record`'s door first, the principal,
absolute links, no members without the seam.
- `packages/qa/dogfood/test/showcase-mcp-record-files.dogfood.test.ts`
(new): the booted showcase with security, storage and MCP, a member and
the admin over `/api/v1/mcp` — `showcase_invoice_line.receipt` (a file
field under `controlled_by_parent`) and `showcase_invoice` attachments
(owner RLS): mint, page, upload with a forged target, anonymous `401`
and another signed-in user's `404` without spending the link, the
committed `sys_file` row of its byte size, second use, unknown token,
expiry, `read_file` of the PDF's text and its download bytes, the
admin's records refused with `get_record`'s code on both tools, an
attachment of another record unreadable through the member's own, the
race, and the `413` before the spend.
- Updated: the storage route count pins (10 → 12), the ledgered-route
list, the MCP unknown-argument sweep (fifteen tools with main's
delete_records) and skill surface guard (the two tools), and a comment
in the route-ledger parity dogfood.
## Acceptance notes
- **Ruled: the link needs a sign-in (option B, ruling 6691366983).** The
first build served the link as a bearer capability; the booted
anonymous-door sweep (ADR-0138 D2) put its two routes in the
everything-else class, which must answer `401`. The maintainer ruled B:
the link works only for the user it was minted for, signed in, built in
`04ff76bc`. Both routes answer `401 AUTH_REQUIRED` without a session,
and the sweep holds unchanged with no ADR change. The cost, accepted
with the ruling: a person opening the link on a phone that is not signed
in signs in there first, a forwarded link works for nobody else, and an
agent uploading by itself needs a deployment credential. Option A (a
bearer link plus an ADR-0138 amendment) is not taken.
- **Contract review 6691738005: PASS, 0 must-fix.** Should-fix 1 (the
owner check in `openLink`) is in `8d23d823`; should-fix 2 is this body.
Should-fix 3 (a signed-out browser opening the link gets a JSON `401`,
not a sign-in page) is a follow-up card for the storage lane; the `401`
rule is unchanged here.
- **Over 3,000 changed lines.** The diff is about 4,070 changed lines
with the lockfile and the census ledger, so it lands only after an
authorized approval, then by the owning seat. The owner check adds 34 of
them.
- **Merged, not rebased.** `main` (with #30861, which also touches
`mcp-http-tools.ts`, the unknown-argument sweep and `connect-mcp.mdx`)
is merged in at `c5fe7df9`; that merge was clean. `main` was merged
again at `57724d86`, where the only conflict was the tenant-audit
census: both sides had moved its figures, so the artefacts were taken
from `main`, regenerated over the merged tree, and the page's
hand-stated figures moved to match (`check-tenant-audit-census` green).
The branch was already pushed, so it was not rebased. `main` was merged
a third time at `c2a3229b`, after #31024 (`delete_records`) landed.
Three files conflicted, all on the MCP tool list.
`content/docs/ai/connect-mcp.mdx` now reads "Thirteen tools, generated
from your metadata, and two more for files where the storage service is
installed", and its `tools/list` comment reads "the thirteen tools …
(fifteen with read_file and create_upload_link where storage is
installed)". The unknown-argument sweep advertises fifteen tools
(`toHaveLength(15)`), checked against the stub's real `tools/list`,
which equals `VALID_ARGS` (15 keys, `delete_records` included). In
`skill-md.ts` the import lines are a union (`MCP_OAUTH_SCOPE_DATA_WRITE`
and `DELETE_RECORDS_MAX_PER_CALL`), and the tool list auto-merged with
`delete_records` before the file tools' slot. `main` was merged a fourth
time at `b93069ea` (main 5210acf, after #31061); only the tenant-audit
census conflicted, resolved by taking `main`'s artefacts and
regenerating. `main` was merged a fifth time at `da6a24cb`, after #31072
(a member's browser upload attaches through the commit door). Besides
the census pair, three service-storage files conflicted and keep both
sides: `file-attachment-service.ts` carries main's `attachUploadAs` and
this PR's record-file verbs on one class; `registerStorageFamily` takes
main's `attachUpload` sixth and this PR's `getUploadLinkDoor` seventh,
and `composeStorageRoutes` passes both.
- **The quota.** No byte quota exists in the open tree (measured in the
section 1 report: zero hits in `service-storage`). An upload through a
link meets the same `storage.max_upload_mb` gate before the link is
spent, writes the same committed `sys_file` row with its size, and the
same `storage.upload`, so whatever a host counts toward quota counts it.
A byte quota is cloud's half; nothing here needs it.
- **Strict single use.** A refusal before the spend (no session, another
user, a rebound row, the size gate) leaves the link as it was. An upload
refused after the spend (a field's `accept` or `maxSize`, revoked
access) spends the link; the agent asks for a new one. The size gate
runs before the spend so the common refusal does not.
- **An ADR-0069 hold on the minting user is not re-checked at upload**;
the family gate judges the uploading request's own session, which under
the ruling is the minting user's. Grants and the organization claim are
re-checked.
- **The link path is the plugin's `basePath`.** A host that mounts the
storage family under another base would need the link built from that
base.
- **`getWritableFields` is advisory at mint**; the write itself goes
through the record's update door, which is the judgment that holds.
- **Files beyond the claim's listed surface**:
`packages/mcp/package.json` and `pnpm-lock.yaml` (the D1 dependency),
`content/docs/api/plugin-endpoints.mdx` (the two routes), the two census
pages and the counts ledger (gate-forced, above), and the route-ledger
parity dogfood's comment. Inside `service-storage` beyond calling
existing doors: the link routes, the page, and the conditional update in
`metadata-store.ts`, all of them the one single-use upload-link record
the claim allows.
- **Direct protocol upload** stays out of scope. The link door
(`UploadLinkDoor`) and the bound-target types are where an MCP-side
upload would attach later.
## Verification
**Head da6a24c** (merge of main 24bc259; install and a full
declaration build, 119/119): the 13 MCP dogfood files 204/204
(`showcase-mcp-record-files` among them) plus `anonymous-door-sweep`,
`authz-conformance`, `route-ledger-live-mount-parity` 88/88;
`@objectstack/mcp` 60 files 762/762; `@objectstack/service-storage` 75
files 1424/1424; runtime bridge 5/5; console `objectstack-skill.test.ts`
13/13; typecheck on the three packages exit 0; `dispatch-gates
--commands` on the merge delta (the five resolved files) 82 derived, 82
run, all exit 0, `--ran` 82 / 82 / 0 not measured. The tenant-audit
census was regenerated (268 sites).
**Head b93069e** (merge of main 5210acf, which carries #31061's attach
fix; install and a full declaration build, 119/119): dogfood
`showcase-mcp-record-files` 13/13, the three attachments-list cases
green again; the 12 MCP dogfood files plus `anonymous-door-sweep`,
`authz-conformance`, `route-ledger-live-mount-parity`: 16 files 292/292;
`@objectstack/mcp` 60 files 762/762; `@objectstack/service-storage` 74
files 1396/1396; runtime bridge 5/5; console `objectstack-skill.test.ts`
13/13; typecheck on the three packages exit 0; `dispatch-gates
--commands` on the merge delta 47 derived, 47 run, all exit 0. The
tenant-audit census conflicted once more and was regenerated (267
sites).
**Head c2a3229** (merge of main 5cb7d7e; install and a full workspace
build with declarations, 119/119 tasks): `@objectstack/mcp` 59 files
746/746; `@objectstack/service-storage` 74 files 1389/1389; runtime
`mcp-record-file-bridge.test.ts` 5/5; typecheck on the three packages
exits 0; console `objectstack-skill.test.ts` 13/13 with no `ui/**`
change. Dogfood `showcase-mcp-record-files` had 3 attachments-list cases
red on this head, from main's #31031 (a member's `attachFileAs` refused
on the `sys_file` lookup). They are green on `b93069ea` above, after
#31061 landed the fix on `main`; this PR carries no rider.
<details><summary>Head 57724d8: the owner check (contract review
should-fix 1, 8d23d82) and the second main merge</summary>
- `record-file-access.integration.test.ts`: 22 of 22.
`@objectstack/service-storage` whole suite: 74 files, 1389 of 1389 (one
more than `67847d9c`'s 1388: the new pin).
- **Ablation** through `scripts/ablation-replace.mjs --delete` of `if
(row.owner_id !== state.user_id) refuseUnusableLink();` (anchor 1 → 0,
blob e63efe2 → 7625cc64): "a row whose binding names another user than
its stamped uploader is no link" turned red, `AssertionError: expected
200 to be 404` (the rebound row opened for the other user), Tests 1
failed, 21 passed of 22. Restored: blob equals HEAD (e63efe2), `git
diff HEAD` empty.
- After the merge at `57724d86` (install, full workspace build with
declarations, 119 of 119 tasks): `@objectstack/service-storage` 74
files, 1389 of 1389; `@objectstack/mcp` 58 files, 714 of 714;
`@objectstack/runtime` `mcp-record-file-bridge.test.ts` 5 of 5;
`typecheck` on the three packages and `tsc --noEmit` on the dogfood exit
0; dogfood `showcase-mcp-record-files`, `anonymous-door-sweep`,
`authz-conformance`, `route-ledger-live-mount-parity` and
`mcp-system-object-action-exposure`: 5 files, 114 of 114.
- Derived gates on `57724d86` for this round's delta (the two
`record-file-access` files and the two census artefacts): 84 derived, 84
run, every one exit 0; `dispatch-gates --ran` reconciles 84 / 84 / 0 not
measured. (On `8d23d823`, before the merge, the 53 derived for the two
code files also passed 53 of 53.) Two of them needed outputs first and
are counted after: `check:dual-build-cjs-loads` and `check:i18n` exit 3
until a workspace build, and `check-plugin-teardown-shape --self-test`
until its control commit is fetched into the shallow clone.
</details>
<details><summary>Earlier heads</summary>
- **On `04ff76bc`**: the derived gates passed 111 of 111.
`dispatch-gates --ran` reconciled them as 111 derived, 111 run, 0 not
measured. One gate, `check-comment-mask-corpus`, failed with ENOENT the
first time because it ran alongside the dogfood and read a temporary
`objectstack.config.bundled_*.mjs`; rerun after the dogfood finished, it
passed (21155 files, 0 disagree). The full dogfood passed:
`os-verify-lock: VERDICT command-exit 0`, Test Files 426 passed and 3
skipped of 429, Tests 3556 passed and 41 skipped of 3597, 0 failed. That
includes the anonymous-door sweep and the authz-conformance pin.
- **On `c5fe7df9`** (after the merge) and `67847d9c`:
`@objectstack/service-storage` 74 files and 1388 of 1388 tests;
`@objectstack/mcp` 58 files and 711 of 711 tests (with #30861's);
`@objectstack/runtime` `mcp-record-file-bridge.test.ts` 5 of 5; `tsc
--noEmit` on all three packages and the dogfood exits 0; 111 of 111
derived gates; the full dogfood on the merge tree: Test Files 429 passed
and 3 skipped of 432, Tests 3573 passed and 41 skipped of 3614, 0
failed. CI on `67847d9c`: 59 success, 7 skipped, 0 failed.
- **Ablations** (on the service-storage integration suite): changing the
spend guard `if (!spent)` to `if (spent < 0)` turns "of two concurrent
uses exactly one lands" red (1 failed of 19 at the time); removing
`deadline <= this.now()` turns the expiry test red. Both restored, blobs
equal to HEAD.
- Earlier heads `ebd0dbc4` and `e3f0aa4e` also passed 111 of 111 gates.
On `bc5ad15b` (before B), the full dogfood had 2 failures: the authz pin
(fixed in `bc5ad15b`) and the anonymous-door sweep, which led to the
sign-in choice the maintainer ruled above.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(`packages/spec` is not touched.)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran. `pnpm lint` is CI's run.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: not applicable.
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
<!-- signed: domain:server#2 · os-tesla ·
session_01L9qibyvxbrLP4jm4PargoH -->
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9c1edbc commit 704efef
30 files changed
Lines changed: 3978 additions & 84 deletions
File tree
- .changeset
- content/docs
- ai
- api
- permissions
- docs/audits
- packages
- mcp
- src
- qa/dogfood/test
- runtime/src
- domains
- services/service-storage/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
245 | 245 | | |
246 | 246 | | |
247 | 247 | | |
248 | | - | |
| 248 | + | |
| 249 | + | |
249 | 250 | | |
250 | 251 | | |
251 | 252 | | |
| |||
257 | 258 | | |
258 | 259 | | |
259 | 260 | | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
260 | 295 | | |
261 | 296 | | |
262 | 297 | | |
| |||
454 | 489 | | |
455 | 490 | | |
456 | 491 | | |
457 | | - | |
| 492 | + | |
458 | 493 | | |
459 | 494 | | |
460 | 495 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
161 | 161 | | |
162 | 162 | | |
163 | 163 | | |
| 164 | + | |
| 165 | + | |
164 | 166 | | |
165 | 167 | | |
166 | 168 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
186 | 186 | | |
187 | 187 | | |
188 | 188 | | |
189 | | - | |
| 189 | + | |
190 | 190 | | |
191 | 191 | | |
192 | 192 | | |
| |||
679 | 679 | | |
680 | 680 | | |
681 | 681 | | |
| 682 | + | |
682 | 683 | | |
683 | 684 | | |
684 | 685 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
93 | 93 | | |
94 | 94 | | |
95 | 95 | | |
96 | | - | |
| 96 | + | |
97 | 97 | | |
98 | 98 | | |
99 | 99 | | |
| |||
132 | 132 | | |
133 | 133 | | |
134 | 134 | | |
135 | | - | |
| 135 | + | |
136 | 136 | | |
137 | 137 | | |
138 | 138 | | |
| |||
160 | 160 | | |
161 | 161 | | |
162 | 162 | | |
163 | | - | |
164 | | - | |
| 163 | + | |
| 164 | + | |
165 | 165 | | |
166 | 166 | | |
167 | 167 | | |
| |||
197 | 197 | | |
198 | 198 | | |
199 | 199 | | |
200 | | - | |
201 | | - | |
202 | | - | |
203 | | - | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
204 | 204 | | |
205 | 205 | | |
206 | 206 | | |
| |||
210 | 210 | | |
211 | 211 | | |
212 | 212 | | |
213 | | - | |
| 213 | + | |
214 | 214 | | |
215 | 215 | | |
216 | 216 | | |
217 | 217 | | |
218 | 218 | | |
219 | 219 | | |
220 | | - | |
| 220 | + | |
221 | 221 | | |
222 | 222 | | |
223 | 223 | | |
224 | | - | |
| 224 | + | |
225 | 225 | | |
226 | 226 | | |
227 | | - | |
| 227 | + | |
228 | 228 | | |
229 | 229 | | |
230 | 230 | | |
| |||
233 | 233 | | |
234 | 234 | | |
235 | 235 | | |
236 | | - | |
237 | | - | |
238 | | - | |
239 | | - | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
240 | 240 | | |
241 | | - | |
| 241 | + | |
242 | 242 | | |
243 | 243 | | |
244 | | - | |
245 | | - | |
| 244 | + | |
| 245 | + | |
246 | 246 | | |
247 | 247 | | |
248 | | - | |
| 248 | + | |
249 | 249 | | |
250 | 250 | | |
251 | 251 | | |
252 | | - | |
253 | | - | |
| 252 | + | |
| 253 | + | |
254 | 254 | | |
255 | 255 | | |
256 | 256 | | |
257 | | - | |
| 257 | + | |
258 | 258 | | |
259 | 259 | | |
260 | | - | |
| 260 | + | |
261 | 261 | | |
262 | 262 | | |
263 | 263 | | |
| |||
307 | 307 | | |
308 | 308 | | |
309 | 309 | | |
310 | | - | |
| 310 | + | |
311 | 311 | | |
312 | 312 | | |
313 | 313 | | |
314 | | - | |
315 | | - | |
| 314 | + | |
| 315 | + | |
316 | 316 | | |
317 | | - | |
| 317 | + | |
318 | 318 | | |
319 | 319 | | |
0 commit comments