Skip to content

feat(runtime): a script body sends mail and adds a timeline entry through platform doors that stamp who sent it - #32966

Merged
os-zhuang merged 10 commits into
mainfrom
claude/project-thread-cz7rky
Nov 13, 2026
Merged

os-zhuang merged 10 commits into
mainfrom
claude/project-thread-cz7rky

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Refs #32004 (step 1 of ruling 6796076408: the doors come first).

维护者速读

  • 改了什么:应用脚本新增两个受控通道。动作和定时任务里的脚本可以发邮件;动作、定时任务和保存时触发的脚本都可以写记录的时间线。发件人、发件用户、组织和操作人都由平台盖章,脚本不能自己填;脚本只能指向调用者自己看得到的记录;每个组织每小时的发信量有上限;邮件必须先记进发信日志才会发出。按你 11-13 的裁决,保存时触发的脚本一律不能发邮件(保存、校验、构建、绑定、调用各环节都会拒绝),要发信请用按钮动作、定时任务或流程。时间线记录随保存一起成功或一起撤销。
  • 为什么改:这是 [PM seat] security · seat 2 — 🟢 os-tesla #32004 裁决 6796076408 的第 1 步:先给应用开正规的发信和时间线通道,第 2 步(另一个 PR)再收紧其他写法。
  • 风险与回滚:对已发布版本是纯新增(Clause-② 扩展,minor)。发信箱一侧标了 BREAKING:应用直接插入的邮件行,发件人一律改为系统配置的发件人;没有配置发件人的部署,这类行会被拒。HotCRM 的 21 个保存自动化都不发信,不需要迁移。回滚就是还原本 PR。改动约 3,900 行,属 Tier H。
  • 先看: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。
  • 需要你做的:请 os-zhuang 或 hotlong 审阅并批准(或你亲手合并)。合同复核最终 PASS 评论 6844228594,Head-sha 3a2c53069ab126fd38aef64f7accc859daec5f1a。批准时请一并确认三点:第 1 步的设计(裁决 6796076408);发信箱发件人限制放在第 1 步(你 11-12 22:18Z 已在线程里同意);动作和定时任务在事务里发信也会被拒,这一点超出了 11-13 裁决的字面范围。
  • 席位意见(security#1):接受(ACCEPT 6844746125)。CI 全绿,四轮复核收口,只剩两个小 nit 留到第 2 步;获批后由席位翻 ready 并入队。

What changes

A hook, action or job body gets two platform doors, each behind its own capability:

  • ctx.email.send(input) (capability email.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) (capability activity.append) adds an entry to a record's activity timeline. The calling user is the actor, and the package and body are the entry's origin.

The rules each door holds:

  • The sender, the sending user, the organization, the actor and the origin are stamped by the platform and are not inputs. A call that names one is refused with 400 VALIDATION_ERROR.
  • A record the call points at (related, the entry's record, its source) must be one the caller can read in the caller's own scope, or the call is refused with 404 RECORD_NOT_FOUND and 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.
  • Every string either input takes has an upper bound. A message names at most 50 recipients; each of to, cc and bcc fits 4,000 characters as the mail log records it, and a replyTo 320; an entry's metadata fits 8,000 characters as JSON. The limits that read across fields are stated in each schema's published description.
  • While an organization's mail log records OS_EMAIL_APP_SEND_HOURLY_LIMIT messages (default 200) in the last hour, a send is refused with 429 RATE_LIMIT_EXCEEDED. The count is read before the send, so it bounds volume rather than an exact number.
  • A message is sent only if the mail log records it (SendEmailInput.requireLogRow); otherwise the call is refused with 503 SERVICE_UNAVAILABLE.
  • A message cannot be taken back, so a hook body cannot send mail (ruling on #32004). A hook whose body is granted email.send is 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 with 403 PERMISSION_DENIED. Mail on a record change goes through an action, a job or a flow.
  • The same rule, carried to the other bodies: an action or a job body that runs inside a transaction is refused with 409 RESOURCE_CONFLICT.
  • A timeline entry commits or rolls back with the write that runs the body.
  • A door call inside the body's own ctx.api.transaction is refused with 409 RESOURCE_CONFLICT.
  • An entry's origin names the package that holds the body.
  • An app's timeline entry may not use a type or a metadata key only the platform writes, and may not be about an object the feed does not show or an append-only object.
  • A body with no calling user (a job) sends and writes with no user recorded.

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.
  • BREAKING (@objectstack/plugin-email): a queued mail-log row the mail service did not write is sent from the configured sender, with no From, Sender or Return-Path header of its own; with no configured sender the mail log refuses it. The changeset carries the migration and the ADR-0087 disposition.
  • sys_activity gains a read-only origin column, empty on an entry not written through the activity door.
  • @objectstack/objectql exports seedHookUserGrants and seedHookUnreadMembership beside deriveHookApi, and openTransactionKind, which answers whether a write runs inside a transaction that a write beside it joins ('joined'), one it cannot reach ('separate'), or none.
  • os build infers both capabilities from a handler's calls; a hook handler that calls ctx.email.send fails the build, which refuses the inferred grant. The Console's capability picker lists both, and on a hook does not offer email.send and flags a stored grant.
  • Inserting into an append-only object from app automation that runs as system is deprecated, and the platform says so once per kind and object, naming the two doors.

Why

Step 1 of ruling 6796076408 on #32004: apps get declared doors for mail and timeline entries before anything else changes.

Verification

  • New tests (names state the rule): hook-body-doors.test.ts and hook-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.ts and email-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.ts and body-doors-transaction.integration.test.ts (a real engine, SQLite driver and hook wrapper; the second also pins openTransactionKind on a real engine) and app-artifact-handlers.test.ts (runtime), open-transaction.test.ts (objectql), the deprecation notice in flow-system-identity-write-refusal.test.ts (plugin-security), and capability inference in extract-hook-body.test.ts / hook-body-parameter-rebind.test.ts (cli).
  • Full suites green on this head: runtime (596 files, 11,676 tests; two files that read a dist/ a concurrent closure build was rewriting were re-run green), spec src/data 6201, objectql open-transaction + hook-run-as 39, 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-body pin, whose ratified capabilities union names the two tokens.
  • Ablation, one rule at a time, then restored green: the door stamps the caller (3 red), the door reads a pointed-at record in the caller's scope (2 red), a hook body's door reads with the firing write's grants and not as the system, through a real engine (2 red), an entry's origin names the package that declares the body (1 red), the capability gates each door (2 red), the outbox sends a non-service row from the configured sender (4 red), an app entry may not use a platform-only type (1 red), the shared bound refuses another user (2 red), a hook body sends no mail (2 red), the parse refuses a hook body's mail grant (2 red), the bind refuses it (2 red), a timeline entry commits or rolls back with a write that carries its own transaction handle (1 red), openTransactionKind reads the engine it is handed (1 red).
  • Gates: node scripts/pm/dispatch-gates.mjs --ran reconciled 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 lint is green. main was merged once, before the first push. The diff is 3,893 changed lines, over the 3,000-line human-merge threshold.
  • Generated artifacts regenerated: spec (check:generated 14/14, including the API surface, export origins and the reference page for the new bounds), i18n bundles for sys_activity.origin, the strictness ledger, the tenant-audit and system-context censuses, llms.txt counts, the ADR-0122 pin. BodyMailSendInput and BodyActivityAppendInput are z.input; the alias gate refuses an isomorphic pin for a name ending in Input, so they carry none.

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up owed — skills/objectstack-data/references/data-hooks.md (the ctx surface table and "Capabilities — the complete list" do not yet name email.send and activity.append)

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cwV8gsjrvCXCqKoaiG

…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
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation protocol:data tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 13, 2026
@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 9 package(s): @objectstack/cli, @objectstack/core, @objectstack/objectql, @objectstack/plugin-audit, @objectstack/plugin-email, @objectstack/plugin-security, @objectstack/rest, @objectstack/runtime, @objectstack/spec, touching 175 documentable anchor(s). ⚠️ 9 changed file(s) yielded no anchor (packages/objectql/src/hook-run-as.ts, packages/objectql/src/index.ts, packages/spec/api-surface/data.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

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

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

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

Which tree this was computed on

This run read content/docs from a919081c10d612db806b8b4fff2571d28087fb36 — the merge of head 3a2c53069ab126fd38aef64f7accc859daec5f1a into base 994c7d673d3cebeccdf15993e190c2a0f03a2353, which is what actions/checkout gives a pull_request run. Not the PR head.

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

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: efbf75c0352fc21b9d096c68c10544176c93ffd1
Local-runs: none
Scope: packages/spec/src/data/feed.zod.ts, packages/spec/src/data/hook-body-doors.zod.ts, packages/spec/src/data/hook-body.zod.ts, packages/spec/src/data/index.ts + packages/runtime/src/sandbox/body-doors.ts, packages/runtime/src/sandbox/body-runner.ts, packages/runtime/src/sandbox/quickjs-runner.ts, packages/runtime/src/sandbox/script-runner.ts, packages/runtime/src/app-plugin.ts, packages/runtime/src/app-artifact-handlers.ts, packages/core/src/security/caller-mail-bounds.ts, packages/core/src/security/index.ts, packages/plugins/plugin-email/src/outbox-sender-bound.ts, packages/plugins/plugin-email/src/email-plugin.ts, packages/plugins/plugin-audit/src/app-activity-door.ts, packages/plugins/plugin-audit/src/audit-plugin.ts, packages/plugins/plugin-audit/src/objects/sys-activity.object.ts, packages/plugins/plugin-security/src/flow-system-identity-write-refusal.ts, packages/plugins/plugin-security/src/security-plugin-authz-middleware.ts, packages/rest/src/email-routes.ts, packages/cli/src/utils/extract-hook-body.ts, ui/packages/app-shell/src/views/metadata-admin/inspectors/ScriptCapabilitiesField.tsx

Inputs read: issue #32004 (body and all 21 comments), PR #32966 (body, comments 6840737853 and 6840743876, the 81-file list, the net diff against main from merge-base 1a6c3309), and the check-runs on the head as of 2026-11-13T01:26Z. Read outside the diff, not reviewed: the engine's read options and hook session (packages/objectql/src), the permission-set resolver and the authorization middleware (plugin-security), the mail service and the sys_email object, the activity stamp hook, scripts/check-spec-parsed-alias.mjs, packages/spec/src/shared/refinement-projection.ts. Tests, the changeset text, docs/**, content/docs/**, generated files and translations are the lane seat's ACCEPT.

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

  1. HookBodyCapability gains email.send and activity.append. Right. It is one accept set, read by the capabilities array of a hook, action and job body, and it only widens. Declared equals enforced on all three faces: the VM checks the token before it reads the input or reaches the host seam (installDoor, throwCapabilityDenial never returns), and a host built without kernel services answers 501 NOT_IMPLEMENTED.
  2. BodyDoorRecordRefSchema, BodyMailSendInputSchema, BodyActivityAppendInputSchema, their three types and BODY_MAIL_RECIPIENT_LIMIT are new exports of @objectstack/spec/data. Right as additions. Every object is .strict(), so a stamped key is refused, not dropped, and the PR body's 400 VALIDATION_ERROR sentence holds. requiredOneOf(['text', 'html']) is an arm of the closed projectable list, so the body rule reaches the published JSON Schema. Wrong in two respects: the published schema does not state the 50-recipient total (S1), and several strings carry no upper bound (S4).
  3. SYS_ACTIVITY_PLATFORM_TYPES is a new constant. Right. satisfies holds it to a subset of SYS_ACTIVITY_BUILTIN_TYPES, which is unchanged, so the writer census is not owed. The column's vocabulary stays open: the door refuses ten values and admits every other.
  4. ExtractedBody.capabilities on @objectstack/cli/hook-body widens in output position. Right: the ratified union in published-subpath-hook-body.pin.test.ts moves with it and the changeset names it.
  5. @objectstack/core gains boundCallerMail, mailAddressOf, isPlainMailObject, SENDER_HEADER_NAMES and three types. Right, additive. @objectstack/rest keeps SENDER_HEADER_NAMES by re-export. The REST door's bounds were read against main sentence by sentence: same order (sentBy, headers, from, organizationId, identityMail), same texts, same forwarded message. "The REST door's answers are unchanged" holds.
  6. sys_activity.origin is a new read-only column. Right, additive.
  7. AppArtifactHandlerBindingOptions.getService, ScriptContext.email and ScriptContext.activity are optional additions to the runtime's public types. Right.
  8. plugin-email: an outbox row the mail service did not write has its from_address replaced by the configured sender before it lands, and emptied where none is configured. This is no accept-set change. It changes what an existing direct insert sends; see ② and ③.
  9. plugin-security: a notice only, once per elevation kind and object, and no refusal. Right against ruling 6796076408 ("until step 3 lands, elevated automation's inserts into append-only objects are not judged").
  10. Check-runs on the head: 44 success, 9 skipped, 0 failed, 12 still running. Every lint, build and gate job is green, including Check Changeset, Governed Surface Queue Guard, Spec property liveness, PR-body guards, Lint · docs & code guards, Lint · PM dispatch-gates, the three finished Type Check jobs and Live E2E UI. Pending, not judged: Test Core 1/8, 3/8, 7/8 and 8/8; Test UI 1, 6, 9, 10, 13 and 16 of 16; Type Check UI; Type Check · workspace.

② Semver level

  • @objectstack/spec takes minor: right. Metadata-protocol: added is the right kind (two new accepted values an author can write), and a kind takes at least minor on a package the diff moves.
  • The other eight packages take patch: right by AGENTS.md step 3 (a non-protocol change ships patch).
  • Clause-②: yes (widening) is right for the accept sets, and both declaration lines are line-initial in the PR body and in the changeset.
  • The PR body carries no Skills-docs: line. A kind other than none owes one. The lane seat settles it before ACCEPT.
  • The changeset ships the outbox sender bound as a patch line with no BREAKING banner. It takes the choice of sender away from a row an app inserted itself. Whether that is a breaking behaviour change is the lane seat's reading at ACCEPT, together with the escalation in ③.

③ Boundary flags

  • The PR body declares no open question. The two declared deviations are judged in ④.
  • Escalated, rule text. On [PM seat] security · seat 2 — 🟢 os-tesla #32004 the only rule text for this step is ruling 6796076408 (2026-11-11T12:40Z; the brief dates it 2026-11-09), read with 6787890900. It says: one door sends mail through the mail service, and the other appends to the timeline through a platform door that stamps the actor and the provenance. No step design beyond that is on the issue. The 50-recipient cap, the hourly limit and its environment variable, the platform-only type list, the reserved metadata keys, the outbox sender bound and the deprecation notice stand on the PR body alone. The lane seat names where the maintainer approved them, or puts them to the maintainer.
  • Escalated, step order. The ruling puts first-party apps onto the doors in step 2 and judges inserts only in step 3. The outbox sender bound changes what a direct insert sends in step 1, before any app has moved. The maintainer confirms that it belongs to step 1, or it moves.
  • Open question for step 2. The deprecation notice also fires for a flow's system identity and names two body doors. A flow has no door of its own in this diff; it reaches one only through an action it calls.
  • Check-runs: 12 pending, listed in ①.10. This verdict does not rest on them.
  • The PR is a draft with no auto-merge armed, so no disarm is owed. Its author is objectstack-fleet[bot].

④ Design judgment

Deviation (a): the 50-recipient total is held in the runtime door, and the schema caps each slot. Acceptable as built. recipientCount counts a bare address as one and a list by its length, over all three slots, before anything is read or sent, and the schema's max bounds each list. The sum has no exact JSON Schema spelling, so it cannot join the closed projectable list. One gap follows, S1.

Deviation (b): the hourly limit counts every sys_email row of the organization in the last hour, so a job's sends count too. Acceptable as built. It is the conservative direction, it is read from the shared log so every node sees one count, and a job is the sender a volume limit most needs to cover. The code agrees with the PR body's sentence where the caller has an organization. Mail the door did not send (the HTTP door, other services) counts toward the number and is never refused by it, so that mail can close the door; identity mail is in its own log and is not counted. See N4.

M1 (must-fix), body-runner.ts hookDoorCaller. On the hook face the door does not read the pointed-at record in the caller's scope. readContext is executionContextFromHook(engineCtx): four fields copied from the hook session (userId, tenantId, positions, isSystem). That function's own docblock calls it "the closest scope reachable on this path rather than a copy of the read path's". The read door judges a caller by the permission-set names its context carries (resolvePermissionSetsForContextUnmemoized reads context.permissions), and invitation-placement.ts records that a hand-built context resolves "to the additive baseline and NOTHING else". The hook session carries no permissions. So the record is judged against a grant set that is not the caller's, and a record the caller reads through an assigned permission set is answered RECORD_NOT_FOUND. The PR body's sentence, "a record the call points at must be one the caller can read", is therefore not what the hook face holds. The action face is right: callerScopeOf hands back the caller's own envelope with the body's elevation removed. The hook face owes the same scope, built by the same function from the context of the operation that fired the hook. The hook test (body-doors-capability.test.ts) runs on a ql double that ignores the context, so no test reads through the real door.

S1 (should-fix), hook-body-doors.zod.ts. The published surface does not state the recipient total. BodyMailSendInputSchema's description, its JSON Schema and the generated reference page carry the per-slot max and nothing about the sum, so a document they accept is refused by the door. That is the direction refinement-projection.ts calls silent. State the total where the published schema is read: in the schema's .describe() text, or as a declared refinement whose site the dropped-refinement ledger names on the artifact.

S2 (should-fix), quickjs-runner.ts installDoor and body-doors.ts. A door call does not ride a transaction the body opened. installDoor never reads txState, and the host seam reads and writes through the root engine with contexts that carry no transaction handle. The engine's own docblock says a VM body's ctx.api.transaction handle is threaded explicitly and is invisible to the ambient store. The neighbouring ctx.title seam reads txState.api at call time for this reason, and its comment records the cost of not doing so: a second connection, and a deadlock on a one-connection pool, the objectstack dev default. On a driver with transactions, a door call inside ctx.api.transaction therefore cannot read a record the body just wrote, and its timeline entry stays when the body rolls back. Ride the open transaction, or refuse a door call inside one with a sentence that says so.

S3 (should-fix), app-artifact-handlers.ts and body-runner.ts installBodyDoors. The package part of origin is the binder's appId for every group. The binder groups each package body's hooks and actions under that package's own id (hookBindingGroups, actionBindingGroups), but builds one body runner per bundle with the bundle's appId. In a bundle that carries packages[], origin names the bundle and not the package that holds the body, and two packages' bodies of one name stamp the same origin. The column's description and the PR body both say the package. Jobs take the same appId.

S4 (should-fix), hook-body-doors.zod.ts. subject, replyTo, an address's text, type, recordLabel, object, id and the size of metadata carry no upper bound; only summary does. On the timeline side every sys_activity field is read-only, so the record validator bounds none of its columns and the door is the only place a bound can sit. A bound added after this schema is published is a narrowing. Decide the bounds now.

N1 (nit), hook-body-doors.zod.ts. BodyMailSendInput and BodyActivityAppendInput read z.infer; ADR-0122 gives the author-state name to z.input. The two shapes coincide today. check-spec-parsed-alias.mjs treats a name ending in Input as not bare, so the gate is green and neither schema is pinned isomorphic. Write both as z.input and pin them, as the diff does for BodyDoorRecordRef.

N2 (nit), feed.zod.ts. The docblock of SYS_ACTIVITY_BUILTIN_TYPES still tells an author that "an app's server-side action can insert one directly". This diff deprecates that insert and adds the door. Name ctx.activity.append there.

N3 (nit), body-doors.ts callerScopeOf. For an absent context it answers the system scope. No caller in this diff passes one. An exported function named for the caller's scope should answer a scope with no principal.

N4 (nit), body-doors.ts refuseOverHourlyLimit. A caller with no organization is counted against the whole log, and the refusal still says "this organization's mail log". Say which count was read.

N5 (nit), outbox-sender-bound.ts. The docblock says a row with no configured sender is stored with the column empty and fails at delivery. sys_email.from_address is required, so the insert itself may be the refusal. Not measured; make the sentence match what happens.

Files that hold as written: hook-body.zod.ts and index.ts (①.1, ①.2); feed.zod.ts apart from N2; caller-mail-bounds.ts, core/src/security/index.ts and rest/src/email-routes.ts (①.5); script-runner.ts; app-plugin.ts (all three runner sites and the artifact binder pass the service lookup); app-activity-door.ts (the platform-only types, the reserved metadata keys, the excluded objects and the append-only bucket are refused before the row is built; the actor snapshot is the platform's; the row is written under a plain system context that carries no automation mark, as the ruling's "platform's ledger writer" item requires); audit-plugin.ts; sys-activity.object.ts; flow-system-identity-write-refusal.ts and security-plugin-authz-middleware.ts (①.9); extract-hook-body.ts (an under-inferred token is a sandbox refusal at run time, the closed direction, as for log and crypto); ScriptCapabilitiesField.tsx. outbox-sender-bound.ts and email-plugin.ts do what their sentence says for from_address: field defaults are applied before beforeInsert, so a row that omits status is seen at queued, and a batch is dispatched one row at a time.

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: claude/project-thread-cz7rky
Reviewed-by: session_016WV4cx5nHfga3Hm9BzikGw

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
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8878bc87787b68a7ace0f8947f1e3e461cf54c46
Local-runs: none
Scope: packages/spec/src/contracts/email-service.ts, packages/spec/src/data/feed.zod.ts, packages/spec/src/data/hook-body-doors.zod.ts, packages/spec/src/data/hook-body.zod.ts, packages/spec/src/data/index.ts + packages/runtime/src/sandbox/body-doors.ts, packages/runtime/src/sandbox/body-runner.ts, packages/runtime/src/sandbox/quickjs-runner.ts, packages/runtime/src/sandbox/script-runner.ts, packages/runtime/src/app-plugin.ts, packages/runtime/src/app-artifact-handlers.ts, packages/objectql/src/index.ts, packages/core/src/security/caller-mail-bounds.ts, packages/core/src/security/index.ts, packages/plugins/plugin-email/src/email-service.ts, packages/plugins/plugin-email/src/outbox-sender-bound.ts, packages/plugins/plugin-email/src/email-plugin.ts, packages/plugins/plugin-audit/src/app-activity-door.ts, packages/plugins/plugin-audit/src/audit-plugin.ts, packages/plugins/plugin-audit/src/objects/sys-activity.object.ts, packages/plugins/plugin-security/src/flow-system-identity-write-refusal.ts, packages/plugins/plugin-security/src/security-plugin-authz-middleware.ts, packages/rest/src/email-routes.ts, packages/cli/src/utils/extract-hook-body.ts, ui/packages/app-shell/src/views/metadata-admin/inspectors/ScriptCapabilitiesField.tsx

This is the delta review on record 6841079039 (head efbf75c0, FAIL), by the same reviewer. One push separates the two heads: 48 files in two commits.

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 efbf75c0 to this head, and the net diff against main from merge-base 1a6c3309), and the check-runs on this head as of 2026-11-13T02:59Z. Read outside the diff, not reviewed: hook-run-as.ts, hook-wrappers.ts, engine-scoped-context.ts and engine-driver-options.ts (packages/objectql/src), the sys_email object (packages/platform-objects), the error catalogue (packages/spec/src/api/errors.zod.ts), scripts/check-spec-parsed-alias.mjs. Tests, the changeset text, docs/**, content/docs/**, generated files and translations are the lane seat's ACCEPT.

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 judgments

Disposition of record 6841079039:

  1. M1 is fixed. On the hook face hookDoorCaller no longer copies four fields of the hook session. It reads the pointed-at record through the hook's own api, derived by the two steps a runAs: 'user' hook takes: deriveHookApi(api, 'user', ...), which spreads the whole context of the write that fired the hook and sets isSystem: false, then seedHookUserGrants, which resolves the user's grants only where the context carries no permissions. The fallback is seedHookUnreadMembership, the wrapper's own fail-safe (an empty membership set). The context is the engine's own, so the caller's permissions, principalKind, delegation and transaction handle ride with it. body-doors-caller-scope.integration.test.ts reads through a real engine, a real SQL driver and the real hook wrapper. The PR body's sentence for a hook body now holds on the engine path.
  2. S1 is fixed. BodyMailSendInputSchema's .describe() states the 50 total, the 4000 characters each slot takes as recorded and the 320 of a replyTo; the generated reference page carries the text.
  3. S2 is fixed by the refusal arm. installDoor reads txState.open at call time, before the first await, and answers 409 RESOURCE_CONFLICT with a sentence that tells the author to call the door after the transaction commits. It reads the calling body's own transaction only; see S5.
  4. S3 is fixed. The binder builds one hook body runner and one action body runner per owner and passes originPackageId (handlerOwnerPackage); a job's runner takes the package that declares the job, and the bundle's id where no one package does. installBodyDoors reads originPackageId before appId.
  5. S4 is fixed. Every string of both inputs carries a max: an address 320, a display name 200, subject 998, text and html 1,000,000, a record reference 100 and 100, the entry's object, recordId and recordLabel 255, type 100, summary 500. The values agree with the sys_email columns (subject 998, reply_to 320, the three address columns 4000, related_object and related_id 100). The door holds what a schema cannot spell: each slot as recorded, a replyTo as recorded, metadata at 8000 characters of JSON, and it cuts the sender's display name so the recorded sender fits 320. recordedAddressLength counts every display name as quoted, which is never less than what the service records.
  6. N1: both aliases now read z.input. They carry no pin; judged in ④. N2, N3, N4 and N5 are fixed as written: the built-in types docblock names ctx.activity.append, callerScopeOf answers a scope with no principal for an absent context, the hourly refusal says which count was read, and the outbox docblock says the mail log refuses the insert where no sender is configured.

New surface on this head:

  1. SendEmailInput.requireLogRow is a new optional member of a published contract (@objectstack/spec/contracts). Right, additive. Declared equals enforced: the one implementer of IEmailService in the tree, EmailService, has one send path, and it reads the member after the insert attempt and before the queue publish and the inline delivery. SERVICE_UNAVAILABLE and 503 are a pair in the error catalogue. Absent or false keeps the earlier answer. The HTTP mail door forwards the member from a request body, where it can only narrow that caller's own send.
  2. BODY_MAIL_ADDRESS_LIST_LIMIT, BODY_MAIL_ADDRESS_LIMIT and BODY_ACTIVITY_METADATA_LIMIT are new exports of @objectstack/spec/data. Right, additive. The API surface and the export origins carry them, and the door reads the same three constants the schema's description states.
  3. @objectstack/objectql exports seedHookUserGrants and seedHookUnreadMembership beside deriveHookApi. Right as additions: both take any api, and hand back unchanged one that cannot seed. The symbol key they reach stays unexported, so ScopedContext gains no nameable member. See N6.
  4. DeliverAttemptOptions.asConfiguredSender is a new optional member of a type @objectstack/plugin-email exports. Right, additive. deliverPersistedRow applies it after the row becomes a message: the sender becomes the configured one, every name in SENDER_HEADER_NAMES is taken off, and a deployment with no configured sender lands the row at failed. The afterInsert drain passes it, as the module docblock of outbox-sender-bound.ts says.
  5. BodyDoorCaller.readContext became optional, readRecord is new, and FactoryOptions.originPackageId is new and optional. All three are types this pull request introduces or widens in output position for its own callers. Right.
  6. Check-runs on this head: 35 success, 11 skipped, 0 failed, 23 still running. Green and finished: Check Changeset, Governed Surface Queue Guard, Spec property liveness, PR-body guards, every Lint job, Lint · PM dispatch-gates, Type Check · source gates, Type Check · consumer gates, Type Check · debt ledger, Build Core, Build Docs, Build & E2E UI, Live E2E UI, Dogfood Regression Gate, Test Core 5/8 and Test UI 3/16 and 16/16. Pending, not judged: Test Core 1/8, 2/8, 3/8, 4/8, 6/8, 7/8 and 8/8; Test UI 1, 2 and 4 to 15 of 16; Type Check UI; Type Check · workspace.

② Semver level

  • @objectstack/spec stays minor: right. The new contract member and the three constants are additions inside the same level, and Metadata-protocol: added is unchanged.
  • @objectstack/objectql joins at patch: right. Two additive exports, no protocol change (AGENTS.md step 3).
  • The other packages stay patch: right.
  • The changeset now opens the @objectstack/plugin-email change with a BREAKING banner, a FROM and TO migration, and the marker adr-0087: not-required (no-migration-prescription). A non-protocol change ships patch and keeps its banner and its disposition, so the level holds. Whether that disposition is the right one is the lane seat's reading at ACCEPT.
  • The PR body now carries its Skills-docs: line (follow-up owed, naming skills/objectstack-data/references/data-hooks.md). The lane seat confirms the follow-up exists before ACCEPT.
  • Clause-②: yes (widening) and Metadata-protocol: added are unchanged and line-initial in the PR body and in the changeset.

③ Boundary flags

  • Still escalated, rule text. No comment was added to [PM seat] security · seat 2 — 🟢 os-tesla #32004 after 6841079039, so the only rule text for this step is still ruling 6796076408. This head adds to what stands on the PR body alone: the 4000, 320 and 8000 bounds, the 409 inside a body's transaction, the 503 when the mail log cannot record a message, the removal of sender headers from a row an app inserted, and the delivery-time sender on the drain. The lane seat names where the maintainer approved them, or puts them to the maintainer.
  • Still escalated, step order. The outbox sender bound is now wider than at efbf75c0 (it also takes sender headers off, it is applied again at delivery on the drain, and a deployment with no configured sender refuses the row) and is now declared BREAKING. The ruling moves first-party apps onto the doors in step 2. The maintainer confirms that the bound belongs to step 1, or it moves.
  • Open for step 2, unchanged: a flow has no door of its own, and the deprecation notice names two body doors to it.
  • The PR body states the diff is 3,156 changed lines, over the 3,000-line human-merge threshold. The merge is a person's.
  • The pull request is a draft with no auto-merge armed, so no disarm is owed. Its author is objectstack-fleet[bot].
  • Check-runs: 23 pending, listed in ①.12. This verdict does not rest on them.

④ Design judgment

The 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 runAs: 'user', so there is one derivation of a user's scope and one fail-safe. Three arms were read. With a calling user, the read is that user's, whatever elevation the write or the hook's own runAs carried. With no calling user, the read goes through the hook's own api unchanged, so the door reads what the body already reads and stamps no user; a runAs: 'user' hook whose trigger has no user is refused, since its api refuses. Where the api cannot derive a scope (a hook context a host assembled by hand, not the engine's), the read falls back to the session's user and organization with no elevation, which is the baseline alone; the docblock says so, and the engine never takes this arm.

Declared deviation N1: BodyMailSendInput and BodyActivityAppendInput read z.input and carry no isomorphic pin. Acceptable as built. z.input is the author-state spelling ADR-0122 asks for. check-spec-parsed-alias.mjs does not count a name ending in Input as a bare alias (isBareName), and its rule 3 reports a pin no bare alias relies on as stale, so the pin this record asked for cannot be written under the gate. The names are domain names, not synonyms of a bare alias, which rule 4 leaves alone.

S5 (should-fix), quickjs-runner.ts installDoor and body-doors.ts. The S2 refusal reads the calling body's own txState. A hook body runs in its own VM with its own txState, so a hook fired by a write made inside another body's ctx.api.transaction is not refused. The engine's begin, commit and rollback trio, which a VM body's transaction uses, threads its handle on the context and never publishes it to the ambient store (engine-scoped-context.ts). On that path the hook face's record read rides the transaction, through the hook api, but the hourly count, the two sender-name reads, the mail service's row insert and the activity writer go through the root engine with contexts that carry no handle. That is the cost 6841079039 named under S2, reached through a hook: a second connection, a wait on a one-connection pool (the objectstack dev default), and a timeline entry that stays when the outer transaction rolls back. Read from the code, not measured. Give the hook face the same answer: refuse where the firing write's context carries an open handle, or ride it.

N6 (nit), packages/objectql/src/hook-run-as.ts. The docblock of SEED_USER_GRANTS says only the hook wrapper reaches the seeding, through seedHookUserGrants. After this head the runtime's body runner reaches it through the same function, and the function is a package export. The file is not in the diff. Make the sentence name both callers.

N7 (nit), body-runner.ts hookDoorCaller. Where the grants cannot be resolved, the door falls back without a word. The hook wrapper logs a warn on the same branch, because the symptom is a narrower read with no visible cause. The factory holds a logger; say it here too.

Files that hold as written on this head: contracts/email-service.ts (①.7); hook-body-doors.zod.ts (①.2, ①.5, ①.8); feed.zod.ts; hook-body.zod.ts and data/index.ts, unchanged since 6841079039; body-doors.ts apart from S5; body-runner.ts apart from N7; quickjs-runner.ts apart from S5; app-artifact-handlers.ts (①.4); objectql/src/index.ts (①.9); email-service.ts, email-plugin.ts and outbox-sender-bound.ts (①.7, ①.10): the insert-time bound decodes headers_json, drops each sender header by its trimmed, lower-cased name and leaves a value it cannot decode for delivery to refuse; sys-activity.object.ts: the origin description now reads "empty on an entry not written through the activity door", which is true while an app's direct insert is still taken. The files 6841079039 found to hold and this push did not touch still hold: caller-mail-bounds.ts, core/src/security/index.ts, rest/src/email-routes.ts, script-runner.ts, app-plugin.ts, app-activity-door.ts, audit-plugin.ts, flow-system-identity-write-refusal.ts, security-plugin-authz-middleware.ts, extract-hook-body.ts, ScriptCapabilitiesField.tsx.

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: claude/project-thread-cz7rky
Reviewed-by: session_016WV4cx5nHfga3Hm9BzikGw

VERDICT: PASS

…its or rolls back with its write

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

Copy link
Copy Markdown
Contributor Author

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.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5ce459b5aed4833256ee87beea8851002026bcd1
Local-runs: none
Scope: packages/spec/src/contracts/email-service.ts, packages/spec/src/data/feed.zod.ts, packages/spec/src/data/hook-body-doors.zod.ts, packages/spec/src/data/hook-body.zod.ts, packages/spec/src/data/index.ts + packages/runtime/src/sandbox/body-doors.ts, packages/runtime/src/sandbox/body-runner.ts, packages/runtime/src/sandbox/quickjs-runner.ts, packages/runtime/src/sandbox/script-runner.ts, packages/runtime/src/app-plugin.ts, packages/runtime/src/app-artifact-handlers.ts, packages/objectql/src/index.ts, packages/objectql/src/open-transaction.ts, packages/objectql/src/hook-run-as.ts, packages/core/src/security/caller-mail-bounds.ts, packages/core/src/security/index.ts, packages/plugins/plugin-email/src/email-service.ts, packages/plugins/plugin-email/src/outbox-sender-bound.ts, packages/plugins/plugin-email/src/email-plugin.ts, packages/plugins/plugin-audit/src/app-activity-door.ts, packages/plugins/plugin-audit/src/audit-plugin.ts, packages/plugins/plugin-audit/src/objects/sys-activity.object.ts, packages/plugins/plugin-security/src/flow-system-identity-write-refusal.ts, packages/plugins/plugin-security/src/security-plugin-authz-middleware.ts, packages/rest/src/email-routes.ts, packages/cli/src/utils/extract-hook-body.ts, ui/packages/app-shell/src/views/metadata-admin/inspectors/ScriptCapabilitiesField.tsx

This is the delta review on record 6842516227 (head 8878bc87, PASS with S5 and one held finding owed a fix), by the same reviewer. One commit separates the two heads: 17 files.

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 8878bc87 to this head, and the net diff against main from merge-base 1a6c3309), and the check-runs on this head as of 2026-11-13T03:51Z. Read outside the diff, not reviewed: the after-hook unit and the hook-context build sites in packages/objectql/src (engine-hooks-and-functions.ts, engine-hook-dispatch.ts, engine-insert.ts, engine-update.ts, engine-delete-and-aggregate.ts, engine.ts), buildActionExecutionContext (packages/runtime/src/action-execution.ts). Tests, the changeset text, docs/**, content/docs/**, generated files and translations are the lane seat's ACCEPT.

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 judgments

Disposition of record 6842516227:

  1. S5 is fixed. A message is no longer sent from any body that stands in a transaction (2, 3), so the mail door's count, sender-name reads and log write no longer run beside one. The timeline door now writes inside the transaction of the write that runs the body (4). The refusal inside the body's own ctx.api.transaction is unchanged for both doors (quickjs-runner.ts is not in this delta).
  2. A hook body sends no mail. buildBodyDoors answers 403 PERMISSION_DENIED on origin.kind === 'hook' as the first statement of send, before the input is parsed, a record is read, the log is counted or the service is reached. The rule is by face and does not read the transaction, the driver or any other hook. The sentence names the remedy: an action, a job or a flow.
  3. An action or a job body sends no mail inside a transaction: 409 RESOURCE_CONFLICT when openTransaction() answers anything but 'none', again before the parse. The body runner feeds it the engine's ambient transaction and the handle the caller's execution context carries (actionCtx.executionContext.transaction; a job has none). Judged in ④.
  4. A timeline entry commits or rolls back with the write that runs the body. Where the engine's ambient transaction is open, the writer's insert joins it with no handle passed. Where the write carries a handle the ambient store does not hold, the door passes it: appendAppActivity(entry, stamp, { transaction }) puts it on the context of the actor snapshot read and of the insert. On the hook face the handle is the hook context's own transaction member, which every hook-context build site in the engine fills from the operation's context; the record reads already ride it through the hook api.
  5. N6 is fixed: the docblock of SEED_USER_GRANTS names both callers and says the entry exports the function. N7 is fixed: the door's fallback logs a warn that names the hook and the symptom.

New surface on this head:

  1. @objectstack/objectql exports openTransactionKind and the type OpenTransactionKind ('none', 'joined', 'separate'). Right as additions. The function agrees with the engine's own rule in buildDriverOptions: an explicit handle wins over the ambient one, and a handle whose unit has closed is no handle. It reads the engine's ambient store and unit ledger through a structural cast; see N8.
  2. AppActivityWriteOptions and the third parameter of appendAppActivity (@objectstack/plugin-audit) are optional additions. Right.
  3. BodyDoorOptions.openTransaction and BodyDoorTransaction are optional additions to types this pull request introduces. Right.
  4. packages/spec: no schema shape moves in this delta. The docblocks of HookBodyCapability, BodyMailSendInputSchema and BodyActivityAppendInputSchema and both .describe() texts now state the two rules, so the published descriptions and the reference page carry them. The accept set itself still admits email.send in a hook body's capabilities; see S6.
  5. Check-runs on this head: 28 success, 11 skipped, 0 failed, 30 still running. Green and finished: Check Changeset, Governed Surface Queue Guard, Spec property liveness, PR-body guards, Lint & Repo Gates, Lint · ESLint, Lint · docs & code guards, Lint · repo & tooling gates, Lint · CI, release & engine gates, Lint · PM dispatch-gates, the three scoped lint jobs, Type Check · source gates, Type Check · consumer gates, Type Check · debt ledger, Build Docs, Check Documentation Links, Dogfood Regression Gate. Pending, not judged: Build Core, Build & E2E UI, Live E2E UI, Lint UI, all eight Test Core shards, all sixteen Test UI shards, Type Check UI, Type Check · workspace.

② Semver level

  • @objectstack/spec stays minor and @objectstack/objectql stays patch: right. This delta adds one export and one type to objectql and moves no accept set.
  • The other packages stay patch: right. Refusing a call that no released version ever served is no break against main.
  • Clause-②: yes (widening) and Metadata-protocol: added are unchanged and line-initial in the PR body and in the changeset. The Skills-docs: line is unchanged. The changeset text now states the two rules; its reading is the lane seat's.

③ Boundary flags

  • Escalated, rule text. The rule this delta implements (a hook body sends no mail; a timeline entry follows the write) is not on [PM seat] security · seat 2 — 🟢 os-tesla #32004: no comment was added after 6842516227, and the PR body's Why section still cites ruling 6796076408 alone. The lane seat records the maintainer's ruling of 2026-11-13 on the issue in rule terms, or names where it is recorded, and the PR body cites it.
  • For the maintainer's eye, not a block: the ruling as reported speaks to hook bodies. This head also refuses mail from an action or a job body that runs inside a transaction its caller opened. That goes past the letter of the ruling in the closed direction and for the ruling's own reason; judged right in ④.
  • Still escalated from 6841079039 and 6842516227: the remaining step design that stands on the PR body alone, and the step order of the outbox sender bound.
  • The PR body states the diff is 3,584 changed lines, over the 3,000-line human-merge threshold. The merge is a person's.
  • The pull request is a draft with no auto-merge armed. Its author is objectstack-fleet[bot].
  • Check-runs: 30 pending, listed in ①.10, among them every test shard. This verdict does not rest on them.
  • The PR body was read for this record: it states rules, answers and verification, and nothing else.

④ Design judgment

The 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 (opensAfterAbortUnit), so a rule that read the transaction would give one hook two answers in two deployments. The face is known when the door is built, the answer is the same everywhere, and it can be widened later without a break.

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 8878bc87.

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 ctx.api.transaction the door still refuses, so an entry can follow the write that runs the body but not a transaction the body opens itself. The PR body states both sentences.

S6 (should-fix), hook-body.zod.ts, extract-hook-body.ts, ScriptCapabilitiesField.tsx, body-runner.ts. Three authoring surfaces still offer a hook body the capability the door now always refuses. The accept set admits email.send in a hook body's capabilities. The build infers the token for a hook handler that calls ctx.email.send and lowers it without a word. The Console's capability picker lists it in the hook inspector. Nothing before the first call says the call will be refused, and a hook whose onError is the default aborts the write that fired it when the call is refused. Refuse or warn at the earliest place that knows the face, with the door's own sentence: the build for a hook handler, and the hook body runner at bind time for a body that declares the token. The picker follows.

N8 (nit), packages/objectql/src/open-transaction.ts. openTransactionKind reads two private members of the engine (txStore, isClosedUnitHandle) through a structural cast, and answers from the carried handle alone for an engine that shows neither. Its unit test builds the engine by hand, and the two real-engine cases in body-doors-transaction.integration.test.ts pass whichever way it answers for the ambient case, since a hook is refused by face and an ambient insert joins by itself. Read against engine.ts today, both members exist under those names. Pin the ambient answer against a real ObjectQL, so a rename cannot change it unseen.

Files that hold as written on this head: body-doors.ts (①.2 to ①.4); body-runner.ts apart from S6; open-transaction.ts apart from N8; objectql/src/index.ts; hook-run-as.ts (①.5); app-activity-door.ts (①.4, ①.7); hook-body-doors.zod.ts and hook-body.zod.ts apart from S6. Every other file in Scope is unchanged since 6842516227 and holds as that record found it.

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: claude/project-thread-cz7rky
Reviewed-by: session_016WV4cx5nHfga3Hm9BzikGw

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
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

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.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3a2c53069ab126fd38aef64f7accc859daec5f1a
Local-runs: none
Scope: packages/spec/src/contracts/email-service.ts, packages/spec/src/data/feed.zod.ts, packages/spec/src/data/hook-body-doors.zod.ts, packages/spec/src/data/hook-body.zod.ts, packages/spec/src/data/hook.zod.ts, packages/spec/src/data/index.ts + packages/runtime/src/sandbox/body-doors.ts, packages/runtime/src/sandbox/body-runner.ts, packages/runtime/src/sandbox/quickjs-runner.ts, packages/runtime/src/sandbox/script-runner.ts, packages/runtime/src/app-plugin.ts, packages/runtime/src/app-artifact-handlers.ts, packages/objectql/src/index.ts, packages/objectql/src/open-transaction.ts, packages/objectql/src/hook-run-as.ts, packages/core/src/security/caller-mail-bounds.ts, packages/core/src/security/index.ts, packages/plugins/plugin-email/src/email-service.ts, packages/plugins/plugin-email/src/outbox-sender-bound.ts, packages/plugins/plugin-email/src/email-plugin.ts, packages/plugins/plugin-audit/src/app-activity-door.ts, packages/plugins/plugin-audit/src/audit-plugin.ts, packages/plugins/plugin-audit/src/objects/sys-activity.object.ts, packages/plugins/plugin-security/src/flow-system-identity-write-refusal.ts, packages/plugins/plugin-security/src/security-plugin-authz-middleware.ts, packages/rest/src/email-routes.ts, packages/cli/src/utils/extract-hook-body.ts, ui/packages/app-shell/src/views/metadata-admin/inspectors/ScriptCapabilitiesField.tsx, ui/packages/app-shell/src/views/metadata-admin/inspectors/HookDefaultInspector.tsx

This is the delta review on record 6843313448 (head 5ce459b5, PASS with S6 owed a fix), by the same reviewer. Two commits separate the two heads: 16 files, one of them a new hit, packages/spec/src/data/hook.zod.ts.

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 5ce459b5 to this head, and the net diff against main from merge-base 1a6c3309), and the check-runs on this head as of 2026-11-13T04:40Z. Read outside the diff, not reviewed: the hook binder's handling of a runner that throws (packages/objectql/src/hook-binder.ts), packages/spec/dropped-refinements.baseline.json, the generated reference pages for the hook and the hook body. Tests, the changeset text, docs/**, content/docs/**, generated files and translations are the lane seat's ACCEPT.

Findings are graded M (must-fix), S (should-fix), N (nit).

① Derived judgments

  1. S6 is fixed, at each surface the record named.
    • Parse. HookSchema gains a fourth object-level check, refuseHookBodyMailGrant: a hook whose js body lists email.send in capabilities is refused with one issue at body.capabilities.N, and the sentence gives the reason and the remedy. The check sits on the hook, not on the body schema, so an action or a job body is granted the token as before. Every door that parses a hook holds it.
    • Bind. hookBodyRunnerFactory throws 403 PERMISSION_DENIED for a js body that carries the token. It is thrown, not answered with no handler, so the binder records it against the hook, logs it at error, and rethrows under strict.
    • Build. The inference is unchanged, so a hook handler that calls ctx.email.send lowers to a body that carries the token, and the hook's parse refuses that body.
    • Console. ScriptCapabilitiesField takes a withheld set; the hook inspector withholds email.send. A stored grant is still shown, flagged, and can be removed.
    • Call. The door's refusal by face is unchanged and stays the last line.
  2. The new check is no narrowing of a released accept set. On main the capability enum has no email.send, so main refuses the same document at the enum. This head refuses it at the object-level check. No document main accepts is refused here; against main the accept sets still only widen. data/Hook already names its root site in the dropped-refinement ledger, so the ledger does not move.
  3. Migration impact, stated. No released parser has admitted the token on any body, so no hook stored through a released version carries it, and no conversion is owed. A hook stored by a deployment that ran an earlier head of this branch is refused when it is saved again and is not bound: under default binding the hook is skipped with an error line that names it and the remedy, and under strict binding the boot fails. The Console shows such a grant flagged, so it is removed by hand.
  4. N8 is fixed. body-doors-transaction.integration.test.ts now reads openTransactionKind off a real engine in the four positions (outside a transaction, inside the engine's own with and without the carried handle, after its commit, and under a handle the ambient store does not hold), and the docblock of open-transaction.ts says the members are private and names the pin.
  5. @object-ui/i18n joins the changeset at patch: right. Its studio catalogue gains one key in two locales, read by the hook inspector.
  6. Check-runs on this head: 13 success, 11 skipped, 0 failed, 1 cancelled, 24 still running. Green and finished: Check Changeset, Governed Surface Queue Guard, Spec property liveness, PR-body guards, Check Documentation Links, Dogfood Regression Gate. The cancelled run is an earlier PR-body guards run beside a green one of the same name. Pending, not judged: every Lint job, the four Type Check jobs listed, Build Core, Build Docs, Build UI Deps, Live E2E UI and all eight Test Core shards. The UI test and type-check jobs are planned and not yet listed.

② Semver level

  • @objectstack/spec stays minor: right. By ①.2 the new check refuses nothing a released version accepts, so no narrowing is declared and no BREAKING banner is owed for it.
  • Clause-②: yes (widening) and Metadata-protocol: added hold as written, line-initial in the PR body and in the changeset.
  • The ADR-0087 marker stands as it was: by ①.3 no stored document of a released version changes validity, so no conversion is prescribed.
  • The other packages stay patch, with @object-ui/i18n added at patch (①.5).
  • The Skills-docs: line is unchanged.

③ Boundary flags

  • Closed from 6843313448: the ruling is on [PM seat] security · seat 2 — 🟢 os-tesla #32004 as comment 6843314411 (a hook body may not send mail; the timeline door stays open to hook bodies and an entry follows its write; mail deferred until after the save is not in this step), and the PR body cites it. The comment is the fleet account's record of a ruling made on a decision card; the lane seat stands behind that attribution.
  • For the maintainer's eye, not a block: the PR body now states the refusal of mail from an action or a job body inside a transaction as "the same rule, carried to the other bodies". Comment 6843314411 speaks to hook bodies only.
  • Still escalated from the earlier records: the rest of the step design that stands on the PR body alone, and the step order of the outbox sender bound.
  • The PR body states the diff is 3,893 changed lines, over the 3,000-line human-merge threshold. The merge is a person's.
  • The pull request is a draft with no auto-merge armed. Its author is objectstack-fleet[bot].
  • Check-runs: 24 pending and the UI jobs not yet listed (①.6). This verdict does not rest on them.
  • The PR body was read for this record: it states rules, answers and verification, and nothing else.

④ Design judgment

Refusing 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), packages/spec/src/data/hook.zod.ts. The published description of a hook does not state the rule. The body key reads "Hook body — expression (L1) or sandboxed JS (L2)", the hook body's reference page lists email.send among the tokens, and the ledger names the root site, as it did before this check. A reader of the hook's own page learns the rule only from the mail door's page. Say it in the body description.

N10 (nit), packages/cli/src/utils/extract-hook-body.ts. The comment over the capability patterns calls an over-inferred token safe. For this one token on a hook that is no longer so: a handler whose text spells the call without making it now fails the build. Not measured. Amend the comment.

Files that hold as written on this head: hook.zod.ts apart from N9; hook-body.zod.ts; body-runner.ts; open-transaction.ts; ScriptCapabilitiesField.tsx and HookDefaultInspector.tsx; extract-hook-body.ts, unchanged, apart from N10. Every other file in Scope is unchanged since 6843313448 and holds as that record and its predecessors found it.

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: claude/project-thread-cz7rky
Reviewed-by: session_016WV4cx5nHfga3Hm9BzikGw

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

security#1 · os-elon-musk · session_016WV4cx5nHfga3Hm9BzikGw

席位核对 — ACCEPT · security#1 · head 3a2c53069ab126fd38aef64f7accc859daec5f1a

  • 形态: step 1 of ruling 6796076408 on [PM seat] security · seat 2 — 🟢 os-tesla #32004 (Refs; the issue stays open for step 2). 99 files, +3,670/−224. Diff over 3,000 lines ⇒ Tier H: approval by a governed approver (os-zhuang or hotlong), or a direct human merge.
  • 前提实测: the maintainer's ruling on mail from hook bodies (2026-11-13T03:10Z) is recorded on [PM seat] security · seat 2 — 🟢 os-tesla #32004 (6843314411) and cited in the PR body. The step-1 design packet, including the outbox sender bound landing in this step, was approved by the maintainer on 2026-11-12T22:18Z in the lane's project thread.
  • 契约复核: FAIL 6841079039 → delta PASS 6842516227 → 6843313448 → 6844228594 (final, at this head; nits N9/N10 only). Clause-② yes (widening), minor; no migration against any released version (the reviewer checked that no released parser admitted the new grant).
  • 门禁 check-runs: every check-run on this head green (60 success, 11 skipped). One PR-body guards run was cancelled when the body edit re-ran it, and the re-run is green.
  • 接受的偏差: N1 (the two input aliases read z.input with no isomorphic pin, since the alias gate refuses one); an action or job body inside a transaction is refused mail (409). That is the hook rule carried to the other bodies, so it goes past the ruling's letter, and it is named for the approver here.
  • 范围外去向: Skills-docs follow-up docs(skills): name the ctx.email.send and ctx.activity.append body doors in the data-hooks reference #33033 (blocked by this PR). N9/N10 and the remaining step-2 items go to step 2 (the next PR on [PM seat] security · seat 2 — 🟢 os-tesla #32004), after this one is released.
  • 落地条款: needs a governed approver's review approval; then the seat marks it ready and queues it. A human may instead merge it directly. The approver should confirm three things the reviewer escalated: the step design (ruling 6796076408); the outbox sender bound in step 1 (marked BREAKING for an app that inserts mail rows directly); and the 409 for action and job mail inside a transaction.

@objectstack-fleet objectstack-fleet Bot added the pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue label Nov 13, 2026
@objectstack-fleet
objectstack-fleet Bot requested a review from hotlong November 13, 2026 05:10
@os-zhuang
os-zhuang marked this pull request as ready for review November 13, 2026 05:21
@os-zhuang
os-zhuang added this pull request to the merge queue Nov 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 43371520158 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

分类:failure —— 按下面的日志分诊。

失败的 job(日志抽取,best effort):

  • Dogfood Regression Gate (1/3) — 失败步骤: Boot example apps and exercise real user flows

    @objectstack/dogfood:test:  FAIL   isolated  test/security-catalog-showcase.dogfood.test.ts > showcase, 'single': the security catalog read > permission: lists exactly the declared names
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    @objectstack/dogfood:test:  FAIL   isolated  test/security-catalog-showcase.dogfood.test.ts > showcase, 'single with the Default Organization': the security catalog read > permission: lists exactly th
      ↳ 失败原因: (这条 FAIL 之后 12 行内没有可识别的原因行 —— 点进 job 看)
    @objectstack/dogfood:test:  FAIL   isolated  test/security-catalog-showcase.dogfood.test.ts > showcase, 'walled': the security catalog read > permission: lists exactly the declared names
      ↳ 失败原因: @objectstack/dogfood:test: AssertionError: expected [ 'admin_full_access', …(18) ] to deeply equal [ 'admin_full_access', …(17) ]
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 18 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit d0fbe11 Nov 13, 2026
80 of 82 checks passed
@os-zhuang
os-zhuang deleted the claude/project-thread-cz7rky branch November 13, 2026 05:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation pm:awaiting-merge PR only: landing waits on the maintainer's own review and merge (SKILL.md 复核 ②); never on an issue protocol:data size/xl tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants