@sema-agent/settings-schema 14.7.0 → 14.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +209 -0
- package/README.md +58 -2
- package/conformance/VERSION +1 -1
- package/dist/atomic-write.d.ts +15 -0
- package/dist/atomic-write.js +109 -5
- package/dist/catalog-manages.d.ts +15 -0
- package/dist/catalog-manages.js +60 -0
- package/dist/center-refs.d.ts +68 -0
- package/dist/center-refs.js +70 -0
- package/dist/config-dir-migration.js +1 -0
- package/dist/config-fns.js +5 -3
- package/dist/config-root-documents.d.ts +106 -0
- package/dist/config-root-documents.js +93 -0
- package/dist/config-root-node.d.ts +117 -0
- package/dist/config-root-node.js +260 -0
- package/dist/conformance/run.d.ts +3 -0
- package/dist/conformance/run.js +10 -6
- package/dist/conformance/temp-roots.d.ts +31 -0
- package/dist/conformance/temp-roots.js +184 -0
- package/dist/default-model.d.ts +1 -1
- package/dist/default-model.js +5 -4
- package/dist/env-model-table.d.ts +163 -0
- package/dist/env-model-table.js +257 -0
- package/dist/fleet.d.ts +25 -7
- package/dist/fleet.js +24 -6
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/mcp-request-entry.d.ts +58 -6
- package/dist/mcp-request-entry.js +45 -13
- package/dist/mcp-server-name.d.ts +17 -0
- package/dist/mcp-server-name.js +19 -0
- package/dist/migration-receipt.d.ts +10 -6
- package/dist/migration-receipt.js +5 -3
- package/dist/node.d.ts +1 -0
- package/dist/node.js +2 -0
- package/dist/one-time-migrations.d.ts +4 -2
- package/dist/one-time-migrations.js +29 -1
- package/dist/own-key-read.d.ts +1 -1
- package/dist/retired-form.d.ts +19 -0
- package/dist/retired-form.js +24 -1
- package/dist/safety-merge-spec.d.ts +6 -6
- package/dist/tier-words.d.ts +33 -0
- package/dist/tier-words.js +43 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +24 -0
- package/dist/worker-row-verdict.js +7 -1
- package/docs/API-SURFACE.md +68 -4
- package/docs/CONSUMER-INTEGRATION.md +72 -2
- package/llms-full.txt +4 -1
- package/llms.txt +9 -3
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,215 @@
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 14.8.0 — 2026-10-27 — minor:配置根上的两份根级文档(env 模型表被目录顶替的决定文档 `config.env-models.json` 与按 id 一份的迁移运行记录)· 两只读者 `readEnvModelsDecision` / `readMigrationMarker` 与决定文件的唯一写函数 `writeEnvModelsDecision`(`./node`)· 一次性迁移 SS-M11(`migrateEnvModelsDecision`)· 目录管不管模型面的原文判官 `catalogManagesModelPlane` 与计数 `enabledEntryCount` · env 模型表的一份读法与写者的计划助手(`readEnvModelSlots` · `buildEnvModelBaseline` · `planEnvModelTableWrite`)· 两只模型面组拒码 `CATALOG_REPLACES_ENV_MODELS_CODE` / `SUMMARIZE_MODEL_MISSING_CODE` · 中心引用解析口 `resolveCenterRefs` 与记录读器 `readPlainStringRecord` · MCP 条目的 `sessionMetaKey` / `startupTimeoutMs`(解析透明)· MCP 服务器名保留前缀 `REMOTE_MCP_SERVER_NAME_PREFIX` 与请求面判官一对 `isUsableMcpServerName` / `describeBadMcpServerName` · 引擎钉 7.58.0 与上游漂移门 ⑧ / ⑪
|
|
12
|
+
|
|
13
|
+
> 档位(按发版纪律):**minor** —— 加性:新导出(包根 +53 名,其中两只组拒码常量同时在 `./fleet` 上;`./node` +9 名。包根:根级文档的名字、闭集答案表与读数身份拼法、原文判官两只、env 模型表读法与计划助手族、两只组拒码常量、中心引用解析口 `resolveCenterRefs` 与它的两张词表和读数型、记录读器 `readPlainStringRecord` 与它的原因词表 / 保留成员名 / 读数型、MCP 服务器名的保留前缀 `REMOTE_MCP_SERVER_NAME_PREFIX` 与请求面 MCP 名判官一对 `isUsableMcpServerName` / `describeBadMcpServerName`;`./node`:决定文档与运行记录的两只读者、一只写函数、一次性迁移 SS-M11 的迁移体与它的 I/O 座;导出面登记表合计〔同名在多条子路径上各计一次〕1396 → 1460 名);
|
|
14
|
+
> 公开闭集拓宽五处:`MODEL_PLANE_REFUSAL_CODES` 尾插两码、`ONE_TIME_MIGRATION_IDS` 尾插 `SS-M11`、`MIGRATION_APPLIED_AS` 尾插 `minted`(`MigrationReceipt` 的 `appliedAs` 随它)、`OneTimeMigrationWindow.oldForm` 加 `read-as-default`、`MigrationDocument` 加 `"config.env-models"`(对这五处持穷举 `switch` / `Record<…, …>` / `never` 兜底的消费方提货时编译红 —— 提货成本,不是 major)。
|
|
15
|
+
> `McpServerSpec` 多两只可选键:`sessionMetaKey` 与 `startupTimeoutMs`(都解析透明,`PARSE_TRANSPARENT_KEYS` 各多一行,接在表尾);不带这两只键的条目解析结果逐字不变;
|
|
16
|
+
> 带着它们的条目,v14.7.0 剥掉这两把键并报 `unknown-keys-dropped`,本版原样带过、不告警。归 minor 的依据是「发版纪律」的「新键 / 新导出 / 词表加词 = minor:可选键(无默认)」;
|
|
17
|
+
> 两把键解析透明 ⇒ 本包一字不拒、不是校验收紧,坏值由引擎拒(存量里写着有效值的条目,交给引擎后按它生效;写着坏值或 `null` 的,此前键被剥掉并报 `unknown-keys-dropped`,本版原样交给引擎 —— 见下文两条各自的「谁受伤」)。
|
|
18
|
+
> **读口行为变化有两处**:① `readModelRefusals` 对 `plane` 是这两只新码的体,以前把码当未知收窄成缺席,现在读出来;② `readWorkersRows` 扣下带旧字段的行时,`held[].reason` 改了措辞(`field` 不变;见下文「读口 `readWorkersRows` 扣下带旧字段的行时」那一条)。别的既有导出答案逐位不变(`McpServerSpec` 族另有一处:上一行的两只新键;`readEntryIdentity` 改调共享的「显式停用」读法,同一读法搬家,钉逐形核过;搬出来的那一次描述符读在自有描述符读法的普查登记表里按名多登一处,访问器判定用内核唯一拼法 `isAccessorDescriptor`;`materializeLocalRoot` 不是读口,另有一处变化:它铸的临时根里多一个心跳文件 `.alive`,本进程第一次铸根时顺带清扫心跳停了 60 分钟以上的旧件,见下文临时根那一条)。
|
|
19
|
+
> 没有新子路径、没有删改的导出。
|
|
20
|
+
> **SS-M11 的窗**:本包出组拒码常量 `CATALOG_REPLACES_ENV_MODELS_CODE` 与决定文档供引擎读,强制在引擎一侧,今天引擎(`@sema-agent/core` 7.58.0)不拒这个组;所以窗的起算点 = 引擎开始强制这道闸的那一天,到期日 = 起算日 + 30 天;本包登记表里 SS-M11 那一行按本包的发布日计,管的是本包何时能删迁移码。
|
|
21
|
+
|
|
22
|
+
- **配置根上的两份根级文档 + 两只读者 / 决定文件的唯一写函数(`src/config-root-documents.ts` 包根,`src/config-root-node.ts` 经 `./node`)**。「一份管模型面的 `config.d/models.json` 能不能顶替 env 模型表」是**配置根级的部署决定**,
|
|
23
|
+
落在 `<根>/config.env-models.json`(`ENV_MODELS_DOCUMENT`,`{"envModels":"replaced"|"kept"}`,strict);一次性迁移的运行记录**按 id 一份**,落在 `<根>/config.migration.<id>.json`(今天只有 SS-M11:`{"migration":"SS-M11"}`,strict;没有聚合文件、没有追加写)。
|
|
24
|
+
两份都在 `config.d` 之外 —— 已核的各版文件仓库只扫 `<根>/config.d`(抽样,不证全称),被检视的配置读者不把它们当域文档:写者不探引擎版本。`CONFIG_ROOT_DOCUMENTS` 是两份的名单(引擎的目录信任判官、客户端的持久文件登记取它,不手抄)。
|
|
25
|
+
读只有两只:`readEnvModelsDecision(root)` ⇒ `absent | replaced{digest} | kept{digest} | unreadable{bad_json|bad_shape, digest} | unreadable{io, errno}`(闭集;**只有文件不存在才是 `absent`** —— 截断的文件、目录、悬空的符号链接都读不懂;
|
|
26
|
+
有字节的四臂带同一次读的字节 SHA-256 前 16 位,没字节的带 errno;可信属主的 0000 文件读恒 `EACCES`,`io` 可以是持久的)· `envModelsDecisionTag(decision)` 是读数身份的一种拼法(`absent` / `d:<digest>` / `io:<errno>`),
|
|
27
|
+
引擎读这份决定时(今天引擎还不读),「每份内容只告警一次」与本地 etag 都取它,不再开文件;`readMigrationMarker(root, id)` ⇒ `absent | present | unreadable`(「跑过」只问这一份文件,读不懂的按跑过处置)。两只的读数从不抛(`readMigrationMarker` 只对不写运行记录的 id 抛 `TypeError`,不拿它拼路径);都接一只注入的 I/O 座(`ConfigRootIo`,真绑定 `nodeConfigRootIo`,读字节)。
|
|
28
|
+
决定文件的写只有一只(迁移铸确认也经它的 `createIfAbsent`;运行记录不经它,由迁移独占创建):`writeEnvModelsDecision(root, "replaced"|"kept", { write })` —— `overwrite`(写者:用户确认之后整份覆盖,读不懂的旧文件也换掉)/ `createIfAbsent`(不存在才建,临时文件 fsync 后 `link` 成目标名;只有 `EEXIST` 算已在 ⇒ `present`、一字不动,
|
|
29
|
+
悬空的符号链接也算已在)。模式与 `config.d` 域文档**同一条规则**(原子写器的 `config-document` 类,模式表不变):新建 0o644 经进程 umask(更严的 umask 只会更严:umask 077 下是 0o600),已有的文件沿用它原来的模式
|
|
30
|
+
(运维 chmod 过的决定文件不被一次覆盖写改回;一份被改成 0000 的决定文件读恒 `EACCES`,要修由运维自己 chmod)。各端自写在 umask 002 下落 0664,会被引擎的目录信任判官拒 —— 一律调写函数。本包不判属主 / 写位:那是引擎在读之前的判官的事。
|
|
31
|
+
三问 —— **谁需要**:引擎(本地配置腿强制这道闸时读决定、起服时调迁移读写两份;今天引擎都还没接)与三个写者(写目录之前读决定、确认后写 `replaced`);**谁受伤**:无 —— 新导出、新文件名,没有既有读者;
|
|
32
|
+
**补偿**:无需。钉:`test/config-root-documents-14.8.0.test.ts`(语料逐格:决定 16 格、运行记录 11 格,语料文件 `test/fixtures/config-root-documents-corpus.json` 两条包线共用、原样再跑 · io 格注入读座与真 0000 文件 · 注入读座抛出读不出的东西(会抛的 `code` getter / 已撤销的 Proxy)⇒ `io`、errno `uncoded`,只靠继承的 `code` 不算缺席(errno 只认自有的)· 目录 / 悬空链接 = io ·
|
|
33
|
+
digest 按内容 · 读数身份三拼法 · 聚合形旧草案文件一律不读 · overwrite / createIfAbsent 两写法 · 决定文件一个写点(结构:迁移经写函数)· 竞态不吞 · 与域文档同一条模式规则(umask 077 ⇒ 0o600、022 / 002 ⇒ 0o644;逐 umask 与 `config-document` 对拍,退化的 umask 也一样)· 已有 0600 / 0640 / 0664 / 0000 的决定文件被覆盖写后模式不动 · 写失败点名路径 ·
|
|
34
|
+
根级文档对文件仓库惰性:effective / version / 告警逐字不变 · 结构:名字字面一处、文本读法只被读者调、迁移经两只读者读);`test/atomic-write.test.ts`(模式表两行,`existing` 只有 keep / reset 两个词)、`test/atomic-write-exclusive-14.8.0.test.ts`(不存在才建:短写不发布半截文件 · `link` 只有 `EEXIST` 算已在 · fsync 失败落点 · 任何写器都不 fchmod)、`test/file-modes.test.ts`(结构:`src/` 里没有 fchmod 与精确模式一臂)。
|
|
35
|
+
- **一次性迁移 SS-M11:配置根上铸一次「env 模型表被目录顶替」的决定(`migrateEnvModelsDecision`,`./node`;登记见 `ONE_TIME_MIGRATIONS` 那一行)**。决定文档出现以前,管模型面的目录**静默**整表顶替 env 模型表;
|
|
36
|
+
引擎开始按决定执法之后,存量根要保住这个读法。规则一条:**运行记录缺席才跑;先读决定 D0 与 models.json 管不管 M0,再以「不存在才建」写运行记录 —— 建成的那个进程是本根唯一的铸造者,别的进程见 `EEXIST` 即停、永不铸;
|
|
37
|
+
铸造者只在「D0 缺席 ∧ M0 管」时以「不存在才建」铸 `replaced`(env 表声没声明都一样:保的是升级前「管模型面的目录整表顶替」的读法),此后不再读决定**(D0 是预置的 `replaced` 而它在选举之后被删 ⇒ 不重铸;D0 缺席而选举之后有人写了 `kept` ⇒ `exists`、一字不动)。
|
|
38
|
+
结局闭集 `EnvModelsMigration`:`already_ran` / `marker_unreadable`(按已跑处理)/ `not_minter` / `marker_write_failed{errno, error, wayOut}` / `recorded{createdRoot, decision, mint}`,`mint` = `minted` / `exists` / `failed{errno, error, wayOut}` /
|
|
39
|
+
`skipped{decision_present|not_managing}` / `models_unreadable{errno, error, wayOut}`(判序如列;只有 `ENOENT` 算没有 models.json,坏文件算不管;别的读错不折成「不管」、单列一词 —— 要运维动手:不替人铸(fail-closed),带 errno(底层错误带码时;悬空链接 / 不是常规文件没有)与手工恢复句,运行记录已在 ⇒ 此后不再尝试);env 只在铸成之后读一次、只读两槽;铸了时带被顶替的 env 模型(名 + 槽 + 来源键;env 表没声明 ⇒ 空),调用方据此发**一行**告警(本包不出声)。不建 `config.d`;根缺席 ⇒ 逐级以恰是 0o700 的目录建根(umask 0777 下也进得去)、只放运行记录。
|
|
40
|
+
运行记录在场之后,决定缺席 ≡ `kept`,读这份决定的一律这么读(引擎强制这道闸之后、迁移、写者)。撤销的终局按运行记录的创建者算:只有创建者能写确认;以运行记录在场为前提,创建者写完 / 判定不写 / 失败(结局 `minted` / `exists` / `skipped` / `models_unreadable` / `failed`,调用方各记一行)之后,删掉决定文件就是撤销且是终局(删掉运行记录再起服 ⇒ 迁移重跑,决定缺席即重铸);其它副本读到记录在场 = 已跑、跳过(或得 `EEXIST` 答 `not_minter`),它们的返回不构成终局;创建者当选到结局落定之间的窗口在挂起 / I/O 卡顿下无上界,窗口内唯一安全的撤销是写 `kept`(独占创建护住它;删掉的会被创建者照它先读的 D0 铸回);创建者在写确认之前崩溃 ⇒ 永久 `kept`(fail-closed,披露)。收据词表尾插 `minted`(只属 SS-M11:在缺席处写下一份新事实,不改任何已有的值);
|
|
41
|
+
登记表旧形待遇尾插 `read-as-default`(旧形是文档的缺席,读者按缺省读、没有拒句可点名)。窗与删法只在登记表那一行;删码之后缺席读作 `kept`;引擎强制这道闸之后,出路由它的组拒句点名(今天引擎不拒这个组)。
|
|
42
|
+
三问 —— **谁需要**:升级前已经用目录顶替 env 模型的本地部署(升级后照旧由目录服务,只多一行告警点名被顶替的 env 模型);**谁受伤**:① 迁移窗内首启、预置了管模型面目录的**新装**根 —— 迁移分不出「升级前」与「新装」,一样铸 `replaced`(告警点名);
|
|
43
|
+
② 整根只读(ConfigMap)的部署 —— 写不了运行记录就不铸,引擎开始强制这道闸之后升级当拍目录被拒、换 env 模型服务(今天引擎不拒这个组);③ 进程恰在写完运行记录、铸确认之前中断,或铸失败、或那一刻 models.json 读不出的根 —— 此后无人再铸,换 env 模型服务(披露);
|
|
44
|
+
④ 创建者当选到结局落定之间(窗口在挂起 / I/O 卡顿下无上界)有人删了确认 —— 被创建者照先读的 D0 铸回;其它副本的返回不构成终局(披露);⑤ 配置根缺席的部署 —— 迁移以引擎进程的用户、0o700 建根,与引擎不同用户运行的客户端之后进不去(披露);**补偿**:①:删掉决定文件或写 `kept`(运行记录在,迁移不再跑;下一次起服生效 —— 运行中的引擎保留它在服的那一代目录);②:升级前把 `config.env-models.json` 放进同一份 ConfigMap 的配置根;
|
|
45
|
+
③:按结局里的手工恢复句手写决定文件;④:窗口内撤销写 `kept`(不存在才建守住它;删除只在运行记录在场、创建者结局落定之后是终局);⑤:客户端在引擎首次起服之前自己建好配置根。
|
|
46
|
+
钉:`test/ss-m11-env-models-migration-14.8.0.test.ts`(登记 · 记录在场跳过 / 坏记录(语料每种 + 0000)按已跑 / 聚合旧草案文件不影响判定 / 一次性 · 铸的两合取逐格 · env 表未声明(含只有 cheap 槽)照铸、被顶替的为空,纯空白照点名 · env 表空且目录不管 ⇒ not_managing ·
|
|
47
|
+
不管 / 无 models.json / 坏文件不铸 · models.json 读错(真 0000 / 注入 EIO / 悬空链接 / 目录占位)⇒ models_unreadable、不铸、带 errno 与恢复句,决定已在时判序先答 decision_present · 带 BOM 的 models.json 照样算管 · 不建 config.d · 语料逐格处置(只有 D0 缺席铸)· 读写顺序 · 记录建不成不铸(注入 + 真只读根,带 errno 与恢复句)·
|
|
48
|
+
独占创建与唯一铸造者:建记录后写 kept ⇒ exists · 两副本交错 B 永不铸、删后不铸回 · 预置 replaced 建记录后删 ⇒ 不重铸 · 跨副本 ②″(B 返回之后删 ⇒ 创建者铸回 / 窗口内写 kept 护住 / 创建者写前崩溃 ⇒ 永久 kept)· 三处文字一致 · 铸失败 ENOSPC / EIO / EPERM / 硬链接不被支持 ⇒ failed 且不重试 · 中断在记录之后不补铸 ·
|
|
49
|
+
根缺席 umask 002 / 0777 下逐级 0o700 · 铸出的确认与运行记录与域文档同模式规则(umask 077 ⇒ 两份 0o600、022 ⇒ 0o644));`test/one-time-migrations-11.0.0.test.ts`(闭集尾插)。
|
|
50
|
+
- **「目录管不管模型面」的原文判官 `catalogManagesModelPlane(doc)` 与计数 `enabledEntryCount(doc)`(`src/catalog-manages.ts`,包根)**。各端各写一份「有没有启用条目」的判法,`enabled` 怎么读各说各的。
|
|
51
|
+
现在一条律:**`models` 里至少一条不是「显式停用」的条目 ⇔ 管**;「显式停用」= 纯 JSON 对象的自有数据属性 `enabled` 恰为 `false` —— 与被丢条目身份(`entry-dropped` 告警的 `identity.enabled`)读的是**同一只**谓词,本包只剩一份读法;
|
|
52
|
+
判官 = 计数 > 0。文档不是对象、`models` 缺席或不是数组 ⇒ 0;坏条目(不是对象)⇒ 计入(引擎读腿的载体半场同答:形不对 ⇒ 算管);读不出的值(撤销的 Proxy、访问器 / 隐藏键、类实例)⇒ `TypeError`(两个调用方失败方向相反,不替它们猜)。
|
|
53
|
+
读后表示(引擎自己的被丢条目载体那一半)不在本包。
|
|
54
|
+
三问 —— **谁需要**:引擎(强制这道闸时,「管不管」与日志计数调它;今天引擎还没接)、桌面与命令行(启用判改调它)、迁移与计划助手(本包内);**谁受伤**:无 —— 新导出;被丢条目身份的读数逐形不变;**补偿**:无需。
|
|
55
|
+
钉:`test/catalog-manages-14.8.0.test.ts`(逐形 14 格 · 只认自有数据属性 / 访问器零调用 / 原型污染不读 · 读不出抛 · 洞不是条目(只认自有下标,被污染的 `Array.prototype[i]` 不算条目)· 同一条坏条目:身份 `enabled:false` ⇔ 判官视作停用 · 结构:`readEntryIdentity` 调共享谓词的「形已判过」内核 `ownEnabledFlag`、不自读 `enabled`;同一条目经 `salvageEntries` 只被问 2 次原型,与 v14.7.0 同数)。
|
|
56
|
+
- **env 模型表的一份读法与写者的计划助手(`src/env-model-table.ts`,包根)**。`MODEL_ID` / `MODEL_CHEAP_ID` 两槽与它们铸出的角色底此前只在引擎里读;写者要在写目录之前知道「这道闸强制起来会不会拒、会顶掉什么」,没有同一份读法可调。
|
|
57
|
+
现在:`readEnvModelSlots(env)`(`MODEL_ID` 缺席 / 空串 = 没有主槽,纯空白算在场;cheap 槽四态 `EnvCheapSlot` + `mintedCheapId`)· `envModelTableDeclared(env)`(引擎一侧的顶替闸〔今天引擎还不强制〕与计划助手共用的那一只谓词)·
|
|
58
|
+
`buildEnvModelBaseline(env)`(条目 + 角色底 + `MODEL_VERIFIER` 读数;键名表 `ENV_MODEL_TABLE_KEYS`、别名键 `DEFAULT_MODEL_KEY`)。env 快照按「读到即算」(下标读)。
|
|
59
|
+
计划 `planEnvModelTableWrite({ env, target, decision, candidate, intent? })`(结局闭集 `write{not_read|not_managing|env_table_empty|acknowledged}` / `write_with_decision` / `confirm{confirmKey, intent: absent|stale}` / `fix_first`),判序:
|
|
60
|
+
目标腿不读 `config.d`(remote / none)⇒ 照写 → 候选不管 ⇒ 照写 → summarize 会悬空 ⇒ `fix_first`(不可确认)→ env 表未声明 ⇒ 照写、**不写确认** → 决定 `replaced` ⇒ 照写 → 确认键对得上 ⇒ 先写决定再写目录 → 否则问。
|
|
61
|
+
回执按**上游 id**:退场的 env 条目带 `servedAs`(候选里同 id 的启用条目名),每个 env 底角色带写前绑谁(env 表键 + 它解到的 id)与采纳后落点 `{name, id}`(缺省落点经 `resolveCatalogDefaultModel`;档位词 / CC 档位别名 / `best` 经候选的活跃档位组展开 —— 与引擎装配同一序:先摘掉绑到停用条目的档,再按 core 的档序向下落(绑到目录外的名过不了 schema:候选读不进 ⇒ `TypeError`);绑到停用条目或目录外、展开也落不到 = `dangles`);
|
|
62
|
+
回执带决定读数(读不懂的 —— 含持久 io —— 确认后整份覆盖);确认键 = 回执的定序 JSON,后果一变旧键就过期。候选读不出或整份读不进 models 域 ⇒ `TypeError`(先修文档,不替它算回执)。拒句投影 `envModelTableWriteRefusal(plan)`(`confirm` / `fix_first` 各一句;只带 env 键名、目录名与模型 id —— 可披露的原样、秘密形的打成记号;凭据与别的 env 值从不进句子)。
|
|
63
|
+
三问 —— **谁需要**:三个写者(命令行、桌面、客户端核心)在改写 models.json 的每一条路上调它;引擎改读 `readEnvModelSlots` / `buildEnvModelBaseline`(不再自写一份);**谁受伤**:无 —— 新导出;**补偿**:无需。
|
|
64
|
+
可用性(引擎的 brain 腿、端点、读腿逐条丢)计划判不了;决定性的 summarize 那一格由引擎的不变量兜。
|
|
65
|
+
钉:`test/env-model-table-plan-14.8.0.test.ts`(两槽读法 / 纯空白在场 / 原型分层快照 · 角色底(verifier 指 cheap、code 角色装饰、rejected、表空)· 判序七格各一 · 回执逐格(同 id 异名 servedAs、同名异 id 落点 id)· 档位缺省落点 · 继承的档位词按活跃组展开(flash / pro / opus / best,绑到停用条目的档摘掉后沿档序落)· {select} 缺省 · 停用绑定 dangles ·
|
|
66
|
+
确认键定序且随后果变 · 语料逐格处置与持久 io 读数 · 入参闭集 / 读不出 / 读不进抛 · 拒句不回显凭据);`test/tier-words-parity-14.8.0.test.ts`(档序与 CC 别名表 = devDependency core 的两张表;展开逐夹具逐词对拍 core `expandTiers` 的摘后表,含不可服务绑定四形;core 对未摘的原表整张抛)。
|
|
67
|
+
- **两只模型面组拒码尾插:`CATALOG_REPLACES_ENV_MODELS_CODE` / `SUMMARIZE_MODEL_MISSING_CODE`(`src/fleet.ts`,包根与 `./fleet`)**。本地配置腿上「管模型面的目录会顶替已声明的 env 表、决定不是 `replaced`」的那道闸,与 summarize 不变量(每个任务都要解的 summarize 落不到可服务条目)
|
|
68
|
+
各有一只组拒码;码名是契约词,由本包定,本包出常量供引擎读;强制在引擎一侧,今天引擎(`@sema-agent/core` 7.58.0)不拒这两个组。`MODEL_PLANE_REFUSAL_CODES` 尾插 `config.catalog_replaces_env_models`、`config.summarize_model_missing`(序即声明序)。
|
|
69
|
+
三问 —— **谁需要**:引擎(铸码)与读 `/health.modelRefusals` 的管理面 / 三端(渲染「为什么目录没落地」);**谁受伤**:对组拒码穷举的读者 —— 提货时编译红;**补偿**:为两码各补一臂。读者没提货之前,`readModelRefusals` 照旧把两码收窄成缺席(读不出原因,但不误判)。
|
|
70
|
+
钉:`test/env-model-table-plan-14.8.0.test.ts`(码逐字 + 尾插)、`test/model-refusals-6.2.0.test.ts`(闭集六码)。
|
|
71
|
+
- **随包文字与导出面登记表**。README 新增升级指引一节(写者动作:读决定 → 计划 → 确认后经 `writeEnvModelsDecision` 写、不自写;撤销 = 删掉或写 `kept`;整根只读部署的升级前置步;中断与铸造窗口的披露;SS-M11 的窗的到期日公式)、
|
|
72
|
+
本地文件布局补两份根级文档与「根归客户端、引擎只写这两份」及写序(离线预置(引擎在本根起服之前铺好)时两份文档与域文档可按任意顺序写;运行时写序是定的:写者先写决定、再写 `config.d/models.json`(同一把锁),迁移先建运行记录、再铸决定)、功能清单一条;`docs/API-SURFACE.md` 重生成。
|
|
73
|
+
三问 —— **谁需要**:三端与引擎的接线者;**谁受伤**:无;**补偿**:无需。
|
|
74
|
+
- **符合性 kit 的临时根不再在进程死掉后一直残留(`materializeLocalRoot` 的临时目录收口,包内 `src/conformance/temp-roots.ts`,不进导出面)**。每个 case 的本地腿建一个 `<tmp>/settings-conformance-<随机>/`,清理只在适配器腿的 `finally` 里 ——
|
|
75
|
+
跑语料的进程被 `process.exit` 截断或未捕获异常时 `finally` 不跑,根就留在宿主 tmp 下(跑符合性语料的机器上实测累积过万个)。收口两道,一个模块:① **活性证据是根里的心跳文件**:铸根时在根内以 `O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW` 建一个空文件 `.alive`(静态为空),`fstat` 记下它的 `(dev, ino)` 就**关掉**,再起一只 `unref()` 的定时器,每 5 分钟(`ALIVE_TOUCH_INTERVAL_MS`)一拍:按路径 `O_WRONLY | O_NOFOLLOW | O_NONBLOCK` 打开 `.alive`(不带 `O_CREAT`;`O_NONBLOCK` 让被换成没有读端的命名管道的 `.alive` 当场打不开〔`ENXIO`〕作罢,不在文件系统线程池里一直阻塞)、`fstat` 比 `(dev, ino)`,**相等才**写一个字节(换行,空白)再截回空、mtime 由文件系统自己打(两步都刷新它),写完关掉;不相等就关掉、这一拍不写;**心跳按 inode 身份写、不持句柄**:规则只有一条「同一 inode 才写」—— `.alive` 被换成符号链接(有 `O_NOFOLLOW` 的平台上打开就被拒;没有它的平台上打开会跟过去,身份比对仍拦住指向别的文件的链接)、硬链接到别的文件、换成另一个文件或目录、被删,或根被删后同名重建,心跳都不写别的文件,也不把任何路径建出来;模块里没有常驻的文件描述符(铸根时那一只用完即关,每一拍那一只一拍一开一关);`cleanup()` 停定时器、再删根(不开不关任何文件);
|
|
76
|
+
进程以任何方式死掉(`process.exit`、未捕获异常、信号〔`SIGINT` / `SIGTERM`〕的默认动作、SIGKILL),心跳随它停,根留着,`.alive` 就变老。本进程第一次铸根时清扫:名字恰是 `<前缀><六位随机>`(`mkdtemp` 直铸的形)、是目录、属主是当前用户、根内有 `.alive` 常规文件、
|
|
77
|
+
且 `.alive` 的 mtime 早于 60 分钟(`STALE_LOCAL_ROOT_AGE_MS`)的旧件,整根删 —— 规则只有这一条:`.alive` 超过 60 分钟没更新之后,由下一轮第一次铸根时的清扫收。不问进程号、不问主机名:证据与文件系统同域,共享同一 tmp 的几个进程、几个容器(跨 pid 命名空间也一样)读到的是同一个答案,`.alive` 在更新的根不被清(限制见下);
|
|
78
|
+
根里没有 `.alive` 的(更早的版本铸的、运维手造的同形目录)一概不动,拿不准就不删;符号链接与文件不动;
|
|
79
|
+
② 正常路径:适配器腿 `finally` 里的 `cleanup()` 停这个根的心跳定时器、再删根(只删自己的根;不开不关任何文件;还在飞的一拍随后在已摘出目录的文件上做完、或打不开而落空,被吞掉),语料落盘的文件名闸不变。本模块不在进程上装任何监听(不碰 `exit`,也不碰 `SIGINT` / `SIGTERM`;定时器 `unref`,不拖住进程退出)—— 宿主自己的 `exit` / 信号处理器(含排空、含铸根之后才装的)一概不受影响,宿主的 `exit` 处理器里照样读得到根。
|
|
80
|
+
清扫是尽力而为:删不掉的(别的用户的、没权限的)留着,之后的清扫再试,不改一次符合性结论。本模块不读进程的钟:清扫的「现在」是刚铸出的根目录的 mtime(建 `.alive` 那一刻由文件系统打),心跳的 mtime 同样由文件系统打,二者同一只钟,进程自己的钟拨偏多少都不改变清扫的答案。
|
|
81
|
+
三问 —— **谁需要**:跑符合性语料的 cli / core(语料大、进程常被 CI 超时杀);**谁受伤**:心跳停了 60 分钟的活进程 —— 它的根会被别的进程当作死进程的根清掉,这是已知限制,不是本版给出的保证。心跳停有两种:事件循环停转超过 60 分钟(被 `SIGSTOP` 暂停、容器被冻结、停在调试器断点上);心跳连续 60 分钟写不进去(文件系统写满、被重挂成只读 —— 这一拍作罢、不抛、不报);
|
|
82
|
+
别的部署只多一次对 tmp 目录的读、每个根里一个空文件、每 5 分钟一次对它的一拍(一次打开、一字节写入与截回、一次关,不占常驻的文件描述符),以及清扫里对符合条件旧件的删除(名字恰是前缀加六位随机、是目录、属主是当前用户、`.alive` 老过年龄线才删;符号链接与文件不动);
|
|
83
|
+
**补偿**:`ALIVE_TOUCH_INTERVAL_MS`(5 分钟)与 `STALE_LOCAL_ROOT_AGE_MS`(60 分钟)是源里的具名常量、读得到,**不是旋钮**(没有 env、没有参数改它们);一个 case 的一条腿只要几毫秒、整套语料也只要几秒,60 分钟远长于一次正常的跑。
|
|
84
|
+
披露:`process.exit` 未 cleanup、未捕获异常、被信号的默认动作或 SIGKILL 收走的进程,都留一个根,`.alive` 超过 60 分钟没更新之后,由下一轮第一次铸根时清掉;宿主要在退出时即时收根的,在收尾处自己调 `cleanup()`。
|
|
85
|
+
宿主进程活着时,它铸出而没 cleanup 的根一直带着新鲜的 `.alive`,清扫不碰,进程死后才开始变老;
|
|
86
|
+
宿主活着却没 `cleanup()` 的根,它的心跳定时器(`unref`,不拖住进程退出)每 5 分钟(`ALIVE_TOUCH_INTERVAL_MS`)开写一次、直到进程结束 —— 代价是一次一字节的小文件写(一拍一开一关,不占常驻的文件描述符),补偿是调 `cleanup()`(契约本就要求);
|
|
87
|
+
心跳一拍可能卡在文件系统上(挂住的网络盘):上一拍还没做完时,到期的这一拍直接跳过(每个根一面在飞旗)—— 不叠写、不排队;挂住的那一拍做完(成功或失败)后旗复位,心跳照常。
|
|
88
|
+
`mkdtemp` 与写 `.alive` 之间被杀的进程留下一个没有 `.alive` 的根,清扫不认它,要清的手工删。
|
|
89
|
+
定时器取自 `node:timers`:只接管全局 `setInterval` 的伪计时器(vitest 的 `vi.useFakeTimers()` 默认如此)接管不了它;连 `timers` 模块一起接管、且先于本包首次 import 装上的伪计时器库会把这只心跳接过去,跑尽定时器的调用(如 `runAll`)会撞上它 —— 补偿是跑尽定时器之前先调 `cleanup()`(契约本就要求;它清掉的正是这只心跳)。
|
|
90
|
+
`.alive` 静态为空,只给清扫看:`loadLocalConfig` 只按名读 `config.d/<域>.json`、`config.d/remote-exec.json` 与 `.env`,列文件只列 `config.d/`,所以它不被 `loadLocalConfig` 自动读到;谁在 `.env` 里用 `file:` 显式引用它,读到的是空文件、被拒(`file:` 引用不接受去掉首尾空白后为空的文件,与引用一个不存在的文件同拒),不会解析成一个值;心跳写进去的那一个字节是换行(空白),只存在于心跳那两步之间,截回失败或进程恰在两步之间死掉时它会留在文件里,但 `file:` 引用读到后会去掉首尾空白,留着一个换行的文件去掉首尾空白后仍为空,照样被拒。
|
|
91
|
+
钉:`test/conformance-temp-roots-14.8.0.test.ts`(真进程:描述符数限(软、硬都是 256)的子进程里先铸 300 个根再统一 `cleanup()`,300 个都铸成、都清掉、tmp 里一个不剩,与不带心跳的 v14.7.0 同答;建根后 `process.exit` / 未捕获异常没 cleanup ⇒ 根留着、退出码照旧,把根里 `.alive` 的 mtime 拨到两小时前,再起一个进程铸根的清扫收走它;宿主在铸根之后装的 `exit` 处理器里读得到 `config.d/models.json`(`process.exit` / 自然退出 / 未捕获异常三种退出);
|
|
92
|
+
SIGKILL 与没有宿主处理器的 SIGTERM / SIGINT 留一个根、进程死于该信号,清扫收掉它而 `.alive` 新鲜的 / 没有 `.alive` 的 / 异名的 / 文件 / 符号链接不动;宿主已装优雅退出处理器(`on` / `once`)时不被杀;宿主装着排空处理器(`on` / `once`,含铸根之后才用 `prependOnceListener` 装的)时信号到来、200 ms 后根与语料文件仍在、case 跑完;
|
|
93
|
+
宿主在投递中把监听整个换成自己的一只 ⇒ 根不动、宿主跑完;首次铸根的清扫中途来信号、宿主吞掉它 ⇒ 铸根照常完成、根还在;名字是 `<前缀><六位随机>`、根内是空的 `.alive`;铸了根没 cleanup 而手上没别的事 ⇒ 进程自己退出(定时器 `unref`);连 `timers` 模块一起接管、先于本包首次 import 装上的伪计时器库取走心跳,`cleanup()` 清掉的就是它;
|
|
94
|
+
根目录自己的 mtime 再老、`.alive` 新鲜 ⇒ 不动(铸它的进程早已死也一样),`.alive` 老了才清;没有 `.alive` 的同形旧目录(含真进程里 `mkdtemp` 直铸的)再老也不动;`.alive` 是目录或符号链接不算;名字形的边界与年龄线的边界;
|
|
95
|
+
铸根之后进程上每一种事件的监听数不变、`src/conformance/` 下没有给进程挂 / 摘监听的调用;`cleanup()` 只删自己的根;随包文字与实现同读)、`test/conformance-temp-roots-alive-14.8.0.test.ts`(进程内:铸根的名字与根内空的 `.alive` · 铸根不问主机名 · 定时器取自 `node:timers`、间隔与 `unref`,由注入的驱动逐拍触发 · 间隔到期才写、下一个间隔再写 · 每一拍做完 `.alive` 的大小回到 0、mtime 前进、仍是同一个文件 · 截回空失败时留着的那一个字节是空白、`file:` 引用仍被拒 · 上一拍还没做完(写挂住)时本拍跳过、不叠写,做完(成功或失败)后旗复位 · 心跳不持句柄、按 inode 身份写:`.alive` 被换成指向受害文件的符号链接 / 硬链接、根被删后同名重建并放符号链接,几拍之后受害文件的内容 / 大小 / inode / mtime 不变、根里不多任何文件、没有一次写或截;`.alive` 被另一个常规文件原子顶替或换成目录 ⇒ 每一拍都开始又收场、不写不建不抛;铸根与打开之间路径被放了符号链接 ⇒ 铸根抛、不留根、不起定时器;`.alive` 被换成没有读端的命名管道 ⇒ 这一拍当场打不开、整拍收场;身份比 `(dev, ino)` 两样、用 `bigint` 比;铸根返回时与每一拍之间手里没有开着的句柄,每拍的打开标志位恰是 `O_WRONLY | O_NOFOLLOW | O_NONBLOCK`、铸根的恰是 `O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW`,`cleanup()` 不开不关任何文件;`cleanup()` 撞上还在飞的一拍不抛、不冒未处理的拒绝、不重建路径;重叠的两次 `cleanup()` 都不抛;记下身份、关掉 `.alive` 之后铸根失败(如起定时器抛)回滚到什么也没留下:根删、无持有项;源里零 `FileHandle`、按路径打开恰两处(铸根一处、心跳一拍一处,后者不带 `O_CREAT`、`close()` 在 `finally` 里) · 进程自己的钟拨偏两小时后 `.alive` 的 mtime 仍是文件系统的现在、清扫不删 · 心跳不创建 `.alive` · 删根即停这个根的定时器 · 根里的 `.alive` 不被 `loadLocalConfig` 自动读到:没有谁用 `file:` 显式引用它时,整份答案与没有它的同内容根逐字相同;`.env` 里用 `file:` 引用它被拒,与引用一个不存在的文件同拒 · 源里没有取主机名 / 取进程号 / 问进程在不在的调用 · 导出面是闭集)、`test/conformance-temp-roots-fake-timers-14.8.0.test.ts`(消费方开着只接管全局的伪计时器〔`vi.useFakeTimers()`〕铸根,`vi.runAllTimers()` 不抛、伪计时器里没有心跳)、`test/conformance-local-root.test.ts`(文件名闸原样绿)。
|
|
96
|
+
- **「公开导出有哪些」全仓只枚举一遍(`scripts/api-surface.mjs` 的 `readModuleExports`,仓内脚本,不进包)**。导出面登记表的生成器与发版准备时重算「受影响导出表」的助手(`test/helpers/affected-exports.ts`)此前各枚举一遍 exports 表里每条子路径的导出:
|
|
97
|
+
生成器沿别名链逐跳问「是不是 type-only 再导出」,助手只问导出符号自己、再一步解到真身 —— 中间一跳是 `export type { X } from …` 的再导出链上,生成器答「只是类型」(运行期拿不到,登记表的「判据自证」格对着运行期模块对象核过),助手答「值导出」。
|
|
98
|
+
现在生成器接收一棵树的根(缺省本仓)并暴露符号级读法(子路径 + 名字 + 形 + 走完别名链的真身符号;别名链不设跳数上限,环靠「见过」收住),`computeApiSurface` 是它的投影,助手取它结果里形不是 `type` 的当公开值导出、删掉自带枚举。真树上两份答案今天逐名一致(651 个名字),`docs/API-SURFACE.md` 逐字不变。
|
|
99
|
+
三问 —— **谁需要**:发版准备的人(受影响导出表不会再把一个运行期不存在的名字报进表);**谁受伤**:无 —— 包内容与导出面不变,只改仓内脚本与测试助手;**补偿**:无需。
|
|
100
|
+
钉:`test/public-value-exports-one-reading-14.8.0.test.ts`(链形合成树与真树上两条读法答案逐字同 · `getExportsOfModule(` 在 `scripts/` 与 `test/` 里只有一处 · 助手从生成器引用读法 · 生成器按入参的根读)、`test/affected-exports-since-baseline-14.7.0.test.ts`、`test/api-surface.test.ts`。
|
|
101
|
+
- **读口 `readWorkersRows` 扣下带旧字段的行时,`reason` 不再点名退役键、不再只叫人去跑迁移(`held[].reason`;`field` 不变)**。一行 6.x 形的存量行若被一次性迁移 SS-M4 拒了(例:`runtime.sessionBackend: "tidb"` 而没有 SQL 坐标),迁移不写回半成品、原行留在盘上;
|
|
102
|
+
读口再读到它,`reason` 是退役键墓碑的原句 —— `runtime.sessionBackend: runtime.sessionBackend was retired … data stored before 7.0.0: run the one-time migration SS-M4 …`:点名的是盘上的退役键,出路只有「去跑那条已经拒过的迁移」。
|
|
103
|
+
现在读口对墓碑 issue 换成读面的说法:`a field retired in 7.0.0 is still in the row — now <现行位>; data stored before 7.0.0: run the one-time migration SS-M4 (…); if that migration already refused this row, write the current form by hand and delete the old field` ——
|
|
104
|
+
退役键名只留在 `field`(结构化坐标,管理面据它标红),文字里没有;读口是纯函数、不知道这一行跑没跑过迁移,所以一句话对两种情形都成立。寻常坏行的 `reason`(`<坐标>: <句子>`)不变,读写两面(整域读、写闸、迁移收据)的墓碑原句不变。
|
|
105
|
+
实现:退役键墓碑(`retiredKey`)在铸造时登记进包内的墓碑表(键名、形、整句),读口按整句认出墓碑 issue、测试从表读名单。
|
|
106
|
+
三问 —— **谁需要**:读 `held[].reason` 把它渲染给运维的管理面(web-admin 的 workers 列表);**谁受伤**:把 `reason` 当机读字符串匹配墓碑原句的消费方(`reason` 一直是给人读的文字,机读的是 `field`)—— 提货后匹配落空;**补偿**:改读 `held[].field`(退役键的坐标)。
|
|
107
|
+
钉:`test/worker-held-sentence-14.8.0.test.ts`(迁移拒过的行 · 读口 held 句不含退役键名 · 墓碑表 ↔ schema 对账 · 七个旧字段 × 各一行:拒句不含表里任何退役键名、含现行位与读面整句、`field` 仍指到那个键 · 读面整句逐字)、`test/workers-rows-11.0.0.test.ts`(翻钉)。
|
|
108
|
+
- **中心引用的解析口 `resolveCenterRefs(refs, lookup)`(`src/center-refs.ts`,包根)+ 记录读器 `readPlainStringRecord` 公开导出、原因词写成闭集表(`src/mcp-request-entry.ts`,包根)**。中心名册里 MCP server 的 `envRefs` / `headerRefs` 只存变量名;此前本包只发这两张表的形、不发读它们的法 ——
|
|
109
|
+
变量没设、设了但读到的不是串、读它的时候抛了,留给每个宿主自己写,各按各的口径处置、各留各的句子。现在一个读法:**每个成员一句判定,整台一个结论;本口只判,不决定挂不挂**。
|
|
110
|
+
① 解析口:`refs` 经公开读器读(见 ②),读不出整张不交 ⇒ 整台 `refs_unreadable`、带读器的词、**一个变量都不问**;读器会把叫 `__proto__` 的串值成员剔出副本,本口不许这样悄悄丢一条引用再挂一台少了鉴权的 server,同样答 `refs_unreadable` / `member_name_reserved`。
|
|
111
|
+
**查找函数 `lookup(name)` 由调用方给,本口不读 `process.env`**(源里没有 `process` / `globalThis`);没给 / 不是函数 ⇒ `TypeError`(调用方的编程错,不折成成员的 `read_threw`,不回落任何环境)。
|
|
112
|
+
每个成员恰问一次 `lookup`(实参只有变量名),判成**一个词**:`resolved`(答了串,空串也是串)· `missing_name`(答 `undefined`)· `not_a_string`(答了别的种类;对普通对象做 `obj[name]` 读到继承来的函数就是这一形)· `read_threw`(`lookup` 抛了,不传出);一个成员坏了不早停。
|
|
113
|
+
整台结论三个词:`resolved` / `unresolved`(有成员没解到)/ `refs_unreadable`;**值只在 `resolved` 臂的 `values` 里**,`unresolved` 臂的行只有词(失败读数会进日志)。`refs` 是 `undefined`(没声明引用)⇒ `resolved`、零成员、零次查找,与空表同答。
|
|
114
|
+
词表常量 `CENTER_REF_FAULTS`(三词)· `CENTER_REFS_VERDICTS`(三词)`as const`,型由它推出;读数型 `CenterRefsResolution` 按 `verdict` 分三臂,消费方穷尽 `switch`。
|
|
115
|
+
② 记录读器:`readPlainStringRecord(v)` 加 `export`(判据与每一句原因句一字不改;传输读器读 `env` / `headers` 用的就是这一只),读数型 `PlainStringRecordRead` 与保留成员名 `PLAIN_STRING_RECORD_RESERVED_MEMBER`(`"__proto__"`)一并导出。
|
|
116
|
+
原因词写成一张闭集表 `PLAIN_STRING_RECORD_FAULTS`(`as const`,三词、`PlainStringRecordFault` 由它推出;`McpEntryKeyIssue.fault` 与读数的 `fault` 都引这个型,不再有字面量并集,词与 v14.7.0 的机读位同):`not_plain_record`(整张不是纯数据)·
|
|
117
|
+
`member_not_string`(成员的值不是串,或读成员就抛 —— 自有访问器抛了、描述符陷阱抛了;两种情形句子分说)· `member_name_reserved`(叫 `__proto__` 的成员;名坏优先,值坏或读不出时句子并列说全、词仍是这一个)。
|
|
118
|
+
传输读器的 `lost[]` 与 v14.7.0 逐位同答(机读位、句子、`member`、整键不交的处置)。
|
|
119
|
+
三问 —— **谁需要**:读中心名册引用表的宿主(客户端核心的本地腿、服务端的中心腿);**谁受伤**:无 —— 新导出,既有读口的答案不变;**补偿**:无需。
|
|
120
|
+
钉:`test/center-refs-14.8.0.test.ts`(成员三词各一格〔含每种非串形、五种抛法〕· 整台三词 · 不早停且每名恰问一次 · 空表 / 缺席 · 引用表读不出的十三形词与读器逐格同且一个变量都不问 · 不读 `process.env`〔AST 零 `process` / `globalThis` / `import.meta` / `node:` 引入,运行期 `process.env` 换成全陷阱抛的 Proxy,缺 lookup 抛 `TypeError`〕·
|
|
121
|
+
`unresolved` 臂不含变量值 · `Object.prototype` 数字键 setter 吞不掉 · 一事一源〔只引公开读器与读内核检视口〕· 词表穷尽 `switch`)、`test/plain-string-record-public-14.8.0.test.ts`(读器与词表导出同一引用 · 词表三词 · 成员级一词、句子分说 · 名坏优先 · 传输读器 `lost` 是读器读数的投影 · 声明只有一处 · 读器从不抛)、
|
|
122
|
+
`test/mcp-request-entry-14.0.0.test.ts` 与 `test/descriptor-law-r6-14.7.0.test.ts`(读成员就抛的三格:机读位与 v14.7.0 同)。随包文字:`docs/CONSUMER-INTEGRATION.md` §14(两方的读法与处置)、README 与 `llms.txt` 各一条、导出面登记表。
|
|
123
|
+
- **MCP 条目的 `sessionMetaKey`(`McpServerSpec.sessionMetaKey`,解析透明;`PARSE_TRANSPARENT_KEYS` 多一行 `mcp.servers[].sessionMetaKey`,`validatedBy: "engine-ingest"`)**。引擎在发给一台 MCP 服务器的每个 `tools/call` / `resources/read` 的 `params._meta[key]` 里放发送腿自己的会话 id,`key` 由这台服务器的声明给;
|
|
124
|
+
此前本包的条目没有这把键,本地腿与 wire 腿都把它剥掉(`unknown-keys-dropped`),引擎读到缺席。现在逐字承运、缺席不铸键。这把键只有一只判官 —— 引擎包根导出的 `judgeSessionMetaKey`(类型、非空、上界、MCP `_meta` 键名文法、MCP 保留前缀、`progressToken`、引擎的 `sema/` 命名空间一只函数一并判,坏键在引擎的准备门整条拒、点名这把键);
|
|
125
|
+
本包不判(与 `toolAxes` 同形),任何 JSON 值原样过 —— 本包一字不拒、不是校验收紧,坏值由引擎拒。
|
|
126
|
+
三问 —— **谁需要**:要按(主体, 会话)保存状态的 MCP 服务器(声明一把自家前缀下的键,如 `computer-use/sessionId`);**谁受伤**:本包一字不拒、不带它的条目解析结果逐字不变;存量里已经写着这把键的条目 —— 此前这把键被剥掉并报 `unknown-keys-dropped`(没有效果),现在原样交给引擎:值合引擎判官的,引擎从此按这把键在发给这台服务器的每个 `tools/call` / `resources/read` 的 `params._meta` 里放发送腿的会话 id(运维当初写下的意图被兑现,行为随升级而变);
|
|
127
|
+
值不合的(含 `null`),交给引擎后引擎在准备门整条拒这一台服务器、点名这把键;
|
|
128
|
+
**补偿**:删掉这把键,或改成引擎收的键名。写声明的一方要早判,调引擎的同一只判官。
|
|
129
|
+
钉:`test/mcp-session-meta-key-14.8.0.test.ts`(三道门同答 · 缺席不铸 · 文档读法 / wire / 本地三处承运且零告警 · 任何 JSON 值原样过 · 登记行)、`test/upstream-drift.test.ts` 门 ⑩(引擎条目型有这把键、判官入参 `unknown` · 登记行点名它 · 语料上本包恒原样承运、引擎的答案逐值钉)、`test/parse-transparent-keys.test.ts`。
|
|
130
|
+
- **MCP 条目的 `startupTimeoutMs`(`McpServerSpec.startupTimeoutMs`,解析透明;`PARSE_TRANSPARENT_KEYS` 多一行 `mcp.servers[].startupTimeoutMs`,`validatedBy: "engine-ingest"`)**。引擎的每台 MCP 服务器可以带自己的启动界(整数毫秒):握手(连接 + initialize)与首次工具列举各带它,缺席读进程级的缺省界;它只界启动阶段,不动单次调用的超时。
|
|
131
|
+
此前本包的条目没有这把键,本地腿与 wire 腿都把它剥掉(`unknown-keys-dropped`),配置声明的服务器只剩进程级的缺省界。现在逐字承运、缺席不铸键。**格式与界由引擎判**:是不是数、整数性、范围只有一只判官 —— 引擎包根导出的 `mcpStartupTimeoutMs`,在拨号时判,不合 ⇒ 拒这一台服务器的拨号(其余服务器照挂)、点名 `config.mcp_startup_timeout_invalid`,不夹取、不当缺省;
|
|
132
|
+
本包不判(与 `sessionMetaKey` 同形),任何 JSON 值原样过 —— 本包一字不拒、不是校验收紧,坏值由引擎拒;哪怕只判「是不是数」,也是第二只判官的一部分,并且会让存量里已经带着这把键的条目由本包改拒,那才是校验收紧。写声明的一方要早判,调引擎的同一只。
|
|
133
|
+
三问 —— **谁需要**:有启动慢的 MCP 服务器、要单独放宽(或收紧)它的启动界而不想动进程级 `MCP_TIMEOUT` 的部署;**谁受伤**:本包一字不拒、不带它的条目解析结果逐字不变;存量里已经写着这把键的条目 —— 此前这把键被剥掉并报 `unknown-keys-dropped`(没有效果),现在原样交给引擎:值合引擎判官的,引擎从此按这把键给这一台服务器的启动设界(运维当初写下的意图被兑现,行为随升级而变);
|
|
134
|
+
值不合的(含 `null`),交给引擎后引擎在拨号时拒这一台服务器(其余服务器照挂,点名 `config.mcp_startup_timeout_invalid`);**补偿**:删掉这把键,或改成引擎收的整数毫秒。
|
|
135
|
+
钉:`test/mcp-startup-timeout-14.8.0.test.ts`(在场逐字承运,三道门 × 两种传输同答 · 缺席不铸 · 文档读法 / 远端读腿 / 本地读腿承运且零告警 · 任何值原样过、读腿不丢条目 · 登记行点名 `mcpStartupTimeoutMs` · 随包文字的说法与行为一致)、`test/upstream-drift.test.ts` 门 ⑪(这一行是解析透明 · 引擎的判官 `mcpStartupTimeoutMs` 在钉住的引擎里真在、语料上本包恒原样承运而引擎收一个拒其余)、`test/parse-transparent-keys.test.ts`。随包文字:README 与 `llms.txt` / `llms-full.txt` 各一条。
|
|
136
|
+
- **MCP 服务器名的 `remote-` 前缀保留给设备执行端提供的远程 MCP 服务器(常量 `REMOTE_MCP_SERVER_NAME_PREFIX`;请求面 `isUsableMcpServerName` / `describeBadMcpServerName`)**。设备执行端提供的远程 MCP 服务器挂成 `remote-<名>`,工具名 `mcp__remote-<名>__<工具>`;
|
|
137
|
+
请求带来的 MCP 条目若用同一前缀,就与设备上的服务器同名、落进同一个命名空间。本节只落**加性的两面**:前缀常量,与请求面新增的 `isUsableMcpServerName`(= `isUsableWireName` 再加这一臂)和它的摘要 `describeBadMcpServerName`
|
|
138
|
+
(线上名本身坏的与 `describeBadWireName` 逐字同,保留名说那一句原因:`the "remote-" prefix is reserved for the remote MCP servers a device executor offers`)。比较按字面、区分大小写(引擎铸工具名前缀时保留大小写,`Remote-x` 不在这个命名空间里)。
|
|
139
|
+
请求面 MCP 服务器名的判官是 `isUsableMcpServerName`;`isUsableWireName` 不变,管 A2A 对端名(A2A 工具名前缀是 `a2a__`)。
|
|
140
|
+
**两面的读法不同**:请求面判官拒 `remote-` 前缀;配置面 `McpServerSpec.name` 今天照旧受理这个前缀(本包一字不拒;此前合法的名由本包改拒才是校验收紧,按「发版纪律」归 major)。
|
|
141
|
+
三问 —— **谁需要**:请求面的受理方与壳的发送前预检(拒带保留前缀的 MCP 条目名)、设备执行端的远程 MCP 挂载(名字空间不撞);**谁受伤**:本包的既有判官与配置面答案不变(加性);请求面受理方改调 `isUsableMcpServerName` 之后,请求带来的 `remote-` 起头的 MCP 条目名被拒、点名那一句原因;配置面已有的 `remote-` 起头名字照旧解析、不被丢;
|
|
142
|
+
**残留**:配置面可声明 `remote-` 起头的名,它与设备上同名的挂载服务器落进同一个命名空间 —— **补偿**:部署方自己不用这个前缀(存量里有这样的名,建议现在就改;工具名随之变,按名写的规则 / `allowTools` 跟着改)。
|
|
143
|
+
钉:`test/mcp-remote-name-reserved-14.8.0.test.ts`(配置面仍受理、三道门同答 · 非字面前缀照旧过(含 `Remote-x`)· 请求面与线上名判官差分 · 线上名判官不变 · 保留名的拒句),两版 zod(区间两端)各跑一遍。
|
|
144
|
+
- **引擎钉与上游漂移门 ⑧ / ⑪**:devDependency `@sema-agent/core` 7.53.0 → 7.58.0(锁文件多一只传递依赖 `@sema-agent/primitives`)。引擎的 MCP 条目传输形删了字面值 `env` / `headers`、加了不透明引用 `credentials`;请求面的传输形(`RequestMcpTransport`)**不变**,仍收字面 `env` / `headers`,
|
|
145
|
+
它与引擎传输形的关系改由一张处置表对拍:两边每种传输的每个键恰好一行(`engine` 同键同型 · `literal` 请求带来的字面件,交给引擎之前由消费方拿走、按自己的对端座答 · `carried` 本包承载的 `assertionHeader` · `engine_only` 请求面不收的 `credentials`),表的行 ≡ 两边键的并集,谁多一个键都编译红;读器交出的键 ≡ 请求型的行。
|
|
146
|
+
同一张处置表的形铺到配置面的 `McpServerSpec`(门 ⑪:条目层与两种传输层各一张表,判官与 ⑧ 是同一只):引擎的每个条目键在配置面恰好一行 —— 同键同型 / 解析透明 / 本包承载 / 有意不读 / 欠账未裁,
|
|
147
|
+
引擎加键、本包加键而没写行、行的词与两边的型对不上、两边的传输种类不同都编译红;欠账行是公开的未决(传输层 `credentials`:配置面没有这把键,也没有一处写明是有意不读),门只保证欠账不被悄悄跳过;这一门本身不加键、不改任何校验。
|
|
148
|
+
其余镜像门(② ③ ④ ⑤ ⑥ ⑦)对新钉逐门复核同。新钉的引擎把模型座的「没写」收成只有缺席(`undefined`)一形:`roles.default` 写出来的空串 `model` 按名拒(`unknown_model_ref`)、`model` 在场就不看 `select`;本包的 `resolveCatalogDefaultModel` 照旧按真值判(答案与 v14.7.0 逐位同)—— 对绕过 schema 递入的空串 `model`(`{model:"", select}` 交回 `select`、`{model:""}` 跳过那一级)两边不同答;schema 读过的值没有这一形(`RoleTarget.model` 是非空串),钉:`test/default-model-11.0.0.test.ts` Y4(本包的答案与引擎对同一绑定的拒各钉一格)。
|
|
149
|
+
三问 —— **谁需要 / 谁受伤 / 补偿**:无运行期变化(只改 devDependency 与测试)。钉:`test/upstream-drift.test.ts` 门 ⑧(每种传输过一遍共用判官 · 处置表读数 · 读器的键 · 五颗牙)· 门 ⑪(三张表过判官 · 读数 · 欠账行清单 · 不读的键各有 why · 表的本包一侧 ≡ 真 zod 形的键 · 十一颗牙 · `startupTimeoutMs` 对拍引擎判官的一格)。
|
|
150
|
+
- **`SERVING_LEASE_DRIFT_MARGIN_MS` 的注改成现行用法(只改注,值与导出面不动)**。旧注写的是「中心把它加到自己记录的租约到期上再判过期」,这条用法已退役;现在的注写它的用法:读引擎报的 `EngineSignal.leaseUntil`(引擎时钟上的 epoch 毫秒)时,读的一方拿它对**自己的** now,两个钟不必一致 ——
|
|
151
|
+
「租约还在」只在 `leaseUntil >= now + SERVING_LEASE_DRIFT_MARGIN_MS` 时成立,取保守方向(偏斜不超过余量时,还在的租约可能读短,过了期的租约绝不读成还在);它不属于租约本身的计时(引擎按自己的单调钟执法,中心按库时间记自己的到期,两边不比钟)。钉:`test/engine-signal-8.1.0.test.ts`(注里有这条用法、没有旧用法;值仍是 `DEFAULT_HEARTBEAT_MS`)。
|
|
152
|
+
- **披露(补全 14.7.0「不在本批」那一条):zod 4.4 上,`Object.prototype` 上有可枚举键时才第一次被读的判别联合,永久读不出 —— 撤掉污染也不恢复**。14.7.0 写的是访问器形的 `verdict` 污染在 zod 4.4 上让 `readServingLeaseResponse` 冷路径答 `undefined`;完整的读数更宽:zod 4.4 把判别联合的判别值表惰性物化(物化时按键枚举各臂的形),
|
|
153
|
+
物化之前 `Object.prototype` 上就有一个可枚举键、且它的值不是带 `_zod` 成员的对象时,物化当场抛,zod 4.4 记下「物化中」就不再重算 —— 这个联合此后永久读不出(已核:值为字符串 / 数 / `null` / 普通对象 / 函数都触发,键名 `value` / `zzz`;带 `_zod` 成员的对象不触发;判别键 `verdict` 的访问器形与数据形不论可枚举与否都触发)。
|
|
154
|
+
租约应答读口 `readServingLeaseResponse` 是其中一只(fleet 另外六只读口没有判别联合,读数:zod 4.4 线,污染窗口里与撤掉之后都照常读出),包里另有 11 处 `z.discriminatedUnion`(`McpTransport`(经 `McpServerSpec` / `McpConfig`)· `ServingLeaseResponse` · `RemoteExecSpec` · `HookInput` · `PermissionUpdate` · `TightenRule` · `AtMention` · `ProjectContextItem` · `PluginSource` · `SettingSource` · `SandboxGitOriginAnswer`)在 zod 4.4 上同样
|
|
155
|
+
(已核:`McpServerSpec` 的合法条目在污染之后永久抛;其余各联合的判别值表读数同),直接 `safeParse` 它们的消费方一样读不出。zod 4.6 上这些在污染窗口里读不出,撤掉污染即恢复。v14.6.0 同,**非本包造成**(zod 4.4 自己的惰性物化缺陷)。
|
|
156
|
+
失败方向仍是读不出:租约读口读不出 = 引擎按中心不可达处置,不读成 `granted`、也不读成 `denied`。`Object.prototype` 被污染本身是嵌入方进程被攻破,本包不替它兜底。
|
|
157
|
+
**出路**:zod 升到 4.6 或更高(包的区间 `^4.4.0` 两端都在测);或在加载本包之后、嵌入方的代码跑之前,让每一个要用的判别联合都被一次干净的读走到 —— 租约读口:读一次干净的应答即可(已核);嵌套的联合要有一份合法输入走到它:`McpConfig` 读一条合法条目才暖到 `McpTransport`,空配置 `{ servers: [] }` 走不到、污染之后 zod 4.4 上仍永久读不出(已核)。
|
|
158
|
+
钉:`test/descriptor-law-r8-14.7.0.test.ts` 的「🚧 披露的缺口」一组(特征钉:冷 / 暖 / 嵌套 / 触发的值形,两版 zod 各一张读数;缺口补上则 4.4 臂翻成恢复,这一条随之改)。
|
|
159
|
+
|
|
160
|
+
**自 v14.7.0 起运行期代码有改动的公开导出(机器重算;上界 —— 可达 ≠ 一定变,不可达 ⇒ 一定不变;本版新增的导出不入表)**
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
DEFAULT_ROLE_TIER_LADDER [.]
|
|
164
|
+
DOMAINS [., ./types]
|
|
165
|
+
DOMAIN_SCHEMAS [., ./types]
|
|
166
|
+
FileConfigStore [./node]
|
|
167
|
+
MIGRATION_APPLIED_AS [./one-time-migrations]
|
|
168
|
+
MODEL_PLANE_REFUSAL_CODES [., ./fleet]
|
|
169
|
+
McpConfig [., ./types]
|
|
170
|
+
McpServerSpec [., ./types]
|
|
171
|
+
ModelPlaneRefusalCode [., ./fleet]
|
|
172
|
+
ModelsConfig [., ./types]
|
|
173
|
+
NON_PORTABLE_DOMAINS [./conformance]
|
|
174
|
+
ONE_TIME_MIGRATIONS [./one-time-migrations]
|
|
175
|
+
ONE_TIME_MIGRATION_IDS [./one-time-migrations]
|
|
176
|
+
PARSE_TRANSPARENT_KEYS [., ./types]
|
|
177
|
+
PromptsConfig [., ./types]
|
|
178
|
+
RosterEntry [., ./types]
|
|
179
|
+
RostersConfig [., ./types]
|
|
180
|
+
WorkerSpec [., ./types]
|
|
181
|
+
WorkersConfig [., ./types]
|
|
182
|
+
buildEffective [., ./config-fns]
|
|
183
|
+
createContractAdapter [./conformance]
|
|
184
|
+
createFileSchedulerStore [./node]
|
|
185
|
+
deriveLocalExpectation [./conformance]
|
|
186
|
+
deriveWireExpectation [./conformance]
|
|
187
|
+
domainFilePath [./node]
|
|
188
|
+
emptyEffective [., ./config-fns]
|
|
189
|
+
foreignKeysOf [./conformance]
|
|
190
|
+
isDomainName [., ./types]
|
|
191
|
+
isStringRecord [.]
|
|
192
|
+
loadLocalConfig [./node]
|
|
193
|
+
materializeLocalRoot [./conformance]
|
|
194
|
+
migrateConfigDirectory [./node]
|
|
195
|
+
migrateConfigDocuments [./node]
|
|
196
|
+
migrateEffectiveSnapshot [./one-time-migrations]
|
|
197
|
+
migrateStoredDocuments [./one-time-migrations]
|
|
198
|
+
mutateSchedulerStore [./node]
|
|
199
|
+
nodeConfigDirectoryIo [./node]
|
|
200
|
+
parseDomain [., ./config-fns]
|
|
201
|
+
parseDomainLoud [., ./config-fns]
|
|
202
|
+
parseDomainSalvaged [., ./config-fns]
|
|
203
|
+
readBundle [./node]
|
|
204
|
+
readEffectiveWire [., ./config-fns]
|
|
205
|
+
readModelRefusals [., ./fleet]
|
|
206
|
+
readRequestMcpTransport [.]
|
|
207
|
+
readWorkersRows [.]
|
|
208
|
+
resolveCatalogDefaultModel [.]
|
|
209
|
+
resolveLegExpectation [./conformance]
|
|
210
|
+
runConformance [./conformance]
|
|
211
|
+
salvageEntries [., ./config-fns]
|
|
212
|
+
saveSchedulerStore [./node]
|
|
213
|
+
urlSeatPatterns [./one-time-migrations]
|
|
214
|
+
validateDomain [./node]
|
|
215
|
+
writeBundle [./node]
|
|
216
|
+
writeDomainFile [./node]
|
|
217
|
+
writeRemoteExec [./node]
|
|
218
|
+
```
|
|
219
|
+
|
|
11
220
|
## 14.7.0 — 2026-10-25 — minor:沙箱 git 凭据平面的契约件 —— origin 规范形判官与有效配置块 `sandboxGit`(`SandboxGitConfig`)· 凭据取值口 `sandbox_git` 的 wire 型与三张词表(`SANDBOX_GIT_ORIGIN_VERDICTS` · `SANDBOX_GIT_REFUSALS` · `SANDBOX_GIT_PERMANENCE`)· `VerifiedIdentity.principalInc` 与令牌声明 `sub_inc` · 「后面要拼路径的基址」座位的禁用成分集与判官(`PATH_BASE_URL_FORBIDDEN_PARTS` · `pathBaseUrlFlaws`)与公告地址 env 名常量 `FLEET_ADVERTISE_ADDRESS_ENV`
|
|
12
221
|
|
|
13
222
|
> 档位(按发版纪律):**minor** —— 加性:新导出(包根 +28 名,其中 `FLEET_ADVERTISE_ADDRESS_ENV` 同时在 `./fleet` 上;`./api/auth-bridge` 子路径 +2 名:`PRINCIPAL_INC_CLAIM` · `PrincipalIncarnation`);`VerifiedIdentity` 多可选键 `principalInc`;`EffectiveWire` 多可选键 `sandboxGit?: unknown`;`HttpHookUrlBlock.rule` 闭集多一词 `malformed`(判官读不出的层的判决词,见 http hook 名单条)。
|
package/README.md
CHANGED
|
@@ -47,6 +47,37 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
|
|
|
47
47
|
> zod 3, nested inside every zod-4 host). The move to zod 4 did not change what any schema accepts or produces —
|
|
48
48
|
> the conformance corpus reads byte-identically on both — only the wording of validation errors (see CHANGELOG 11.0.0).
|
|
49
49
|
|
|
50
|
+
## 升级指引:14.7.x → 14.8.0(加性:配置根上的两份根级文档 · env 模型表的一份读法与写者的计划助手 · 一次性迁移 SS-M11)
|
|
51
|
+
|
|
52
|
+
- **新面**:配置根(持有 `config.d` 的那个目录)上多两份**根级文档** —— `config.env-models.json`(`{"envModels":"replaced"|"kept"}`:一份管模型面的
|
|
53
|
+
`config.d/models.json` 能不能顶替 env 模型表 `MODEL_ID` / `MODEL_CHEAP_ID`;运行记录在场之后缺席 ≡ `kept`)与 `config.migration.SS-M11.json`(一次性迁移 SS-M11 的运行记录,按 id 一份)。
|
|
54
|
+
读只经 `readEnvModelsDecision` / `readMigrationMarker`;决定文件的写只经 `writeEnvModelsDecision`(迁移铸确认也经它),运行记录由迁移独占创建(都在 `./node`);名字、闭集答案表与读数身份 `envModelsDecisionTag` 在包根。
|
|
55
|
+
这道闸的两样东西由本包出、供引擎读:组拒码常量 `CATALOG_REPLACES_ENV_MODELS_CODE`(`config.catalog_replaces_env_models`)与上面的决定文档;码的意思是 —— env 表已声明、决定不是 `replaced` 时,管模型面的目录被整组拒、env 模型照服。
|
|
56
|
+
强制在引擎一侧:今天引擎(`@sema-agent/core` 7.58.0)不拒这个组。
|
|
57
|
+
- **写者要做的**(改写 `config.d/models.json` 的每一条路):写之前读 `readEnvModelsDecision(root)`,调 `planEnvModelTableWrite({ env, target, decision, candidate, intent? })`;
|
|
58
|
+
`confirm` ⇒ 把回执给用户看(句子可用 `envModelTableWriteRefusal`),确认后带回 `confirmKey` 再调;`write_with_decision` ⇒ 先
|
|
59
|
+
`writeEnvModelsDecision(root, "replaced", { write: "overwrite" })`、再写 models.json(两次写在你对 models.json 的同一把锁里);`fix_first` ⇒ 先改文档。env 表未声明时计划照写、不写确认。
|
|
60
|
+
**不要自己写这份文件**:写函数给的模式与 `config.d` 域文档同一条规则(新建 0o644 经进程 umask,已有的文件沿用它原来的模式);自写在 umask 002 下落 0664,引擎的目录信任判官会拒。
|
|
61
|
+
撤销 = 删掉决定文件或写 `kept`(运行记录在场、创建者的迁移结局落定之后两者同义;落定之前只有写 `kept` 安全,见下一条)。撤销在下一次起服生效:运行中的引擎保留它在服的那一代目录,env 模型要到下一次起服才回来。
|
|
62
|
+
- **存量**:引擎首次以本地配置起服时跑一次 SS-M11(`migrateEnvModelsDecision`):运行记录缺席才跑;**先读**决定与 models.json,再以「不存在才建」写运行记录 ——
|
|
63
|
+
建成的那个进程是本根唯一的铸造者(别的副本见记录已在即停、永不铸),只在「先读到的决定缺席 ∧ 目录管模型面」时以「不存在才建」铸 `replaced`(env 表声没声明都一样 —— 保的是升级前目录整表顶替的读法)
|
|
64
|
+
(结局 `minted` / `exists` / `failed`,只有「已在」算 `exists`)。根缺席 ⇒ 逐级以恰是 0o700 的目录建根、只放运行记录;不建 `config.d`。记录建不成 / 确认建不成 / models.json 读不出(只有 `ENOENT` 算没有)⇒ 不铸(按 kept),
|
|
65
|
+
结局带 errno(若有)与手工恢复句;读不懂的运行记录按「已跑」处置。**披露**:进程恰在写完运行记录、铸确认之前中断,或铸失败、或那一刻 models.json 读不出 ⇒ 此后无人再铸,升级前由目录服务的根换成 env 模型服务 ——
|
|
66
|
+
出路是手写决定文件。撤销的终局按运行记录的创建者算:只有创建者能写确认;以运行记录在场为前提,创建者写完 / 判定不写 / 失败(结局 `minted` / `exists` / `skipped` / `models_unreadable` / `failed`,调用方各记一行)之后,删掉决定文件就是撤销且是终局(删掉运行记录再起服 ⇒ 迁移重跑,决定缺席即重铸);其它副本读到记录在场 = 已跑、跳过(或得 `EEXIST` 答 `not_minter`),它们的返回不构成终局;创建者当选到结局落定之间的窗口在挂起 / I/O 卡顿下无上界,窗口内唯一安全的撤销是写 `kept`(独占创建护住它;删掉的会被创建者照它先读的 D0 铸回);创建者在写确认之前崩溃 ⇒ 永久 `kept`(fail-closed,披露)。
|
|
67
|
+
- **MCP 条目**:`McpServerSpec` 多两只可选键,都解析透明、原样承运。`sessionMetaKey`(引擎在发给这台服务器的请求 `_meta` 里放发送腿会话 id 的那把键;唯一判官是引擎包根导出的 `judgeSessionMetaKey` —— 写声明的一方要早判就调它;存量里已写着这把键的条目(此前被剥掉并报 `unknown-keys-dropped`),本版原样交给引擎:值合引擎判官的,引擎从此按这把键放会话 id(运维当初写下的意图被兑现,行为随升级而变);值不合的(含 `null`),交给引擎后在准备门整条被拒、点名这把键:删掉这把键,或改成引擎收的键名)。
|
|
68
|
+
`startupTimeoutMs`(这一台服务器自己的启动界,整数毫秒,握手与首次工具列举各带它;缺席读进程级的缺省界):解析透明,**格式与界由引擎判** —— 唯一判官是引擎包根导出的 `mcpStartupTimeoutMs`,在拨号时判:不合 ⇒ 拒这一台服务器的拨号(其余服务器照挂),本包不判、任何 JSON 值原样过 —— 写声明的一方要早判就调它;存量里已写着这把键的条目(此前被剥掉并报 `unknown-keys-dropped`),本版原样交给引擎:值合引擎判官的,引擎从此按这把键给这一台服务器的启动设界(运维当初写下的意图被兑现,行为随升级而变);值不合的(含 `null`),交给引擎后会在拨号时被拒:删掉这把键,或改成引擎收的整数毫秒。
|
|
69
|
+
MCP 服务器名的 `remote-` 前缀保留给设备执行端提供的远程 MCP 服务器(`REMOTE_MCP_SERVER_NAME_PREFIX`):请求面 MCP 服务器名的判官是 `isUsableMcpServerName` / `describeBadMcpServerName`(= 线上名判官再加这一臂;线上名判官 `isUsableWireName` / `describeBadWireName` 管 A2A 对端名);配置面 `McpServerSpec.name` 受理这个前缀(拒是校验收紧,按发版纪律归 major)—— 部署声明 MCP 服务器时请自己不用这个前缀。
|
|
70
|
+
- **中心引用解析口**:`resolveCenterRefs(refs, lookup)` 读中心名册里 MCP server 的 `envRefs` / `headerRefs`(变量名,不是值):**每个成员一句判定**(`resolved` / `missing_name` / `not_a_string` / `read_threw`,
|
|
71
|
+
闭集常量 `CENTER_REF_FAULTS`)、**整台一个结论**(`resolved` / `unresolved` / `refs_unreadable`,`CENTER_REFS_VERDICTS`);只判不决定挂不挂。`lookup` 由调用方给 —— 本包不读 `process.env`,没给 ⇒ `TypeError`。
|
|
72
|
+
引用表经公开的记录读器 `readPlainStringRecord` 读(传输读器读 `env` / `headers` 用的就是这一只;原因词表 `PLAIN_STRING_RECORD_FAULTS` 三词:`not_plain_record` / `member_not_string` / `member_name_reserved`,与传输读器 `lost[].fault` 同一张表)。两方读法见 `docs/CONSUMER-INTEGRATION.md` §14。
|
|
73
|
+
- **已知边界(zod 4.4 臂)**:`Object.prototype` 上有可枚举键(值不是带 `_zod` 成员的对象)时才第一次被读的判别联合,在 zod 4.4 上永久读不出,撤掉污染也不恢复 —— 租约应答读口 `readServingLeaseResponse` 与包里另外 11 处 `z.discriminatedUnion`(如 `McpServerSpec` 的传输)同(读数:zod 4.4 线,键 `value` / `zzz`;zod 4.6 上撤掉污染即恢复;v14.6.0 同,非本包造成,zod 4.4 自己的惰性物化缺陷)。
|
|
74
|
+
`Object.prototype` 被污染本身是嵌入方进程被攻破,本包不替它兜底;失败方向是读不出(租约读口读不出 = 引擎按中心不可达处置,不读成 `granted` 也不读成 `denied`)。出路:zod 升到 4.6 或更高,或加载本包之后、嵌入方的代码跑之前让每一个要用的判别联合都被一次干净的读走到(租约读口读一次干净应答即可;`McpConfig` 要读一条合法条目才暖到传输联合,空配置走不到)。
|
|
75
|
+
- **整根只读的部署**(配置根是 ConfigMap / 只读挂载):迁移写不了运行记录、不会替你铸 —— **升级引擎之前**把 `config.env-models.json` 放进同一份 ConfigMap 的配置根
|
|
76
|
+
(要目录继续优先就写 `{"envModels":"replaced"}`)。
|
|
77
|
+
- **SS-M11 的窗**:起算点 = 引擎开始强制这道闸的那一天(上文:今天引擎不拒这个组,窗还没有起算),到期日 = 起算日 + 30 天;本包登记表里 SS-M11 那一行按本包的发布日计,
|
|
78
|
+
管的是本包何时能删迁移码。
|
|
79
|
+
- **回滚**:回滚到已核的各版旧引擎都安全 —— 两份根级文档在 `config.d` 之外,被检视的配置读者不把它们当域文档。
|
|
80
|
+
|
|
50
81
|
## 升级指引:13.x → 14.0.0(BREAKING:prompts 域单轴 = 中心 artifact;请求面 MCP 条目判官入包)
|
|
51
82
|
|
|
52
83
|
- **prompts**:域里只装引擎当期 schema 的中心 artifact。存储 `PromptsConfig = { artifact?, workers? }`(每份是 artifact 的身份 `{ payload, artifactDigest }`);
|
|
@@ -406,6 +437,20 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
|
|
|
406
437
|
writer (the orchestrator that renders the value) and the reader (the engine at boot) import. Judge the value AFTER placeholders are expanded: `http://$(POD_IP):8090` passes,
|
|
407
438
|
`http://[$(POD_IP)]:8090` does not (a bracketed host must parse as IPv6). Placeholder expansion, reachability and the absent / empty-string reading are not this package's;
|
|
408
439
|
the env name is not a reserved worker env name. Both sides' reading is in `docs/CONSUMER-INTEGRATION.md` §13.
|
|
440
|
+
- **Config-root documents and the env model table** (14.8.0; names / types / judges / plan in the main barrel, readers / writer / migration in `./node`): the config
|
|
441
|
+
root (the directory holding `config.d`) carries two root-level documents outside `config.d` — `config.env-models.json` (`ENV_MODELS_DOCUMENT`: whether a catalog
|
|
442
|
+
that manages the model plane may replace the env model table, `replaced` | `kept`; absent ≡ `kept` once the run marker exists) and `config.migration.SS-M11.json`
|
|
443
|
+
(the per-id run marker of one-time migration SS-M11); `CONFIG_ROOT_DOCUMENTS` is the list an engine's directory trust judge walks. ONE reader each —
|
|
444
|
+
`readEnvModelsDecision(root)` → `absent | replaced{digest} | kept{digest} | unreadable{bad_json|bad_shape, digest} | unreadable{io, errno}` (the engine gate, the
|
|
445
|
+
one-time migration and the writer's plan answer from it; `envModelsDecisionTag` is the one tagged identity of a reading — `absent` / `d:<digest>` / `io:<errno>` —
|
|
446
|
+
for warning dedup and the etag) and `readMigrationMarker(root, id)` → `absent | present | unreadable` — and ONE writer of the decision,
|
|
447
|
+
`writeEnvModelsDecision(root, "replaced"|"kept", { write: "overwrite" | "createIfAbsent" })` (the package's atomic writer, the same mode rule as a `config.d` domain document: a new file is 0o644 through the process umask, an existing file keeps its own mode;
|
|
448
|
+
the migration mints through it too, while the run marker is created only by the migration). One-time migration SS-M11 = `migrateEnvModelsDecision({ root, env })` (read first, then elect by creating the marker exclusively: the creator is
|
|
449
|
+
the only minter; a `config.d/models.json` that cannot be read — anything but `ENOENT` — mints nothing and is reported with its errno when the error carries one). The judge `catalogManagesModelPlane(doc)` (raw half: at least one entry not explicitly disabled — the same `enabled` reading as a dropped entry's
|
|
450
|
+
identity) and `enabledEntryCount(doc)`. The env model table read once — `readEnvModelSlots` / `envModelTableDeclared` (`MODEL_ID` present and non-empty) /
|
|
451
|
+
`buildEnvModelBaseline` — and the writer's plan `planEnvModelTableWrite({ env, target, decision, candidate, intent? })` (receipt by upstream id with landings
|
|
452
|
+
`{name, id}`, `confirmKey` = the receipt's sorted JSON) with its one sentence `envModelTableWriteRefusal(plan)`. Group-refusal codes
|
|
453
|
+
`CATALOG_REPLACES_ENV_MODELS_CODE` / `SUMMARIZE_MODEL_MISSING_CODE` (tail of `MODEL_PLANE_REFUSAL_CODES`).
|
|
409
454
|
- **Session web link** (`web-admin-session-url.ts`, main barrel): `webAdminSessionUrl(webBaseUrl, { worker, sessionId })` — the ONE
|
|
410
455
|
template for a web-admin session link, `<webBaseUrl>/chat?agent=<worker>&session=<sessionId>`: both parts required (a link with only
|
|
411
456
|
the session id lands on whichever worker the page picks and may not find it), each part `encodeURIComponent`-escaped, the base a
|
|
@@ -531,7 +576,7 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
|
|
|
531
576
|
is the source whose tier in `SETTINGS_SOURCE_CONTRACT` is `managed`; a tighten-only source contributes its deny list only (allow lists
|
|
532
577
|
concatenate across layers, so reading its allow list would widen the others').
|
|
533
578
|
- **Request-plane MCP entry judges** (`mcp-request-entry.ts`, 14.0.0): `isUsableWireName` / `describeBadWireName` (the engine's wire-name bound
|
|
534
|
-
`TOOL_WIRE_NAME_MAX_CHARS`, mirrored; a name with leading or trailing whitespace is a bad name too — never trimmed, never normalised; inner whitespace stays legal), `isContentOriginWord` / `describeBadContentOrigin`, `readMcpIdentityHeaders`, `readRequestMcpTransport`
|
|
579
|
+
`TOOL_WIRE_NAME_MAX_CHARS`, mirrored; a name with leading or trailing whitespace is a bad name too — never trimmed, never normalised; inner whitespace stays legal), `isUsableMcpServerName` / `describeBadMcpServerName` (14.8.0; the request plane's judge of an MCP server's name: the wire-name judge plus one arm — the `remote-` prefix, `REMOTE_MCP_SERVER_NAME_PREFIX`, is reserved for the remote MCP servers a device executor offers; the config plane's `McpServerSpec.name` accepts it — refusing would be a validation tightening, which the release discipline files under a major; A2A peer names keep `isUsableWireName`), `isContentOriginWord` / `describeBadContentOrigin`, `readMcpIdentityHeaders`, `readRequestMcpTransport`
|
|
535
580
|
(hard faults `ok:false` naming the key path; soft faults dropped by the reader and listed in `lost`; `env` / `headers` are read as plain-data string records — a `Map`, a class instance or an own `__proto__` member is not one, the delivered record is a copy, and a `lost` entry for a record carries machine-readable `member` / `fault`; the two describers strip control and bidi characters and cut at 64 characters, the service's echo form) and the value-shape describers — one reading for
|
|
536
581
|
the service's request legs and the shell's pre-send check. A `toolFaces` declaration stays parse-transparent here: its shape is the engine ingest side's to judge, and none of the tool-face vocabularies is mirrored in this package.
|
|
537
582
|
- **One-line echo strip set** (`control-and-bidi.ts`, main barrel): `CONTROL_AND_BIDI_CHARS` (C0 + DEL + the 12 `Bidi_Control=Yes` code points,
|
|
@@ -608,7 +653,7 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
|
|
|
608
653
|
carries verbatim WITHOUT checking their shape, each row naming who does check it (the engine's ingest side or
|
|
609
654
|
a consumer's enforcement point). Today: `models[].compat` (1.10.0), `models[].headers` (6.4.0; the header-name
|
|
610
655
|
allowlist × lane is sema-server's, not restated here), `mcp.servers[].source` (1.7.2), `mcp.servers[].toolFaces`
|
|
611
|
-
(1.9.0), `mcp.servers[].toolAxes` / `authForm` / `entryKind` (12.2.0; connector-entry declarations — `toolAxes` has exactly one shape judge, the engine's root export `readMcpToolAxes`; the other two keys' semantics are the consumer's),
|
|
656
|
+
(1.9.0), `mcp.servers[].toolAxes` / `authForm` / `entryKind` (12.2.0; connector-entry declarations — `toolAxes` has exactly one shape judge, the engine's root export `readMcpToolAxes`; the other two keys' semantics are the consumer's), `mcp.servers[].sessionMetaKey` (14.8.0; the `_meta` key the engine sends the leg's session id under — one judge, the engine's root export `judgeSessionMetaKey`), `mcp.servers[].startupTimeoutMs` (one server's own startup bound — one judge, the engine's root export `mcpStartupTimeoutMs`),
|
|
612
657
|
and an entitlement override's `value`. `models[].extraBody` is NOT one of them — it has its own shape
|
|
613
658
|
gate (a plain JSON object); what it leaves open is the key vocabulary inside. `compat` / `headers` / `extraBody`
|
|
614
659
|
still clear the one free-form safety judgment (secret-shaped key/value, accessor, cycle, non-JSON, too deep ⇒ rejected).
|
|
@@ -661,9 +706,20 @@ local `.env`). This package never carries a credential.
|
|
|
661
706
|
models.json rosters.json skills.json mcp.json scenarios.json plugins.json
|
|
662
707
|
collab.json runtime.json governance.json execution.json prompts.json limits.json
|
|
663
708
|
remote-exec.json # NOT a domain (the exec backend; read by loadRemoteExec)
|
|
709
|
+
config.env-models.json # the env-model-table decision {"envModels":"replaced"|"kept"} (readEnvModelsDecision / writeEnvModelsDecision)
|
|
710
|
+
config.migration.SS-M11.json # the run marker of one-time migration SS-M11 {"migration":"SS-M11"} (readMigrationMarker; written only by the migration)
|
|
664
711
|
.env # secret VALUES only (KEY=value); gitignored; never read into any *.json
|
|
665
712
|
```
|
|
666
713
|
|
|
714
|
+
**The root belongs to the client (TOC).** An engine writes exactly two files in it — the two root-level documents above, both outside `config.d` (the sampled
|
|
715
|
+
file stores read only `config.d`, and no inspected config reader treats these files as domain documents, so they are inert to old and new engines alike); a client
|
|
716
|
+
keeps both in its own registry of persistent files (hygiene, export and reset must not drop or flag them). Bundles (`exportBundle` / `readBundle`) carry only the
|
|
717
|
+
portable `config.d` domains: these two are deployment decisions and do not travel. Laid down offline (before any engine starts on the root: an image, a
|
|
718
|
+
ConfigMap, a restore), the two documents and the domain documents may be written in any order; at runtime the order is fixed — a writer writes the
|
|
719
|
+
decision before `config.d/models.json` (both under its lock on `models.json`), and the one-time migration creates its run marker before it mints the
|
|
720
|
+
decision. When the root itself is absent, the engine's one-time migration creates it (and any missing parent) owner-only (0o700), owned by the engine's
|
|
721
|
+
user: a client that runs under a different user should create the root itself before the engine first starts.
|
|
722
|
+
|
|
667
723
|
Fleet-only domains (`workers`, `hosts`, `systems`) are omitted on a single machine — they resolve to schema
|
|
668
724
|
defaults. Each `config.d/<domain>.json` is exactly that domain's zod object.
|
|
669
725
|
|
package/conformance/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
14.
|
|
1
|
+
14.8.0
|
package/dist/atomic-write.d.ts
CHANGED
|
@@ -29,3 +29,18 @@ export declare function writeFileAtomic(target: string, text: string, kind: Atom
|
|
|
29
29
|
* 入参 `dir` 是已解析到真目录的路径;返回目录的权限位是否让本进程能增删其中的目录项。
|
|
30
30
|
*/
|
|
31
31
|
export declare function directoryAdmitsWrite(dir: string): boolean;
|
|
32
|
+
/** 新建目录的模式:仅属主可读写进(引擎的目录信任判官要求路径上的组件不得组 / 他人可写;0o700 在任何 umask 下都过)。 */
|
|
33
|
+
export declare const OWNER_ONLY_DIRECTORY_MODE = 448;
|
|
34
|
+
/**
|
|
35
|
+
* 原子地**不存在才建** `target`(见文件头「不存在才建」):建成 ⇒ `"created"`;目标名已在(只有 `EEXIST`)⇒ `"present"`(一字不动)。
|
|
36
|
+
* 临时文件独占建出、写完、fsync({@link writeTempExclusive})后 `link` 成目标名。模式 = `kind` 在表里的新建模式经进程 umask(目标按定义不在,没有「已有的模式」可沿用)。
|
|
37
|
+
* 临时文件建不出 / 写不完 / fsync 不了 ⇒ 抛 {@link FsFailure}(落点「往目录里写临时文件」,与 {@link writeFileAtomic} 建临时文件失败同落点同句);`link` 的别的失败原样抛
|
|
38
|
+
* (调用方经 `fsFailureMessage` 取句)。父目录须已存在。
|
|
39
|
+
*/
|
|
40
|
+
export declare function createFileExclusive(target: string, text: string, kind: AtomicFileClass): "created" | "present";
|
|
41
|
+
/**
|
|
42
|
+
* 目录不在就建(连同缺的上级):**逐级** mkdir、每建一级立刻 chmod 到 {@link OWNER_ONLY_DIRECTORY_MODE} —— 模式恰是它,不经 umask(umask 0777 下一次性的递归 mkdir 会先建出
|
|
43
|
+
* 一级 0000 的目录、再也进不去建下一级)。本进程建了 `dir` ⇒ `"created"`;`dir` 已在(或恰被别的进程同时建成)⇒ `"present"`(它的模式一位不动)。
|
|
44
|
+
* 路径上有一节不是目录 ⇒ 抛。
|
|
45
|
+
*/
|
|
46
|
+
export declare function ensureOwnerOnlyDirectory(dir: string): "created" | "present";
|
package/dist/atomic-write.js
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
* `mutateSchedulerStore` 的提交点),异步调用方(`writeDomainFile` 等)直接调它 —— 一只写器,没有第二份同样的写法。
|
|
8
8
|
*
|
|
9
9
|
* ## 文件模式:一张按文档类的表({@link ATOMIC_FILE_CLASSES}),不在任何调用点另写
|
|
10
|
-
* · `config-document` ——
|
|
11
|
-
*
|
|
12
|
-
* · `scheduler-store` —— 调度店耐久记录(可能带 principal / session 标识):新建 = 0o600,且**每次写都回到 0o600**(
|
|
13
|
-
*
|
|
10
|
+
* · `config-document` —— 契约 JSON:`config.d` 下的域文档与 `remote-exec.json`,以及配置根上的两份根级文档(决定文档 `config.env-models.json` / 运行记录 `config.migration.<id>.json`)——
|
|
11
|
+
* 只带 env-NAME 或几个固定词、不带秘密值(README 契约),新建 = 0o644;**已有的文件沿用它原来的模式**(运维 chmod 过的文件不被一次写回覆盖)。
|
|
12
|
+
* · `scheduler-store` —— 调度店耐久记录(可能带 principal / session 标识):新建 = 0o600,且**每次写都回到 0o600**(同样经进程 umask:已有的更宽的文件被收回,更严的 umask 下更严)。
|
|
13
|
+
* 新建**文件**的模式照常经进程 umask 遮一道(`writeFile` 的 `mode` 语义):比表更严的 umask 只会更严,不会被这里放宽;沿用的模式原样恢复(不再过 umask)。这是**文件**的律;目录是另一条律(见下面「仅属主可进的目录」)。
|
|
14
14
|
*
|
|
15
15
|
* ## 目标是符号链接
|
|
16
16
|
* 域文件可以是符号链接 —— 运维用链接把 `config.d` 指到别处管理的部署,那是有意的布局,写器不替他改布局。规则一条:**先把目标解析到真文件(`realpath`),
|
|
@@ -20,6 +20,16 @@
|
|
|
20
20
|
* 「链接 ⇒ 真文件、悬空 ⇒ 响亮抛」这一只判法是 {@link resolveSymlinkTarget}:目录迁移的读口(`./config-dir-io`)读目录 / 读文件前也问它,
|
|
21
21
|
* 悬空链接在读阶段与写阶段是**同一句**({@link danglingSymlinkSentence})。
|
|
22
22
|
*
|
|
23
|
+
* ## 不存在才建({@link createFileExclusive})
|
|
24
|
+
* 另一种写法只有一处不同:最后一步不是 `rename` 盖过目标,而是把(已 fsync 的)临时文件 `link` 成目标名 —— 目标名已在(任何东西:文件、目录、哪怕是悬空的符号链接)⇒
|
|
25
|
+
* 内核拒(`EEXIST`),答 `present`、一字不动。读到「缺席」与写入之间别人写下的东西因此不会被吞(一次性迁移铸配置根的决定文档时用它)。
|
|
26
|
+
* 临时文件与目标同目录、模式按同一张表(目标不在 ⇒ 表的新建模式,经进程 umask,与 {@link writeFileAtomic} 的新建同一条规则);临时名无论成败都删掉。文件系统不支持硬链接(`EPERM` / `ENOTSUP` …)⇒ 照常抛,调用方按写失败处置。
|
|
27
|
+
*
|
|
28
|
+
* ## 仅属主可进的目录({@link ensureOwnerOnlyDirectory})
|
|
29
|
+
* 配置根缺席时一次性迁移要建它:建出来的每一级都是 {@link OWNER_ONLY_DIRECTORY_MODE} 的**精确值** —— 逐级 `mkdir` 之后立刻 `chmod`,所以目录的模式**不经进程 umask**:
|
|
30
|
+
* **不受 umask 放宽,也不受 umask 收窄**(前者:`mkdir` 给的 mode 本身就是 0o700,umask 只能清掉位、加不了位;后者:例如 umask 0o277 下 `mkdir` 先出 0o500,紧跟的 `chmod` 把它定回 0o700)。
|
|
31
|
+
* 这是目录的另一条律,与上面「新建文件经进程 umask」不是同一条律的两个特例;已在的目录一位不动。目录模式与文件模式同在本文件一处定。
|
|
32
|
+
*
|
|
23
33
|
* ## 写失败(12.0.0;句子归文件系统失败句的唯一铸点)
|
|
24
34
|
* 三步各一句({@link FsFailure},句子在 `./fs-failure`):临时文件建不出 ⇒ 「cannot write into directory <dir> (<errno>): …」(点名目录 —— 目录不可写时
|
|
25
35
|
* 文件本身没错);沿用原模式的文件给临时文件设模式失败 ⇒ 「cannot set the mode of <file> (<errno>): …」(点名设模式这一步 —— 文件系统不让改模式,目录没有问题);`rename` 失败 ⇒ 「cannot replace <file> (<errno>): …」(点名真文件)。
|
|
@@ -28,7 +38,7 @@
|
|
|
28
38
|
* 写器在建临时文件那一步读了经落点递给铸点(`EPERM` ∧ 可写 ⇒ 真因是不可变标志,首选出路是清标志;`EACCES` ∧ 可写 ⇒ 真因是 ACL / 安全模块;不可写 ⇒ 先让目录可写);`chmod` / `rename` 那两步不读 —— 临时文件刚在同目录建成,可写是已证明的事实,铸点自己给。
|
|
29
39
|
*/
|
|
30
40
|
import { randomBytes } from "node:crypto";
|
|
31
|
-
import { chmodSync, lstatSync, readlinkSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
41
|
+
import { chmodSync, closeSync, existsSync, fsyncSync, linkSync, lstatSync, mkdirSync, openSync, readlinkSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, writeSync } from "node:fs";
|
|
32
42
|
import path from "node:path";
|
|
33
43
|
import { FsFailure } from "./fs-failure.js";
|
|
34
44
|
/** 按文档类的模式表(闭集)。 */
|
|
@@ -138,3 +148,97 @@ export function directoryAdmitsWrite(dir) {
|
|
|
138
148
|
const shift = st.uid === euid ? 6 : group ? 3 : 0;
|
|
139
149
|
return ((st.mode >> shift) & 0o3) === 0o3;
|
|
140
150
|
}
|
|
151
|
+
/** 新建目录的模式:仅属主可读写进(引擎的目录信任判官要求路径上的组件不得组 / 他人可写;0o700 在任何 umask 下都过)。 */
|
|
152
|
+
export const OWNER_ONLY_DIRECTORY_MODE = 0o700;
|
|
153
|
+
/**
|
|
154
|
+
* 把临时文件**独占地**(`wx`)建出来、写到写完为止(`writeSync` 可以少写,按返回值续写)、fsync 到盘上:模式是 `mode` **经进程 umask**(`open` 的 `mode` 语义,与 {@link writeFileAtomic}
|
|
155
|
+
* 新建临时文件同一条规则),建出来之后不再改它。`wx` 只管这个临时名没被占;「目标名已在」是 {@link createFileExclusive} 里 `link` 的事。
|
|
156
|
+
*/
|
|
157
|
+
function writeTempExclusive(tmp, text, mode) {
|
|
158
|
+
const fd = openSync(tmp, "wx", mode);
|
|
159
|
+
try {
|
|
160
|
+
const bytes = Buffer.from(text, "utf8");
|
|
161
|
+
for (let written = 0; written < bytes.length;)
|
|
162
|
+
written += writeSync(fd, bytes, written, bytes.length - written);
|
|
163
|
+
fsyncSync(fd);
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
closeSync(fd);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* 原子地**不存在才建** `target`(见文件头「不存在才建」):建成 ⇒ `"created"`;目标名已在(只有 `EEXIST`)⇒ `"present"`(一字不动)。
|
|
171
|
+
* 临时文件独占建出、写完、fsync({@link writeTempExclusive})后 `link` 成目标名。模式 = `kind` 在表里的新建模式经进程 umask(目标按定义不在,没有「已有的模式」可沿用)。
|
|
172
|
+
* 临时文件建不出 / 写不完 / fsync 不了 ⇒ 抛 {@link FsFailure}(落点「往目录里写临时文件」,与 {@link writeFileAtomic} 建临时文件失败同落点同句);`link` 的别的失败原样抛
|
|
173
|
+
* (调用方经 `fsFailureMessage` 取句)。父目录须已存在。
|
|
174
|
+
*/
|
|
175
|
+
export function createFileExclusive(target, text, kind) {
|
|
176
|
+
const spec = ATOMIC_FILE_CLASSES[kind];
|
|
177
|
+
const dir = path.dirname(target);
|
|
178
|
+
const tmp = path.join(dir, `.${path.basename(target)}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`);
|
|
179
|
+
try {
|
|
180
|
+
writeTempExclusive(tmp, text, spec.newMode);
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
try {
|
|
184
|
+
unlinkSync(tmp);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
/* 临时文件没建成:没什么可清 */
|
|
188
|
+
}
|
|
189
|
+
throw new FsFailure({ landing: "dir-create-temp", path: tmp, directoryWritable: directoryAdmitsWrite(dir) }, e);
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
linkSync(tmp, target);
|
|
193
|
+
return "created";
|
|
194
|
+
}
|
|
195
|
+
catch (e) {
|
|
196
|
+
if (e.code === "EEXIST")
|
|
197
|
+
return "present";
|
|
198
|
+
throw e;
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
try {
|
|
202
|
+
unlinkSync(tmp);
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
/* 已不在 */
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* 目录不在就建(连同缺的上级):**逐级** mkdir、每建一级立刻 chmod 到 {@link OWNER_ONLY_DIRECTORY_MODE} —— 模式恰是它,不经 umask(umask 0777 下一次性的递归 mkdir 会先建出
|
|
211
|
+
* 一级 0000 的目录、再也进不去建下一级)。本进程建了 `dir` ⇒ `"created"`;`dir` 已在(或恰被别的进程同时建成)⇒ `"present"`(它的模式一位不动)。
|
|
212
|
+
* 路径上有一节不是目录 ⇒ 抛。
|
|
213
|
+
*/
|
|
214
|
+
export function ensureOwnerOnlyDirectory(dir) {
|
|
215
|
+
const target = path.resolve(dir);
|
|
216
|
+
const missing = [];
|
|
217
|
+
for (let at = target; !existsSync(at);) {
|
|
218
|
+
missing.unshift(at);
|
|
219
|
+
const up = path.dirname(at);
|
|
220
|
+
if (up === at)
|
|
221
|
+
break;
|
|
222
|
+
at = up;
|
|
223
|
+
}
|
|
224
|
+
if (missing.length === 0) {
|
|
225
|
+
if (!statSync(target).isDirectory())
|
|
226
|
+
throw Object.assign(new Error(`${target} exists and is not a directory`), { code: "ENOTDIR" });
|
|
227
|
+
return "present";
|
|
228
|
+
}
|
|
229
|
+
let created = false;
|
|
230
|
+
for (const d of missing) {
|
|
231
|
+
try {
|
|
232
|
+
mkdirSync(d, { mode: OWNER_ONLY_DIRECTORY_MODE });
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
if (e.code === "EEXIST" && statSync(d).isDirectory())
|
|
236
|
+
continue; // a concurrent creator made it: theirs, left as it is
|
|
237
|
+
throw e;
|
|
238
|
+
}
|
|
239
|
+
chmodSync(d, OWNER_ONLY_DIRECTORY_MODE);
|
|
240
|
+
if (d === target)
|
|
241
|
+
created = true;
|
|
242
|
+
}
|
|
243
|
+
return created ? "created" : "present";
|
|
244
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 一个**已核过是纯 JSON 对象**的条目的 `enabled`:自有数据属性且是布尔 ⇒ 它;其余(缺席、只靠继承、访问器、不是布尔)⇒ `undefined`。不调 getter。
|
|
3
|
+
* 反射本身抛(Proxy 的描述符陷阱)⇒ **照抛**,由调用方的失败臂收:被丢条目身份整份按读不出处置(不带半份身份);原文判官读的是捕获出来的快照,不会走到这里。
|
|
4
|
+
* 包内私有(不进包根):这是「`enabled` 怎么读」的**唯一实现**;条目的形由调用方判一次(被丢条目身份在读 `name` / `id` 之前已判过,这里不再问第二次)。
|
|
5
|
+
*/
|
|
6
|
+
export declare function ownEnabledFlag(entry: object): boolean | undefined;
|
|
7
|
+
/** 一条目录条目**原文**的 `enabled`:先判形(不是纯 JSON 对象 ⇒ `undefined`),再读 {@link ownEnabledFlag}。「显式停用」与计划助手的启用过滤都调它。 */
|
|
8
|
+
export declare function entryEnabledFlag(entry: unknown): boolean | undefined;
|
|
9
|
+
/**
|
|
10
|
+
* 文档里**不是显式停用**的条目数(日志计数用;判官 {@link catalogManagesModelPlane} 就是它 > 0)。入参是 models 文档原文或读过的 `ModelsConfig`(两者同形)。
|
|
11
|
+
* 读不出 ⇒ `TypeError`(见文件头)。
|
|
12
|
+
*/
|
|
13
|
+
export declare function enabledEntryCount(doc: unknown): number;
|
|
14
|
+
/** 这份 models 文档管模型面吗(见文件头的律)。读不出 ⇒ `TypeError`。 */
|
|
15
|
+
export declare function catalogManagesModelPlane(doc: unknown): boolean;
|