@sema-agent/settings-schema 6.2.0 → 6.4.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 +197 -0
- package/README.md +21 -9
- package/conformance/VERSION +1 -1
- package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +166 -0
- package/conformance/cases/c41-s792-encoded-pem-identity/input.json +62 -0
- package/conformance/cases/c41-s792-encoded-pem-identity/meta.json +12 -0
- package/conformance/cases/c42-s792-unknown-key-secret-redacted/expected.json +120 -0
- package/conformance/cases/c42-s792-unknown-key-secret-redacted/input.json +25 -0
- package/conformance/cases/c42-s792-unknown-key-secret-redacted/meta.json +11 -0
- package/conformance/cases/c43-s792-form-and-deep-encoded-pem/expected.json +137 -0
- package/conformance/cases/c43-s792-form-and-deep-encoded-pem/input.json +44 -0
- package/conformance/cases/c43-s792-form-and-deep-encoded-pem/meta.json +12 -0
- package/conformance/cases/c44-s792-utf8-escape-glued-token/expected.json +115 -0
- package/conformance/cases/c44-s792-utf8-escape-glued-token/input.json +30 -0
- package/conformance/cases/c44-s792-utf8-escape-glued-token/meta.json +10 -0
- package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +185 -0
- package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/input.json +78 -0
- package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/meta.json +11 -0
- package/conformance/cases/c46-s811-host-name-url-userinfo-characterization/expected.json +88 -0
- package/conformance/cases/c46-s811-host-name-url-userinfo-characterization/input.json +18 -0
- package/conformance/cases/c46-s811-host-name-url-userinfo-characterization/meta.json +6 -0
- package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/expected.json +98 -0
- package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/input.json +26 -0
- package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/meta.json +6 -0
- package/conformance/cases/c48-s935-model-headers-transported/expected.json +115 -0
- package/conformance/cases/c48-s935-model-headers-transported/input.json +37 -0
- package/conformance/cases/c48-s935-model-headers-transported/meta.json +6 -0
- package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/expected.json +98 -0
- package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/input.json +28 -0
- package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/meta.json +9 -0
- package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +0 -8
- package/conformance/cases/cli01-model-name-nfc-vs-nfd/meta.json +2 -1
- package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +0 -8
- package/conformance/cases/cli02-control-and-zero-width-chars/meta.json +2 -1
- package/dist/config-fns.d.ts +53 -22
- package/dist/config-fns.js +87 -33
- package/dist/cross-session.d.ts +8 -5
- package/dist/cross-session.js +8 -5
- package/dist/fleet.d.ts +3 -2
- package/dist/fleet.js +3 -2
- package/dist/hash.d.ts +3 -3
- package/dist/hash.js +3 -3
- package/dist/hooks.d.ts +8 -8
- package/dist/secret-scan.d.ts +39 -0
- package/dist/secret-scan.js +110 -0
- package/dist/types.d.ts +65 -7
- package/dist/types.js +57 -44
- package/package.json +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 秘密扫描读的**归一化**,外加「URL 的 userinfo 就是凭据」那一只判定({@link carriesUserinfo})—— 包内私有模块(6.3.0):不进主 barrel、
|
|
3
|
+
* 不进 `package.json` 的 `exports`,消费方拿不到。
|
|
4
|
+
* 校验侧探测器 `containsSecretToken`(`./types`)只读 {@link percentLayers};展示侧脱敏器 `redactSecrets`(`./config-fns`)
|
|
5
|
+
* 读 {@link secretScanForms}。展示侧的读法(边界读法、`+` 读法、「没走完」那一位)刻意不做公开契约:它们以后要随新发现的
|
|
6
|
+
* 编码形调整,调整不该是契约变更(opus 独立腿 L7)。
|
|
7
|
+
*
|
|
8
|
+
* **单遍、零异常**(opus 独立腿 M1,对 6.2.0 的读路径 CPU 回归):6.2.0 起的宽容解码对每个 `%HH` 各 `try decodeURIComponent`,
|
|
9
|
+
* 非 ASCII 的转义每个都抛一次异常(≈ 20–30 µs / 字节,同步阻塞);6.3.0 首稿又让探测器顺带算它不用的展示形。这里两处解码都
|
|
10
|
+
* 不靠异常:单个转义的判据写成字符集(只有 ASCII 的 `%00`–`%7F` 单独解得开),转义串的判据写成 UTF-8 良构判定 ——
|
|
11
|
+
* 结果与原来的 try/catch 逐字相同(`test/disclosable-identity-6.3.0.test.ts` 的两只冻结 oracle 钉着)。
|
|
12
|
+
*
|
|
13
|
+
* 本模块零 import(`./types` 引它,它不回引,不成环)。
|
|
14
|
+
*/
|
|
15
|
+
/** How many percent-decoded layers a secret scan reads beyond the raw string (a multi-encoded `%2573%256B…` →
|
|
16
|
+
* `%73%6B…` → `sk-…`). Bounded → deterministic. */
|
|
17
|
+
export declare const SECRET_SCAN_DECODE_LAYERS = 3;
|
|
18
|
+
/** The raw string first, then up to {@link SECRET_SCAN_DECODE_LAYERS} tolerant percent-decoded layers, stopping at the first
|
|
19
|
+
* layer that decodes nothing — the forms the validation detector `containsSecretToken` reads, and nothing else. */
|
|
20
|
+
export declare function percentLayers(text: string): readonly string[];
|
|
21
|
+
/** What {@link secretScanForms} returns — plain data, no behavior. */
|
|
22
|
+
export interface SecretScanForms {
|
|
23
|
+
/** {@link percentLayers} of the text. */
|
|
24
|
+
readonly percentLayers: readonly string[];
|
|
25
|
+
/** The forms the display-side redactor judges a whole string on: `percentLayers`, each layer's {@link boundaryReading}, and
|
|
26
|
+
* each of those read the application/x-www-form-urlencoded way (`+` as a space); duplicates dropped, first seen first. */
|
|
27
|
+
readonly forms: readonly string[];
|
|
28
|
+
/** Normalization did not finish: the layer budget ran out while one more layer would still decode something. A caller that
|
|
29
|
+
* shows text withholds it whole — what could not be normalized does not go out. (A lone undecodable escape is a fixed
|
|
30
|
+
* point, not an unfinished one: `%E4%B8%AD` stays as it is and is not flagged.) */
|
|
31
|
+
readonly unresolved: boolean;
|
|
32
|
+
}
|
|
33
|
+
/** The display-side normalization — only `redactSecrets` (config-fns) calls it. */
|
|
34
|
+
export declare function secretScanForms(text: string): SecretScanForms;
|
|
35
|
+
/** 「URL 的 userinfo 就是凭据」的**唯一**判定(6.3.0,opus 腿 L1):username / password 任一在场即是。校验侧 `NON_SECRET_URL` 与 https
|
|
36
|
+
* 插件源据此拒写(`./types`),身份面据此否决(`./config-fns` 的 `isDisclosableIdentity` —— `https://admin:hunter2@db…` /
|
|
37
|
+
* `postgres://svc:pw@h` 的字符全在身份白名单里)。串解析不成 URL ⇒ false:这只判定只说「一个 URL 带没带 userinfo」,
|
|
38
|
+
* 不是 URL 的串不在它的射程里(无 scheme 的 `svc:pw@host` 解析成不透明路径,同样答 false —— 如实登记的边界)。 */
|
|
39
|
+
export declare function carriesUserinfo(u: URL | string): boolean;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 秘密扫描读的**归一化**,外加「URL 的 userinfo 就是凭据」那一只判定({@link carriesUserinfo})—— 包内私有模块(6.3.0):不进主 barrel、
|
|
3
|
+
* 不进 `package.json` 的 `exports`,消费方拿不到。
|
|
4
|
+
* 校验侧探测器 `containsSecretToken`(`./types`)只读 {@link percentLayers};展示侧脱敏器 `redactSecrets`(`./config-fns`)
|
|
5
|
+
* 读 {@link secretScanForms}。展示侧的读法(边界读法、`+` 读法、「没走完」那一位)刻意不做公开契约:它们以后要随新发现的
|
|
6
|
+
* 编码形调整,调整不该是契约变更(opus 独立腿 L7)。
|
|
7
|
+
*
|
|
8
|
+
* **单遍、零异常**(opus 独立腿 M1,对 6.2.0 的读路径 CPU 回归):6.2.0 起的宽容解码对每个 `%HH` 各 `try decodeURIComponent`,
|
|
9
|
+
* 非 ASCII 的转义每个都抛一次异常(≈ 20–30 µs / 字节,同步阻塞);6.3.0 首稿又让探测器顺带算它不用的展示形。这里两处解码都
|
|
10
|
+
* 不靠异常:单个转义的判据写成字符集(只有 ASCII 的 `%00`–`%7F` 单独解得开),转义串的判据写成 UTF-8 良构判定 ——
|
|
11
|
+
* 结果与原来的 try/catch 逐字相同(`test/disclosable-identity-6.3.0.test.ts` 的两只冻结 oracle 钉着)。
|
|
12
|
+
*
|
|
13
|
+
* 本模块零 import(`./types` 引它,它不回引,不成环)。
|
|
14
|
+
*/
|
|
15
|
+
/** How many percent-decoded layers a secret scan reads beyond the raw string (a multi-encoded `%2573%256B…` →
|
|
16
|
+
* `%73%6B…` → `sk-…`). Bounded → deterministic. */
|
|
17
|
+
export const SECRET_SCAN_DECODE_LAYERS = 3;
|
|
18
|
+
/** One TOLERANT percent-decode layer: an escape decodes on its own iff `decodeURIComponent` would accept it alone — exactly
|
|
19
|
+
* the ASCII ones, `%00`–`%7F` — and every other escape (`%C2`, a lone `%E4`, `%ZZ` is not an escape at all) stays as it
|
|
20
|
+
* is, so one bad escape elsewhere can't abort the whole decode (R13's all-or-nothing decodeURIComponent did). Same result
|
|
21
|
+
* as the per-escape try/catch loop it replaces, with no exception per non-ASCII escape. */
|
|
22
|
+
function percentDecodeLayer(s) {
|
|
23
|
+
return s.indexOf("%") === -1 ? s : s.replace(/%([0-7][0-9A-Fa-f])/g, (_m, hex) => String.fromCharCode(parseInt(hex, 16)));
|
|
24
|
+
}
|
|
25
|
+
/** The raw string first, then up to {@link SECRET_SCAN_DECODE_LAYERS} tolerant percent-decoded layers, stopping at the first
|
|
26
|
+
* layer that decodes nothing — the forms the validation detector `containsSecretToken` reads, and nothing else. */
|
|
27
|
+
export function percentLayers(text) {
|
|
28
|
+
const layers = [text];
|
|
29
|
+
let cur = text;
|
|
30
|
+
for (let i = 0; i < SECRET_SCAN_DECODE_LAYERS; i++) {
|
|
31
|
+
const next = percentDecodeLayer(cur);
|
|
32
|
+
if (next === cur)
|
|
33
|
+
break;
|
|
34
|
+
layers.push(next);
|
|
35
|
+
cur = next;
|
|
36
|
+
}
|
|
37
|
+
return layers;
|
|
38
|
+
}
|
|
39
|
+
/** Is this byte sequence well-formed UTF-8 (Unicode Table 3-7: no overlongs, no surrogates, nothing above U+10FFFF)? That is
|
|
40
|
+
* exactly when `decodeURIComponent` accepts a run of escapes spelling these bytes — decided without throwing. */
|
|
41
|
+
function isWellFormedUtf8(bytes) {
|
|
42
|
+
for (let i = 0; i < bytes.length;) {
|
|
43
|
+
const lead = bytes[i];
|
|
44
|
+
if (lead < 0x80) {
|
|
45
|
+
i += 1;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
// [续字节个数, 第一个续字节的下界, 上界];其余续字节恒为 80..BF。
|
|
49
|
+
const shape = lead >= 0xc2 && lead <= 0xdf ? [1, 0x80, 0xbf]
|
|
50
|
+
: lead === 0xe0 ? [2, 0xa0, 0xbf]
|
|
51
|
+
: (lead >= 0xe1 && lead <= 0xec) || lead === 0xee || lead === 0xef ? [2, 0x80, 0xbf]
|
|
52
|
+
: lead === 0xed ? [2, 0x80, 0x9f]
|
|
53
|
+
: lead === 0xf0 ? [3, 0x90, 0xbf]
|
|
54
|
+
: lead >= 0xf1 && lead <= 0xf3 ? [3, 0x80, 0xbf]
|
|
55
|
+
: lead === 0xf4 ? [3, 0x80, 0x8f]
|
|
56
|
+
: undefined;
|
|
57
|
+
if (shape === undefined)
|
|
58
|
+
return false;
|
|
59
|
+
const [need, lo, hi] = shape;
|
|
60
|
+
for (let k = 1; k <= need; k++) {
|
|
61
|
+
const c = bytes[i + k];
|
|
62
|
+
if (c === undefined || c < (k === 1 ? lo : 0x80) || c > (k === 1 ? hi : 0xbf))
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
i += need + 1;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
/** The display-side BOUNDARY reading of one layer (codex r2): every run of %HH escapes is decoded as UTF-8 in one go when it
|
|
70
|
+
* is well-formed (an encoded NBSP / zero-width space becomes the delimiter it really is); otherwise its ASCII escapes decode
|
|
71
|
+
* on their own and every other escape reads as a space — an escape never glues its hex digits onto the token after it
|
|
72
|
+
* (`%C2%A0sk-live-…` read escape by escape is `%C2%A0sk-…`, whose tokens are `C2` / `A0sk-…`: the `sk-` boundary is gone,
|
|
73
|
+
* though any standard decoder restores it). Display-only: the validation detector never reads it. */
|
|
74
|
+
function boundaryReading(s) {
|
|
75
|
+
if (s.indexOf("%") === -1)
|
|
76
|
+
return s;
|
|
77
|
+
return s.replace(/(?:%[0-9A-Fa-f]{2})+/g, (run) => {
|
|
78
|
+
const bytes = [];
|
|
79
|
+
for (let i = 0; i < run.length; i += 3)
|
|
80
|
+
bytes.push(parseInt(run.slice(i + 1, i + 3), 16));
|
|
81
|
+
return isWellFormedUtf8(bytes) ? decodeURIComponent(run) : bytes.map((b) => (b < 0x80 ? String.fromCharCode(b) : " ")).join("");
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/** The display-side normalization — only `redactSecrets` (config-fns) calls it. */
|
|
85
|
+
export function secretScanForms(text) {
|
|
86
|
+
const layers = percentLayers(text);
|
|
87
|
+
const last = layers[layers.length - 1];
|
|
88
|
+
const unresolved = layers.length === SECRET_SCAN_DECODE_LAYERS + 1 && percentDecodeLayer(last) !== last;
|
|
89
|
+
const readings = [...layers, ...layers.map((layer) => boundaryReading(layer))];
|
|
90
|
+
const forms = [...new Set([...readings, ...readings.map((reading) => (reading.indexOf("+") === -1 ? reading : reading.replace(/\+/g, " ")))])];
|
|
91
|
+
return { percentLayers: layers, forms, unresolved };
|
|
92
|
+
}
|
|
93
|
+
/** 「URL 的 userinfo 就是凭据」的**唯一**判定(6.3.0,opus 腿 L1):username / password 任一在场即是。校验侧 `NON_SECRET_URL` 与 https
|
|
94
|
+
* 插件源据此拒写(`./types`),身份面据此否决(`./config-fns` 的 `isDisclosableIdentity` —— `https://admin:hunter2@db…` /
|
|
95
|
+
* `postgres://svc:pw@h` 的字符全在身份白名单里)。串解析不成 URL ⇒ false:这只判定只说「一个 URL 带没带 userinfo」,
|
|
96
|
+
* 不是 URL 的串不在它的射程里(无 scheme 的 `svc:pw@host` 解析成不透明路径,同样答 false —— 如实登记的边界)。 */
|
|
97
|
+
export function carriesUserinfo(u) {
|
|
98
|
+
let url;
|
|
99
|
+
if (typeof u === "string") {
|
|
100
|
+
try {
|
|
101
|
+
url = new URL(u);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false; // 不是 URL:不在这只判定的射程里(见头注),不是回退
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else
|
|
108
|
+
url = u;
|
|
109
|
+
return url.username !== "" || url.password !== "";
|
|
110
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
/** A private-key PEM header (`-----BEGIN [RSA |EC |OPENSSH |ENCRYPTED ]PRIVATE KEY`), unanchored — the ONE definition
|
|
20
20
|
* of that shape: {@link SECRET_LITERAL_RE} is composed from it (anchored, as one of its branches), and
|
|
21
|
-
* `redactSecrets` (config-fns) tests it ANYWHERE in a string. It carries spaces,
|
|
22
|
-
* {@link containsSecretToken} only sees it at the very start of a string; a PEM behind
|
|
23
|
-
* slipped past (S-712 codex review [high])
|
|
21
|
+
* `redactSecrets` (config-fns) tests it ANYWHERE in a string, on every display-side normalized form. It carries spaces,
|
|
22
|
+
* so the token-splitting detector {@link containsSecretToken} only sees it at the very start of a string; a PEM behind
|
|
23
|
+
* a newline or a `key: ` prefix slipped past (S-712 codex review [high]), and a `key=`-prefixed percent-encoded one
|
|
24
|
+
* hid it behind `%20` on the raw form, a form-urlencoded one behind `+` (6.3.0). Public keys / certificates never match. */
|
|
24
25
|
export declare const PRIVATE_KEY_PEM_HEADER: RegExp;
|
|
25
26
|
export declare const SECRET_LITERAL_RE: RegExp;
|
|
26
27
|
export declare const ENV_NAME: z.ZodEffects<z.ZodString, string, string>;
|
|
@@ -29,9 +30,15 @@ export declare const ENV_NAME: z.ZodEffects<z.ZodString, string, string>;
|
|
|
29
30
|
* fs path — so the worker dies at exec/boot, not at config edit. A NUL is never legitimate in a command / path
|
|
30
31
|
* / host / image, so reject it on every exec-bound string field (mcp stdio + the whole remoteExec contract). R20. */
|
|
31
32
|
export declare const hasNul: (s: string) => boolean;
|
|
32
|
-
/** Does `s` contain a secret-shaped token? Scans the raw string AND
|
|
33
|
-
* as `%73%6B
|
|
34
|
-
*
|
|
33
|
+
/** Does `s` contain a secret-shaped token? Scans every percent layer of it — the raw string AND up to three tolerant
|
|
34
|
+
* percent-decoded layers (a secret can hide as `%73%6B-…`; the ONE definition of those layers is `percentLayers` in the
|
|
35
|
+
* package-private `./secret-scan`) — so mcp args / dockerHost / URL components all get uniform percent-decode coverage.
|
|
36
|
+
* Tokenized + JWT-aware (see scanForSecret). Shared by NON_SECRET_URL + dockerHost + mcp stdio. Round-4/5/11. (6.3.0 moved
|
|
37
|
+
* the decode loop out, unchanged in result — same layers, same bound, same verdict — and made it exception-free; the
|
|
38
|
+
* display-side readings the redactor also judges are not this detector's.) We do NOT fail-closed on a bare `%` — that
|
|
39
|
+
* would false-reject legit values like `100%` / `%s`. A secret glued to a malformed escape (`%ZZsk-`) is an accepted
|
|
40
|
+
* coarse-heuristic gap (no consumer reconstructs it; the real guarantee is the env-NAME boundary on structured fields).
|
|
41
|
+
* R11/R13/R14. */
|
|
35
42
|
export declare function containsSecretToken(s: string): boolean;
|
|
36
43
|
/** Scan a URL's PATH + query + fragment (KEYS and values, decoded, tokenized) for a secret-shaped literal.
|
|
37
44
|
* Userinfo is handled separately (NON_SECRET_URL bans it; dockerHost scans the ssh user). Round-4/5. */
|
|
@@ -110,7 +117,8 @@ export declare const SealedApiKey: z.ZodObject<{
|
|
|
110
117
|
export type SealedApiKey = z.infer<typeof SealedApiKey>;
|
|
111
118
|
/** 谁来校验一个**解析透明键**的形。闭集:新增一员要在这里加词,穷举消费方即编译红。
|
|
112
119
|
* · `engine-ingest` —— 引擎摄入侧单点校验(不合形的那一条不挂并通告一次);
|
|
113
|
-
* · `consumer` ——
|
|
120
|
+
* · `consumer` —— 消费方的投影/执法点。坏值怎么处置(只丢那一个键 / 整条宿主条目不可用)是那个执法点自己的
|
|
121
|
+
* 契约,写在登记行的 `why` 里 —— 本包不替它定。 */
|
|
114
122
|
export type ParseTransparentValidator = "engine-ingest" | "consumer";
|
|
115
123
|
/** {@link PARSE_TRANSPARENT_KEYS} 的一行。 */
|
|
116
124
|
export interface ParseTransparentKey {
|
|
@@ -240,6 +248,16 @@ export declare const ModelEntry: z.ZodEffects<z.ZodObject<{
|
|
|
240
248
|
* deep to hash later. That is not shape validation — it is the boundary every free-form value must clear to
|
|
241
249
|
* enter the persisted config and `stableHash` (R18). */
|
|
242
250
|
compat: z.ZodEffects<z.ZodOptional<z.ZodUnknown>, unknown, unknown>;
|
|
251
|
+
/** Per-model extra request headers, deployment ↔ provider (e.g. `{"anthropic-beta": "context-1m-2025-08-07"}` on an
|
|
252
|
+
* `anthropic-messages` entry). Real need: a beta that a shell passes as `--betas` / `ANTHROPIC_BETAS` never reaches the
|
|
253
|
+
* request the engine sends; the catalog entry is the deployment-level seat for it — and without this key the entry's
|
|
254
|
+
* declaration is stripped at parse on every read leg, so it never reaches the consumer at all.
|
|
255
|
+
* ABSENT = key not minted = today's behaviour, zero migration.
|
|
256
|
+
*
|
|
257
|
+
* **Parse-transparent** — who validates the shape, and why not here: {@link PARSE_TRANSPARENT_KEYS}
|
|
258
|
+
* (`models.models[].headers`). The ONE rejection that remains is the free-form SAFETY judgment shared with `compat` /
|
|
259
|
+
* `extraBody` ({@link vetFreeFormJson}). */
|
|
260
|
+
headers: z.ZodEffects<z.ZodOptional<z.ZodUnknown>, unknown, unknown>;
|
|
243
261
|
/** 配额倍率(EXPERT-REDESIGN §4-1):该模型烧 token 双窗额度的倍率,默认 1。折算时点=weight-at-burn
|
|
244
262
|
* (记账时固化到 UsageEntry.quotaWeightAtUse/weightedTokens):改倍率只影响此后记账,不回算窗口内历史。 */
|
|
245
263
|
quotaWeight: z.ZodDefault<z.ZodNumber>;
|
|
@@ -276,6 +294,7 @@ export declare const ModelEntry: z.ZodEffects<z.ZodObject<{
|
|
|
276
294
|
} | undefined;
|
|
277
295
|
extraBody?: Record<string, unknown> | undefined;
|
|
278
296
|
compat?: unknown;
|
|
297
|
+
headers?: unknown;
|
|
279
298
|
notes?: string | undefined;
|
|
280
299
|
}, {
|
|
281
300
|
id: string;
|
|
@@ -306,6 +325,7 @@ export declare const ModelEntry: z.ZodEffects<z.ZodObject<{
|
|
|
306
325
|
} | undefined;
|
|
307
326
|
extraBody?: unknown;
|
|
308
327
|
compat?: unknown;
|
|
328
|
+
headers?: unknown;
|
|
309
329
|
quotaWeight?: number | undefined;
|
|
310
330
|
enabled?: boolean | undefined;
|
|
311
331
|
notes?: string | undefined;
|
|
@@ -340,6 +360,7 @@ export declare const ModelEntry: z.ZodEffects<z.ZodObject<{
|
|
|
340
360
|
} | undefined;
|
|
341
361
|
extraBody?: Record<string, unknown> | undefined;
|
|
342
362
|
compat?: unknown;
|
|
363
|
+
headers?: unknown;
|
|
343
364
|
notes?: string | undefined;
|
|
344
365
|
}, {
|
|
345
366
|
id: string;
|
|
@@ -370,6 +391,7 @@ export declare const ModelEntry: z.ZodEffects<z.ZodObject<{
|
|
|
370
391
|
} | undefined;
|
|
371
392
|
extraBody?: unknown;
|
|
372
393
|
compat?: unknown;
|
|
394
|
+
headers?: unknown;
|
|
373
395
|
quotaWeight?: number | undefined;
|
|
374
396
|
enabled?: boolean | undefined;
|
|
375
397
|
notes?: string | undefined;
|
|
@@ -578,6 +600,16 @@ export declare const ModelsConfig: z.ZodEffects<z.ZodObject<{
|
|
|
578
600
|
* deep to hash later. That is not shape validation — it is the boundary every free-form value must clear to
|
|
579
601
|
* enter the persisted config and `stableHash` (R18). */
|
|
580
602
|
compat: z.ZodEffects<z.ZodOptional<z.ZodUnknown>, unknown, unknown>;
|
|
603
|
+
/** Per-model extra request headers, deployment ↔ provider (e.g. `{"anthropic-beta": "context-1m-2025-08-07"}` on an
|
|
604
|
+
* `anthropic-messages` entry). Real need: a beta that a shell passes as `--betas` / `ANTHROPIC_BETAS` never reaches the
|
|
605
|
+
* request the engine sends; the catalog entry is the deployment-level seat for it — and without this key the entry's
|
|
606
|
+
* declaration is stripped at parse on every read leg, so it never reaches the consumer at all.
|
|
607
|
+
* ABSENT = key not minted = today's behaviour, zero migration.
|
|
608
|
+
*
|
|
609
|
+
* **Parse-transparent** — who validates the shape, and why not here: {@link PARSE_TRANSPARENT_KEYS}
|
|
610
|
+
* (`models.models[].headers`). The ONE rejection that remains is the free-form SAFETY judgment shared with `compat` /
|
|
611
|
+
* `extraBody` ({@link vetFreeFormJson}). */
|
|
612
|
+
headers: z.ZodEffects<z.ZodOptional<z.ZodUnknown>, unknown, unknown>;
|
|
581
613
|
/** 配额倍率(EXPERT-REDESIGN §4-1):该模型烧 token 双窗额度的倍率,默认 1。折算时点=weight-at-burn
|
|
582
614
|
* (记账时固化到 UsageEntry.quotaWeightAtUse/weightedTokens):改倍率只影响此后记账,不回算窗口内历史。 */
|
|
583
615
|
quotaWeight: z.ZodDefault<z.ZodNumber>;
|
|
@@ -614,6 +646,7 @@ export declare const ModelsConfig: z.ZodEffects<z.ZodObject<{
|
|
|
614
646
|
} | undefined;
|
|
615
647
|
extraBody?: Record<string, unknown> | undefined;
|
|
616
648
|
compat?: unknown;
|
|
649
|
+
headers?: unknown;
|
|
617
650
|
notes?: string | undefined;
|
|
618
651
|
}, {
|
|
619
652
|
id: string;
|
|
@@ -644,6 +677,7 @@ export declare const ModelsConfig: z.ZodEffects<z.ZodObject<{
|
|
|
644
677
|
} | undefined;
|
|
645
678
|
extraBody?: unknown;
|
|
646
679
|
compat?: unknown;
|
|
680
|
+
headers?: unknown;
|
|
647
681
|
quotaWeight?: number | undefined;
|
|
648
682
|
enabled?: boolean | undefined;
|
|
649
683
|
notes?: string | undefined;
|
|
@@ -678,6 +712,7 @@ export declare const ModelsConfig: z.ZodEffects<z.ZodObject<{
|
|
|
678
712
|
} | undefined;
|
|
679
713
|
extraBody?: Record<string, unknown> | undefined;
|
|
680
714
|
compat?: unknown;
|
|
715
|
+
headers?: unknown;
|
|
681
716
|
notes?: string | undefined;
|
|
682
717
|
}, {
|
|
683
718
|
id: string;
|
|
@@ -708,6 +743,7 @@ export declare const ModelsConfig: z.ZodEffects<z.ZodObject<{
|
|
|
708
743
|
} | undefined;
|
|
709
744
|
extraBody?: unknown;
|
|
710
745
|
compat?: unknown;
|
|
746
|
+
headers?: unknown;
|
|
711
747
|
quotaWeight?: number | undefined;
|
|
712
748
|
enabled?: boolean | undefined;
|
|
713
749
|
notes?: string | undefined;
|
|
@@ -832,6 +868,7 @@ export declare const ModelsConfig: z.ZodEffects<z.ZodObject<{
|
|
|
832
868
|
} | undefined;
|
|
833
869
|
extraBody?: Record<string, unknown> | undefined;
|
|
834
870
|
compat?: unknown;
|
|
871
|
+
headers?: unknown;
|
|
835
872
|
notes?: string | undefined;
|
|
836
873
|
}[];
|
|
837
874
|
roles: Partial<Record<"default" | "summarize" | "subagent" | "team" | "synthesize" | "verifier" | "classifier" | "consolidate", {
|
|
@@ -887,6 +924,7 @@ export declare const ModelsConfig: z.ZodEffects<z.ZodObject<{
|
|
|
887
924
|
} | undefined;
|
|
888
925
|
extraBody?: unknown;
|
|
889
926
|
compat?: unknown;
|
|
927
|
+
headers?: unknown;
|
|
890
928
|
quotaWeight?: number | undefined;
|
|
891
929
|
enabled?: boolean | undefined;
|
|
892
930
|
notes?: string | undefined;
|
|
@@ -946,6 +984,7 @@ export declare const ModelsConfig: z.ZodEffects<z.ZodObject<{
|
|
|
946
984
|
} | undefined;
|
|
947
985
|
extraBody?: Record<string, unknown> | undefined;
|
|
948
986
|
compat?: unknown;
|
|
987
|
+
headers?: unknown;
|
|
949
988
|
notes?: string | undefined;
|
|
950
989
|
}[];
|
|
951
990
|
roles: Partial<Record<"default" | "summarize" | "subagent" | "team" | "synthesize" | "verifier" | "classifier" | "consolidate", {
|
|
@@ -1001,6 +1040,7 @@ export declare const ModelsConfig: z.ZodEffects<z.ZodObject<{
|
|
|
1001
1040
|
} | undefined;
|
|
1002
1041
|
extraBody?: unknown;
|
|
1003
1042
|
compat?: unknown;
|
|
1043
|
+
headers?: unknown;
|
|
1004
1044
|
quotaWeight?: number | undefined;
|
|
1005
1045
|
enabled?: boolean | undefined;
|
|
1006
1046
|
notes?: string | undefined;
|
|
@@ -9494,6 +9534,16 @@ export declare const DOMAIN_SCHEMAS: {
|
|
|
9494
9534
|
* deep to hash later. That is not shape validation — it is the boundary every free-form value must clear to
|
|
9495
9535
|
* enter the persisted config and `stableHash` (R18). */
|
|
9496
9536
|
compat: z.ZodEffects<z.ZodOptional<z.ZodUnknown>, unknown, unknown>;
|
|
9537
|
+
/** Per-model extra request headers, deployment ↔ provider (e.g. `{"anthropic-beta": "context-1m-2025-08-07"}` on an
|
|
9538
|
+
* `anthropic-messages` entry). Real need: a beta that a shell passes as `--betas` / `ANTHROPIC_BETAS` never reaches the
|
|
9539
|
+
* request the engine sends; the catalog entry is the deployment-level seat for it — and without this key the entry's
|
|
9540
|
+
* declaration is stripped at parse on every read leg, so it never reaches the consumer at all.
|
|
9541
|
+
* ABSENT = key not minted = today's behaviour, zero migration.
|
|
9542
|
+
*
|
|
9543
|
+
* **Parse-transparent** — who validates the shape, and why not here: {@link PARSE_TRANSPARENT_KEYS}
|
|
9544
|
+
* (`models.models[].headers`). The ONE rejection that remains is the free-form SAFETY judgment shared with `compat` /
|
|
9545
|
+
* `extraBody` ({@link vetFreeFormJson}). */
|
|
9546
|
+
headers: z.ZodEffects<z.ZodOptional<z.ZodUnknown>, unknown, unknown>;
|
|
9497
9547
|
/** 配额倍率(EXPERT-REDESIGN §4-1):该模型烧 token 双窗额度的倍率,默认 1。折算时点=weight-at-burn
|
|
9498
9548
|
* (记账时固化到 UsageEntry.quotaWeightAtUse/weightedTokens):改倍率只影响此后记账,不回算窗口内历史。 */
|
|
9499
9549
|
quotaWeight: z.ZodDefault<z.ZodNumber>;
|
|
@@ -9530,6 +9580,7 @@ export declare const DOMAIN_SCHEMAS: {
|
|
|
9530
9580
|
} | undefined;
|
|
9531
9581
|
extraBody?: Record<string, unknown> | undefined;
|
|
9532
9582
|
compat?: unknown;
|
|
9583
|
+
headers?: unknown;
|
|
9533
9584
|
notes?: string | undefined;
|
|
9534
9585
|
}, {
|
|
9535
9586
|
id: string;
|
|
@@ -9560,6 +9611,7 @@ export declare const DOMAIN_SCHEMAS: {
|
|
|
9560
9611
|
} | undefined;
|
|
9561
9612
|
extraBody?: unknown;
|
|
9562
9613
|
compat?: unknown;
|
|
9614
|
+
headers?: unknown;
|
|
9563
9615
|
quotaWeight?: number | undefined;
|
|
9564
9616
|
enabled?: boolean | undefined;
|
|
9565
9617
|
notes?: string | undefined;
|
|
@@ -9594,6 +9646,7 @@ export declare const DOMAIN_SCHEMAS: {
|
|
|
9594
9646
|
} | undefined;
|
|
9595
9647
|
extraBody?: Record<string, unknown> | undefined;
|
|
9596
9648
|
compat?: unknown;
|
|
9649
|
+
headers?: unknown;
|
|
9597
9650
|
notes?: string | undefined;
|
|
9598
9651
|
}, {
|
|
9599
9652
|
id: string;
|
|
@@ -9624,6 +9677,7 @@ export declare const DOMAIN_SCHEMAS: {
|
|
|
9624
9677
|
} | undefined;
|
|
9625
9678
|
extraBody?: unknown;
|
|
9626
9679
|
compat?: unknown;
|
|
9680
|
+
headers?: unknown;
|
|
9627
9681
|
quotaWeight?: number | undefined;
|
|
9628
9682
|
enabled?: boolean | undefined;
|
|
9629
9683
|
notes?: string | undefined;
|
|
@@ -9748,6 +9802,7 @@ export declare const DOMAIN_SCHEMAS: {
|
|
|
9748
9802
|
} | undefined;
|
|
9749
9803
|
extraBody?: Record<string, unknown> | undefined;
|
|
9750
9804
|
compat?: unknown;
|
|
9805
|
+
headers?: unknown;
|
|
9751
9806
|
notes?: string | undefined;
|
|
9752
9807
|
}[];
|
|
9753
9808
|
roles: Partial<Record<"default" | "summarize" | "subagent" | "team" | "synthesize" | "verifier" | "classifier" | "consolidate", {
|
|
@@ -9803,6 +9858,7 @@ export declare const DOMAIN_SCHEMAS: {
|
|
|
9803
9858
|
} | undefined;
|
|
9804
9859
|
extraBody?: unknown;
|
|
9805
9860
|
compat?: unknown;
|
|
9861
|
+
headers?: unknown;
|
|
9806
9862
|
quotaWeight?: number | undefined;
|
|
9807
9863
|
enabled?: boolean | undefined;
|
|
9808
9864
|
notes?: string | undefined;
|
|
@@ -9862,6 +9918,7 @@ export declare const DOMAIN_SCHEMAS: {
|
|
|
9862
9918
|
} | undefined;
|
|
9863
9919
|
extraBody?: Record<string, unknown> | undefined;
|
|
9864
9920
|
compat?: unknown;
|
|
9921
|
+
headers?: unknown;
|
|
9865
9922
|
notes?: string | undefined;
|
|
9866
9923
|
}[];
|
|
9867
9924
|
roles: Partial<Record<"default" | "summarize" | "subagent" | "team" | "synthesize" | "verifier" | "classifier" | "consolidate", {
|
|
@@ -9917,6 +9974,7 @@ export declare const DOMAIN_SCHEMAS: {
|
|
|
9917
9974
|
} | undefined;
|
|
9918
9975
|
extraBody?: unknown;
|
|
9919
9976
|
compat?: unknown;
|
|
9977
|
+
headers?: unknown;
|
|
9920
9978
|
quotaWeight?: number | undefined;
|
|
9921
9979
|
enabled?: boolean | undefined;
|
|
9922
9980
|
notes?: string | undefined;
|
package/dist/types.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
import { MAX_EXTRA_BODY_DEPTH } from "./hash.js"; // reconcile the extraBody depth cap with the hash cap (R18)
|
|
20
20
|
import { DispatchPolicy } from "./fleet.js"; // WorkerSpec.fleet.dispatch (scheduling framework D3)
|
|
21
|
+
import { carriesUserinfo, percentLayers } from "./secret-scan.js"; // package-private normalization + the userinfo judgement (not in the barrel)
|
|
21
22
|
// COARSE, best-effort literal-secret SHAPE detector (NOT exhaustive — the real boundary is the env-NAME schema
|
|
22
23
|
// on structured fields). Rejected in free-form value fields (worker `env`, model `extraBody`), URL components,
|
|
23
24
|
// AND as an env-NAME value (an AWS key id is a valid env-NAME charset yet is a secret VALUE, not a NAME). Case-
|
|
@@ -28,9 +29,10 @@ import { DispatchPolicy } from "./fleet.js"; // WorkerSpec.fleet.dispatch (sched
|
|
|
28
29
|
// which review flagged as a bypass (a private key misnamed `*_PUBKEY` skipped scanning entirely).
|
|
29
30
|
/** A private-key PEM header (`-----BEGIN [RSA |EC |OPENSSH |ENCRYPTED ]PRIVATE KEY`), unanchored — the ONE definition
|
|
30
31
|
* of that shape: {@link SECRET_LITERAL_RE} is composed from it (anchored, as one of its branches), and
|
|
31
|
-
* `redactSecrets` (config-fns) tests it ANYWHERE in a string. It carries spaces,
|
|
32
|
-
* {@link containsSecretToken} only sees it at the very start of a string; a PEM behind
|
|
33
|
-
* slipped past (S-712 codex review [high])
|
|
32
|
+
* `redactSecrets` (config-fns) tests it ANYWHERE in a string, on every display-side normalized form. It carries spaces,
|
|
33
|
+
* so the token-splitting detector {@link containsSecretToken} only sees it at the very start of a string; a PEM behind
|
|
34
|
+
* a newline or a `key: ` prefix slipped past (S-712 codex review [high]), and a `key=`-prefixed percent-encoded one
|
|
35
|
+
* hid it behind `%20` on the raw form, a form-urlencoded one behind `+` (6.3.0). Public keys / certificates never match. */
|
|
34
36
|
export const PRIVATE_KEY_PEM_HEADER = /-----BEGIN[A-Z0-9 ]*PRIVATE KEY/i;
|
|
35
37
|
export const SECRET_LITERAL_RE = new RegExp(`^(sk-|gh[opus]_|github_pat_|xox[baprse]-|xapp-|bearer\\s|basic\\s|eyJ[A-Za-z0-9_-]{10,}\\.|AKIA[0-9A-Z]{12,}|AIza[0-9A-Za-z_-]{20,}|${PRIVATE_KEY_PEM_HEADER.source})`, "i");
|
|
36
38
|
// 所有 *Env / *tokenEnv 字段的值是宿主机 env 变量的 NAME(不是值)。合法 env 变量名 + 拒绝 secret 形状的值(AKIA…
|
|
@@ -71,33 +73,17 @@ function scanForSecret(s) {
|
|
|
71
73
|
* fs path — so the worker dies at exec/boot, not at config edit. A NUL is never legitimate in a command / path
|
|
72
74
|
* / host / image, so reject it on every exec-bound string field (mcp stdio + the whole remoteExec contract). R20. */
|
|
73
75
|
export const hasNul = (s) => s.includes("\0");
|
|
74
|
-
/** Does `s` contain a secret-shaped token? Scans the raw string AND
|
|
75
|
-
* as `%73%6B
|
|
76
|
-
*
|
|
76
|
+
/** Does `s` contain a secret-shaped token? Scans every percent layer of it — the raw string AND up to three tolerant
|
|
77
|
+
* percent-decoded layers (a secret can hide as `%73%6B-…`; the ONE definition of those layers is `percentLayers` in the
|
|
78
|
+
* package-private `./secret-scan`) — so mcp args / dockerHost / URL components all get uniform percent-decode coverage.
|
|
79
|
+
* Tokenized + JWT-aware (see scanForSecret). Shared by NON_SECRET_URL + dockerHost + mcp stdio. Round-4/5/11. (6.3.0 moved
|
|
80
|
+
* the decode loop out, unchanged in result — same layers, same bound, same verdict — and made it exception-free; the
|
|
81
|
+
* display-side readings the redactor also judges are not this detector's.) We do NOT fail-closed on a bare `%` — that
|
|
82
|
+
* would false-reject legit values like `100%` / `%s`. A secret glued to a malformed escape (`%ZZsk-`) is an accepted
|
|
83
|
+
* coarse-heuristic gap (no consumer reconstructs it; the real guarantee is the env-NAME boundary on structured fields).
|
|
84
|
+
* R11/R13/R14. */
|
|
77
85
|
export function containsSecretToken(s) {
|
|
78
|
-
|
|
79
|
-
// → `sk-…`). TOLERANT decode: each valid %HH is decoded, a malformed escape (`%ZZ`, `%` at end) is left as-is
|
|
80
|
-
// so one bad escape elsewhere can't abort the whole decode (R13's all-or-nothing decodeURIComponent did). We
|
|
81
|
-
// do NOT fail-closed on a bare `%` — that would false-reject legit values like `100%` / `%s`. A secret glued
|
|
82
|
-
// to a malformed escape (`%ZZsk-`) is an accepted coarse-heuristic gap (no consumer reconstructs it; the real
|
|
83
|
-
// guarantee is the env-NAME boundary on structured fields). Bounded → deterministic. R11/R13/R14. */
|
|
84
|
-
let cur = s;
|
|
85
|
-
for (let i = 0; i < 4; i++) {
|
|
86
|
-
if (scanForSecret(cur))
|
|
87
|
-
return true;
|
|
88
|
-
const next = cur.replace(/%[0-9A-Fa-f]{2}/g, (m) => {
|
|
89
|
-
try {
|
|
90
|
-
return decodeURIComponent(m);
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
return m;
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
if (next === cur)
|
|
97
|
-
break;
|
|
98
|
-
cur = next;
|
|
99
|
-
}
|
|
100
|
-
return false;
|
|
86
|
+
return percentLayers(s).some((form) => scanForSecret(form));
|
|
101
87
|
}
|
|
102
88
|
/** Scan a URL's PATH + query + fragment (KEYS and values, decoded, tokenized) for a secret-shaped literal.
|
|
103
89
|
* Userinfo is handled separately (NON_SECRET_URL bans it; dockerHost scans the ssh user). Round-4/5. */
|
|
@@ -141,7 +127,7 @@ export const NON_SECRET_URL = z
|
|
|
141
127
|
.refine((s) => {
|
|
142
128
|
try {
|
|
143
129
|
const u = new URL(s);
|
|
144
|
-
if (!(u.protocol === "http:" || u.protocol === "https:") || !u.host || u
|
|
130
|
+
if (!(u.protocol === "http:" || u.protocol === "https:") || !u.host || carriesUserinfo(u))
|
|
145
131
|
return false;
|
|
146
132
|
return !urlQueryFragmentHasSecret(u);
|
|
147
133
|
}
|
|
@@ -215,9 +201,9 @@ export function extraBodyUnsafe(v) {
|
|
|
215
201
|
}
|
|
216
202
|
return false;
|
|
217
203
|
}
|
|
218
|
-
/** THE vetting pipeline for every FREE-FORM JSON value that lands in the persisted config — `ModelEntry.extraBody
|
|
219
|
-
* and `ModelEntry.
|
|
220
|
-
* free-form field would have copied it). The judgment is identical for every such field, so it lives in one place:
|
|
204
|
+
/** THE vetting pipeline for every FREE-FORM JSON value that lands in the persisted config — `ModelEntry.extraBody`,
|
|
205
|
+
* `ModelEntry.compat` and `ModelEntry.headers` share this ONE function (1.10.0; before that it was inline in `extraBody`
|
|
206
|
+
* and a second free-form field would have copied it). The judgment is identical for every such field, so it lives in one place:
|
|
221
207
|
*
|
|
222
208
|
* 1. getter-free descriptor walk ({@link extraBodyUnsafe}) over the RAW value — never invokes a getter, and any
|
|
223
209
|
* reflective throw (a hostile Proxy trap could throw a secret-bearing message) collapses into a FIXED message;
|
|
@@ -248,6 +234,22 @@ function vetFreeFormJson(v, field) {
|
|
|
248
234
|
return { ok: false, message: `${field}: its serialized form carries a secret` };
|
|
249
235
|
return { ok: true, value: clone };
|
|
250
236
|
}
|
|
237
|
+
/** The transform of every **parse-transparent free-form** key (`z.unknown().optional().transform(createFreeFormTransform(field))`:
|
|
238
|
+
* `ModelEntry.compat` / `ModelEntry.headers`) — ONE closure, so those keys cannot drift apart: absent stays absent (key not
|
|
239
|
+
* minted); anything else is the {@link vetFreeFormJson} safety judgment and nothing more (no SHAPE rule — whose job that is,
|
|
240
|
+
* per key, is {@link PARSE_TRANSPARENT_KEYS}). `extraBody` is not one of these: it has its own shape gate first. */
|
|
241
|
+
function createFreeFormTransform(field) {
|
|
242
|
+
return (v, ctx) => {
|
|
243
|
+
if (v === undefined)
|
|
244
|
+
return undefined;
|
|
245
|
+
const vetted = vetFreeFormJson(v, field);
|
|
246
|
+
if (!vetted.ok) {
|
|
247
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: vetted.message });
|
|
248
|
+
return z.NEVER;
|
|
249
|
+
}
|
|
250
|
+
return vetted.value;
|
|
251
|
+
};
|
|
252
|
+
}
|
|
251
253
|
/** A k8s resource quantity ("250m", "1", "0.5", "512Mi", "1Gi", "128M") — catches garbage at config-submit
|
|
252
254
|
* rather than surfacing as a reconcile ApplyError at the apiserver (consistent with ENV_NAME/DURATION guards). */
|
|
253
255
|
export const K8S_QUANTITY = z.string().regex(/^\d+(\.\d+)?(m|[kKMGTPE]i?)?$/, 'must be a k8s quantity like "250m", "512Mi", "1Gi"');
|
|
@@ -327,6 +329,13 @@ export const PARSE_TRANSPARENT_KEYS = [
|
|
|
327
329
|
validatedBy: "engine-ingest",
|
|
328
330
|
why: "镜像引擎的 per-model wire-compat 声明(thinkingFormat 等五键);那张词表随引擎走,本包复述一份必漂。",
|
|
329
331
|
},
|
|
332
|
+
{
|
|
333
|
+
domain: "models",
|
|
334
|
+
path: "models[].headers",
|
|
335
|
+
since: "6.4.0",
|
|
336
|
+
validatedBy: "consumer",
|
|
337
|
+
why: "部署 ↔ 供应商的 per-model 附加请求头;判据(头名闭集白名单 × 读它的线路 × 值形)的唯一属主是 server 的目录条目判据(坏声明 ⇒ 该条目整条不可用,不是丢头照常),本包复述一份那张表必漂、两处各一份就是两个写者。",
|
|
338
|
+
},
|
|
330
339
|
{
|
|
331
340
|
domain: "mcp",
|
|
332
341
|
path: "servers[].source",
|
|
@@ -438,16 +447,20 @@ export const ModelEntry = z.object({
|
|
|
438
447
|
compat: z
|
|
439
448
|
.unknown()
|
|
440
449
|
.optional()
|
|
441
|
-
.transform((
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
450
|
+
.transform(createFreeFormTransform("compat")),
|
|
451
|
+
/** Per-model extra request headers, deployment ↔ provider (e.g. `{"anthropic-beta": "context-1m-2025-08-07"}` on an
|
|
452
|
+
* `anthropic-messages` entry). Real need: a beta that a shell passes as `--betas` / `ANTHROPIC_BETAS` never reaches the
|
|
453
|
+
* request the engine sends; the catalog entry is the deployment-level seat for it — and without this key the entry's
|
|
454
|
+
* declaration is stripped at parse on every read leg, so it never reaches the consumer at all.
|
|
455
|
+
* ABSENT = key not minted = today's behaviour, zero migration.
|
|
456
|
+
*
|
|
457
|
+
* **Parse-transparent** — who validates the shape, and why not here: {@link PARSE_TRANSPARENT_KEYS}
|
|
458
|
+
* (`models.models[].headers`). The ONE rejection that remains is the free-form SAFETY judgment shared with `compat` /
|
|
459
|
+
* `extraBody` ({@link vetFreeFormJson}). */
|
|
460
|
+
headers: z
|
|
461
|
+
.unknown()
|
|
462
|
+
.optional()
|
|
463
|
+
.transform(createFreeFormTransform("headers")),
|
|
451
464
|
/** 配额倍率(EXPERT-REDESIGN §4-1):该模型烧 token 双窗额度的倍率,默认 1。折算时点=weight-at-burn
|
|
452
465
|
* (记账时固化到 UsageEntry.quotaWeightAtUse/weightedTokens):改倍率只影响此后记账,不回算窗口内历史。 */
|
|
453
466
|
quotaWeight: z.number().finite().nonnegative().default(1),
|
|
@@ -2053,7 +2066,7 @@ export const PLUGIN_ORIGIN = z
|
|
|
2053
2066
|
return true; // 已被上一条拒;避免对非 URL 再报一条噪声
|
|
2054
2067
|
try {
|
|
2055
2068
|
const u = new URL(s);
|
|
2056
|
-
return !u
|
|
2069
|
+
return !carriesUserinfo(u) && !urlQueryFragmentHasSecret(u);
|
|
2057
2070
|
}
|
|
2058
2071
|
catch {
|
|
2059
2072
|
return false; // claims to be https but does not parse → reject
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/settings-schema",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.0",
|
|
4
4
|
"description": "Sema settings schema — pure config contract (zod domains + effective-config + roster resolution + ref-integrity + remoteExec) shared by sema-registry, sema-server, and the TOC desktop/CLI. One source of truth. (Renamed at 1.0.0 from @sema-agent/registry-core — name and docs only, no behavior change; note the range moves 0.19.0 -> 1.0.0, so a dependency must become ^1.0.0. The old names @sema-agent/registry-core <=0.19.0 and @sema-ai/registry-core remain published, frozen, for migration.)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|