Repository navigation
feat(runtime): a script body sends mail and adds a timeline entry through platform doors that stamp who sent it - #32966
Conversation
…ough platform doors that stamp who sent it A sandboxed hook, action or job body gains two doors, each behind its own capability token: - `email.send` -> `ctx.email.send(input)`: the message goes through the mail service. The sender is the deployment's configured address, with a display name naming the calling user beside the organization; the sending user and the organization are stamped by the platform. At most 50 recipients per message, and at most OS_EMAIL_APP_SEND_HOURLY_LIMIT (default 200) caller-sent messages per organization per hour. - `activity.append` -> `ctx.activity.append(input)`: one entry on a record's activity timeline, written by the activity feed's owner. The actor, the organization and the origin (`<package>/<kind>:<name>`, new readonly `sys_activity.origin` column) are stamped by the platform; the values only the platform records are refused. Every record a door is pointed at is read in the caller's scope first. The mail bounds are one implementation shared with the HTTP send door. An outbox row the mail service did not write is sent as the configured sender. Clause-②: yes (widening) Metadata-protocol: added Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwV8gsjrvCXCqKoaiG
…he platform writes Tests, generated artifacts, docs and the changeset for the two body doors: each door refuses an input naming a stamped value, refuses a record the caller cannot read, and holds its recipient and hourly limits; the outbox delivers a row the mail service did not write from the configured sender; an app's timeline entry may not use a platform-only type or key. Clause-②: yes (widening) Metadata-protocol: added Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwV8gsjrvCXCqKoaiG
…emas and writer Clause-②: no Metadata-protocol: none Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwV8gsjrvCXCqKoaiG
…ope read Clause-②: no Metadata-protocol: none Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwV8gsjrvCXCqKoaiG
📓 Docs Drift CheckThis PR changes 9 package(s): 106 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 18 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 197 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin a919081c10d612db806b8b4fff2571d28087fb36 && git checkout a919081c10d612db806b8b4fff2571d28087fb36
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 994c7d673d3cebeccdf15993e190c2a0f03a2353 3a2c53069ab126fd38aef64f7accc859daec5f1a && git checkout -B drift-repro 994c7d673d3cebeccdf15993e190c2a0f03a2353 && git merge --no-ff 3a2c53069ab126fd38aef64f7accc859daec5f1a
node scripts/docs-audit/affected-docs.mjs --json 994c7d673d3cebeccdf15993e190c2a0f03a2353
|
|
security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw Contract-review-claim: session_016WV4cx5nHfga3Hm9BzikGw · head efbf75c · 2026-11-13T01:08Z The implementing thread may not review its own change; the lane seat takes it (CONTRACT_REVIEW_TIER). |
|
security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw Contract reviewServed-tier: Inputs read: issue #32004 (body and all 21 comments), PR #32966 (body, comments 6840737853 and 6840743876, the 81-file list, the net diff against Findings are graded M (must-fix), S (should-fix), N (nit). Part of M1, two should-fix findings and two notes are held by the lane seat and are not written in this record; their grades count in the verdict. ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentDeviation (a): the 50-recipient total is held in the runtime door, and the schema caps each slot. Acceptable as built. Deviation (b): the hourly limit counts every M1 (must-fix), S1 (should-fix), S2 (should-fix), S3 (should-fix), S4 (should-fix), N1 (nit), N2 (nit), N3 (nit), N4 (nit), N5 (nit), Files that hold as written: M1 is a FAIL. The fixes of M1 and S1 to S4, and of the findings the lane seat holds, earn a delta review by this reviewer on the new head. Implemented-by: VERDICT: FAIL |
…input, and stay out of a body's transaction - A hook body's door reads a pointed-at record in the scope of the user whose write fired it. - Every string a door input takes is bounded; cross-field limits are stated in the published description. - A door call inside ctx.api.transaction is refused; an entry's origin names the package that holds the body. - A body's message is sent only if the mail log records it. - A mail-log row the mail service did not write is sent from the configured sender with no sender header of its own. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwV8gsjrvCXCqKoaiG
… refill the origin help from its source Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwV8gsjrvCXCqKoaiG
|
security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw Contract-review-claim: session_016WV4cx5nHfga3Hm9BzikGw · head 8878bc8 · 2026-11-13T02:47Z Delta review of the FAIL (record 6841079039) by the same reviewer. |
|
security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw Contract reviewServed-tier: This is the delta review on record 6841079039 (head Inputs read: issue #32004 (body and all 21 comments; none was added after 6841079039), PR #32966 (the rewritten body, comments 6840737853, 6840743876, 6841079039 and 6842306427, the 88-file list, the delta from Findings are graded M (must-fix), S (should-fix), N (nit). The findings of 6841079039 that the lane seat holds were read again on this head, and one finding on this delta is held the same way; none of them is written in this record, their dispositions go to the lane seat, and their grades count in the verdict. ① Derived judgmentsDisposition of record 6841079039:
New surface on this head:
② Semver level
③ Boundary flags
④ Design judgmentThe hook face (M1). Taking the scope from the hook api and not from the session is the right design: only the api holds the whole context of the write, and the door now makes the same two calls the hook wrapper makes for Declared deviation N1: S5 (should-fix), N6 (nit), N7 (nit), Files that hold as written on this head: No must-fix stands on this head. S5 and the finding the lane seat holds are should-fix: each fix earns a delta review by this reviewer, or the lane seat records, with the maintainer's word, that it is carried into step 2. N6 and N7 are nits. The two escalations in ③ are the maintainer's to answer before the merge. Implemented-by: VERDICT: PASS |
…its or rolls back with its write Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwV8gsjrvCXCqKoaiG
|
security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw Contract-review-claim: session_016WV4cx5nHfga3Hm9BzikGw · head 5ce459b · 2026-11-13T03:48Z Delta review of record 6842516227 (S5 and the maintainer's 2026-11-13 ruling on mail from hook bodies, N6, N7), same reviewer. |
|
security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw Contract reviewServed-tier: This is the delta review on record 6842516227 (head Inputs read: issue #32004 (body and all 21 comments; none was added after 6842516227), PR #32966 (the body on this head, comments 6842516227 and 6843211443, the 92-file list, the delta from Findings are graded M (must-fix), S (should-fix), N (nit). The finding of 6842516227 that the lane seat holds was read again on this head; its disposition goes to the lane seat and is not written here. ① Derived judgmentsDisposition of record 6842516227:
New surface on this head:
② Semver level
③ Boundary flags
④ Design judgmentThe hook rule. Refusing by face is the right implementation of the ruling. Whether a hook's write stands in a transaction depends on the driver and on the error policy of the other hooks on the same object ( The action and job rule (item 3). Right. The reason the ruling gives for hooks, that a message cannot be taken back, holds for any body whose surrounding work can still roll back. An action or a job stands in a transaction only when its caller opened one, so the answer follows the caller's own choice and not unrelated metadata. It is also the same answer the door has given inside the body's own transaction since The timeline rule (item 4). Right, and the simpler of the two readings: the ambient case needs no code, and the threaded case passes one handle to one writer. One limit stays by design: inside the body's own S6 (should-fix), N8 (nit), Files that hold as written on this head: No must-fix stands on this head. S5 is closed. S6 is should-fix: its fix earns a delta review by this reviewer, or the lane seat records, with the maintainer's word, that it is carried into step 2. N8 is a nit. The escalations in ③ are the maintainer's and the lane seat's to settle before the merge. Implemented-by: VERDICT: PASS |
…d at parse, at bind or in the picker Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwV8gsjrvCXCqKoaiG
…s the hook grant flag Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cwV8gsjrvCXCqKoaiG
|
security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw Contract-review-claim: session_016WV4cx5nHfga3Hm9BzikGw · head 3a2c530 · 2026-11-13T04:39Z Delta review of record 6843313448 (S6, N8 and the PR body's citation of the ruling), same reviewer. |
|
security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw Contract reviewServed-tier: This is the delta review on record 6843313448 (head Inputs read: issue #32004 (body and all 22 comments; 6843314411 was added after 6843313448), PR #32966 (the body on this head, comment 6844161549, the 99-file list, the delta from Findings are graded M (must-fix), S (should-fix), N (nit). ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentRefusing at three places is the right shape, and each has its own reader. The parse is where an author meets the rule, before a deployment. The bind covers a hook that reaches the runner without that parse, and it fails loudly for one hook and leaves the others bound. The call stays for any body that reaches the door some other way. One sentence, with the same reason and remedy, serves all three. Placing the parse check on the hook and not on the shared body schema keeps one capability set for the three faces, which is how the published enum reads. Leaving the build's inference alone is also right: the build then fails on the handler's own call, at the author's desk, and the extractor needs no knowledge of the face. N9 (nit), N10 (nit), Files that hold as written on this head: No must-fix and no should-fix stands on this head. S6 is closed. N9 and N10 are nits, and nits alone end the round. The two escalations that remain in ③ are the maintainer's to answer before the merge. Implemented-by: VERDICT: PASS |
|
security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw 席位核对 — ACCEPT · security#1 · head
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 43371520158 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Requested by Elon · project thread
Refs #32004 (step 1 of ruling 6796076408: the doors come first).
维护者速读
packages/runtime/src/sandbox/body-doors.ts(两个通道的规则)、packages/spec/src/data/hook.zod.ts(保存时脚本不能申请发信权限)、packages/plugins/plugin-email/src/outbox-sender-bound.ts(发信箱发件人限制)。pnpm --filter @objectstack/runtime exec vitest run body-doors,预期全部通过。也可以在 Console 打开一个 hook 的能力选择器,确认里面没有email.send。3a2c53069ab126fd38aef64f7accc859daec5f1a。批准时请一并确认三点:第 1 步的设计(裁决 6796076408);发信箱发件人限制放在第 1 步(你 11-12 22:18Z 已在线程里同意);动作和定时任务在事务里发信也会被拒,这一点超出了 11-13 裁决的字面范围。What changes
A hook, action or job body gets two platform doors, each behind its own capability:
ctx.email.send(input)(capabilityemail.send, in an action or a job body) hands a message to the mail service. The message is sent from the configured sender, as the calling user, under the caller's organization. Its display name names the user beside the organization.ctx.activity.append(input)(capabilityactivity.append) adds an entry to a record's activity timeline. The calling user is the actor, and the package and body are the entry'sorigin.The rules each door holds:
400 VALIDATION_ERROR.related, the entry's record, itssource) must be one the caller can read in the caller's own scope, or the call is refused with404 RECORD_NOT_FOUNDand nothing is written or sent. For a hook body, the caller is the user of the write that fired it, read with that user's grants and never as the system.to,ccandbccfits 4,000 characters as the mail log records it, and areplyTo320; an entry'smetadatafits 8,000 characters as JSON. The limits that read across fields are stated in each schema's published description.OS_EMAIL_APP_SEND_HOURLY_LIMITmessages (default 200) in the last hour, a send is refused with429 RATE_LIMIT_EXCEEDED. The count is read before the send, so it bounds volume rather than an exact number.SendEmailInput.requireLogRow); otherwise the call is refused with503 SERVICE_UNAVAILABLE.email.sendis refused at parse (the metadata save door,os validate, the build, an artifact's parse) and at bind; the Console's picker does not offer the grant on a hook; and a call from a hook body is refused with403 PERMISSION_DENIED. Mail on a record change goes through an action, a job or a flow.409 RESOURCE_CONFLICT.ctx.api.transactionis refused with409 RESOURCE_CONFLICT.originnames the package that holds the body.Around the doors:
boundCallerMail(@objectstack/core) holds the bounds a caller-sent message is held to, and the REST mail door and the body door share it. The REST door's answers are unchanged.@objectstack/plugin-email): aqueuedmail-log row the mail service did not write is sent from the configured sender, with noFrom,SenderorReturn-Pathheader of its own; with no configured sender the mail log refuses it. The changeset carries the migration and the ADR-0087 disposition.sys_activitygains a read-onlyorigincolumn, empty on an entry not written through the activity door.@objectstack/objectqlexportsseedHookUserGrantsandseedHookUnreadMembershipbesidederiveHookApi, andopenTransactionKind, which answers whether a write runs inside a transaction that a write beside it joins ('joined'), one it cannot reach ('separate'), or none.os buildinfers both capabilities from a handler's calls; a hook handler that callsctx.email.sendfails the build, which refuses the inferred grant. The Console's capability picker lists both, and on a hook does not offeremail.sendand flags a stored grant.Why
Step 1 of ruling 6796076408 on #32004: apps get declared doors for mail and timeline entries before anything else changes.
Verification
hook-body-doors.test.tsandhook-body-mail-grant.test.ts(spec),hook-body-mail-grant.test.ts(cli),HookDefaultInspector.capabilities-26726.test.tsx(app-shell),caller-mail-bounds.test.ts(core),outbox-sender-bound.test.ts,email-service.test.tsandemail-plugin.outbox-sweep.test.ts(plugin-email),app-activity-door.test.ts(plugin-audit),body-doors.test.ts,body-doors-capability.test.ts(a real QuickJS body end to end),body-doors-caller-scope.integration.test.tsandbody-doors-transaction.integration.test.ts(a real engine, SQLite driver and hook wrapper; the second also pinsopenTransactionKindon a real engine) andapp-artifact-handlers.test.ts(runtime),open-transaction.test.ts(objectql), the deprecation notice inflow-system-identity-write-refusal.test.ts(plugin-security), and capability inference inextract-hook-body.test.ts/hook-body-parameter-rebind.test.ts(cli).dist/a concurrent closure build was rewriting were re-run green), specsrc/data6201, objectqlopen-transaction+hook-run-as39, app-shell metadata-admin inspectors 2992, and the CLI hook-body, extractor and lowering tests 142. Unchanged since the previous heads and green there: plugin-audit 1458, plugin-email 1028, core 3466, plugin-security 6575, rest 8765, platform-objects 1955, including the published@objectstack/cli/hook-bodypin, whose ratifiedcapabilitiesunion names the two tokens.openTransactionKindreads the engine it is handed (1 red).node scripts/pm/dispatch-gates.mjs --ranreconciled 133 derived families, 133 run, on head 8878bc8. On the heads since, the families each commit touches were rerun and are green (53 on this round: spec generated artifacts, dropped refinements and API surface, the type-check, census, error-status, i18n, test-source-alias and docs families, the UI script tests, the changeset gates);pnpm lintis green.mainwas merged once, before the first push. The diff is 3,893 changed lines, over the 3,000-line human-merge threshold.check:generated14/14, including the API surface, export origins and the reference page for the new bounds), i18n bundles forsys_activity.origin, the strictness ledger, the tenant-audit and system-context censuses,llms.txtcounts, the ADR-0122 pin.BodyMailSendInputandBodyActivityAppendInputarez.input; the alias gate refuses an isomorphic pin for a name ending inInput, so they carry none.Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up owed — skills/objectstack-data/references/data-hooks.md (the
ctxsurface table and "Capabilities — the complete list" do not yet nameemail.sendandactivity.append)Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_016WV4cwV8gsjrvCXCqKoaiG