Skip to content

fix(cli): the boot merge no longer discards the authored api block (#4002) - #4008

Merged
os-zhuang merged 1 commit into
mainfrom
claude/boot-api-merge
Jul 30, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/boot-api-merge

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Closes #4002。这是 #3963 第 2 节"值丢在哪一环"的答案,已实测定位。

根因

serve.ts(dev 是 spawn 它的子进程)这样拼有效配置:

config = { ...originalConfig, ...bootResult }     // ← 浅展开

而 createStandaloneStack() / createDefaultHostConfig() 返回的 api 只携带 environment scoping 的决定:

api: { enableProjectScoping: false, projectResolution: 'none' }

浅展开下这个对象整块替换掉作者写的 api,把它自己没设的键全部静默丢掉。

丢掉的是两个活着的旋钮

键 后果
api.requireAuth ADR-0056 D2 的公开托管开关 —— v12 把它当成"一行迁移"来文档化。写了等于没写:值到不了 REST / dispatcher 插件,匿名照旧 401,而那句"让 fail-open 姿态可见"的启动告警也从来没打过
api.enforceProjectMembership ADR-0024 D9 那个 sys_environment_member 403 门的 opt-out,静默退回 dispatcher 默认

实测证据

在 examples/app-crm 写 api: { requireAuth: false },在 serve.ts:1745 打点:

[DBG] configKeys= actions,api,apps,...            ← api 键在
[DBG] apiConfig= {"enableProjectScoping":false,"projectResolution":"none"}
      | resolved requireAuth= true

apiConfig 没有 requireAuth,还带着一个 schema 枚举('required'|'optional'|'auto')根本不允许的 "none" —— 那是 boot 的对象,不是作者的。

修复后端到端复验:同样的配置,匿名 POST /data/crm_account/query 修复前 401、修复后返回记录。

修法

新增一个纯函数 mergeBootConfig:api 按键合并,作者的声明留下,boot 仍然在它真正负责的键上取胜(standalone 主机上 scoping 不是作者的选择)。其余顶层键保持整值语义 —— artifact-serve 路径就是要用产物里的 objects / permissions / manifest / plugins,所以没有一并改动。

两点需要说清的边界

1. auth-less stack 的兜底从来没受影响,也没有改。 它在 ?? ((tierEnabled('auth') || hasAuthPlugin) ? true : false) 这个 fallback 里 —— 而它之所以一直生效,恰恰是因为作者的值不见了。丢的只有显式写的值。这也消解了我在 #3963 里担心的"那类 stack 可能已经砖了"。

2. projectResolution: 'none' 我没有动。 它不在 ObjectStackDefinitionSchema.api.projectResolution 的枚举里,但 enableProjectScoping: false 时它是惰性的;改它要么把 'none' 加进作者可写的枚举(反而拓宽了面),要么去动另一个包的契约却换不来任何行为收益。已在 #4002 记录,留待单独判断。

顺带:#3960 的可达性终于补齐了

这个开关一旦真的能用,我就有了一个匿名可达的部署来验证 #3960 那个我标注"未实测"的一半 —— 带上对照组跑了一遍:

匿名读 sys_user,不带 context      → 1 行
匿名 + 伪造 context.isSystem      → 1 行
匿名 + 伪造 context.__expandRead  → 1 行

三组完全一样,所以:#3960 的修复成立(伪造的 context 被剥掉了,什么都没做成);sys_user 被读到纯粹是 requireAuth: false 本身的语义。

顺便也说明了这个开关能用的时候会做什么 —— 同一个匿名调用者可以读身份表。这正是 #3963 主张退役它的理由,现在是实测出来的,不是推演的。

验证

  • 新增 7 条单测(merge-boot-config.test.ts):作者键留存、boot 在自己负责的键上取胜、其余顶层键仍整值替换、两侧都没有 api 时不凭空造一个、单侧有时透传、非对象 api 不被展开成字符索引、不修改入参;
  • @objectstack/cli 835 条全绿;改动文件 ESLint 干净;
  • 诊断探针与临时配置改动均已清理,未提交。

🤖 Generated with Claude Code

https://claude.ai/code/session_01TzLE9cw4gZKNyPN2ZP4iTt


Generated by Claude Code

…4002)

serve (and dev, which spawns it) assembled the effective config as
`{ ...authored, ...bootResult }`. createStandaloneStack() /
createDefaultHostConfig() return an `api` block carrying only the
environment-scoping decision — { enableProjectScoping: false, projectResolution:
'none' } — and under a shallow spread that object REPLACED the author's entire
`api`, silently dropping every key it did not itself set.

Two of those keys are live knobs the CLI reads a few lines later:

- api.requireAuth — the documented one-line opt-out for serving data publicly
  (ADR-0056 D2; v12 presents it as the whole migration). Authoring it did
  nothing: the value never reached the REST or dispatcher plugin, so anonymous
  requests kept getting 401 AND the boot warning that exists to make a fail-open
  posture visible never fired either.
- api.enforceProjectMembership — the ADR-0024 D9 opt-out from the
  sys_environment_member 403 gate. Silently fell back to the dispatcher default.

`api` now merges per key via a small pure mergeBootConfig helper: the author's
declarations survive and the boot builder still wins on the keys it decides
(scoping is not the author's call on a standalone host). Every other top-level key
keeps whole-value semantics — the artifact-serve path deliberately serves the boot
result's objects / permissions / manifest / plugins.

The auth-less carve-out was never affected: it lives in the
`?? ((tierEnabled('auth') || hasAuthPlugin) ? true : false)` fallback, which fired
precisely BECAUSE the authored value had gone missing. Only an explicitly authored
value was lost.

Verified end to end on the CRM example: with api: { requireAuth: false } an
anonymous POST /data/crm_account/query returned 401 before and returns records
after.

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

vercel Bot commented Jul 30, 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 Jul 30, 2026 2:40am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

18 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/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/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)

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.

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 size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

serve/dev 的 boot 合并把作者写的整个 api 块替换掉 —— api.requireAuth 和 enforceProjectMembership 被静默丢弃

2 participants