@sema-agent/settings-schema 14.5.0 → 14.6.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 +265 -0
- package/README.md +15 -0
- package/conformance/VERSION +1 -1
- package/dist/atomic-write.js +10 -7
- package/dist/bundle.d.ts +1 -1
- package/dist/bundle.js +1 -1
- package/dist/config-dir-migration.js +2 -1
- package/dist/config-fns.js +2 -2
- package/dist/conformance/canonical.js +4 -7
- package/dist/conformance/corpus.js +3 -3
- package/dist/conformance/derive.js +6 -5
- package/dist/conformance/run.js +2 -1
- package/dist/definition-copy.d.ts +23 -0
- package/dist/definition-copy.js +52 -0
- package/dist/file-store.d.ts +1 -31
- package/dist/file-store.js +3 -30
- package/dist/fleet.js +2 -1
- package/dist/fs-failure.d.ts +12 -7
- package/dist/fs-failure.js +53 -29
- package/dist/hooks-config.d.ts +2 -2
- package/dist/hooks-config.js +112 -31
- package/dist/http-hook-allowlists.d.ts +7 -3
- package/dist/http-hook-allowlists.js +102 -16
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/mcp-request-entry.d.ts +9 -6
- package/dist/mcp-request-entry.js +225 -93
- package/dist/plain-json-object.d.ts +28 -3
- package/dist/plain-json-object.js +48 -7
- package/dist/portable-domains.d.ts +31 -0
- package/dist/portable-domains.js +30 -0
- package/dist/safety-merge-spec.d.ts +88 -6
- package/dist/safety-merge-spec.js +233 -17
- package/dist/scheduler-store.js +3 -2
- package/dist/settings-units.d.ts +85 -0
- package/dist/settings-units.js +175 -0
- package/dist/types.js +57 -53
- package/dist/url-shape.d.ts +11 -5
- package/dist/url-shape.js +11 -5
- package/dist/workers-rows.d.ts +2 -1
- package/dist/workers-rows.js +5 -4
- package/docs/API-SURFACE.md +17 -3
- package/llms-full.txt +12 -2
- package/llms.txt +4 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,271 @@
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 14.6.0 — 2026-10-24 — minor:唯一比较器 `tightenVerdict`(`TightenRule` 尾插 `setUnion` / `numericLower`,无参)· 成员比较器由成员行的值类座推出(`MemberAdmission.list` · `memberTighten`)· settings 单元表与单元读法(`SETTINGS_UNITS` · `unitTighten` · `readSettingsUnit`)· 便携域表搬纯叶模块 · 失败句出路并成一张表与 `chmod` 落点 · 「纯 JSON 对象」谓词只有一个座位 · 请求面 url 两个答案的读数
|
|
12
|
+
|
|
13
|
+
> 档位(按发版纪律):**minor** —— 加性:`TightenRule` 尾插两词(无参;发版纪律的裁定例外:持穷举 `Record<TightenRule["rule"], …>` 或无 `default` 穷举 `switch` 的消费方**提货时编译红** = 提货成本,
|
|
14
|
+
> 迁移句见第一条);`MemberAdmission` 多可选键 `list`(只收 `true`),五个列表成员行写上它;新导出 `tightenVerdict` · `TIGHTEN_VERDICTS` · `TightenVerdict` · `memberTighten` · `resolveMemberTighten`
|
|
15
|
+
> (包根与 `./safety-merge-spec`)、`SETTINGS_UNITS` · `SettingsUnitName` · `unitTighten` · `readSettingsUnit`(包根);`PORTABLE_DOMAINS` / `PortableDomain` / `NON_PORTABLE_DOMAIN_REASONS` 的定义搬家,公共面零变化;
|
|
16
|
+
> 合并表头注改写是文案。失败句的出路表、设模式失败的落点是文案(句子字面不承诺);请求面 url 两个答案的读数是特征钉,读器一个字没动。既有导出对既有输入的答案逐字不变(成员准入读数不变 —— `list` 不带地板;`SAFETY_MERGE_SPEC` 任何一行的规则值都没动);
|
|
17
|
+
> **答案变了的既有导出,列全如下**(其余逐字不变):① `MemberAdmission` 的可选键 `list` 只收 `true` —— 此前 `{ direction, list: false }` 的 `list` 当未知键被剥掉、成员行读成功,现在被拒(无默认的可选键;钉在 `test/member-tighten-14.6.0.test.ts`「false 不是一个值类」);
|
|
18
|
+
> ② `TightenRule` / `MergeCategorySpec` 现在收 `setUnion` / `numericLower`(此前拒;词表尾插,上面已列);③ 「纯 JSON 对象」谓词收口(后文那一条),分两类:
|
|
19
|
+
> **(甲)在非 JSON 值(类实例 / `Map` / `Date`)上答案变了**:受影响的公开导出**全表见下方第二张机器重算的表**(上界;手写枚举漏过 `deriveWireInput` —— 类实例的 skill 条目以前转成 `contentHash`,现在原样带 `content` —— 所以改由算法重算)——
|
|
20
|
+
> 这些入参本就不在各自文档契约内(契约写的是「JSON 对象 / 不是类实例」),按修 bug 记;`readHttpHookAllowlistLayer` 对非朴素**对象**袋是安全轴上的特例:fail-closed(两键读成 `[]` 并点名 `malformed`),不是「没有设置」。
|
|
21
|
+
> **(乙)跨 realm 的普通 JSON 对象**(`vm` 新上下文、iframe 里 `JSON.parse` 出来的)从此到处都认作朴素,由「拒」变「收」。**判档口径(维护方裁定)**:发版纪律里的「同一份配置 / 合法输入」以本包读口自己的 JSON 面为准(文本在本 realm 经 `JSON.parse`);
|
|
22
|
+
> 跨 realm 对象属嵌入方的 JS 对象面,**放宽方向按 minor**。受影响的既有公开导出**全表见下方机器重算的那张**(下面只挑几处写变化)—— v14.5.0 里按「原型恒等本 realm 的 `Object.prototype`」判的五个座位:`plain-json-object`(读者:读域键集对比、一次性迁移 `migrateStoredDocuments`、config.d 文档形 `judgeConfigDValue`)·
|
|
23
|
+
> hooks 分类器的内层记录判 · 请求面传输读器的 `env` / `headers` 记录读法 · `ModelEntry.extraBody` 的形闸与秘密走查:`classifyHooksConfig`(跨 realm 文档里的 hooks 块此前整块丢弃、守卫一并丢,答 `loaded-partial`;现在照常读、守卫在 —— fail-closed 方向)·
|
|
24
|
+
> `ModelEntry.extraBody`(此前报「extraBody must be a plain JSON object」,现在收下)· `readRequestMcpTransport` 的 `env` / `headers`(此前整键进 `lost`,现在收下)· `judgeConfigDValue`(此前抛「must be a JSON object (got non-plain object)」,现在收下)·
|
|
25
|
+
> `migrateStoredDocuments`(此前对跨 realm 的 `docs` 抛 `TypeError`,现在往下走)· 读域键集对比等 `config-fns` 读口(同一个座位,同向)。此前按弱合取判、跨 realm 本来就收的读口(比较器 · workers 行读口 · ……)答案不变。JSON 值上、本 realm 内逐字不变。
|
|
26
|
+
|
|
27
|
+
**跨 realm 的普通 JSON 对象:答案由 false 变 true 的公开导出(机器重算,逐字同 `test/cross-realm-affected-exports-14.6.0.test.ts`;少一个、多一个都红)**
|
|
28
|
+
|
|
29
|
+
```text
|
|
30
|
+
AgentHookEntry [., ./hooks] 翻(false → true)
|
|
31
|
+
CommandHookEntry [., ./hooks] 翻(false → true)
|
|
32
|
+
DOMAINS [., ./types] 只可达(答案不变)
|
|
33
|
+
DOMAIN_SCHEMAS [., ./types] 翻(false → true)
|
|
34
|
+
FileConfigStore [./node] 只可达(答案不变)
|
|
35
|
+
HookEntry [., ./hooks] 翻(false → true)
|
|
36
|
+
HookMatcherEntry [., ./hooks] 翻(false → true)
|
|
37
|
+
HooksConfig [., ./hooks] 翻(false → true)
|
|
38
|
+
HttpHookEntry [., ./hooks] 翻(false → true)
|
|
39
|
+
McpToolHookEntry [., ./hooks] 翻(false → true)
|
|
40
|
+
ModelEntry [., ./types] 翻(false → true)
|
|
41
|
+
ModelsConfig [., ./types] 翻(false → true)
|
|
42
|
+
NON_PORTABLE_DOMAINS [./conformance] 只可达(答案不变)
|
|
43
|
+
PromptHookEntry [., ./hooks] 翻(false → true)
|
|
44
|
+
buildEffective [., ./config-fns] 翻(false → true)
|
|
45
|
+
classifyHooksConfig [., ./hooks] 翻(false → true)
|
|
46
|
+
deriveLocalExpectation [./conformance] 只可达(答案不变)
|
|
47
|
+
deriveWireExpectation [./conformance] 只可达(答案不变)
|
|
48
|
+
domainFilePath [./node] 只可达(答案不变)
|
|
49
|
+
emptyEffective [., ./config-fns] 只可达(答案不变)
|
|
50
|
+
extraBodyUnsafe [., ./types] 翻(false → true)
|
|
51
|
+
foreignKeysOf [./conformance] 只可达(答案不变)
|
|
52
|
+
isDomainName [., ./types] 只可达(答案不变)
|
|
53
|
+
isStringRecord [.] 翻(false → true)
|
|
54
|
+
loadLocalConfig [./node] 只可达(答案不变)
|
|
55
|
+
loadRemoteExec [./node] 只可达(答案不变)
|
|
56
|
+
migrateConfigDirectory [./node] 只可达(答案不变)
|
|
57
|
+
migrateConfigDocuments [./node] 翻(false → true)
|
|
58
|
+
migrateEffectiveSnapshot [./one-time-migrations] 翻(false → true)
|
|
59
|
+
migrateStoredDocuments [./one-time-migrations] 翻(false → true)
|
|
60
|
+
parseDomain [., ./config-fns] 翻(false → true)
|
|
61
|
+
parseDomainLoud [., ./config-fns] 翻(false → true)
|
|
62
|
+
parseDomainSalvaged [., ./config-fns] 翻(false → true)
|
|
63
|
+
readBundle [./node] 只可达(答案不变)
|
|
64
|
+
readEffectiveWire [., ./config-fns] 翻(false → true)
|
|
65
|
+
readRequestMcpTransport [.] 翻(false → true)
|
|
66
|
+
refuseRetiredDomain [., ./config-fns] 翻(false → true)
|
|
67
|
+
resolveLegExpectation [./conformance] 只可达(答案不变)
|
|
68
|
+
runConformance [./conformance] 只可达(答案不变)
|
|
69
|
+
salvageEntries [., ./config-fns] 翻(false → true)
|
|
70
|
+
urlSeatPatterns [./one-time-migrations] 只可达(答案不变)
|
|
71
|
+
validateDomain [./node] 翻(false → true)
|
|
72
|
+
writeBundle [./node] 翻(false → true)
|
|
73
|
+
writeDomainFile [./node] 翻(false → true)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
列法 = 导出名 `[入口, …]` 判定;判定两词闭集:`翻(false → true)` = **行为核**证明:判官对跨 realm 的普通 JSON 对象(`vm` 新上下文、iframe 里 `JSON.parse` 出来的)由「非朴素」变「朴素」,该导出对这类入参的答案随之由拒 / 丢变收 / 读;
|
|
77
|
+
`只可达(答案不变)` = 引用链走得到种子,但导出的答案不取决于跨 realm 对象入参(行为核两版本都钝感,或造不出对象入参)。
|
|
78
|
+
**可达**由算法给(纯静态,读 `src/` 与 `package.json`):TypeScript 检查器把 `src/**` 顶层值声明连成引用图(类型位置的引用不算),从 v14.5.0 里**按原型恒等读形状**的读口(座位的读者 31 个顶层声明,去掉座位本身;
|
|
79
|
+
出处:`git show v14.5.0:src/**` 用同一把扫描机重算,有 tag 的树上有一格核对)反向可达,再按 `package.json` exports 表展开成公开**值**导出;机器给的是上界,**宁可多列,不许漏列**:不可达 ⇒ 一定不翻。
|
|
80
|
+
**判定**由行为核得出,不是猜的:能造出对象入参的导出各有一个探针(`test/helpers/cross-realm-probes.mjs`),在子进程里把「v14.5.0 构建」(`git show v14.5.0:src/**` 转译)与 HEAD 构建各用一个跨 realm 的普通 JSON 对象和一个本 realm 的同形对象实跑一次 ——
|
|
81
|
+
v14.5.0 分得出两者、HEAD 分不出、两版本的本 realm 答案同 ⇒ `翻`;两版本都分不出 ⇒ `只可达`;别的组合测试红。造不出对象入参的导出(只读名字集合 / 入参是路径与磁盘文本)在 helper 的未探针例外表里,写 `只可达`,每条带理由。
|
|
82
|
+
14.6.0 才有的导出(v14.5.0 没有它们,没有旧答案可比;两版公开值导出面相比的 7 个新导出 —— `SETTINGS_UNITS` · `TIGHTEN_VERDICTS` · `memberTighten` · `readSettingsUnit` · `resolveMemberTighten` · `tightenVerdict` · `unitTighten`;名单由两版导出面算出、不手写,有 tag 的树上有一格核对)不入表。v14.5.0 里本来就按弱合取读(跨 realm 本来就收)的读口(`deepEqual` · `canonicalize` · `readWorkersRows` · `readDrainRequest` …)不在表里。
|
|
83
|
+
|
|
84
|
+
**非 JSON 值(类实例 / `Map` / `Date`)上答案可能变的公开导出(机器重算,逐字同 `test/cross-realm-affected-exports-14.6.0.test.ts`;上界 —— 可达 ≠ 一定变,不可达 ⇒ 一定不变)**
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
AgentHookEntry [., ./hooks]
|
|
88
|
+
CommandHookEntry [., ./hooks]
|
|
89
|
+
DOMAINS [., ./types]
|
|
90
|
+
DOMAIN_SCHEMAS [., ./types]
|
|
91
|
+
FileConfigStore [./node]
|
|
92
|
+
HookEntry [., ./hooks]
|
|
93
|
+
HookMatcherEntry [., ./hooks]
|
|
94
|
+
HooksConfig [., ./hooks]
|
|
95
|
+
HttpHookEntry [., ./hooks]
|
|
96
|
+
McpToolHookEntry [., ./hooks]
|
|
97
|
+
MemberAdmission [., ./safety-merge-spec]
|
|
98
|
+
MergeCategorySpec [., ./safety-merge-spec]
|
|
99
|
+
ModelEntry [., ./types]
|
|
100
|
+
ModelsConfig [., ./types]
|
|
101
|
+
NON_PORTABLE_DOMAINS [./conformance]
|
|
102
|
+
NON_PORTABLE_DOMAIN_REASONS [./node]
|
|
103
|
+
PORTABLE_DOMAINS [./node]
|
|
104
|
+
PromptHookEntry [., ./hooks]
|
|
105
|
+
SAFETY_MERGE_SPEC [., ./safety-merge-spec]
|
|
106
|
+
TightenRule [., ./safety-merge-spec]
|
|
107
|
+
buildEffective [., ./config-fns]
|
|
108
|
+
canonicalize [./conformance]
|
|
109
|
+
classifyHooksConfig [., ./hooks]
|
|
110
|
+
createFileSchedulerStore [./node]
|
|
111
|
+
deepEqual [./conformance]
|
|
112
|
+
deriveLocalExpectation [./conformance]
|
|
113
|
+
deriveWireExpectation [./conformance]
|
|
114
|
+
deriveWireInput [./conformance]
|
|
115
|
+
describeNotStringArray [.]
|
|
116
|
+
describeNotStringRecord [.]
|
|
117
|
+
describeValueShape [.]
|
|
118
|
+
diffJson [./conformance]
|
|
119
|
+
diffWarningMultiset [./conformance]
|
|
120
|
+
domainFilePath [./node]
|
|
121
|
+
emptyEffective [., ./config-fns]
|
|
122
|
+
exportBundle [./node]
|
|
123
|
+
extraBodyUnsafe [., ./types]
|
|
124
|
+
findSecretLeaks [./conformance]
|
|
125
|
+
foreignKeysOf [./conformance]
|
|
126
|
+
httpHookEnvVarsAllowed [.]
|
|
127
|
+
httpHookUrlVerdict [.]
|
|
128
|
+
isDomainName [., ./types]
|
|
129
|
+
isSchedulerRecord [., ./scheduler-store]
|
|
130
|
+
isStringArray [.]
|
|
131
|
+
isStringRecord [.]
|
|
132
|
+
kitIdentity [./conformance]
|
|
133
|
+
layerAdmission [.]
|
|
134
|
+
loadCase [./conformance]
|
|
135
|
+
loadCorpus [./conformance]
|
|
136
|
+
loadLocalConfig [./node]
|
|
137
|
+
loadRemoteExec [./node]
|
|
138
|
+
loadSchedulerStore [./node]
|
|
139
|
+
memberAdmission [., ./safety-merge-spec]
|
|
140
|
+
memberMinTrust [., ./safety-merge-spec]
|
|
141
|
+
membersOf [., ./safety-merge-spec]
|
|
142
|
+
migrateConfigDirectory [./node]
|
|
143
|
+
migrateConfigDocuments [./node]
|
|
144
|
+
migrateEffectiveSnapshot [./one-time-migrations]
|
|
145
|
+
migrateStoredDocuments [./one-time-migrations]
|
|
146
|
+
mutateSchedulerStore [./node]
|
|
147
|
+
nodeConfigDirectoryIo [./node]
|
|
148
|
+
normalizeProjection [./conformance]
|
|
149
|
+
parseDomain [., ./config-fns]
|
|
150
|
+
parseDomainLoud [., ./config-fns]
|
|
151
|
+
parseDomainSalvaged [., ./config-fns]
|
|
152
|
+
parseSchedulerStore [., ./scheduler-store]
|
|
153
|
+
projectionKey [./conformance]
|
|
154
|
+
projectionValue [./conformance]
|
|
155
|
+
readBundle [./node]
|
|
156
|
+
readCapabilityReading [., ./fleet]
|
|
157
|
+
readDrainReceipt [., ./fleet]
|
|
158
|
+
readDrainRequest [., ./fleet]
|
|
159
|
+
readEffectiveWire [., ./config-fns]
|
|
160
|
+
readEngineSignal [., ./fleet]
|
|
161
|
+
readHttpHookAllowlistLayer [.]
|
|
162
|
+
readMcpIdentityHeaders [.]
|
|
163
|
+
readModelRefusals [., ./fleet]
|
|
164
|
+
readRequestMcpTransport [.]
|
|
165
|
+
readServingLeaseRequest [., ./fleet]
|
|
166
|
+
readServingLeaseResponse [., ./fleet]
|
|
167
|
+
readWorkersRows [.]
|
|
168
|
+
refuseRetiredDomain [., ./config-fns]
|
|
169
|
+
resolveLegExpectation [./conformance]
|
|
170
|
+
resolveMemberAdmission [., ./safety-merge-spec]
|
|
171
|
+
runConformance [./conformance]
|
|
172
|
+
salvageEntries [., ./config-fns]
|
|
173
|
+
saveSchedulerStore [./node]
|
|
174
|
+
stableKey [./conformance]
|
|
175
|
+
toManifestDoc [./conformance]
|
|
176
|
+
urlSeatPatterns [./one-time-migrations]
|
|
177
|
+
validateDomain [./node]
|
|
178
|
+
writeBundle [./node]
|
|
179
|
+
writeDomainFile [./node]
|
|
180
|
+
writeRemoteExec [./node]
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
同一把算法,**种子 = 自 v14.5.0 以来运行期代码有改动的全部顶层声明 ∪ 新增 / 搬来的全部顶层声明 ∪ 现行树里直接调座位的全部顶层声明**(不只调座位的:判据只有一条,答案**可能**变 —— 「未列的导出一定没变」按构造为真:没改的声明、且不引用任何改动声明的导出,答案不会变)。「运行期代码有改动」= 两版源文件各自擦类型、去注释后,同一个顶层声明经打印机打出的文本不同(`git show v14.5.0:src/**` 重算,有 tag 的树上有一格核对;45 个声明改了、52 个新增 / 搬来、12 个消失);新增 / 搬来的声明也是种子:搬走的声明,它的引用方函数体没动(只改了 import 行)—— `file-store.ts` 的 `PORTABLE_DOMAINS` / `NON_PORTABLE_DOMAIN_REASONS` 搬去了 `portable-domains.ts`,引用它们的导出因此入表(`NON_PORTABLE_DOMAIN_REASONS` · `PORTABLE_DOMAINS` · `exportBundle`)。**检测器的盲区由它自己报**:两版顶层声明名集合相等,或差集全在写明的新增 / 消失两张表里;非「具名函数 / 类 / 标识符变量」的顶层语句(解构声明 / 表达式语句 / 循环)恰是白名单〔现在只有 `retired-envs.ts` 两条冻结语句,两版逐字同〕—— 多一个、少一个、搬一个都红。种子里有:v14.5.0 没有调座位的读口(14.5.0 按弱合取读形状,14.6.0 改读座位:类实例 / `Map` / `Date` 由「当对象读」变「不是朴素对象」),**v14.5.0 就调座位的读口**(谓词本身也让它们的答案在已知的边上变〔原型链上第二环是 `null` 的非 JSON 值,如 `class X extends null` 的实例:读成朴素 —— `isStringRecord` / `extraBodyUnsafe` / `ModelEntry.safeParse(…).success` 由 `[false, true, false]` 变 `[true, false, true]`〕),以及**不调座位但函数体改了的读口**(`readMcpIdentityHeaders` 改读自有键、`isStringArray` 改走串数组读法、`httpHookUrlVerdict` / `httpHookEnvVarsAllowed` 换了名单读法 …)。上界的代价:表里也有与非 JSON 值无关的改动带进来的行(如原子写出的 chmod 落点经 `writeFileAtomic` 走到的导出、闭集规格常量的增词);反向可达,再按 `package.json` exports 表展开成公开**值**导出。
|
|
184
|
+
没有判定列:这一类的入参是 14.6.0 才改判的非 JSON 值,没有跨 realm 那样的行为核。14.6.0 才有的导出(同一张名单:两版公开值导出面相比的 7 个新导出 —— `SETTINGS_UNITS` · `TIGHTEN_VERDICTS` · `memberTighten` · `readSettingsUnit` · `resolveMemberTighten` · `tightenVerdict` · `unitTighten`)不入表。
|
|
185
|
+
|
|
186
|
+
- **唯一比较器 `tightenVerdict(rule, existing, candidate)` + 判词闭集 `TIGHTEN_VERDICTS`(`equal` / `stricter` / `looser` / `unordered` / `unreadable`)+ `TightenRule` 尾插 `setUnion` / `numericLower`**。
|
|
187
|
+
此前包里的 `TightenRule` 只是一个 schema 字段,没有执行器 ——「谁更严」由每个消费方各写一份(命令行壳的比较用真值化读布尔,字符串 `"false"` 读成真;供稿工具在比较器里拆 csv、认空集哨兵、做「≤ 0 = 无上限」)。
|
|
188
|
+
现在比较只有这一只,它**只认序、不认拼写**:每条规则有一个规范化域,任一侧不在域内 ⇒ `unreadable` —— `enumRank` 收序里的词(秩高更严)· `boolStricter` 收布尔(严格相等,不真值化)· `setUnion` / `setNarrower` 收字符串数组、按集合比
|
|
189
|
+
(序与重复不计;超集 / 子集更严,互不包含 ⇒ `unordered`)· `numericLower` 收有限数或 `+Infinity`(更小更严,`+Infinity` = 无上限 = 最松;`NaN` / `-Infinity` ⇒ `unreadable`)· `none` 收**严格 JSON**(`null` / 布尔 / 有限数 / 字符串 / 数组 / 纯对象,
|
|
190
|
+
嵌套不深于 `MAX_HASH_DEPTH`,与 `stableStringify` 同一计深口径;`NaN` / `±Infinity` / 任何位置的 `undefined` / `Date` / 类实例 / 访问器属性 / 稀疏数组的洞 / 更深 ⇒ `unreadable`,成员按属性描述符读、不调 getter;相等按 `stableStringify`,键序不计,否则 `unordered`)·
|
|
191
|
+
`policyDenyWins` ⇒ RangeError(它是类目里成员之间的折叠律,不是一格值的序 —— 成员请走 `memberTighten`)。两侧都必须在场:`undefined` ⇒ `unreadable`(「落点缺席 ⇒ 写」是装载律的事)。
|
|
192
|
+
两只新词**无参**:参数只许改变序(`strict` / `order`);「某个字面读成空集 / 无上限」是拼写,由值的属主在比较之前读成规范值。`numericHigher` 不加(今天零读者)。
|
|
193
|
+
三问 —— **谁需要**:每个要判「谁更严」的消费方(命令行壳的合并、服务端的 env 目录比较列、profile 装载器),此后三份实现退成一只;**谁受伤**:持 `TightenRule["rule"]` 穷举表或无 `default` 穷举 `switch` 的消费方提货时编译红
|
|
194
|
+
(已知消费方:命令行壳的投影函数有 `default` 臂,不红;服务端今天不读 `TightenRule`);**补偿**:迁移句 —— 给两词各加一臂,或整只换读 `tightenVerdict`。
|
|
195
|
+
钉:`test/tighten-verdict-14.6.0.test.ts`(词表尾插与无参 · 每条规则的域与序 · 集合两词各四格与重复元素 · `+Infinity` 最松 · `none` 的深度边界与 `stableStringify` 同口径、非 JSON 五形、访问器 / 洞 · `enumRank` 读规则自带的序而不是写死的序 · `numericLower` 不取整 · 交换两侧翻转律 · 缺席不是值)。
|
|
196
|
+
- **成员比较器由成员行推出:`MemberAdmission.list?: true` 值类座 + `memberTighten(category, member)` / `resolveMemberTighten(spec, member)`**。此前列表成员(权限 `allow` / `deny` / `ask`、沙箱 `filesystem.denyWrite` / `denyRead`)
|
|
197
|
+
只有「方向」这一个事实;要比较它们只能读**类目**的 `tighten` —— 沙箱类目是 `setNarrower`(子集更严),拿来比 deny 列表,`[]` 反倒「更严」、能抹掉既有 deny;读成员方向又是「超集更严」:同一行两读法。现在行上有显式值类座
|
|
198
|
+
(`boolPole` = 布尔、`list` = 字符串列表,每行恰其一),成员的比较器 = f(值类座, 方向):`boolPole` ⇒ `boolStricter{strict = boolPole.strict}`;`list` ⇒ tighten 成员 `setUnion`、loosen 成员 `setNarrower`;**两座皆无或皆有 ⇒ RangeError**
|
|
199
|
+
(将来的数值成员两座皆无,不会被静默推成列表)。类目的 `tighten` 不再回答成员:它只回答映进本类目的整值单元(今天:`permissionMode` 的序、`additionalDirectories` 的 `setNarrower`、其余 `none`);memberized 类目的整值与未声明成员
|
|
200
|
+
**没有比较器**(只有类目地板上的准入),问了就 RangeError。误用臂(整键类目 / 未声明成员 / 未知类目)与成员准入同一套拒句。
|
|
201
|
+
三问 —— **谁需要**:比较沙箱 deny 列表与权限三桶的消费方(此后与准入读同一行);**谁受伤**:无 —— 老消费方的成员 schema 剥掉 `list`,准入照旧(冻结 1.1.0 钉看着);自己构造成员行交给 `resolveMemberTighten` 而不声明值类的调用方拿 RangeError(新函数,无存量);
|
|
202
|
+
**补偿**:给行声明 `boolPole` 或 `list`。
|
|
203
|
+
钉:`test/member-tighten-14.6.0.test.ts`(16 行逐行 · 每行恰一座、列表行恰五个 · 合成行两座皆无 / 皆有 · 值类座的形 · 老 schema 读数逐字);`test/safety-merge-spec.test.ts` 冻结 1.1.0 钉加第五个豁免(`permissions` 三行多 `list`,其余字段逐字)。
|
|
204
|
+
- **settings 单元表 `SETTINGS_UNITS` + 单元比较器 `unitTighten(unit, member?)` + 单元读法 `readSettingsUnit(unit, raw, member?)`**。「settings 文档里哪些键有合并契约、在文档的哪个位置、归哪个类目、原值怎么读」此前住在消费方:命令行壳一张十单元表
|
|
205
|
+
(两个关停位映到 `permissionMode` 再本地盖「只许托管层」)、供稿工具手抄第二张(关停位映对了,与壳差两格),读法各写各的。现在单元表由本包发布:单元名沿壳既有十个(`permissions` · `permissionMode` · `additionalDirectories` · `sandbox` ·
|
|
206
|
+
`disableBypassPermissionsMode` · `disableAutoMode` · `hooks` · `env` · `model` · `outputStyle`),两个关停位映 `killSwitches` 的成员;每行写明寻址(`whole` / `member` / `members`)与读法 —— 整值 / 单成员单元带 `reading` 词,逐成员单元按成员行的值类座读。
|
|
207
|
+
读法逐单元:权限三桶与 `additionalDirectories` 收「元素皆非空字符串的数组」(裸串**不**自动包成数组);`permissionMode` 收序里除 `bypassPermissions` 外的五个词,`"manual"` 读成 `"default"`,`"bypassPermissions"` 答 `launch_only`;
|
|
208
|
+
两个关停位收布尔,`"disable"` 读成 `true`,**`false` 读成 `false`**(松极,是一个值);沙箱布尔成员只收布尔、列表成员同权限桶;`hooks` 收唯一判官 `classifyHooksConfig` 判 `loaded` 的值;`env` 收「键非空、值皆字符串」的纯对象;`model` / `outputStyle` 收非空字符串。
|
|
209
|
+
表外一切 ⇒ `unreadable`(没有默认臂);`undefined` / `null` 对任何单元都是 `unreadable`;收下的值原样返回(同一引用),唯二改写是上面两处拼写。**读得出就比得了**:每条读法的最后一步就是单元比较器自己的域判(`tightenVerdict(unitTighten(…), v, v)` 须为 `equal`)——域只有比较器一个属主,读法只在其上加自己要的(列表元素非空、hooks 判官……);所以 hooks 判官照剥照 `loaded`、而被剥的契约外键里藏着深于 `MAX_HASH_DEPTH` 的数据或 `undefined` 的文档,读成 `unreadable`,不会「读得出却比不了」。寻址错(逐成员单元不给成员、给未声明成员如沙箱的 `failIfUnavailable`、
|
|
210
|
+
整值单元给成员、未知单元)⇒ RangeError,两只函数同一判。env **文档**的字面文法(布尔词集、csv 与空集哨兵、上限关断)不在本包,归引擎的 env 目录。http hook 名单两键(`allowedHttpHookUrls` / `httpHookAllowedEnvVars`)也有合并契约,却**不在**这张表里:它们的键表与读法归 `./http-hook-allowlists`(坏形读成 `[]` 并记 `malformed`,与单元读法的 `unreadable` 不同);并表要先裁哪种读法对,是设计件,本版不做 —— 所以「读这张表就不必自留键 → 类目表」说的是这十个单元。
|
|
211
|
+
三问 —— **谁需要**:命令行壳(删本地单元表、本地托管戳与三处本地读法,换读本表)、供稿工具(删手抄的单元表);**谁受伤**:本包内零行为变化(新导出)。行为变化发生在消费方**采用**时,由采用方的提货承担 —— 已知一条:
|
|
212
|
+
关停位的 `false` 与未知值今天在壳里被读成「缺席」,换读本读法后 ① **后层**写 `false`、而**先层**已立 ON 的用户:后层这个 `false` 由静默忽略变成一条「放宽被拒」告警,生效值不变(ON 仍在);先层 `false`、后层 `true` 照常收紧,不出告警;
|
|
213
|
+
② 写了未知值(如 `"yes"`)的层由静默缺席变成响亮拒;**补偿**:告警点名键与层,生效值零变化;采用方逐单元对照本读法表写自己的三问。
|
|
214
|
+
钉:`test/settings-units-14.6.0.test.ts`(单元表逐行 · 寻址律五格 · 关停位四格 × 两键 · 模式词 · 逐单元一正多反 · `undefined` / `null` · 只认形不改形 · 读者收下的值恒在比较器域内 · hooks 超深 / 藏 `undefined` 时读者与比较器同答 `unreadable` · 每个单元与声明成员都有读法与比较器 · 合并表头注)。
|
|
215
|
+
- **`PORTABLE_DOMAINS` / `PortableDomain` / `NON_PORTABLE_DOMAIN_REASONS` 的定义搬进纯叶模块(只依赖类型)**。此前它们住在 `file-store`(import `node:fs/promises` / `node:path`),主桶里的模块要用便携域表只能把 node 件拉进浏览器包或手抄第二份。
|
|
216
|
+
现在定义在叶上,`./node` 子路径原名转出口(逐名同值),主桶**不新增**这三个名字。三问 —— **谁需要**:要在主桶里用便携域表的模块(S-1947 15.0.0 半的 profile 文档形);**谁受伤**:无(`./node` 逐名同值,导出面登记表该子路径零 diff);**补偿**:无需。
|
|
217
|
+
钉:`test/portable-domains-leaf-14.6.0.test.ts`(主桶静态闭包无 `node:` 内建 · 叶模块闭包无 `node:` · 一处定义、`./node` 同值 · 主桶不出这三个名字)。
|
|
218
|
+
- **合并表头注改写(`safety-merge-spec`,文案)**。删掉「每个消费方自留一张键 → 类目表」与点名一个不存在的管理面读者的句子,改成:settings 文档的单元表由本包发布(`SETTINGS_UNITS`);新增一段「比较」:比较器只有 `tightenVerdict`、只认序;
|
|
219
|
+
类目的 `tighten` 只回答映进本类目的整值单元;声明成员的比较器由值类座推出;memberized 类目的整值与未声明成员没有比较器(问了 RangeError,不回退类目规则)。三问 —— **谁需要**:读 `dist/safety-merge-spec.d.ts` 接线的消费方;**谁受伤**:无;**补偿**:无需。
|
|
220
|
+
- **权限类出路:三个落点一张表、一只函数;目录权限位已放行时 `EACCES` 不再叫人让目录可写**。14.5.0 让换目标(`rename`)落点的句子在「目录可写」已是事实时不再说「make the directory writable」,可建临时文件、删目录项两个落点的 `EACCES` 仍说 ——
|
|
221
|
+
目录权限位已经放行(调用方在 Node 一侧读出来的事实),chmod 无用,照做无效;剩下的真因是 ACL 条目或安全模块策略(SELinux / AppArmor)。出路本身还散在 `permissionWayOut` / `renameWayOut` / 三个 errno 集合 / 一条粘滞目录句尾里,
|
|
222
|
+
`rename` 的 `EACCES` 是写死的字面句,`RENAME_DIRECTORY_WRITABLE` 实际只管 `EPERM` 一支。现在出路只有 `permissionWayOut(落点, errno, 目录模式位是否可写)` 一只函数、两层表:落点表(这个落点认哪些 errno、清标志点名谁、动已有目录项的落点才有粘滞目录一句)×
|
|
223
|
+
errno 表(穷举;每个 errno 两格 ——「可写」/「不可写或没读过」,外加「可写时是否接粘滞句」一列:只有 `EPERM`)。
|
|
224
|
+
**句子改动面(如实,只有三格)**:① 建临时文件 / 删目录项落点的「目录可写 ∧ `EACCES`」:由「make the directory writable」换成「the directory's permission bits already admit this process, so chmod will not help; an ACL entry may still deny this process (macOS: ls -le; Linux: getfacl);
|
|
225
|
+
so may a security-module policy (SELinux / AppArmor: the audit log names the rule)」(不接粘滞目录句:那条属主规则给的是 `EPERM`);② 换目标落点的 `EACCES`:14.4.0 起的字面句「make the file replaceable — clear an immutable flag; in a sticky directory only the file's owner may replace it」退役 ——
|
|
226
|
+
**14.5.0 修回的 14.4.0 句,在本版再次改写,理由 = `EACCES` 的真因**:不可变标志与粘滞目录的属主规则给的都是 `EPERM`(macOS `man 2 unlink` / `rename`;Linux 同,推断),`EACCES` 在目录权限位已放行时只剩 ACL 与安全模块,所以换成同一张表的 ACL / 安全模块句,句尾不再接粘滞句;
|
|
227
|
+
③ 删目录项落点的「目录可写 ∧ `EPERM`」句尾多一句「in a sticky directory only the file's owner may remove it」(动已有目录项的落点同一条律,换目标落点原本就有;建临时文件落点不加 —— 建的是新文件名,没有属主规则)。其余 errno × 模式位的格逐字不变。
|
|
228
|
+
三问 —— **谁需要**:写 / 删 `config.d` 文档时撞 `EACCES`、而目录权限位其实放行的运维(部署在带 ACL 或 SELinux 的机器上:此前被叫去 chmod,照做无效);**谁受伤**:按失败句原文字面匹配的脚本(句子是文案,`code` / `cause` 不变,不承诺字面);**补偿**:按 `code` 分流。
|
|
229
|
+
钉:`test/fs-failure-way-out-table-14.6.0.test.ts`(三落点 × `EPERM` / `EACCES` / `EROFS` / `ENOENT` × 可写 / 不可写 / 没读过整格,期望手写;退役句不再出现;可写的格都不再说 make the directory writable;「名字带 WayOut 的函数恰一只」反向门);
|
|
230
|
+
翻钉:`test/fs-failure-way-out-order-14.5.0.test.ts` 的 `EACCES` 格 · remove 的 `EPERM` 整句 · 换目标的 `EACCES` 格,`test/fs-failure-mint-12.0.1.test.ts` 的 rename 格,`test/fs-failure-landings-12.0.1.test.ts` 的建临时文件 `EACCES` 格。
|
|
231
|
+
- **设模式失败有自己的落点 `chmod`(原子写器)**。已有的文件沿用它原来的模式:写器在临时文件建成之后、`rename` 之前对临时文件 `chmod`。这一步失败(文件系统不让改模式 —— 挂载点不带 POSIX 权限之类)时,落点此前还停在「建临时文件」,
|
|
232
|
+
句子于是说「cannot write into directory <dir> (EPERM): 清目录的不可变标志 / 让目录可写」—— 真因不在目录,照做无效(14.4.0 起就是这样,只是指错的方向不同)。现在这一步有自己的落点:
|
|
233
|
+
`cannot set the mode of <真文件> (EPERM): the filesystem does not allow changing file modes — keep this file on a filesystem that supports POSIX permissions`,不提目录、不提不可变标志。认的 errno 只有 `EPERM`(创建者对自己刚建成的临时文件 chmod 被拒,只能是文件系统 / 挂载不支持);`EACCES` / `EROFS` 在刚建成的文件上撞不到,不替它们猜出路;`ENOTSUP` 同样意味着文件系统不支持改模式,但没有真实挂载点验证过,今天给 Node 自己的句子。
|
|
234
|
+
落点的 `path` 是被保留模式的真文件(chmod 作用在替换它的临时副本上,用户认得的是真文件名)。文件系统失败句的铸点是包内私有的,`FsFailure` 不在导出面;只有从 `./node` 的写 / 删口抛出的错对象上读 `landing` 的消费方看得到新值 `"chmod"`(`code` / `cause` 不变)。
|
|
235
|
+
三问 —— **谁需要**:把 `config.d` 放在不支持改文件模式的文件系统上的运维;**谁受伤**:按失败句原文字面匹配的脚本(句子是文案);读 `FsFailure.landing` 并对它穷举的消费方(新值 `"chmod"`;已知消费方不读);**补偿**:`code` 仍是底层 errno,按 `code` 分流。
|
|
236
|
+
钉:`test/fs-failure-chmod-landing-14.6.0.test.ts`(铸点整句逐字 · 认的 errno 闭集〔`EPERM` 一个;`EACCES` / `EROFS` / `ENOSPC` / `ENOTSUP` / `EIO` 给 Node 句子〕与 `ENAMETOOLONG` · `FsFailure` 记着落点 · 桩 `chmodSync` 抛 `EPERM` 的端到端:点名设模式、`code` / `landing` / `cause` 留着、原文件原样、临时文件已清 · 新建文件与调度店类不走这一步的对照);
|
|
237
|
+
`test/fs-failure-single-mint-gate-12.0.1.test.ts` 的开头正则加「cannot set the mode of」(铸点之外手写同类句即红)。
|
|
238
|
+
- **「纯 JSON 对象」的谓词只有一个座位(包内私有,`./plain-json-object`)**。14.5.0 把三处读域 / 迁移 / config.d 的副本收进这一个模块,头注却自称「唯一」,树里还有同义或同名的副本:`extraBody` 形闸的内联一份、`conformance/canonical` 与 `conformance/derive` 里**同名**的 `isPlainObject`
|
|
239
|
+
(注释写「不是类实例」,实际不查原型 —— 两个不同的 `Date` 或 `Map` 都被读成「没有键的对象」,比较器判它们相等),同一个两行原型判在别处各抄了一遍(settings 单元表的 `isEnvMap`、严格 JSON 深度判、请求面传输的记录读法、`extraBody` 的秘密走查),三只更弱的 `is Record<string, unknown>` 守卫
|
|
240
|
+
(`mcp-request-entry` 的 `isRecord`、`workers-rows` 的 `isRec`、`conformance/corpus` 的 `isRecord`),五处内联的「typeof 对象 ∧ 非 null ∧ 非数组」弱合取(`readEffectiveWire`、`readHttpHookAllowlistLayer`、fleet 线上载荷的受护判、`conformance/run` 的元数据拆分、`migrateConfigDocuments` 的入参判),
|
|
241
|
+
两处两子句弱判(`readRequestMcpTransport` 的传输对象判、`isSchedulerRecord` 的记录与 `when` 判,都是 JSON 面),以及 hooks 分类器的顶层文档判。现在十七处读座位(十四处调 `isPlainJsonObject`、三处调座位旁的形状分类 `jsonShapeOf`),分类器的顶层文档判读闭包里的同文那一份。弱语义不是「确需」:这些读口读的都是 JSON 值,JSON 解析出来的对象恒是朴素的,弱版本唯一的"功能"是把类实例 / `Map` / `Date` 读成空对象 —— 假绿,不是需求。
|
|
242
|
+
**判据本身也改对了**:朴素 = 原型是 `null`,或原型自己的原型是 `null`。此前按「原型恒等本 realm 的 `Object.prototype`」判,另一个 realm(`vm` 新上下文、iframe)里 `JSON.parse` 出来的普通对象会被读成非朴素 —— 在本版新收口的比较器和 workers 行读口上,这是相对 14.5.0 的回归(合并复审抓到);
|
|
243
|
+
任何 realm 的 `Object.prototype` 自身原型都是 `null`,类实例 / `Map` / `Date` / 建在别的对象原型上的记录链更深,所以判据与 realm 无关,不为 realm 加特例。已知的边(座位头注同写):原型是某个 null 原型对象的对象(`Object.create(Object.create(null))`)、`class X extends null` 的实例,链上第二环也是 `null`,按这条判据读成朴素;它们只能由程序造出,JSON 解析不出来;读者按它的**自有**成员读,继承来的成员不计。两侧的后果不同:托管层 http hook 名单**只靠继承可见的名单键读成 `[]` 并点名 `malformed`**(fail-closed,读者里一臂;自有键盖过继承键;http 侧只对这两个键名在 `Object.prototype` 上的污染敏感,方向是拦,不是放 —— 不存在 / 非对象的层在读器里仍是「两键都缺」,但读器造的层是普通对象(与 v14.5.0 同形),污染下判官读到污染的名单,该层被收紧〔fail-closed,判决的 `sources` 点名它〕;读器内部一切中间对象零原型〔`readPlainStringRecord` 的累加器也是:零原型的袋、成员用 `defineProperty` 写、对象展开铸普通对象 —— 只读 getter 污染与数字键 getter / setter 污染都不让它抛、不吞成员〕,原型上被污染成只读的同名键不让读器抛;setter 污染:名单不再被截走〔v14.5.0 的累加器是 `{}`,赋值命中继承来的 setter、托管层的名单被静默丢掉〕;数字键访问器污染〔`Object.prototype` 的 `"0"` 带 getter / setter〕下,名单副本与 `malformed` 只用定义语义造〔展开 / `filter`,不 `push`、不下标赋值 —— 那是 `[[Set]]`,会命中继承的 setter、吞条目、事后读到继承 getter 供出的 `"*"`〕,检视与匹配只读副本自有的下标;判官 `httpHookUrlVerdict` / `httpHookEnvVarsAllowed` 按 `[[Get]]` 读层的名单 —— 名单放在原型上的手造层〔类 getter、`Object.create(defaults)`〕照旧生效、与 v14.5.0 同答 —— 再过一道形判:读到的不是串数组〔getter 返回非数组、含非串条目或洞、读 `length` / 元素的 getter 或 Proxy 陷阱抛〕读成 `[]`,fail-closed、不抛 —— 取值、检视、复制在一个 try 里,判官只读已校验的副本〔元素只读一次〕;**读器与判官共用这一只读法**〔包内私有的 `captureAllowlist`:取值、检视、复制在一个 try 里,名单键与元素都只读一次〕,读器说合法的名单判官不会读成空:带洞 / 读元素时抛的数组,读器点名 `malformed` 并交出 `[]`;稀疏数组的洞〔不是自有下标〕一律坏形,不顺原型链读 —— `Object.prototype` / `Array.prototype` 上的数字键〔数据属性或访问器〕注入不进名单,v14.5.0 会把它读成名单条目;`length` 不是安全非负整数〔读成字符串的 Proxy 等〕同样坏形;逐下标读、遇到第一个坏处就退出,不按 `length` 整份物化〔`length` 很大的稀疏数组立即判坏形,不再堆耗尽〕;名单键的取值抛〔键本身是会抛的自有 getter〕也读成坏形,读器同样点名并交出 `[]`、不抛出读器〔此前读器用 `every` 加展开认带洞的数组合法、交出带 `undefined` 的名单,判官却读成 `[]`〕;v14.5.0 的 `some` / `includes` 在第一条命中处短路、读不到后面抛的元素,本版对这类手造数组整张读空〔同一个方向:拦,不是放〕;原型污染出来的名单只会让准入名单取交后更窄,方向是拦,不是放;读器返回的层是普通对象〔返回值的运行期形状不变〕);请求面 `env` / `headers` 对这一形,由 v14.5.0 的响亮拒变成收成 `{}`(继承来的成员丢了)——
|
|
244
|
+
这一侧不修:两种改法(读者级遍历继承成员、谓词级再加一条判据)都对 `Object.prototype` 上的可枚举污染敏感〔污染之后每个 `JSON.parse` 出来的对象都会读成非朴素或带继承成员〕,而这一侧丢的是数据、失败可见、不在安全轴。
|
|
245
|
+
**形状分类 `jsonShapeOf`(包内私有,座位旁)**:读者要的答案不只是「是不是朴素对象」时调它(`plain` / `non_plain_object` / `array` / `null` / `primitive` / `uninspectable` 六词;函数是对象、不是纯数据,归 `non_plain_object`;读形状就抛的对象 —— 已撤销的 Proxy、`getPrototypeOf` 陷阱抛的活 Proxy —— 是 `uninspectable`),不在读者里再推一遍形状;`jsonShapeOf` 是座位的本体(唯一读原型的函数),`isPlainJsonObject` 由它派生〔就是它的 `plain` 那一格〕。读者对这六个词只用穷举 `switch` 加 `never` 默认臂(加第七个词时每个读者编译红),单座门看着:`jsonShapeOf(…)` 的结果只许存进变量或直接作 `switch` 判别式(不许内联在比较里、当实参、被 `return`),变量(含成员 / 元素访问目标与解构别名)对非 `plain` 的五个词不许用 `===` 链逐个比。`readPlainStringRecord`、请求面传输读器、`readHttpHookAllowlistLayer` 读它。
|
|
246
|
+
**唯一的第二处是 `hooks-config` 的 `hookShapeOf` / `hookIsRecord`**:分类器的闭包要被消费方的生成器整函数抄走(自由名只有 `HOOK_EVENTS`),闭包里不能 import;它们与唯一座位的 `jsonShapeOf` / `isPlainJsonObject` **逐字同文**(调用名归一后),由反向门证明(漂开即红)。并回唯一座位要闭包契约多注入一个名字、生成器同步改,是消费方可见的改动,登 S-2133(major 件)。
|
|
247
|
+
**答案变了的只有非 JSON 值**(其余逐字不变;跨 realm 的放宽见档位段):符合性比较器 `deepEqual` / `diffJson` / `canonicalize` 遇到类实例 / `Map` / `Date` 不再把它们当「没有键的对象」(两个不同的 `Date` 此前判相等;`deepEqual` 为假 ⇒ `diffJson` 非空的承重不变式在这些值上同样成立);
|
|
248
|
+
`toManifestDoc` 对非朴素的 skills 文档原样交回;`describeNotStringRecord(Map)` 回「an object」(此前说「an object with a non-string value」);`readWorkersRows` 对非朴素的行 / 文档与「认不出名字」同一臂、原样放行交整域读侧判 ——
|
|
249
|
+
**非朴素的坏行因此不再单行扣下,而是整域 `parseDomain("workers")` 判,整张舰队清单读不出**(响亮、fail-closed;只落在非 JSON 值上);`readEffectiveWire`(类实例载荷 ⇒ 抛「not a JSON object」)· `readHttpHookAllowlistLayer`(类实例 / 建在原型上的对象袋 / 函数 ⇒ **fail-closed**:两键都读成 `[]` 并点名 `malformed`,托管层的名单不会被静默丢掉;`null` / 原始值 / 数组仍是「整份设置不是对象 ⇒ 两键都缺」)·
|
|
250
|
+
fleet 线上载荷读口(类实例 ⇒ `undefined`)· `migrateConfigDocuments`(类实例入参 ⇒ `TypeError`)· `classifyHooksConfig`(顶层是类实例 ⇒ 走「不是 JSON 对象的文档」那一臂,整棵当子树扫守卫迹象,守卫迹象照旧整份拒 —— 以前同一个函数对同一个实例有两种读法:顶层读成设置文档、放进 `{ hooks: … }` 里却当坏块)。
|
|
251
|
+
这些入参本就不在各自文档契约内(契约写的是 JSON 对象 / 不是类实例),按修 bug 记。
|
|
252
|
+
**执行面的同类收口**(只落在非 JSON 值上,JSON 面逐字不变;**座位自己总(不抛)**:检视就抛的对象〔已撤销的 Proxy、`getPrototypeOf` 陷阱抛〕答「不是纯 JSON 对象」〔`isPlainJsonObject` 为 `false`〕,全包经座位的读口因此拒而不抛 —— v14.5.0 的 http hook 名单读器与调度记录判不检视原型,本版读原型,异常不能从兄弟读器逃出去;律在座位上收一处,不在每个读器各包一个 `try`。
|
|
253
|
+
形状分类 `jsonShapeOf` 是座位的本体(唯一读原型的函数;`isPlainJsonObject` 由它派生,闭包座位的 `hookShapeOf` / `hookIsRecord` 与它们逐字同文),因此是**六词**:`plain` / `non_plain_object` / `array` / `null` / `primitive` / **`uninspectable`**(读形状就抛的对象 —— 已撤销的 Proxy,连 `Array.isArray` 都抛,以及 `getPrototypeOf` 陷阱抛的活 Proxy;不是「不朴素」,「拷成纯对象」的劝告对它做不到)。读者对六个词仍只用穷举 `switch`(加第七词时每个读者编译红),并对 `uninspectable` 一律按最严处置:名单读器两键读成 `[]` 并点名 `malformed`、传输读器 `ok:false`、记录读器整键不交、hooks 分类器拒整份文档。
|
|
254
|
+
**hooks 分类器对「检视不了」fail-closed(安全轴)**:读不出的对象作 `hooks` 块、事件值、matcher、entry、顶层其它键或子树深处 ⇒ 整份拒(进 `rejections`,outcome 不是 `loaded-partial`),扫描遇到读不出的节点按守卫迹象报在这个节点自己的坐标;「读不出」由座位在类型分流**之前**判 —— 分类器没有自带的 `typeof` 前置,函数目标的已撤销 Proxy 〔`typeof` 是 `"function"`〕同样拒,函数是对象一类〔`hooks` 数组里装函数按「数组里装对象」拒〕,`entryFace` 不把不是纯数据的值当 `type` 的载体〔撤销的 Proxy / 类实例作 entry ⇒ `entry-invalid`〕;**已知的边(旧债,另登票)**:分类器只看**自有键**上的守卫迹象 —— `Map` 的**条目**〔`new Map([["PreToolUse", …]])`〕不是自有键,守卫在里头时 `hooks` 块是这样的 `Map` 仍按「不是可读的块」降级丢(`loaded-partial`),别处的 `Map` 不被扫出迹象;`hooks` 块不是可读的块(类实例、读不出的对象)且里头有〔或可能有〕守卫迹象 ⇒ 拒(`hooks-is-matcher`,路径 `hooks`)—— 此前整块按 `hooks-not-object` 丢,带 guard 的块静默消失;没有迹象的类实例仍按今天的降级丢。
|
|
255
|
+
**比较器与 `extraBodyUnsafe` 同律**:`tightenVerdict` 对已撤销的 Proxy(对象 / 数组 / 函数目标)与读不出的成员〔`ownKeys` / 描述符 / `length` 抛的 Proxy、抛的访问器〕一律出定义域 ⇒ `unreadable`、不抛〔此前 `none` / `setUnion` / `setNarrower` 三规则的裸 `Array.isArray` 抛 `TypeError`,另三条答 `unreadable`;串集合经包内的串数组读法 `captureStringArray`;`none` 规则**按描述符视图比较、只读一次**:域判走查把读到的值拷成纯数据副本(与 `captureStringArray` 同律),序列化只读副本 —— 判的就是域判看过的内容,每个成员的描述符恰读一次、成员值不经 `[[Get]]`、键表只问一遍〔此前域判按描述符读、`stableStringify` 按 `[[Get]]` 再读:描述符说 `a = 1` 而 `get` 答 `undefined` 的 Proxy 与 `{ a: null }` 判 `equal`,判的是序列化通道答的〕;自相矛盾的 Proxy 得到的判词 = 描述符视图,不抛;**描述符本身也只读自有键**〔`Object.hasOwn(desc, "value")` 而不是 `"value" in desc`:`Object.prototype.value` 被污染时,会抛的访问器描述符不会因继承的 `value` 被当成存下来的值 —— `none` 的走查、`extraBodyUnsafe`、抢救条目的身份读同〕;非可枚举键与 symbol 键不收〔与 `Object.keys` 同口径:不序列化的不比较〕〔`get` 陷阱抛的对象:成员不走 `[[Get]]` ⇒ 也按描述符视图;数组的 `length` 读不出 ⇒ `unreadable`〕;`extraBodyUnsafe`(旧债同修)读不出的值按「不安全」拒、不抛。
|
|
256
|
+
**名单读器对袋的每一次触碰都经同一只守护读取**:此前裸 `key in bag` 在守护之外,`has` 陷阱抛的袋把异常带出读器〔v14.5.0 不碰 `in`〕;现在一只探针答 `own` / `inherited` / `absent` / `unreadable`,探不出的键读成 `[]` 并点名 `malformed`(fail-closed),探得出的自有名单照读。
|
|
257
|
+
**公开的 `readMcpIdentityHeaders` 与传输读器同一条自有键律**:身份头只读自有属性(读法 `ownValueOf` 自身就是守护读 —— `getOwnPropertyDescriptor` 陷阱抛 / 会抛的自有 getter ⇒ 读不出:传输读器整只不交,公开读者身份头整体读不出〔答 `{}`〕—— `principalHeader` 在场但读不出时**不**落到 `assertionHeader`,只有 principal 干净地读成缺席才读 assertion;快照与公开读者共用同一只身份头读法 `identityFieldsOf` —— principal 读不出 ⇒ 传输整只 `ok:false`(「检视不了」那句)、`assertionHeader` 一次不读;快照按顺序读字段、第一个读不出的就停),原型上注入 / 只在原型上的 `principalHeader` / `assertionHeader` 读不到〔v14.5.0 顺原型链读;两读者对同一输入同答〕。
|
|
258
|
+
**hooks 分类器整只在根上兜一只 `try`**(没有逐节点的):读文档时再抛〔陷阱 / 会抛的 getter —— 座位答得出形状、但读成员抛〕⇒ 根上拒(`guard-outside-hooks`,路径 `[]`)。
|
|
259
|
+
数组的「是不是」同样总:`Array.isArray` 对已撤销的 Proxy 抛,包内私有的 `isJsonArray`(座位旁)与分类器闭包里逐字同文的 `hookIsArray` 是它的总版;`readWorkersRows` 的 `workers` 与 hooks 分类器经它,已撤销的 Proxy 不再抛 `TypeError`(v14.5.0 抛)—— workers 行读口把它读成「不是数组」原样交回。
|
|
260
|
+
请求面的拒句:已撤销 / 陷阱抛的 Proxy 作整只传输、`env`、`headers`、`args` ⇒ **全包同一句**「an object that could not be inspected (a proxy trap threw)」〔机读位 `not_plain_record` 不变;`env` / `headers` 遇 `getPrototypeOf` / `ownKeys` 陷阱抛的 Proxy 说的仍是 14.5.0 起已发的这一句〕。
|
|
261
|
+
传输自己的字段〔`kind` / `command` / `url` / `args` / `env` / `headers` / `principalHeader` / `assertionHeader`〕按传输种类一次读成快照,`get` 陷阱抛的 Proxy 或会抛的自有 getter 作传输 ⇒ `ok:false`、不抛〔目标上有 `kind` 时点名 `transport` 并说「检视不了」那一句;目标上没有 `kind` 时点名 `transport.kind`(缺席)〕,这一种传输用不到的字段一次也不读;**快照一律只读自有属性**〔JSON 解析只铸自有键,JSON 面不变;`Object.prototype` 上被污染的 getter 不触发、注入的值〔如 `args`〕进不来;`assertionHeader` 只在 `principalHeader` 缺席时才读〕。**附带效果(照实写)**:继承来的传输字段一律当缺席 —— 原型**恰是一层带成员的 null 原型袋**的记录〔座位判据把它读成朴素,快照把继承字段当缺席;v14.5.0 顺原型链读得到〕:继承的必填 `kind` ⇒「缺 kind」拒(`transport.kind`),继承的可选 `args` / `env` / `principalHeader` ⇒ 静默不交;原型链再深一层、或由 `Object.prototype` 派生的〔`Object.create(普通对象)`〕座位整只拒(「不是朴素数据」);公开的 `readMcpIdentityHeaders` 对两者都答缺;「是否把继承来的身份头记进 `lost`」是另立的设计票;「检视不了」的律与句在读器一侧单源〔同一只 `inspect` 内核〕,三只描述器〔`describeValueShape` / `describeNotStringArray` / `describeNotStringRecord`〕共用一只安全检视内核,全包那一句只有「an object that could not be inspected (a proxy trap threw)」):`isStringArray`、请求面 `transport.args`、http hook 名单共用**同一只串数组读法**〔包内私有的 `captureStringArray`:按下标、自有下标、一次读、非串即坏、不走迭代协议,交副本〕——
|
|
262
|
+
稀疏数组〔洞〕、`length` 读不出安全非负整数、读元素时抛的数组不再算串数组;`args` 因此整键丢并进 `lost`,交出去的是**副本**〔此前是调用方数组的原引用〕,`Object.prototype` / `Array.prototype` 上的数字键不能再经洞把命令参数注入 `args`〔此前 `every` 跳过洞、又顺原型链认它〕。
|
|
263
|
+
`readRequestMcpTransport` 的 `lost` 不再用 `push` 累加、记录读器的累加也走定义语义:数字键 getter / setter 污染下诊断不被吞、不被读成污染值〔`lost` 是门据以响亮拒的读数,被吞就是静默放行〕;写副本用的描述符零原型〔`Object.prototype.get` / `set` 被污染时合法记录与名单照常交出,不抛、不读成空〕。
|
|
264
|
+
三问 —— **谁需要**:读 `transport.args` 去起子进程的消费方(拿到的是副本、不会被原型上的数字键注入参数,也不与调用方共享引用);**谁受伤**:把稀疏数组递给 `isStringArray` / `args`、且靠旧答案过关的调用方(JSON 里没有洞,按修 bug 记);**补偿**:递之前把洞补成串、用紧凑数组(没有洞的数组;`JSON.parse(JSON.stringify(["x", ,]))` 往返得 `["x", null]`,对 `isStringArray` / `args` 仍是坏形,不是补法)。
|
|
265
|
+
三问 —— **谁需要**:符合性语料的消费方(比较器不再对非 JSON 值假绿);任何从另一个 realm(`vm` 沙箱、iframe)递 JSON 对象进来的嵌入方(不再被误当类实例);**谁受伤**:把非 JSON 值(类实例 / `Map` / `Date`)直接递给上述比较器 / 读口、且靠旧答案过关的调用方
|
|
266
|
+
(服务端 `src/` 对 `readWorkersRows` / `describeNotStringRecord` 零读点,符合性腿递的是读腿产出的 JSON 值;其余消费方未逐一核,见补偿);**补偿**:先显式转成纯记录再递最稳(逐字段拷进字面量对象,或 `Object.fromEntries(map)`)。`JSON.parse(JSON.stringify(…))` / `structuredClone` 只在这些情形可用:本来就是纯数据的对象(如来自另一个 realm 的 JSON 对象)、只带自有数据字段的类实例(原型被丢掉);`Map` 经 JSON 往返静默变成空对象、经 `structuredClone` 原样保留(仍是坏形),`Date` 经 JSON 往返变字符串、经 `structuredClone` 原样保留;类实例原型上的字段与类 getter 会丢。
|
|
267
|
+
钉:`test/plain-json-object-single-seat-14.6.0.test.ts`(座位总〔不抛〕:`isPlainJsonObject` 对检视就抛的对象答 false、`jsonShapeOf` 对已撤销的 Proxy / `getPrototypeOf` 陷阱抛的 Proxy 都答 `uninspectable`、http 名单读器 fail-closed、调度记录判 false、有效配置载荷读口 / workers 行读口 / hooks 分类器同样拒而不抛 · 已撤销的 Proxy 〔含函数目标〕嵌在 hooks 文档与 workers 里不抛 / 整份拒 · 闭包座位 `hookIsArray` 与 `isJsonArray` 同文 · 分类器根上兜〔读成员抛 ⇒ 根上拒〕· 身份头只有一只读法〔principal 读不出 ⇒ 传输整只 `ok:false` 且 assertion 读数 0、公开读者答 `{}` 且读数 0〕· 被丢条目的身份不可被 `Object.prototype.value` 冒充 · 身份头 principal 在场但读不出不落到 assertion · 描述符只读自有键〔`Object.prototype.value` 污染 × 访问器:`none` 对象 / 数组、`extraBodyUnsafe` 对象 / 数组〕· `none` 不收非可枚举 / symbol 键(含 symbol 键上的访问器)· 非 JSON 表的种子 = 自 v14.5.0 以来运行期代码有改动的声明〔六个导出在表中;`CHANGED_SINCE_14_5_0` 与 git 重算同〕· `none` 按描述符视图比较、只读一次〔描述符 vs get 不一致 / ownKeys 翻转 / 数组 get 撒谎 / get 答 2 各一形 ⇒ 判词由描述符副本算出;每个成员描述符读 1 次、`[[Get]]` 0 次、键表问 1 次〕· 非 JSON 表含基线谓词调用方〔`isStringRecord` / `extraBodyUnsafe` / `ModelEntry` 在表中〕· 名单读器 `has` / `getOwnPropertyDescriptor` 陷阱抛 · 公开身份头读者自有键 · 函数 / `Map` 的**自有键**上带守卫迹象拒 · `tightenVerdict` 六规则 × 撤销 Proxy、`none` 走查读不出的成员、`extraBodyUnsafe` · 形状词比较门跟踪成员表达式目标〔`holder.shape = jsonShapeOf(…)` 后的禁用比较也抓;键用路径段表示,`holder["a.b"]` 与 `holder.a.b` 是两个键〕· 反向门:返回类型是 `x is Record<string, unknown>` 的函数恰两只〔唯一座位 + 闭包座位〕· 读原型的调用点恰两处函数 · 内联合取一处都不许有〔门自证:正反写法 / 括号 / 乱序的正控、缺子句 / 异标识符 / 成员参数 / 注释字符串的负控〕· 闭包座位与唯一座位逐字同文 · 门自证正控 / 负控;
|
|
268
|
+
座位的答案;`jsonShapeOf` 六词与已知的边 · 它与 `isPlainJsonObject` 同答;跨 realm 三格〔座位 · 比较器 `deepEqual` 与 `stableKey` · `readWorkersRows`〕;非 JSON 值上的行为钉〔含 `readHttpHookAllowlistLayer` 的 fail-closed 与 JSON 面对照、传输对象、调度店记录〕);
|
|
269
|
+
`test/http-hook-allowlists-13.0.0.test.ts`(托管层交来类实例 / 建在原型上的对象袋 / 函数 ⇒ 发往外部地址的 http hook 被拦下、放行的环境变量名一个都没有;朴素同形袋对照;已知的边:只靠继承可见的名单键 ⇒ `[]` + `malformed`;原型污染:只读 getter 污染不让读器抛、数据属性污染下缺席层〔读器造的、手造的一样〕被收紧;读器返回的层是普通对象;手造层把名单放在原型上〔类 getter / `Object.create(defaults)`〕外部地址被拦、变量只放名单里的;名单读到的不是串数组〔42 / 含非串条目 / getter 抛〕读成 `[]` 不抛;元素 getter 抛〔第二条抛 / 第一条抛 / 稀疏洞 / Proxy 读 `length` 抛四种构造〕读成 `[]` 不抛、副本只读一次元素〔对照:合法名单经元素访问器与普通数组同答〕;读器与判官一只读法〔同四种构造各一格:读器点名 `malformed` 并交出 `[]`,判官读读器的层与读手造的坏名单同答;读器对元素只读一次;名单键的取值抛 ⇒ 读器点名、判官两种层同答;翻转访问器〔第一次名单、之后 `undefined`〕名单键只读一次;元素读两次给两个答案〔第一次受限 pattern、第二次 `"*"`〕只认第一次;数字键访问器污染下名单副本与 `malformed` 是真数组、setter 一次不被调;稀疏名单的洞不顺原型链读〔`Object.prototype` 数据属性 / 访问器、`Array.prototype` 数据属性各一格:读器点名、判官同答、注入的放行模式拦下〕;`length` 不是安全非负整数〔字符串 / 负数 / 非整数 / 不安全整数 / `NaN` / `Infinity` 各一格〕坏形;巨大 `length` 的稀疏数组遇到第一个洞即退出〔计数 Proxy 一格、`length = 2 ** 32 - 1` 的真数组一格〕;描述符零原型〔`Object.prototype.get` / `set` 污染三格:合法名单照常交出〕〕;原型 setter 污染不吞读器的名单〔自有名单 / 坏形空名单 / 继承键臂都落在层上〕);`test/mcp-request-entry-14.0.0.test.ts` ⑩(传输对象的类实例 / Map / 函数 / 数组 / null / 原始值各一句 · env / headers 的函数 · 边对象收成 `{}` · 原型污染:只读 getter 污染、数字键 getter / setter 污染下 env / headers 普通串记录照常交出、访问器成员只读一次 · `args` 与名单同一只读法:稀疏 `args`〔无污染 / 三种数字键污染〕拒且点名、合法 `args` 交副本、带迭代器替换的 `[42]` 仍是坏形 · `lost` 在数字键 getter / setter 污染下不被吞 · 描述符零原型〔`get` / `set` 污染三格〕);`test/scheduler-store.test.ts`(类实例的记录 / `when` / 数组不是记录);单座门另有「`jsonShapeOf` 的结果只许存进变量或直接作 `switch` 判别式;变量(含一跳别名、括号操作数、成员 / 元素访问目标、解构别名)对非 `plain` 五词不许 `===` 链;判别式是形状的 `switch` 须六词穷举,或带 `default` 且里头有初值就是判别式本身的 `: never` 声明〔`as never` 不算〕」一格;`test/hooks-classify-13.0.0.test.ts` ⑤(闭包自含)不动、照绿。
|
|
270
|
+
- **请求面 MCP 传输 `url` 的两个答案写成读数(特征钉 + 文案,读器一个字没动)**。`readRequestMcpTransport` 的 `url` 只看 `http(s)://` 前缀,形判官 `httpUrlFlaws` 要能被 WHATWG 解析、是层级式 URL、scheme ∈ {http, https}。核读器的实际行为:`https://` 这样的裸 scheme 读器**收下**(`ok: true`),判官答 `not-http-url`。
|
|
271
|
+
所以让读器也拒它是一次**收紧** —— 此前能解析出传输的提交体(含重放存量提交体时读它的那一路)从此在读器处硬拒,发版纪律里是 major,本版不做。分歧也不只一个方向:读器要求 `//` 照字面写在 scheme 之后、不容前导空白,`https:host`、` https://host` 它拒而 WHATWG 修成合法 URL、判官收,
|
|
272
|
+
所以「读器直接换成判官」还会**放宽**一类,同样是 major。S-2106 收口的形(major 件)与 `PLUGIN_ORIGIN` 座位同构:读器收 ⟺ `startsWithHttpScheme(url)` ∧ `httpUrlFlaws(url, [座位禁的部件])` 为空 —— 只收紧,不放宽;读器的座位今天不禁 userinfo(拒句却写着凭据放 headers、别放 URL),禁不禁在同一次 major 里定。
|
|
273
|
+
本版把这件事写准:`./url-shape` 头注「第二个答案」从一句"空 host、带 userinfo 的 url …"(不准)改写成两张表 + 部件一格 + 收口形;`startsWithHttpScheme` 的 JSDoc 点明它收裸 scheme。三问 —— **谁需要**:读 `dist/url-shape.d.ts` 接线的消费方(别以为读器收的串判官也收);**谁受伤**:无(行为不变);**补偿**:要在发送前严判的壳 / 服务端,自己再过一次 `httpUrlFlaws(url, [])`。
|
|
274
|
+
钉:`test/request-url-two-answers-14.6.0.test.ts`(🚧 特征钉:读器收 ∧ 判官拒 11 串 · 判官收 ∧ 读器拒 5 串 · 同收 7 串 · 同拒 7 串 · 部件三格 · 读器逐字交回收下的 url;S-2106 收口时第一张表翻成同拒,第二张不变)。
|
|
275
|
+
|
|
11
276
|
## 14.5.0 — 2026-10-23 — minor:同步单口 `migrateConfigDocuments`(存量文档迁移 + 整域终判一次调用)· URL 内嵌凭据判官上移契约包(`findUrlCredential` 等)· 文件系统失败句的出路顺序(不可变标志先行)· `GLOBAL_SCOPE` 注释改真
|
|
12
277
|
|
|
13
278
|
> 档位(按发版纪律):**minor** —— 加性:新导出 `migrateConfigDocuments` + 型 `ConfigDocumentsMigration` / `MigrateConfigDocumentsOptions`(`./node`);新导出 `findUrlCredential` · `findUrlUserinfo` · `urlCredentialRule` · `isCredentialParamName` ·
|
package/README.md
CHANGED
|
@@ -312,6 +312,21 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
|
|
|
312
312
|
use `task`; the widened vocabulary is visible to consumers that hold a `Record<MinTrust, …>` / exhaustive
|
|
313
313
|
switch (compile error on upgrade — migrate to `trustRank`) or that validate external spec values with these
|
|
314
314
|
schemas (they now accept `task` and must rank it — again `trustRank`, never a five-word table of their own).
|
|
315
|
+
**14.6.0 — comparison and the settings units.** "Which of two values is stricter" has ONE implementation,
|
|
316
|
+
`tightenVerdict(rule, existing, candidate)` → `equal` / `stricter` / `looser` / `unordered` / `unreadable` (`TIGHTEN_VERDICTS`).
|
|
317
|
+
It reads order only: each `TightenRule` has a normalized domain (`enumRank` a word of `order`; `boolStricter` a boolean, never
|
|
318
|
+
truthiness; `setUnion` / `setNarrower` a string array compared as a set; `numericLower` a finite number or `+Infinity` = no limit;
|
|
319
|
+
`none` a strict JSON value), a side outside it — or absent — is `unreadable`, and `policyDenyWins` is a RangeError (a fold law
|
|
320
|
+
between members, not an order). `TightenRule` appends `setUnion` (a superset is stricter) and `numericLower` (smaller is stricter),
|
|
321
|
+
both parameter-free: spellings such as an empty-set sentinel or a "≤ 0 means off" cap are normalized by the value's owner first.
|
|
322
|
+
A category's `tighten` answers only a `whole` unit's value; a declared member's comparator comes from its row's value class —
|
|
323
|
+
`MemberAdmission.list: true` marks a string-list member, `boolPole` a boolean one, exactly one per row — via
|
|
324
|
+
`memberTighten(category, member)` (`list` ⇒ setUnion for tighten / setNarrower for loosen; neither or both ⇒ RangeError).
|
|
325
|
+
The settings-document unit table is published here too: `SETTINGS_UNITS` (the ten units `permissions` · `permissionMode` ·
|
|
326
|
+
`additionalDirectories` · `sandbox` · `disableBypassPermissionsMode` · `disableAutoMode` · `hooks` · `env` · `model` · `outputStyle`,
|
|
327
|
+
each with its document `path`, category, addressing and reading), `unitTighten(unit, member?)` (a unit's comparator) and
|
|
328
|
+
`readSettingsUnit(unit, raw, member?)` (its raw-value reading: `"manual"` ⇒ `"default"`, `"disable"` ⇒ `true`, `false` stays
|
|
329
|
+
`false`, `"bypassPermissions"` ⇒ `launch_only`, everything outside the unit's reading — and `undefined` / `null` — ⇒ `unreadable`).
|
|
315
330
|
- **Settings sources + layer admission** (`settings-sources.ts`, main barrel): `SETTINGS_SOURCES` (declaration order =
|
|
316
331
|
merge order: user < project < local < flag < policy), `SETTINGS_SOURCE_CONTRACT` (each source's trust tier and whether a
|
|
317
332
|
repository controls it — `projectSettings` / `localSettings` are repository-controllable and share the `project` tier, as in
|
package/conformance/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
14.
|
|
1
|
+
14.6.0
|
package/dist/atomic-write.js
CHANGED
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
* 悬空链接在读阶段与写阶段是**同一句**({@link danglingSymlinkSentence})。
|
|
22
22
|
*
|
|
23
23
|
* ## 写失败(12.0.0;句子归文件系统失败句的唯一铸点)
|
|
24
|
-
*
|
|
25
|
-
* 文件本身没错);`rename` 失败 ⇒ 「cannot replace <file> (<errno>): …」(点名真文件)
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* 三步各一句({@link FsFailure},句子在 `./fs-failure`):临时文件建不出 ⇒ 「cannot write into directory <dir> (<errno>): …」(点名目录 —— 目录不可写时
|
|
25
|
+
* 文件本身没错);沿用原模式的文件给临时文件设模式失败 ⇒ 「cannot set the mode of <file> (<errno>): …」(点名设模式这一步 —— 文件系统不让改模式,目录没有问题);`rename` 失败 ⇒ 「cannot replace <file> (<errno>): …」(点名真文件)。
|
|
26
|
+
* 认得的 errno 给对应出路,别的给 Node 自己的句子;`code` / `cause` 留着。
|
|
27
|
+
* 可写目录里的只读文件照常写得进(`rename` 换的是目录项)。权限类 errno 的出路要一个只有 Node 一侧读得到的事实 —— 失败那一刻目录的权限位对本进程是否可写({@link directoryAdmitsWrite},模式判在本文件、与模式表同处),
|
|
28
|
+
* 写器在建临时文件那一步读了经落点递给铸点(`EPERM` ∧ 可写 ⇒ 真因是不可变标志,首选出路是清标志;`EACCES` ∧ 可写 ⇒ 真因是 ACL / 安全模块;不可写 ⇒ 先让目录可写);`chmod` / `rename` 那两步不读 —— 临时文件刚在同目录建成,可写是已证明的事实,铸点自己给。
|
|
28
29
|
*/
|
|
29
30
|
import { randomBytes } from "node:crypto";
|
|
30
31
|
import { chmodSync, lstatSync, readlinkSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
@@ -91,8 +92,10 @@ export function writeFileAtomic(target, text, kind) {
|
|
|
91
92
|
let site = { landing: "dir-create-temp", path: tmp };
|
|
92
93
|
try {
|
|
93
94
|
writeFileSync(tmp, text, { encoding: "utf8", mode: kept ?? spec.newMode });
|
|
94
|
-
if (kept !== undefined)
|
|
95
|
+
if (kept !== undefined) {
|
|
96
|
+
site = { landing: "chmod", path: real }; // the temporary file now exists in the directory: a failure from here on is not the directory's; it names the step (the real file whose mode is being kept)
|
|
95
97
|
chmodSync(tmp, kept); // a kept mode is restored exactly (the creation mode above went through the umask)
|
|
98
|
+
}
|
|
96
99
|
site = { landing: "rename", path: real };
|
|
97
100
|
renameSync(tmp, real);
|
|
98
101
|
}
|
|
@@ -103,8 +106,8 @@ export function writeFileAtomic(target, text, kind) {
|
|
|
103
106
|
catch {
|
|
104
107
|
/* 临时文件没建成:没什么可清 */
|
|
105
108
|
}
|
|
106
|
-
// the sentence names the step that failed and its way out (`./fs-failure`, the one mint); the way-out
|
|
107
|
-
// read only at the create step: by the time `rename`
|
|
109
|
+
// the sentence names the step that failed and its way out (`./fs-failure`, the one mint); the way-out table needs one Node-side fact — do the directory's mode bits admit this process —
|
|
110
|
+
// read only at the create step: by the time `chmod` / `rename` run the temporary file has just been created in the same directory, so "writable" is proven there and the mint says so itself
|
|
108
111
|
throw new FsFailure(site.landing === "dir-create-temp" ? { ...site, directoryWritable: directoryAdmitsWrite(dir) } : site, e);
|
|
109
112
|
}
|
|
110
113
|
}
|
package/dist/bundle.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type PortableDomain } from "./
|
|
1
|
+
import { type PortableDomain } from "./portable-domains.js";
|
|
2
2
|
import type { ConfigReader } from "./reader.js";
|
|
3
3
|
/** A bundle carries PORTABLE domains only — the non-portable ones (`NON_PORTABLE_DOMAIN_REASONS`: workers / hosts / systems / projects /
|
|
4
4
|
* entitlement) never travel, and the three bundle functions are that table's other face (types narrow to {@link PortableDomain}; a
|
package/dist/bundle.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Node-only (writeBundle/readBundle touch the filesystem) → imported via `@sema-agent/settings-schema/node`.
|
|
10
10
|
*/
|
|
11
11
|
import { writeDomainFile } from "./file-edit.js";
|
|
12
|
-
import { PORTABLE_DOMAINS } from "./
|
|
12
|
+
import { PORTABLE_DOMAINS } from "./portable-domains.js";
|
|
13
13
|
import { configDPath, readConfigDFile, refuseRetiredDomainFiles } from "./config-d-file.js";
|
|
14
14
|
/** The runtime guard of the same rule (a name that is not even a domain is refused first — no path traversal). */
|
|
15
15
|
function assertPortable(d) {
|
|
@@ -31,6 +31,7 @@ import { z } from "zod";
|
|
|
31
31
|
import { formatConfigPath, parseDomain } from "./config-fns.js";
|
|
32
32
|
import { configDDocumentText, judgeConfigDText, judgeConfigDValue, parseRemoteExecDocument } from "./config-d-file.js";
|
|
33
33
|
import { fsFailureMessage } from "./fs-failure.js";
|
|
34
|
+
import { isPlainJsonObject } from "./plain-json-object.js";
|
|
34
35
|
import { migrateStoredDocuments } from "./one-time-migrations.js";
|
|
35
36
|
import { REMOTE_EXEC_DOCUMENT } from "./remote-exec.js";
|
|
36
37
|
import { RUNTIME_LIFT_TARGETS } from "./runtime-lift-targets.js";
|
|
@@ -150,7 +151,7 @@ function migrateCovered(caller, docs, covered) {
|
|
|
150
151
|
* 覆盖面外的键不读、不判、不进结果;覆盖面内在场(非 `undefined`)的文档必须是 JSON 对象,否则回 `unreadable`(不抛)。`docs` 不是对象 / 覆盖面拼错 ⇒ `TypeError`(调用方的错,不是数据的错)。
|
|
151
152
|
*/
|
|
152
153
|
export function migrateConfigDocuments(docs, opts) {
|
|
153
|
-
if (
|
|
154
|
+
if (!isPlainJsonObject(docs))
|
|
154
155
|
throw new TypeError("migrateConfigDocuments: `docs` must be a { document: value } object");
|
|
155
156
|
const covered = coveredDocuments("migrateConfigDocuments", opts.documents);
|
|
156
157
|
const present = {};
|
package/dist/config-fns.js
CHANGED
|
@@ -348,7 +348,7 @@ function readEntryIdentity(entry) {
|
|
|
348
348
|
return undefined;
|
|
349
349
|
const own = (k) => {
|
|
350
350
|
const d = Object.getOwnPropertyDescriptor(entry, k);
|
|
351
|
-
return d !== undefined && "value"
|
|
351
|
+
return d !== undefined && Object.hasOwn(d, "value") ? d.value : undefined; // 描述符本身也只读自有键
|
|
352
352
|
};
|
|
353
353
|
const name = own("name");
|
|
354
354
|
const id = own("id");
|
|
@@ -1098,7 +1098,7 @@ export function buildEffective(version, getRaw, updatedAt, onWarning) {
|
|
|
1098
1098
|
* and so does any key no domain names(the 0.6-era `teams` key included — not a domain since 0.7, and no engine reads it). Throws ONLY on garbage no caller can partially trust: a non-object payload, or a missing/
|
|
1099
1099
|
* non-finite `version` — that is not "a bad domain", it is not an effective config at all. */
|
|
1100
1100
|
export function readEffectiveWire(raw, onWarning) {
|
|
1101
|
-
if (
|
|
1101
|
+
if (!isPlainJsonObject(raw))
|
|
1102
1102
|
throw new Error("effective wire payload is not a JSON object");
|
|
1103
1103
|
const r = raw;
|
|
1104
1104
|
if (typeof r.version !== "number" || !Number.isFinite(r.version))
|
|
@@ -10,10 +10,7 @@
|
|
|
10
10
|
* `stableKey` 是唯一一处把值转成串的地方,它只用来给**多重集计数**当哈希键 —— 用的是 JSON 编码,
|
|
11
11
|
* 数组边界与段类型(`"1"` vs `1`)在编码里都还在,所以它不是那种有损的点号串。
|
|
12
12
|
*/
|
|
13
|
-
|
|
14
|
-
function isPlainObject(v) {
|
|
15
|
-
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
16
|
-
}
|
|
13
|
+
import { isPlainJsonObject } from "../plain-json-object.js";
|
|
17
14
|
/**
|
|
18
15
|
* 「这个对象**在场**的键」—— 本文件里唯一一处判键在不在 / 取键的值的地方。
|
|
19
16
|
*
|
|
@@ -42,7 +39,7 @@ function presentKeys(o) {
|
|
|
42
39
|
export function canonicalize(value) {
|
|
43
40
|
if (Array.isArray(value))
|
|
44
41
|
return value.map(canonicalize);
|
|
45
|
-
if (
|
|
42
|
+
if (isPlainJsonObject(value)) {
|
|
46
43
|
// `Object.fromEntries`,**不是**逐键 `out[k] = …`:后者对 `"__proto__"` 走的是原型 setter(改掉对象的
|
|
47
44
|
// 原型,既不产生自有键、还污染返回值),于是规范化会把一个叫 `__proto__` 的顶层键**吃掉** —— 两边
|
|
48
45
|
// 同样被吃掉 = 比对器对它全盲 = 一格假绿。与 S-332 在 wire 透传处修的是同一条「键就是键」的律。
|
|
@@ -60,7 +57,7 @@ export function deepEqual(a, b) {
|
|
|
60
57
|
return true;
|
|
61
58
|
if (Array.isArray(a) && Array.isArray(b))
|
|
62
59
|
return a.length === b.length && a.every((x, i) => deepEqual(x, b[i]));
|
|
63
|
-
if (
|
|
60
|
+
if (isPlainJsonObject(a) && isPlainJsonObject(b)) {
|
|
64
61
|
const ka = presentKeys(a);
|
|
65
62
|
const kb = presentKeys(b);
|
|
66
63
|
return ka.length === kb.length && ka.every((k, i) => k === kb[i]) && ka.every((k) => deepEqual(ownValue(a, k), ownValue(b, k)));
|
|
@@ -93,7 +90,7 @@ export function diffJson(expected, actual, at = []) {
|
|
|
93
90
|
}
|
|
94
91
|
return out;
|
|
95
92
|
}
|
|
96
|
-
if (
|
|
93
|
+
if (isPlainJsonObject(expected) && isPlainJsonObject(actual)) {
|
|
97
94
|
const out = [];
|
|
98
95
|
const keys = [...new Set([...presentKeys(expected), ...presentKeys(actual)])].sort();
|
|
99
96
|
for (const k of keys) {
|
|
@@ -20,14 +20,14 @@ import { dirname, join } from "node:path";
|
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
import { LEG_NAMES, REJECT_STAGES } from "./types.js";
|
|
22
22
|
import { LEG_EXPECTATION } from "./derive.js";
|
|
23
|
+
import { isPlainJsonObject } from "../plain-json-object.js";
|
|
23
24
|
/** 本包的根目录 —— `src/conformance/*.ts` 与 `dist/conformance/*.js` 距根同深,两处求值同一个地方。 */
|
|
24
25
|
export const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
25
26
|
/** 语料目录(默认 = 本包的 `conformance/`)。 */
|
|
26
27
|
export const DEFAULT_CORPUS_DIR = join(PACKAGE_ROOT, "conformance");
|
|
27
28
|
const readJson = async (p) => JSON.parse(await readFile(p, "utf8"));
|
|
28
|
-
const isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
29
29
|
function requireRecord(v, where) {
|
|
30
|
-
if (!
|
|
30
|
+
if (!isPlainJsonObject(v))
|
|
31
31
|
throw new Error(`conformance corpus: ${where} must be a JSON object`);
|
|
32
32
|
return v;
|
|
33
33
|
}
|
|
@@ -56,7 +56,7 @@ function checkLegExpectation(v, where) {
|
|
|
56
56
|
if (!REJECT_STAGES.includes(r.stage))
|
|
57
57
|
throw new Error(`conformance corpus: ${where}.stage must be one of ${REJECT_STAGES.join("|")}`);
|
|
58
58
|
if (r.refIssues !== undefined) {
|
|
59
|
-
const ok = Array.isArray(r.refIssues) && r.refIssues.every((x) =>
|
|
59
|
+
const ok = Array.isArray(r.refIssues) && r.refIssues.every((x) => isPlainJsonObject(x) && Array.isArray(x.path) && x.path.every((seg) => typeof seg === "string" || typeof seg === "number"));
|
|
60
60
|
if (!ok)
|
|
61
61
|
throw new Error(`conformance corpus: ${where}.refIssues must be an array of { path: (string|number)[] }`);
|
|
62
62
|
}
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
* 严写的结局不是读结局的一种表示变换,而是另一个判断(读腿抢救的地方严写拒)。把它写成「从 domains 期望派生」
|
|
35
35
|
* 就等于让 domains 金样的一处错同时改写两条腿的期望 —— 两条腿同错、门照绿。它的金样逐条显式入库、各自冻结。
|
|
36
36
|
*/
|
|
37
|
-
import { NON_DOMAIN_CONFIG_FILES
|
|
37
|
+
import { NON_DOMAIN_CONFIG_FILES } from "../file-store.js";
|
|
38
|
+
import { PORTABLE_DOMAINS } from "../portable-domains.js";
|
|
39
|
+
import { isPlainJsonObject } from "../plain-json-object.js";
|
|
38
40
|
import { skillContentHash } from "../skills-manifest.js";
|
|
39
41
|
import { DOMAINS, isDomainName } from "../types.js";
|
|
40
42
|
/** 变换表(本文件头注的机器形;`docs/CONFORMANCE.md` 与 `test/conformance-coverage.test.ts` 都读它)。 */
|
|
@@ -65,18 +67,17 @@ export const LEG_EXPECTATION = {
|
|
|
65
67
|
};
|
|
66
68
|
/** 非便携域 —— 由 {@link PORTABLE_DOMAINS} 派生,零手抄。 */
|
|
67
69
|
export const NON_PORTABLE_DOMAINS = DOMAINS.filter((d) => !PORTABLE_DOMAINS.includes(d));
|
|
68
|
-
const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
69
70
|
/** 一份 `domains` 文档集里,**不是域名**的那些顶层键(T4 的触发面)。 */
|
|
70
71
|
export const foreignKeysOf = (docs) => Object.keys(docs).filter((k) => !isDomainName(k));
|
|
71
72
|
/** T2 的**入参**半场:域形的 skills 文档 → wire 形的 manifest 文档。结构变换,不解析 —— 坏形原样传下去,
|
|
72
73
|
* 让 wire 腿自己的判据去裁(否则语料就没法喂一份坏 manifest 了)。 */
|
|
73
74
|
export function toManifestDoc(doc) {
|
|
74
|
-
if (!
|
|
75
|
+
if (!isPlainJsonObject(doc) || !Array.isArray(doc.skills))
|
|
75
76
|
return doc;
|
|
76
77
|
return {
|
|
77
78
|
...doc,
|
|
78
79
|
skills: doc.skills.map((entry) => {
|
|
79
|
-
if (!
|
|
80
|
+
if (!isPlainJsonObject(entry) || typeof entry.content !== "string")
|
|
80
81
|
return entry;
|
|
81
82
|
const { content, ...rest } = entry;
|
|
82
83
|
return { ...rest, contentHash: skillContentHash(content) };
|
|
@@ -131,7 +132,7 @@ export const EPOCH_ISO = new Date(0).toISOString();
|
|
|
131
132
|
* `conformance/baseline.json`),非便携域取它的那一格 —— 不现算,免得 schema 默认一改 golden 就跟着动。 */
|
|
132
133
|
export function deriveLocalExpectation(source, docs, baseline, localVersion) {
|
|
133
134
|
// T5:文件层先判 —— 任何一个**便携**域的文档不是 JSON 对象 ⇒ 整条腿在解析层被拒。
|
|
134
|
-
const badFile = PORTABLE_DOMAINS.find((d) => Object.prototype.hasOwnProperty.call(docs, d) && !
|
|
135
|
+
const badFile = PORTABLE_DOMAINS.find((d) => Object.prototype.hasOwnProperty.call(docs, d) && !isPlainJsonObject(docs[d]));
|
|
135
136
|
if (badFile !== undefined)
|
|
136
137
|
return { outcome: "rejected", stage: "parse" };
|
|
137
138
|
const effective = clone(source.effective);
|
package/dist/conformance/run.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { mkdtemp, mkdir, readFile, realpath, rm, stat, writeFile, utimes } from "node:fs/promises";
|
|
15
15
|
import { tmpdir } from "node:os";
|
|
16
16
|
import { basename, extname, isAbsolute, join, resolve, sep } from "node:path";
|
|
17
|
+
import { isPlainJsonObject } from "../plain-json-object.js";
|
|
17
18
|
import { canonicalize, deepEqual, diffJson, stableKey } from "./canonical.js";
|
|
18
19
|
import { diffWarningMultiset, findSecretLeaks, projectWarning } from "./warnings.js";
|
|
19
20
|
import { deriveLocalExpectation, deriveLocalInput, deriveWireExpectation, deriveWireInput, toManifestDoc } from "./derive.js";
|
|
@@ -133,7 +134,7 @@ function sameRefIssueBag(expected, actual) {
|
|
|
133
134
|
/** effective 里的元数据两键(独立断言,不进结构比对)。 */
|
|
134
135
|
const METADATA_KEYS = ["version", "updatedAt"];
|
|
135
136
|
function splitMetadata(effective) {
|
|
136
|
-
if (
|
|
137
|
+
if (!isPlainJsonObject(effective))
|
|
137
138
|
return { body: effective };
|
|
138
139
|
const src = effective;
|
|
139
140
|
// fromEntries(不是逐键赋值):`__proto__` 作自有键时逐键赋值会走原型 setter,把它从比对面上抹掉。
|