Skip to content

fix(cli): config.email.persist 接线到 EmailServicePlugin,补上 OS_EMAIL_PERSIST_ENABLED (#5447) - #5470

Merged
baozhoutao merged 1 commit into
mainfrom
claude/issue-5447-email-persist-wire
Aug 5, 2026
Merged

fix(cli): config.email.persist 接线到 EmailServicePlugin,补上 OS_EMAIL_PERSIST_ENABLED (#5447)#5470
baozhoutao merged 1 commit into
mainfrom
claude/issue-5447-email-persist-wire

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Fixes #5447

问题

EmailServiceConfigSchema 一直声明着 persist,生成的参考文档正面写着 “Persist to sys_email (default true)”;插件侧也一直是活的 —— EmailServicePluginpersist === false 时不构造 EmailPersistence,并且在队列投递诊断里引用这个键。缺的只是两者之间那一段路config.email 在全仓唯一的读者是 resolveEmailCapabilityArg,而它读遍了每一个声明键,独独没读这一个。

后果是 Prime Directive #10 的 declared ≠ enforced,且带 PII 半径:作者在 objectstack.config.tsemail: { persist: false } 想把邮件正文挡在库外 —— 类型通过、schema 通过、文档确认,运行时照旧把每一封邮件的收件人 / 主题 / 正文写进 sys_email。按 ADR-0049 enforce-or-remove 取 enforce(分诊裁定)。

改动

packages/cli/src/commands/serve.ts 一处接线 + 一处提取:

  • resolveEmailCapabilityArgcfgEmail.persist,并作为插件构造参数传出。
  • 新增 OS_EMAIL_PERSIST_ENABLED。优先级按本函数自述的「env 逐项覆盖」:OS_EMAIL_PERSIST_ENABLED 优先于 config.email.persist,再退到默认(persist 开)。
  • 真值表不新造第二套OS_EMAIL_QUEUE_ENABLED 原本内联了 1/true/yes/on 这张表,现提取为共用的 envBooleanFlag,两个 flag 同一张表。它是三态的 —— 未设置返回 undefined 从而落到 config,而不是被读成 false 去压掉一个写了 true 的 config。

命名说明:_ENABLED 是 PD #9 的布尔开关形状,但与队列 flag 不同,它默认开。因为它并非启用一个新能力,而是一个一直开着的能力的关闭开关;两侧都不声明时,该键根本不出现在构造参数里,由插件自身默认决定 —— 存量部署逐字不变。

测试

packages/cli/src/commands/serve-email-persist.test.ts,13 例全绿(pnpm --filter @objectstack/cli test 全量 80 文件 / 780 例全绿,typecheck 干净)。

端到端那一组是本文件的重点:只断言 resolver 吐出 persist: false 等于拿键名去钉键名,且完全无法回答「插件到底读不读它」—— 而这正是本单要关的那类缺口。所以它用 resolver 的真实输出去构造真实的 EmailServicePlugin,跑到 kernel:ready,再回读 EmailPersistence 究竟有没有被构造。

反向验证(方向先判后跑,结论与预判一致):删掉接线那一行,13 例中恰好 8 例转红。留绿的 5 例里,3 例是「两侧都不声明 → 行为与现状逐字一致」的兼容锚点(本就该绿),另 2 例(显式 persist: trueOS_EMAIL_PERSIST_ENABLED=true 压 config false是因为期望值恰好等于插件默认值而绿的,并不具备判别力。这一点已写进测试文件的注释,避免后来者把它们误读成接线存在的证据;真正判别接线的是 persist:falseOS_EMAIL_PERSIST_ENABLED=false 那两例。

⚠️#5307 的落地顺序(需要 reviewer 决定合并次序)

issue 与派单都要求「把 serve-email-config-parity.contract.test.tsDECLARED_BUT_UNREAD 豁免数组清空、断言收紧为集合相等」。该文件当前不在 main —— #5307 仍是 open,其 PR 未合并,文件只存在于在飞分支 claude/issue-5307-email-config-keys。因此本 PR 无法完成这一项,它是一个跨 PR 的合并次序问题:

两个分支的文件面不相交(本 PR 只动 serve.ts + 新增测试 + changeset;#5307packages/spec + 新增该 parity 测试),不会产生文本冲突,只有这一处语义耦合。

范围

严格按派单收口:未动 packages/spec 的 schema 本体(键已声明,无需改),未动 packages/plugins/plugin-email(插件侧本就完整),未碰 defaultTemplateContext / appName 相关行(#5448 同函数待派)。changeset 为 @objectstack/cli patch,正文写明 PII 动机并提示:曾声明 persist: false 并据此做过审计的部署,那些行其实存在,值得复查。


🤖 Generated with Claude Code

https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh


Generated by Claude Code

`EmailServiceConfigSchema` declares `persist` and the generated reference
documents it as "Persist to sys_email (default true)".
`EmailServicePlugin` honours the constructor option — it builds no
`EmailPersistence` when `persist === false`. The segment between them did
not exist: `resolveEmailCapabilityArg` is the only reader `config.email`
has in the repo, and it read every declared key except this one.

A deployment that wrote `email: { persist: false }` to keep message bodies
out of the database type-checked, parsed and read as configured, and went
on writing every subject, body and recipient to `sys_email` — Prime
Directive #10's declared != enforced, with a PII blast radius. ADR-0049
enforce-or-remove, answered "enforce".

Resolution order, per setting, as the rest of this resolver:
OS_EMAIL_PERSIST_ENABLED > config.email.persist > default (persist ON).

The new env var reads the same truth table as OS_EMAIL_QUEUE_ENABLED, now
a shared `envBooleanFlag` helper rather than two copies of the list. It is
tri-state so an unset variable falls through to config instead of reading
as false. Declaring neither source leaves the key out of the constructor
options entirely, so the plugin default decides and existing deployments
are unchanged.

Tests boot the real plugin over the real resolver output and read back
whether an EmailPersistence was built, rather than re-pinning the key
name against itself. Reverse verification: deleting the wiring turns 8 of
the 13 red; the 5 that stay green are the default-behaviour pins plus two
completeness cases whose expectation coincides with the plugin default —
annotated in the file so they are not misread as discriminating.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 5, 2026 1:14pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m and removed documentation Improvements or additions to documentation tests tooling labels Aug 5, 2026
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli.

21 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/automation/hook-bodies.mdx (via packages/cli)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/validating-metadata.mdx (via packages/cli)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli)
  • content/docs/plugins/index.mdx (via @objectstack/cli)
  • content/docs/plugins/packages.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli)
  • content/docs/releases/v16.mdx (via @objectstack/cli)
  • content/docs/releases/v17.mdx (via @objectstack/cli)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@baozhoutao
baozhoutao marked this pull request as ready for review August 5, 2026 13:16
@baozhoutao
baozhoutao enabled auto-merge August 5, 2026 13:16
@baozhoutao
baozhoutao added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit cd2efe6 Aug 5, 2026
24 checks passed
@baozhoutao
baozhoutao deleted the claude/issue-5447-email-persist-wire branch August 5, 2026 13:29
os-zhuang pushed a commit that referenced this pull request Aug 5, 2026
`serve-email-config-parity.contract.test.ts` 曾注册唯一一条 `DECLARED_BUT_UNREAD`
豁免:`persist` 被 schema 声明却无人读取(#5447)。cli 车道的 PR #5470
(`cd2efe62a`)已合入 main —— `resolveEmailCapabilityArg` 现在经由新的
`envBooleanFlag` / `OS_EMAIL_PERSIST_ENABLED`(三态,默认仍为 ON)读取
`cfgEmail.persist`。

于是本分支的断言在 merge queue 里必然变红:它 expect `unread == ['persist']`,
而合并后的 main 上 `unread == []`。跨车道协议(#5447 评论)约定后落地方对齐,
即本分支。

对齐做法:删除豁免数组,而非留一个空数组 —— 空注册表是一种邀请,下一个
declared-but-unread 键会被直接追加进去而不必辩论,正是该条目当初要防止的
「静默豁免」。断言随之收紧为两个方向都为空,即 declared 集与 read 集相等,
也就是文件注释当初许诺的 plain set equality。豁免的来龙去脉保留在注释中。

反向验证(方向预先判定为 red,结果一致):保留旧豁免数组对合并后的 main 运行,
`AssertionError: expected [] to deeply equal [ 'persist' ]` —— 这正是本次预先
规避的队列失败;撤销豁免后该文件 6 个用例全绿。

另:changeset 里「它读八个键」是 #5470 之前的读侧计数(现为九个),补时间
限定词「本次改动时」,以免这段 CHANGELOG 文案落地后失真。schema 中 `persist`
的 TSDoc「Persist to sys_email (default true)」经核对在 #5470 之后依然成立
(默认仍为 ON),故不改。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fxLGQdatPbBUvCgiVxg6D
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants