Skip to content

feat(providers): 自定义节点记录的真源存储层 —— <appData>/alpha-code-state/custom-providers/<env>.json,只有 main 写,零消费者 (#1391) - #1402

Merged
jinjunnn merged 1 commit into
alphafrom
feat/1391-custom-provider-truth-store
Sep 22, 2026
Merged

jinjunnn merged 1 commit into
alphafrom
feat/1391-custom-provider-truth-store

Conversation

@jinjunnn

Copy link
Copy Markdown
Owner

#1383 基线 §四 子票 1:自定义节点记录的真源存储层。只做存储,不接任何消费者 —— 本 PR 合入后用户可观察行为零变化(「自己填服务地址加节点」那半入口仍是 #1397 关掉的状态)。

改了什么

文件 角色
packages/ui-mac/src/main/custom-provider-truth.ts 读端:customProviderTruthPath(casBaseRoot, env) = <appData>/alpha-code-state/custom-providers/<env>.json(与三个 env 根、cas/、fence-workspaces/ 同级);readCustomProviderTruth(path, { readFileSync, log }) 严格读;记录判据 invalidCustomProviderRecord / invalidCustomProviderList。零写盘、不 import node:fs(读也经注入的 fs),将来 #1392 把它接进 alpha-models.ts / ext-config.ts 时可以进 sidecar 闭包
packages/ui-mac/src/main/custom-provider-truth-write.ts 写端:writeCustomProviderTruth(path, providers, fs) —— 先过读端同一份判据(坏记录不落盘、连目录都不建),再临时文件 + 同目录 rename(原子),失败收走临时文件、错误原样抛。不进 sidecar 闭包(下面实测)
三份测试 custom-provider-truth.test.ts(8)、custom-provider-truth-write.test.ts(6)、custom-provider-truth-fence.test.ts(2,darwin,真 sandbox-exec)
scripts/gate-files.tsv 三行登记(精确条数 8 / 6 / 2,fence 行 [平台:darwin])
docs/architecture/2026-09-09-req159-process-fence.md §2 加「状态根下 main 独占的真源」一张表(cas/ / fence-workspaces/ / custom-providers/)、§6 判据地图三行、§7 如实记「只有存储层,零消费者」;last_reviewed 抬到 2026-09-22。文档影响类别:architecture(系统形状:新增一个 main 独占的真源位置);无用户可见变化,不写 CHANGELOG

形状:{ "v": 1, "providers": [{ "id", "name", "compat": "openai" | "anthropic", "baseURL", "models": [...] }] }。密钥不进这里(仍在 #1343 的钥匙串库);记录里出现 apiKey 一类多余的键整份拒。compat 直接取 ProviderInput["compat"],读端的判据表用 Record<CustomProviderCompat, true> 钉住 —— 联合加成员这里编译期红。

读端语义(基线 I4 存储侧):缺失 = 「没有记录」({ ok: true, absent: true, providers: [] },零日志 —— 没加过节点是正常状态);坏 JSON / 版本不认识 / providers 不是数组 / 顶层多余键 / 字段不合法 / 同 id 重复 / 读错(EISDIR)= 「没问出来」({ ok: false, reason },不是空数组)且经注入的 log 恰出一行:custom providers: truth file <path> rejected — <reason>; nothing is derived from it (this is "unanswerable", not an empty list) until the app rewrites it。log 是必填依赖,没接日志的消费者编译不过。字段判据只问「是不是这个形状」(baseURL 只要求能被 new URL 解析);https / loopback 一类地址准入不在这里 —— 那是添加时与出网同源的那一份(I4,#1393),不抄第二份。

四条判据:先红后绿

先红(三份测试先落地、实现文件不存在):

--- custom-provider-truth.test.ts ---        error: Cannot find module './custom-provider-truth'   0 pass / 1 fail
--- custom-provider-truth-write.test.ts ---  error: Cannot find module './custom-provider-truth'   0 pass / 1 fail
--- custom-provider-truth-fence.test.ts ---  error: Cannot find module './custom-provider-truth'   0 pass / 1 fail

后绿(实现落地后,worktree .worktrees/1391-truth-store,HEAD = 本 PR):

custom-provider-truth.test.ts        8 pass / 0 fail   exit=0
custom-provider-truth-write.test.ts  6 pass / 0 fail   exit=0
custom-provider-truth-fence.test.ts  2 pass / 0 fail   exit=0   (真 /usr/bin/sandbox-exec,不需要 Electron 二进制)
# 判据 在哪 怎么证明它测得出已知的坏
1 真源路径不在可写集内:对着生产 renderProcessFenceProfile 渲染出的 profile,解析 (allow file-write* 块里每条规则(subpath / literal / regex)及其 ; Wn 注释,枚举全部固定行(17 个 id 手写字面量 = WRITABLE_ROOT_IDS 去掉 W1 = profile 里的 id 集合;W1 是运行时成员,#1390 规则 5b 兜),默认 base 与 dev 覆盖 base × 两种 XDG_STATE_HOME 形态逐条判「不覆盖」真源文件与它的目录;同批规则里 W2 确实罩着 alpha.jsonc(手段不是对什么都答「没覆盖」)。词法判据偏向判「覆盖」(大小写不敏感、按路径段比) custom-provider-truth.test.ts 测试内控制臂:塞一行罩住状态根 / 大小写变形 / regex / literal 各自点名,字符串前缀(alpha-code-state-2)不算,没 W-id 或解析不出的行让手段抛。生产变异(下面 B):在 process-fence-profile.ts 真加一行罩住 <casBaseRoot> ⇒ 6 pass / 2 fail
1′ 真裁判:同一份生产 profile 交给真 sandbox-exec(与 sidecar 里 sandbox_init 同一个 libsandbox),围栏内写真源文件 / 在真源目录建文件 / 在状态根建目录 / 先在 W2 造好再 mv 进真源 ⇒ 全部 Operation not permitted、盘上零落盘(mv 臂第一步真落在 W2 里,证明拦的是第二步);控制臂 W2 / W3 落盘;bare 臂不套围栏写得进,再经生产 writeCustomProviderTruth 落盘并读回,之后围栏内覆盖写 / rm 仍被拦。假的状态根放真 HOME 之下(/private/tmp 与 $TMPDIR 在可写集里) custom-provider-truth-fence.test.ts 生产变异 B:加一行罩住状态根 ⇒ 0 pass / 2 fail(Expected: not 0 —— 围栏内的写真的成功了)
2 原子性:renameSync / writeFileSync 中途抛 ⇒ 盘上仍是旧内容(逐字节 = 写之前的字面量)、目录里只有 prod.json(临时文件收走)、错误原样抛;带「同路径正常 fs 会写」的对照臂 custom-provider-truth-write.test.ts 对照臂:同一路径换回正常 fs ⇒ 内容变成新字面量
3 解析坏了就拒:十六种坏形状各一臂 ⇒ ok:false、reason 点名文件与哪一项、日志恰一行同一原因,没有一种退化成空清单;缺失那一臂零日志;EISDIR 也拒 custom-provider-truth.test.ts 正样本臂:两条记录逐字读回(文件里键序乱 → 固定键序)、与写端往返
4 写模块不在 sidecar import 闭包里:对着生产 sidecarSourceFiles(repoRoot) 实测(集合含基线点名的 alpha-models.ts / ext-config.ts / alpha-environment.ts,证明集合是活的;不含 custom-provider-truth-write.ts);读模块自己的相对 import 闭包只有它自己(type-only import 不拉写模块) —— 将来 #1392 把读模块接进 ext-config / alpha-models 时写模块不会跟着进 custom-provider-truth-write.test.ts 合成三文件控制臂:闭包工具看得见 value import、看不见 type import。生产变异(下面 A)⇒ 5 pass / 1 fail

闭包实测(仓里的 process-fence-write-sites.ts 的 sidecarSourceFiles / relativeImportClosure,本 PR HEAD)

sidecarSourceFiles: 66 files
  in closure: packages/ui-mac/src/main/alpha-environment.ts
  in closure: packages/ui-mac/src/main/alpha-models.ts
  in closure: packages/ui-mac/src/main/ext-config.ts
  write module in closure? false
  read  module in closure? false (本票零消费者,#1392 接进 ext-config/alpha-models 后才会进)
relativeImportClosure(custom-provider-truth.ts): [ "packages/ui-mac/src/main/custom-provider-truth.ts" ]
relativeImportClosure(custom-provider-truth-write.ts): [ ".../custom-provider-truth-write.ts", ".../custom-provider-truth.ts" ]

生产变异(树干净时做,做完 git checkout -- 还原,前后 git status --porcelain 都是 0 行)

A. ext-config.ts 末尾加 import "./custom-provider-truth-write"(把写端拉进闭包):

custom-provider-truth-write.test.ts   exit=1   (fail) 判据 4 … sidecarSourceFiles … 不含 custom-provider-truth-write.ts   5 pass / 1 fail
process-fence-write-sites.test.ts     exit=0   12 pass / 0 fail

第二行如实:写端的 fs 是注入的,AC3 登记簿的 AST 扫描器只认 node:fs 绑定,看不见它 —— 这正是判据 4 要单独存在(而不是靠登记簿 main-only 标签)的理由。

B. process-fence-profile.ts 的 renderProcessFenceProfile 加一行 (subpath "${resolve(alphaGlobalRoot, "..", "..")}") ; W2(罩住状态根):

custom-provider-truth.test.ts        exit=1   6 pass / 2 fail   (I2 逐条规则 + 控制臂两条都点名)
custom-provider-truth-fence.test.ts  exit=1   0 pass / 2 fail   (围栏内往真源写成功了:Expected: not 0)
process-fence-profile.test.ts        exit=1   25 pass / 1 fail  (既有的逐行全等闸也红)

门(worktree .worktrees/1391-truth-store,bash scripts/worktree-bootstrap.sh 装的 node_modules,base = origin/alpha@ee9200a6f)

门 结果
bun run --cwd packages/ui-mac typecheck tsgo -b exit 0,0 条 error TS
新增三份测试 8/8、6/6、2/2(上表)
process-fence-write-sites.test.ts / process-fence-profile.test.ts / process-fence-workspaces.test.ts 12/12、26/26、12/12,exit 0
gate-file-registry.test.ts 21 pass / 1 fail —— 红的那一条 ⑤ 在 base 上同样红(process-fence-wiring.test.ts: 登记 1 行,实测 4 行,#1394 带进来的,与本 PR 无关,下面「主动没做」);三条新登记行过了 ①–④ 与引用绊线
bash scripts/north-star-guard.sh ✓ zero upstream package edits,exit 0
docs gate python3 scripts/check-doc-links.py <改动的 md> ✓ 5 relative link(s) resolve,exit 0;git diff --check 0
check_docs_contract.py --strict(alpha profile) alpha-code PASS(alpha-platform / alpha-web 各有既存 error,不在本仓)
全量 bun test src(ui-mac) 5193 pass / 9 fail / 379 files(base ee9200a6f untouched:5177 pass / 9 fail / 376 files)。+16 pass = 本 PR 的 8+6+2,+3 files;9 条红的集合与 base 逐字相同(diff 为空):process-fence-apply / network-egress-fence(worktree 缺 Electron 二进制的已知假红)、gate-file-registry ⑤、alpha-path-residue ×2、takeover-adapter-coexistence(六行 (fail) 逐字相同);bun 总计的 9 fail 里另外 3 条没有 (fail) 行,两次运行的 error: 行按文件计数也逐字相同(alpha-path-residue 2 / ext-session-grants 2 / gate-file-registry 1 / process-fence-apply 1 / takeover-adapter-coexistence 1 / auth-recovery 3)—— 全部在 base 上同样红

pre-push 的 alpha-check.sh 本机会挂住(已知),用 git push --no-verify;上面每道门都在 worktree 里逐条实跑。

主动没做的

  • 不接任何消费者:buildAlphaModelConfig / readUserProviderIds / deriveEgressDestinations 一行没动(#1392);添加/删除时的准入、旧 alpha.jsonc 记录的处置与那一行日志(#1393);UI(#1392 恢复入口)。
  • 不做 custom-providers 的 topology 注册(基线 OPTIONAL);父目录不在就由写端 mkdirSync 建,与 fence-workspaces/ 同形。
  • 不 fsync:与兄弟真源 writeWorkspaceTruth 同形(tmp + rename);判据 2 讲的是进程内失败不留半截,不是掉电。
  • 不修 base 上既有的红:gate-file-registry.test.ts ⑤(process-fence-wiring.test.ts 源码文本断言条数登记 1 / 实测 4)、alpha-path-residue.test.ts 双向门、takeover-adapter-coexistence.test.ts 纯文本门 —— 都在 untouched 的 ee9200a6f 上量到,与本 PR 无关,回报给主 session。
  • 不动 docs/design/...-1383-...baseline.md(已批基线,状态在票上);不写 CHANGELOG(零用户可见变化)。

Refs #1383
Fixes #1391

🤖 Generated with Claude Code

…roviders/<env>.json,只有 main 写,零消费者 (#1391)

`#1383` 基线 §四 子票 1。真源与三个 env 根、cas/、fence-workspaces/ 同级,不在可写集任何固定行之下;
`{ v: 1, providers: [{ id, name, compat, baseURL, models }] }`,密钥不进这里。读与写拆成两个模块:
custom-provider-truth.ts(读端,零写盘、不 import node:fs,将来可进 sidecar 闭包)与
custom-provider-truth-write.ts(写端,原子 tmp + rename,不进闭包 —— 对着生产 sidecarSourceFiles 实测)。
读端严格:缺失 = 没有记录(ok、空清单);坏 JSON / 版本不对 / providers 不是数组 / 字段不合法 / 同 id 重复 /
多余的键 ⇒ 「没问出来」(不是空数组)+ 一行日志。

判据先红后绿:三份测试在没有实现时各 `Cannot find module './custom-provider-truth'`(0 pass / 1 fail);
实现后 custom-provider-truth 8/8、-write 6/6、-fence 2/2(真 sandbox-exec)。本票不接任何消费者,
用户可观察行为零变化(注入面 / enabled_providers / 出网派生是 #1392,准入与旧记录处置是 #1393)。

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jinjunnn
jinjunnn merged commit cf1bc18 into alpha Sep 22, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

「添加的自定义节点」记录改存一个 AI 写不到的地方(存储层)

1 participant