@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/dist/hooks-config.js
CHANGED
|
@@ -46,6 +46,7 @@
|
|
|
46
46
|
* contract; cycles are tolerated, a throwing accessor propagates.
|
|
47
47
|
*/
|
|
48
48
|
import { z } from "zod";
|
|
49
|
+
import { isPlainJsonObject } from "./plain-json-object.js";
|
|
49
50
|
import { buildQuotedIssue, fixed } from "./quoted-issue.js";
|
|
50
51
|
// ── event vocabulary ───────────────────────────────────────────────────────────────────────────────────────
|
|
51
52
|
// The two halves below are where each word came from (CC 2.1.198's own table, then the later additions) — provenance for the
|
|
@@ -141,8 +142,8 @@ export const HOOKS_CLASSIFICATION_OUTCOMES = ["loaded", "loaded-partial", "rejec
|
|
|
141
142
|
export const HOOKS_DROPPED_PIECE_CODES = ["hooks-not-object", "unknown-event", "event-not-array", "matcher-invalid", "entry-invalid"];
|
|
142
143
|
/**
|
|
143
144
|
* Why the document was REJECTED (closed; each is "a guard may be in this piece"):
|
|
144
|
-
* · `guard-outside-hooks` — a guard sign outside `hooks` (top level, under another key, or the document itself matcher-shaped);
|
|
145
|
-
* · `hooks-is-matcher` — `hooks` is an array holding objects / arrays, a single matcher, or a single entry;
|
|
145
|
+
* · `guard-outside-hooks` — a guard sign outside `hooks` (top level, under another key, or the document itself matcher-shaped), or the document could not be read at all (path `[]`);
|
|
146
|
+
* · `hooks-is-matcher` — `hooks` is an array holding objects / arrays, a single matcher, or a single entry, or an object that is not a readable block (a class instance, a value that cannot be inspected) holding — or possibly holding — a guard sign;
|
|
146
147
|
* · `guard-under-unknown-event` — an unknown event name holding a guard sign;
|
|
147
148
|
* · `guard-event-not-array` — a guard event whose value is not an array (and not null), or a known event whose non-array value holds a sign;
|
|
148
149
|
* · `guard-matcher-invalid` — a bad matcher in a guard event, or a matcher holding a sign (in any event);
|
|
@@ -156,12 +157,57 @@ export const HOOKS_REJECTION_CODES = [
|
|
|
156
157
|
"guard-matcher-invalid",
|
|
157
158
|
"guard-entry-invalid",
|
|
158
159
|
];
|
|
159
|
-
/**
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
160
|
+
/** The value's shape — six words: `plain` is a JSON object (not null, not an array, prototype null or a prototype whose own prototype is null: any realm's `Object.prototype`),
|
|
161
|
+
* `uninspectable` is a value whose shape cannot be read at all (a proxy that has been revoked, a throwing `getPrototypeOf` trap). This function IS `./plain-json-object`'s `jsonShapeOf`, character for
|
|
162
|
+
* character: the classifier is copied as source by consumers' generators, so its closure cannot import — the package has this one second copy, and the test is what keeps it from drifting. */
|
|
163
|
+
function hookShapeOf(v) {
|
|
164
|
+
try {
|
|
165
|
+
if (v === null)
|
|
166
|
+
return "null";
|
|
167
|
+
if (Array.isArray(v))
|
|
168
|
+
return "array";
|
|
169
|
+
if (typeof v === "function")
|
|
170
|
+
return "non_plain_object";
|
|
171
|
+
if (typeof v !== "object")
|
|
172
|
+
return "primitive";
|
|
173
|
+
const proto = Object.getPrototypeOf(v);
|
|
174
|
+
return proto === null || Object.getPrototypeOf(proto) === null ? "plain" : "non_plain_object";
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return "uninspectable"; // 读形状就抛(已撤销的 Proxy 连 `Array.isArray` 都抛、`getPrototypeOf` 陷阱抛):不是「不朴素」,是读不出 —— 读者按最严处置
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/** `./plain-json-object` 的 `isPlainJsonObject`,调各自的形状分类核后逐字同文(闭包里不能 import)。 */
|
|
181
|
+
function hookIsRecord(v) {
|
|
182
|
+
const shape = hookShapeOf(v);
|
|
183
|
+
return shape === "plain";
|
|
184
|
+
}
|
|
185
|
+
/** Is the value object-like — anything but null / a primitive (arrays, functions and values whose shape cannot be read count). The shape is asked first, never a `typeof` split. */
|
|
186
|
+
function hookIsObjectLike(v) {
|
|
187
|
+
const shape = hookShapeOf(v);
|
|
188
|
+
switch (shape) {
|
|
189
|
+
case "null":
|
|
190
|
+
case "primitive":
|
|
191
|
+
return false;
|
|
192
|
+
case "plain":
|
|
193
|
+
case "non_plain_object":
|
|
194
|
+
case "array":
|
|
195
|
+
case "uninspectable":
|
|
196
|
+
return true;
|
|
197
|
+
default: {
|
|
198
|
+
const unhandled = shape;
|
|
199
|
+
throw new Error(`hookIsObjectLike: unhandled shape ${String(unhandled)}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/** `./plain-json-object` 的 `isJsonArray`,逐字同文(闭包里不能 import):`Array.isArray` 的总版 —— 已撤销的 Proxy 读不出 ⇒ 不是数组,不抛。 */
|
|
204
|
+
function hookIsArray(v) {
|
|
205
|
+
try {
|
|
206
|
+
return Array.isArray(v);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return false; // 已撤销的 Proxy:读不出 ⇒ 不是数组
|
|
210
|
+
}
|
|
165
211
|
}
|
|
166
212
|
/** The first own key of `node` (in key order) that names a guard event with a value other than null / undefined / `[]` (CC `ei`). */
|
|
167
213
|
function hookGuardKeyOf(node) {
|
|
@@ -169,7 +215,7 @@ function hookGuardKeyOf(node) {
|
|
|
169
215
|
if (!HOOK_GUARD_EVENTS.includes(key))
|
|
170
216
|
continue;
|
|
171
217
|
const value = node[key];
|
|
172
|
-
if (value !== null && value !== undefined && !(
|
|
218
|
+
if (value !== null && value !== undefined && !(hookIsArray(value) && value.length === 0))
|
|
173
219
|
return key;
|
|
174
220
|
}
|
|
175
221
|
return undefined;
|
|
@@ -179,7 +225,7 @@ function hookLooksLikeMatcher(value) {
|
|
|
179
225
|
if (!hookIsRecord(value))
|
|
180
226
|
return false;
|
|
181
227
|
const inner = Object.hasOwn(value, "hooks") ? value.hooks : undefined;
|
|
182
|
-
if (
|
|
228
|
+
if (hookIsArray(inner))
|
|
183
229
|
return inner.length > 0;
|
|
184
230
|
return hookIsRecord(inner) && (Object.hasOwn(value, "matcher") || typeof inner.type === "string");
|
|
185
231
|
}
|
|
@@ -203,25 +249,48 @@ function hookGuardSignIn(root, rootPath, place) {
|
|
|
203
249
|
};
|
|
204
250
|
while (pending.length > 0) {
|
|
205
251
|
const [node, frame, at] = pending.pop();
|
|
206
|
-
|
|
252
|
+
// the shape is asked BEFORE any `typeof` split: a node whose shape cannot be read — a proxy that has been revoked, of a function target included — cannot be shown free of guard signs
|
|
253
|
+
// ⇒ reported as a sign at its own coordinate (fail-closed: the document is rejected); no `typeof` gate of the classifier's own stands in front of the seat
|
|
254
|
+
const shape = hookShapeOf(node);
|
|
255
|
+
let isList = false;
|
|
256
|
+
switch (shape) {
|
|
257
|
+
case "null":
|
|
258
|
+
case "primitive":
|
|
259
|
+
continue; // not an object: nothing to search
|
|
260
|
+
case "uninspectable":
|
|
261
|
+
return pathTo(frame, []);
|
|
262
|
+
case "array":
|
|
263
|
+
isList = true;
|
|
264
|
+
break;
|
|
265
|
+
case "plain":
|
|
266
|
+
case "non_plain_object":
|
|
267
|
+
break; // searched as an object below
|
|
268
|
+
default: {
|
|
269
|
+
const unhandled = shape;
|
|
270
|
+
throw new Error(`hookGuardSignIn: unhandled shape ${String(unhandled)}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const visited = node;
|
|
274
|
+
if (seen[at].has(visited))
|
|
207
275
|
continue;
|
|
208
|
-
seen[at].add(
|
|
209
|
-
if (
|
|
210
|
-
|
|
276
|
+
seen[at].add(visited);
|
|
277
|
+
if (isList) {
|
|
278
|
+
const list = node;
|
|
279
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
211
280
|
frames.push({ key: i, parent: frame });
|
|
212
|
-
pending.push([
|
|
281
|
+
pending.push([list[i], frames.length - 1, at === "matcherList" ? "matcher" : "other"]);
|
|
213
282
|
}
|
|
214
283
|
continue;
|
|
215
284
|
}
|
|
216
|
-
const guardKey = hookGuardKeyOf(
|
|
285
|
+
const guardKey = hookGuardKeyOf(visited);
|
|
217
286
|
if (guardKey !== undefined)
|
|
218
287
|
return pathTo(frame, [guardKey]);
|
|
219
|
-
if (at !== "matcher" && hookLooksLikeMatcher(
|
|
288
|
+
if (at !== "matcher" && hookLooksLikeMatcher(visited))
|
|
220
289
|
return pathTo(frame, []);
|
|
221
|
-
const keys = Object.keys(
|
|
290
|
+
const keys = Object.keys(visited);
|
|
222
291
|
for (let i = keys.length - 1; i >= 0; i--) {
|
|
223
292
|
frames.push({ key: keys[i], parent: frame });
|
|
224
|
-
pending.push([
|
|
293
|
+
pending.push([visited[keys[i]], frames.length - 1, "other"]);
|
|
225
294
|
}
|
|
226
295
|
}
|
|
227
296
|
return undefined;
|
|
@@ -248,7 +317,7 @@ function hookFieldFits(base, value) {
|
|
|
248
317
|
case "stringRecord":
|
|
249
318
|
return hookIsRecord(value) && Object.values(value).every((v) => typeof v === "string");
|
|
250
319
|
case "stringList":
|
|
251
|
-
if (!
|
|
320
|
+
if (!hookIsArray(value))
|
|
252
321
|
return false;
|
|
253
322
|
for (let i = 0; i < value.length; i++)
|
|
254
323
|
if (typeof value[i] !== "string")
|
|
@@ -283,7 +352,7 @@ function hookEntryRead(entry) {
|
|
|
283
352
|
const base = (optional ? kind.slice(0, -1) : kind);
|
|
284
353
|
if (!hookFieldFits(base, value))
|
|
285
354
|
return { field };
|
|
286
|
-
out[field] =
|
|
355
|
+
out[field] = hookIsArray(value) ? value.slice() : hookIsRecord(value) ? Object.fromEntries(Object.entries(value)) : value;
|
|
287
356
|
}
|
|
288
357
|
return { entry: out };
|
|
289
358
|
}
|
|
@@ -301,18 +370,18 @@ function hookMatcherRead(matcher, at, guardEvent, reject, drop) {
|
|
|
301
370
|
// the rejection's `field` is the matcher's own guard-event key if it has one, else the first member (document order) whose subtree holds a sign —
|
|
302
371
|
// the order hookGuardSignIn reports
|
|
303
372
|
const signField = hookGuardKeyOf(matcher) ??
|
|
304
|
-
Object.keys(matcher).find((key) => !(key === "hooks" &&
|
|
373
|
+
Object.keys(matcher).find((key) => !(key === "hooks" && hookIsArray(entries)) && hookGuardSignIn(matcher[key], [...at, key], "other") !== undefined);
|
|
305
374
|
if (signField !== undefined) {
|
|
306
375
|
reject("guard-matcher-invalid", at, signField);
|
|
307
376
|
return undefined;
|
|
308
377
|
}
|
|
309
378
|
const entrySigns = [];
|
|
310
|
-
if (
|
|
379
|
+
if (hookIsArray(entries))
|
|
311
380
|
for (let j = 0; j < entries.length; j++)
|
|
312
381
|
entrySigns.push(hookGuardSignIn(entries[j], [...at, "hooks", j], "other"));
|
|
313
382
|
const name = Object.hasOwn(matcher, "matcher") ? matcher.matcher : undefined;
|
|
314
|
-
const badField = name !== undefined && typeof name !== "string" ? "matcher" : !
|
|
315
|
-
if (badField !== undefined || !
|
|
383
|
+
const badField = name !== undefined && typeof name !== "string" ? "matcher" : !hookIsArray(entries) ? "hooks" : undefined;
|
|
384
|
+
if (badField !== undefined || !hookIsArray(entries)) {
|
|
316
385
|
if (guardEvent || entrySigns.some((sign) => sign !== undefined))
|
|
317
386
|
reject("guard-matcher-invalid", at, badField);
|
|
318
387
|
else
|
|
@@ -340,7 +409,10 @@ function hookMatcherRead(matcher, at, guardEvent, reject, drop) {
|
|
|
340
409
|
/** The `hooks` value → what loads of it (undefined when nothing does), reporting its bad pieces / signs into the two sinks. */
|
|
341
410
|
function hookBlockRead(hooks, reject, drop) {
|
|
342
411
|
if (!hookIsRecord(hooks)) {
|
|
343
|
-
if (
|
|
412
|
+
if (hookIsArray(hooks) && hooks.some(hookIsObjectLike))
|
|
413
|
+
reject("hooks-is-matcher", ["hooks"]);
|
|
414
|
+
// an object that is not a readable block (a class instance, a proxy that cannot be inspected) holding — or possibly holding — a guard sign: reject, never drop it (a dropped guard silently disappears)
|
|
415
|
+
else if (hookGuardSignIn(hooks, ["hooks"], "other") !== undefined)
|
|
344
416
|
reject("hooks-is-matcher", ["hooks"]);
|
|
345
417
|
else
|
|
346
418
|
drop("hooks-not-object", ["hooks"]);
|
|
@@ -355,14 +427,14 @@ function hookBlockRead(hooks, reject, drop) {
|
|
|
355
427
|
const value = hooks[event];
|
|
356
428
|
const at = ["hooks", event];
|
|
357
429
|
if (!HOOK_EVENTS.includes(event)) {
|
|
358
|
-
if (hookGuardSignIn(value, at,
|
|
430
|
+
if (hookGuardSignIn(value, at, hookIsArray(value) ? "matcherList" : "other") !== undefined)
|
|
359
431
|
reject("guard-under-unknown-event", at);
|
|
360
432
|
else
|
|
361
433
|
drop("unknown-event", at);
|
|
362
434
|
continue;
|
|
363
435
|
}
|
|
364
436
|
const guardEvent = HOOK_GUARD_EVENTS.includes(event);
|
|
365
|
-
if (!
|
|
437
|
+
if (!hookIsArray(value)) {
|
|
366
438
|
if ((guardEvent && value !== null) || hookGuardSignIn(value, at, "other") !== undefined)
|
|
367
439
|
reject("guard-event-not-array", at);
|
|
368
440
|
else
|
|
@@ -392,6 +464,15 @@ function hookBlockRead(hooks, reject, drop) {
|
|
|
392
464
|
* the hooks-classify test file (group ⑤) pins that the free names are exactly these.
|
|
393
465
|
*/
|
|
394
466
|
export function classifyHooksConfig(settingsDoc) {
|
|
467
|
+
try {
|
|
468
|
+
return hookClassify(settingsDoc);
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
// reading the document threw (a trap, an accessor that throws): nothing in it can be shown free of guard signs ⇒ rejected at the root — ONE `try` for the whole classifier, none per node
|
|
472
|
+
return { outcome: "rejected", hooks: undefined, dropped: [], rejections: [{ code: "guard-outside-hooks", path: [] }] };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function hookClassify(settingsDoc) {
|
|
395
476
|
const rejections = [];
|
|
396
477
|
const dropped = [];
|
|
397
478
|
const reject = (code, path, field) => {
|
|
@@ -401,7 +482,7 @@ export function classifyHooksConfig(settingsDoc) {
|
|
|
401
482
|
dropped.push(field === undefined ? { code, path } : { code, path, field });
|
|
402
483
|
};
|
|
403
484
|
let hooks;
|
|
404
|
-
if (
|
|
485
|
+
if (hookIsRecord(settingsDoc)) {
|
|
405
486
|
const doc = settingsDoc;
|
|
406
487
|
for (const key of Object.keys(doc)) {
|
|
407
488
|
const value = doc[key];
|
|
@@ -412,7 +493,7 @@ export function classifyHooksConfig(settingsDoc) {
|
|
|
412
493
|
// the document itself written as a matcher: its top-level `matcher` key is the evidence, reported at that key's place
|
|
413
494
|
const sign = key === "matcher" && hookLooksLikeMatcher(doc)
|
|
414
495
|
? [key]
|
|
415
|
-
: HOOK_GUARD_EVENTS.includes(key) && value !== null && value !== undefined && !(
|
|
496
|
+
: HOOK_GUARD_EVENTS.includes(key) && value !== null && value !== undefined && !(hookIsArray(value) && value.length === 0)
|
|
416
497
|
? [key]
|
|
417
498
|
: hookGuardSignIn(value, [key], "other");
|
|
418
499
|
if (sign !== undefined)
|
|
@@ -450,7 +531,7 @@ function hooksFace(carry, strip, take) {
|
|
|
450
531
|
}
|
|
451
532
|
/** The carrier for one entry of type `type`: the value itself when it says that type, else an untyped object (⇒ the issue names `type`). */
|
|
452
533
|
function entryOfType(value, type) {
|
|
453
|
-
return value
|
|
534
|
+
return isPlainJsonObject(value) && value.type === type ? value : {}; // 读不出 / 不是纯数据的值不当「自己说了这个 type」的载体:⇒ `{}`(issue 点名 `type`)
|
|
454
535
|
}
|
|
455
536
|
const entryFace = (type) => hooksFace((v) => ({ hooks: { Stop: [{ hooks: [entryOfType(v, type)] }] } }), 5, (h) => h.Stop[0].hooks[0]);
|
|
456
537
|
export const CommandHookEntry = entryFace("command");
|
|
@@ -43,11 +43,15 @@ export interface HttpHookAllowlistLayerRead {
|
|
|
43
43
|
readonly malformed: readonly HttpHookAllowlistKey[];
|
|
44
44
|
}
|
|
45
45
|
/**
|
|
46
|
-
* 从一层的原始设置对象读出两键(唯一读法)。键缺席(或值为 `undefined`)⇒ 不设;字符串数组 ⇒
|
|
47
|
-
* ⇒ **读成 `[]`**(CC 同向:坏形名单 = 一个都不许)并在 `malformed` 点名。整份设置不是对象 ⇒ 两键都缺。
|
|
46
|
+
* 从一层的原始设置对象读出两键(唯一读法)。键缺席(或值为 `undefined`)⇒ 不设;字符串数组 ⇒ 原样(过 `captureAllowlist`〔包内私有〕,与判官同一只读法);其余一切(非数组、含非串条目或洞、取值或读元素时抛、`null`)
|
|
47
|
+
* ⇒ **读成 `[]`**(CC 同向:坏形名单 = 一个都不许)并在 `malformed` 点名。整份设置不是对象(`null` / 原始值 / 数组)⇒ 两键都缺。
|
|
48
|
+
* **是对象但不朴素**(类实例 / `Map` / 建在别的对象原型上的记录 / 函数 —— JSON 解析不出来,只能是嵌入方程序拼的)⇒ 同样 fail-closed:两键都读成 `[]` 并点名 `malformed`。
|
|
49
|
+
* 把它当「没有设置」读,等于把一层带名单的托管设置静默丢掉、方向放行(与本模块「坏形 = 一个都不许」的失败方向相反)。
|
|
50
|
+
* 名单键**只靠继承可见**(不是自有键)⇒ 同样 fail-closed:该键读成 `[]` 并点名 `malformed`(自有键盖过继承键;自有键值为 `undefined` 仍是「不设」)。
|
|
51
|
+
* 原型是带成员的 null 原型对象的对象(`Object.create(<null 原型的名单袋>)`)是一例:它按座位的判据读成朴素、按**自有**键读,名单键只在原型上 ⇒ 坏形。它只能由程序造出,JSON 解析不出来(座位头注 `./plain-json-object` 同写)。
|
|
48
52
|
*/
|
|
49
53
|
export declare function readHttpHookAllowlistLayer(source: SettingsSource, settings: unknown): HttpHookAllowlistLayerRead;
|
|
50
|
-
/** 拦下一只 http hook 的原因。`rule`:`empty` = 有一张准入名单是空的(
|
|
54
|
+
/** 拦下一只 http hook 的原因。`rule`:`empty` = 有一张准入名单是空的(那一层改对之前一只都不许;**名单形坏 —— 非串数组、含非串条目或洞、读它或读它的元素时 getter 抛 —— 也读成空**,所以 `empty` 不一定是写成了 `[]`);`url` = 没命中那几张名单里的任何模式。
|
|
51
55
|
* `sources` = 不放行它的那几张名单所在的层(入参序)。零 URL 回显 —— URL 调用方自己有,按受众给显示形。 */
|
|
52
56
|
export interface HttpHookUrlBlock {
|
|
53
57
|
readonly list: "allowedHttpHookUrls";
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* 判官出结构不出句:放行 ⇒ `undefined`;拦下 ⇒ {@link HttpHookUrlBlock}(哪张名单 / 为什么 / 出自哪几层),用户面那一句由各宿主按它铸。
|
|
16
16
|
*/
|
|
17
17
|
import { z } from "zod";
|
|
18
|
+
import { captureStringArray } from "./definition-copy.js";
|
|
19
|
+
import { jsonShapeOf } from "./plain-json-object.js";
|
|
18
20
|
import { layerAdmission } from "./settings-sources.js";
|
|
19
21
|
import { urlAllowPatternMatches } from "./url-pattern.js";
|
|
20
22
|
/** 两键(闭集,声明序)。 */
|
|
@@ -30,26 +32,107 @@ export const HttpHookAllowlistSettings = z
|
|
|
30
32
|
})
|
|
31
33
|
.passthrough();
|
|
32
34
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
+
* 一张名单的**唯一读法**(读器与判官共用,两边永远同答):**取值 + 复制 + 检视整段在一个 try 里**,名单键只读一次;名单本身走包内的串数组捕获读法
|
|
36
|
+
* `captureStringArray`(`./definition-copy`:按下标 / `hasOwn` / 一次读 / 非串即坏 / 定义语义写入,不走迭代协议,交副本)—— 与请求面 `args` 同一只读法。
|
|
37
|
+
* 键没写(取到 `undefined`)⇒ `undefined`(不设);是串数组 ⇒ 一份已校验的捕获副本;其余一切 ⇒ 读成 `[]` 并标 `malformed`(fail-closed:一个都不许)——
|
|
38
|
+
* 取值抛(键本身是会抛的 getter)、不是串数组(含稀疏数组的洞 —— 不顺原型链读;`length` 不是安全非负整数;读元素时抛),一个也不抛出去。
|
|
39
|
+
*/
|
|
40
|
+
function captureAllowlist(holder, key) {
|
|
41
|
+
try {
|
|
42
|
+
const v = holder[key];
|
|
43
|
+
if (v === undefined)
|
|
44
|
+
return undefined;
|
|
45
|
+
const list = captureStringArray(v); // 串数组的唯一读法(`./definition-copy`):按下标 / hasOwn / 一次读 / 非串即坏 / 定义语义写入,交副本
|
|
46
|
+
return list === undefined ? { list: [], malformed: true } : { list, malformed: false };
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return { list: [], malformed: true };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function presenceOf(bag, key) {
|
|
53
|
+
try {
|
|
54
|
+
return Object.hasOwn(bag, key) ? "own" : key in bag ? "inherited" : "absent";
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return "unreadable";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 从一层的原始设置对象读出两键(唯一读法)。键缺席(或值为 `undefined`)⇒ 不设;字符串数组 ⇒ 原样(过 `captureAllowlist`〔包内私有〕,与判官同一只读法);其余一切(非数组、含非串条目或洞、取值或读元素时抛、`null`)
|
|
62
|
+
* ⇒ **读成 `[]`**(CC 同向:坏形名单 = 一个都不许)并在 `malformed` 点名。整份设置不是对象(`null` / 原始值 / 数组)⇒ 两键都缺。
|
|
63
|
+
* **是对象但不朴素**(类实例 / `Map` / 建在别的对象原型上的记录 / 函数 —— JSON 解析不出来,只能是嵌入方程序拼的)⇒ 同样 fail-closed:两键都读成 `[]` 并点名 `malformed`。
|
|
64
|
+
* 把它当「没有设置」读,等于把一层带名单的托管设置静默丢掉、方向放行(与本模块「坏形 = 一个都不许」的失败方向相反)。
|
|
65
|
+
* 名单键**只靠继承可见**(不是自有键)⇒ 同样 fail-closed:该键读成 `[]` 并点名 `malformed`(自有键盖过继承键;自有键值为 `undefined` 仍是「不设」)。
|
|
66
|
+
* 原型是带成员的 null 原型对象的对象(`Object.create(<null 原型的名单袋>)`)是一例:它按座位的判据读成朴素、按**自有**键读,名单键只在原型上 ⇒ 坏形。它只能由程序造出,JSON 解析不出来(座位头注 `./plain-json-object` 同写)。
|
|
35
67
|
*/
|
|
36
68
|
export function readHttpHookAllowlistLayer(source, settings) {
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
69
|
+
const shape = jsonShapeOf(settings);
|
|
70
|
+
let bag = Object.create(null); // 空袋没有原型:不存在 / 非对象的层看不见 `Object.prototype` 上的污染键,仍「两键都缺」
|
|
71
|
+
let refuseBoth = false; // 是对象但不朴素 ⇒ 两键都读成 [](fail-closed)
|
|
72
|
+
switch (shape) {
|
|
73
|
+
case "plain":
|
|
74
|
+
bag = settings;
|
|
75
|
+
break;
|
|
76
|
+
case "non_plain_object":
|
|
77
|
+
case "uninspectable": // 读形状就抛的对象(已撤销的 Proxy)同样 fail-closed:读不出就不能当「没有设置」放行
|
|
78
|
+
refuseBoth = true;
|
|
79
|
+
break;
|
|
80
|
+
case "array":
|
|
81
|
+
case "null":
|
|
82
|
+
case "primitive":
|
|
83
|
+
break; // 整份设置不是对象 ⇒ 两键都缺
|
|
84
|
+
default: {
|
|
85
|
+
const unhandled = shape;
|
|
86
|
+
throw new Error(`readHttpHookAllowlistLayer: unhandled shape ${String(unhandled)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const bad = Object.create(null); // 读成 [] 的坏形键(零原型的标记袋;`malformed` 数组出口只用定义语义造,见下)
|
|
90
|
+
// 累加器也零原型:`{}` 继承 `Object.prototype`,原型上被污染成只读(只有 getter)的同名键会让下面的赋值抛(读器该 fail-closed,不是抛)
|
|
91
|
+
const lists = Object.create(null);
|
|
40
92
|
for (const key of HTTP_HOOK_ALLOWLIST_KEYS) {
|
|
41
|
-
if (
|
|
42
|
-
continue;
|
|
43
|
-
const v = bag[key];
|
|
44
|
-
if (Array.isArray(v) && v.every((x) => typeof x === "string"))
|
|
45
|
-
lists[key] = [...v];
|
|
46
|
-
else {
|
|
93
|
+
if (refuseBoth) {
|
|
47
94
|
lists[key] = [];
|
|
48
|
-
|
|
95
|
+
bad[key] = true;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// 对袋的每一次触碰(`in` / `hasOwn` / 取值)都经守护:先问这个键在袋里怎么在场(一只守护的探针),再取值(`captureAllowlist`,自带守护)
|
|
99
|
+
const presence = presenceOf(bag, key);
|
|
100
|
+
switch (presence) {
|
|
101
|
+
case "absent":
|
|
102
|
+
continue;
|
|
103
|
+
case "inherited": // 名单键只靠继承可见 ⇒ 不能当「没有名单」放行
|
|
104
|
+
case "unreadable": // 袋读不出(`has` / `getOwnPropertyDescriptor` 陷阱抛)⇒ 同样 fail-closed
|
|
105
|
+
lists[key] = [];
|
|
106
|
+
bad[key] = true;
|
|
107
|
+
continue;
|
|
108
|
+
case "own": {
|
|
109
|
+
const read = captureAllowlist(bag, key); // 取值也在 try 里、只读一次:键是会抛的自有 getter ⇒ 坏形,不是抛出读器
|
|
110
|
+
if (read === undefined)
|
|
111
|
+
continue;
|
|
112
|
+
lists[key] = read.list;
|
|
113
|
+
if (read.malformed)
|
|
114
|
+
bad[key] = true;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
default: {
|
|
118
|
+
const unhandled = presence;
|
|
119
|
+
throw new Error(`readHttpHookAllowlistLayer: unhandled presence ${String(unhandled)}`);
|
|
120
|
+
}
|
|
49
121
|
}
|
|
50
122
|
}
|
|
123
|
+
// `malformed` 用 filter 造(定义语义)—— `push` 会命中 `Object.prototype` 上继承的数字键 setter、吞掉点名;层是普通对象(返回值的运行期形状与 v14.5.0 同),`...lists` 是定义语义拷贝,污染下不抛
|
|
124
|
+
const malformed = HTTP_HOOK_ALLOWLIST_KEYS.filter((key) => bad[key] === true);
|
|
51
125
|
return { layer: { source, ...lists }, malformed };
|
|
52
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* 一层的某张名单:按 `[[Get]]` 读(名单放在原型上的手造层 —— 类 getter、`Object.create(defaults)` —— 照旧生效,与 v14.5.0 同答),
|
|
129
|
+
* 过 `captureAllowlist`〔包内私有〕—— 与读器**同一只读法**,产出一份已校验的捕获副本,判官只读这份副本(元素只读一次)。
|
|
130
|
+
* 读到 `undefined` ⇒ 没写(`undefined`);取值时 getter 抛、或读到的不是串数组(污染出非数组、含非串条目或洞、读 `length` / 元素的 getter 或 Proxy 陷阱抛)⇒ 读成 `[]`(fail-closed:一个都不许),不抛。
|
|
131
|
+
* 原型污染出来的名单只会让准入名单取交后更窄 —— 方向是拦,不是放;读器造的缺席层也是普通对象,污染下同样被收紧(fail-closed,判决的 `sources` 点名该层)。
|
|
132
|
+
*/
|
|
133
|
+
function listOf(layer, key) {
|
|
134
|
+
return captureAllowlist(layer, key)?.list;
|
|
135
|
+
}
|
|
53
136
|
/** 进判决的那几层(入参序):`layerAdmission` 答 `admitted` 的层。每一层都问 —— 闭集外的源 / 工作区读数在这里响亮抛,哪怕那一层没写名单。 */
|
|
54
137
|
function admittedLayers(layers, workspace) {
|
|
55
138
|
return layers.filter((l) => layerAdmission(l.source, workspace, "httpHookAllowlists") === "admitted");
|
|
@@ -59,15 +142,18 @@ function admittedLayers(layers, workspace) {
|
|
|
59
142
|
* 没有准入层写这张名单 ⇒ 不限(`undefined`)。`workspace` = 宿主对工作区信任的读数(读口抛或闭集外按 `declined` 交),与 `layerAdmission` 同一个入参。
|
|
60
143
|
*/
|
|
61
144
|
export function httpHookUrlVerdict(url, layers, workspace) {
|
|
62
|
-
const refusing = admittedLayers(layers, workspace).
|
|
145
|
+
const refusing = admittedLayers(layers, workspace).flatMap((l) => {
|
|
146
|
+
const list = listOf(l, "allowedHttpHookUrls");
|
|
147
|
+
return list !== undefined && !list.some((p) => urlAllowPatternMatches(url, p)) ? [{ source: l.source, empty: list.length === 0 }] : [];
|
|
148
|
+
});
|
|
63
149
|
if (refusing.length === 0)
|
|
64
150
|
return undefined;
|
|
65
|
-
return { list: "allowedHttpHookUrls", rule: refusing.some((
|
|
151
|
+
return { list: "allowedHttpHookUrls", rule: refusing.some((r) => r.empty) ? "empty" : "url", sources: refusing.map((r) => r.source) };
|
|
66
152
|
}
|
|
67
153
|
/** 一只 http hook 实际可插值进请求头的环境变量:它自己的 `allowedEnvVars` 与每一张准入的 `httpHookAllowedEnvVars` 取交(保持 hook 自己的序)。 */
|
|
68
154
|
export function httpHookEnvVarsAllowed(hookVars, layers, workspace) {
|
|
69
155
|
const lists = admittedLayers(layers, workspace)
|
|
70
|
-
.
|
|
71
|
-
.
|
|
156
|
+
.map((l) => listOf(l, "httpHookAllowedEnvVars"))
|
|
157
|
+
.filter((list) => list !== undefined);
|
|
72
158
|
return hookVars.filter((v) => lists.every((list) => list.includes(v)));
|
|
73
159
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export * from "./reader.js";
|
|
|
18
18
|
export * from "./cross-domain.js";
|
|
19
19
|
export * from "./remote-exec.js";
|
|
20
20
|
export * from "./safety-merge-spec.js";
|
|
21
|
+
export * from "./settings-units.js";
|
|
21
22
|
export * from "./scheduler-store.js";
|
|
22
23
|
export * from "./cross-session.js";
|
|
23
24
|
export * from "./hooks.js";
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,8 @@ export * from "./config-fns.js";
|
|
|
17
17
|
export * from "./reader.js";
|
|
18
18
|
export * from "./cross-domain.js"; // pure half only (no fleet placement)
|
|
19
19
|
export * from "./remote-exec.js";
|
|
20
|
-
export * from "./safety-merge-spec.js"; // #14 per-category safety merge-spec (settings resolver authority)
|
|
20
|
+
export * from "./safety-merge-spec.js"; // #14 per-category safety merge-spec (settings resolver authority) + 14.6.0 THE comparator tightenVerdict / TIGHTEN_VERDICTS and the member comparator memberTighten / resolveMemberTighten (derived from a member row's value class)
|
|
21
|
+
export * from "./settings-units.js"; // 14.6.0: SETTINGS_UNITS — the settings-document unit table (path / category / addressing / reading) + unitTighten (a unit's comparator) + readSettingsUnit (a unit's raw-value reading; spellings normalized, everything else unreadable), pure/browser-safe
|
|
21
22
|
export * from "./scheduler-store.js"; // R7 self-wake scheduler store CONTRACT (pure: type + parse/serialize + SchedulerStore interface; fs binding in ./node)
|
|
22
23
|
export * from "./cross-session.js"; // CC-parity `settings.json` cross-session keys (crossSessionInbound / dialogExpiry) — pure/browser-safe
|
|
23
24
|
export * from "./hooks.js"; // CC-parity hooks contract (33-event set + owner map + per-event input/output shapes + the config surface: classifyHooksConfig and its zod faces HooksConfig / HookEntry …) & stop attribution (stoppedBy) — pure/browser-safe
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { type McpContentOrigin } from "./types.js";
|
|
2
|
-
/** 值的**种类**(不回显值本身)。 */
|
|
2
|
+
/** 值的**种类**(不回显值本身)。总(不抛):经 {@link inspectSafely}。 */
|
|
3
3
|
export declare function describeValueShape(v: unknown): string;
|
|
4
|
-
/** 「不是字符串数组」的种类:数组里混了非串元素说那一句,否则回落到 {@link describeValueShape}。 */
|
|
4
|
+
/** 「不是字符串数组」的种类:数组里混了非串元素说那一句,否则回落到 {@link describeValueShape}。总(不抛):经 {@link inspectSafely}。 */
|
|
5
5
|
export declare function describeNotStringArray(v: unknown): string;
|
|
6
|
-
/** 「不是字符串值的对象」的种类:对象里混了非串值说那一句,否则回落到 {@link describeValueShape}。 */
|
|
6
|
+
/** 「不是字符串值的对象」的种类:对象里混了非串值说那一句,否则回落到 {@link describeValueShape}。总(不抛):经 {@link inspectSafely}。 */
|
|
7
7
|
export declare function describeNotStringRecord(v: unknown): string;
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* 字符串数组(空数组合法)。与传输读器读 `args` 是**同一只读法**(`./definition-copy` 的 `captureStringArray`:按下标、自有下标、一次读、非串即坏、不走迭代协议):
|
|
10
|
+
* 稀疏数组(洞)、`length` 读不出安全非负整数的数组、读元素时抛的数组都不是 —— 洞不顺原型链读到 `Object.prototype` / `Array.prototype` 上的数字键。
|
|
11
|
+
*/
|
|
9
12
|
export declare function isStringArray(v: unknown): v is string[];
|
|
10
13
|
/**
|
|
11
14
|
* 纯数据的「名 → 串」记录(空对象合法;数组、`Map`、类实例、自有 `__proto__` 成员、有非串成员的对象都不是)。与传输读器读 `env` / `headers` 是**同一个**定义
|
|
@@ -58,7 +61,7 @@ export type RequestMcpTransport = {
|
|
|
58
61
|
* 条目内一处坏形:`path` = 条目内的键路径(`transport.url`),`reason` = 判官的原因句(只说形与词表,不回显值)。
|
|
59
62
|
*
|
|
60
63
|
* `env` / `headers` 这两张「名 → 串」记录上的坏形另带两格机读位,调用方分流不必解析句子:`fault` = 闭集词 ——
|
|
61
|
-
* · `not_plain_record`:这个键的值不是纯数据记录(不是对象 / 数组 / `null
|
|
64
|
+
* · `not_plain_record`:这个键的值不是纯数据记录(不是对象 / 数组 / `null`,或不是朴素 JSON 对象 —— 原型链比座位的判据长:`Map`、`Set`、类实例);整张记录不交;
|
|
62
65
|
* · `member_not_string`:有一个成员的值不是串(或访问器抛了);整张记录不交,`member` 点名它;
|
|
63
66
|
* · `member_name_reserved`:有一个成员叫 `__proto__`(引擎的声明收据拒这个名)。成员的值是串 ⇒ 只剔这个成员、其余照交,`path` 是成员级(`transport.env.__proto__`);
|
|
64
67
|
* 成员的值也不是串 ⇒ 整张记录不交,`path` 是键级,句子一次说全两件事 —— 名坏优先、值坏并列,所以词仍是 `member_name_reserved`。
|
|
@@ -84,7 +87,7 @@ export type RequestMcpTransportRead = {
|
|
|
84
87
|
* · 硬坏形(`transport` 不是对象 / `kind` 出闭集 / stdio 缺 `command` / http 的 `url` 不是 http(s))⇒ `ok:false`,`path` 是读器点名的键路径;
|
|
85
88
|
* · 软坏形(`args` 不是串数组 / `env` `headers` 不是纯数据的串记录 / `principalHeader` 非串)⇒ 读器**丢这个键**(重放存量提交体时的升级前读法),并把它记进 `lost`
|
|
86
89
|
* (受理新提交的门据它响亮拒)。`assertionHeader` 不在 `lost` 里:两个身份头的键组合与 `assertionHeader` 的值形由调用方在读之前对原始传输判。
|
|
87
|
-
* · `env` / `headers` 只收**纯数据**的记录({@link readPlainStringRecord})
|
|
90
|
+
* · `env` / `headers` 只收**纯数据**的记录({@link readPlainStringRecord}):必须是朴素 JSON 对象(`./plain-json-object` 的判据;`Map`、`Set`、类实例整张不收),每个自有成员的值都是串;
|
|
88
91
|
* 交出的是**副本**(不是调用方的对象,改原对象不影响读后的形)。名叫 `__proto__` 的成员(JSON 文本铸得出这个自有键,对象字面量写不出)不进副本、单独记进 `lost`。
|
|
89
92
|
* 坏形的 `lost` 条目带机读位(见 {@link McpEntryKeyIssue})。
|
|
90
93
|
* url 的拒句不回显值:它常规地带凭据(userinfo)。
|