@sema-agent/settings-schema 14.6.0 → 14.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +475 -0
- package/README.md +89 -5
- package/conformance/VERSION +1 -1
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/expected.json +153 -0
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/input.json +23 -0
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/meta.json +5 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/expected.json +153 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/input.json +23 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/meta.json +5 -0
- package/dist/api/auth-bridge.d.ts +16 -2
- package/dist/api/auth-bridge.js +59 -24
- package/dist/api/credentials.d.ts +281 -0
- package/dist/api/credentials.js +149 -0
- package/dist/api/scopes.js +5 -1
- package/dist/atomic-write.d.ts +15 -0
- package/dist/atomic-write.js +109 -5
- package/dist/catalog-manages.d.ts +15 -0
- package/dist/catalog-manages.js +60 -0
- package/dist/center-refs.d.ts +68 -0
- package/dist/center-refs.js +70 -0
- package/dist/config-d-file.js +33 -15
- package/dist/config-dir-migration.js +1 -0
- package/dist/config-fns.js +147 -81
- package/dist/config-root-documents.d.ts +106 -0
- package/dist/config-root-documents.js +93 -0
- package/dist/config-root-node.d.ts +117 -0
- package/dist/config-root-node.js +260 -0
- package/dist/conformance/canonical.js +4 -4
- package/dist/conformance/contract-adapter.js +26 -1
- package/dist/conformance/corpus.js +4 -4
- package/dist/conformance/derive.js +2 -2
- package/dist/conformance/run.d.ts +3 -0
- package/dist/conformance/run.js +53 -12
- package/dist/conformance/temp-roots.d.ts +31 -0
- package/dist/conformance/temp-roots.js +184 -0
- package/dist/cross-session.d.ts +4 -4
- package/dist/declared-capture.d.ts +41 -0
- package/dist/declared-capture.js +344 -0
- package/dist/default-model.d.ts +1 -1
- package/dist/default-model.js +5 -4
- package/dist/env-model-table.d.ts +163 -0
- package/dist/env-model-table.js +257 -0
- package/dist/file-edit.js +11 -15
- package/dist/fleet.d.ts +36 -7
- package/dist/fleet.js +83 -32
- package/dist/hash.d.ts +0 -9
- package/dist/hash.js +64 -7
- package/dist/hooks-config.js +18 -3
- package/dist/http-hook-allowlists.d.ts +22 -9
- package/dist/http-hook-allowlists.js +169 -91
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/mcp-request-entry.d.ts +67 -13
- package/dist/mcp-request-entry.js +132 -93
- package/dist/mcp-server-name.d.ts +17 -0
- package/dist/mcp-server-name.js +19 -0
- package/dist/migration-receipt.d.ts +10 -6
- package/dist/migration-receipt.js +5 -3
- package/dist/node.d.ts +1 -0
- package/dist/node.js +2 -0
- package/dist/one-time-migrations.d.ts +4 -2
- package/dist/one-time-migrations.js +236 -96
- package/dist/own-key-read.d.ts +247 -0
- package/dist/own-key-read.js +449 -0
- package/dist/plain-json-object.d.ts +11 -1
- package/dist/plain-json-object.js +37 -1
- package/dist/remote-exec.js +9 -1
- package/dist/retired-form.d.ts +19 -0
- package/dist/retired-form.js +24 -1
- package/dist/safety-merge-spec.d.ts +6 -6
- package/dist/safety-merge-spec.js +50 -37
- package/dist/sandbox-git.d.ts +64 -0
- package/dist/sandbox-git.js +89 -0
- package/dist/scheduler-store.js +19 -11
- package/dist/settings-units.js +14 -12
- package/dist/tier-words.d.ts +33 -0
- package/dist/tier-words.js +43 -0
- package/dist/types.d.ts +12 -2
- package/dist/types.js +35 -6
- package/dist/url-shape.d.ts +16 -0
- package/dist/url-shape.js +18 -0
- package/dist/web-admin-credential.js +5 -2
- package/dist/web-admin-session-url.js +2 -2
- package/dist/worker-row-verdict.d.ts +11 -0
- package/dist/worker-row-verdict.js +58 -2
- package/dist/workers-rows.js +38 -10
- package/dist/zod-unwrap.d.ts +6 -0
- package/dist/zod-unwrap.js +14 -7
- package/docs/API-SURFACE.md +100 -5
- package/docs/CONSUMER-INTEGRATION.md +250 -2
- package/llms-full.txt +26 -4
- package/llms.txt +11 -4
- package/package.json +2 -2
- package/dist/definition-copy.d.ts +0 -23
- package/dist/definition-copy.js +0 -52
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* env 模型表 —— `MODEL_ID` / `MODEL_CHEAP_ID` 两槽与它们铸出的角色底 —— 的**一份读法**,与写者在写 `config.d/models.json` 之前调的**计划助手**
|
|
3
|
+
* (纯、零 I/O、浏览器安全;读配置根上的决定文档的函数在 `./node`)。
|
|
4
|
+
*
|
|
5
|
+
* ## 律
|
|
6
|
+
* · **env 两槽只在这里读一次**({@link readEnvModelSlots}):`MODEL_ID` 缺席或空串 = 没有主槽(纯空白算在场 —— 与引擎的 env 路由读口同尺);
|
|
7
|
+
* cheap 槽四态({@link EnvCheapSlot})。「env 模型表已声明」({@link envModelTableDeclared})与角色底({@link buildEnvModelBaseline})都由它派生 ——
|
|
8
|
+
* 这道闸的执行方(引擎一侧;今天引擎还不强制)与写者的计划助手调同一份(一次性迁移 SS-M11 只用两槽点名被顶替的 env 模型,铸不铸不看它),不各写一份。env 快照按「读到即算」(下标读;原型分层的快照里继承来的键算在场)。
|
|
9
|
+
* 引擎另有「名册占位 ⇒ env 表算空」一格,那是引擎自己的部署事实,不在本包。
|
|
10
|
+
* · **计划助手判序**({@link planEnvModelTableWrite}):目标腿不读 `config.d`(remote / none)⇒ 照写 → 候选不管模型面 ⇒ 照写 → summarize 会悬空 ⇒
|
|
11
|
+
* `fix_first`(不可确认:每个任务都要解 summarize,采纳后任务全拒)→ env 表未声明 ⇒ 照写、**不写确认** → 配置根的决定是 `replaced` ⇒ 照写 →
|
|
12
|
+
* 调用方带回的确认键等于这一次的 ⇒ `write_with_decision`(先经 `writeEnvModelsDecision` 写 `replaced`,再写目录;两次写在写者对 models.json 的同一把锁里)→
|
|
13
|
+
* 否则 `confirm`(把回执给用户看)。执法在引擎一侧(本地腿的顶替闸;今天引擎不拒这个组),本助手只是让写者在写之前知道这道闸强制起来会不会拒、会顶掉什么。
|
|
14
|
+
* · **回执按 id**:每个退场的 env 条目带 `servedAs`(候选里同 id 的启用条目名 —— 上游模型仍被服务,形可能不同);每个 env 底角色带写前绑谁
|
|
15
|
+
* (env 表键 + 它解到的 id)与采纳后落到哪个条目(`{name, id}`)。缺省落点经 `resolveCatalogDefaultModel`(与引擎的 stage 同一只解析器);
|
|
16
|
+
* 绑到停用条目或目录外的名 = `dangles`。确认键 = 回执的定序 JSON —— 后果(落点名 / id、env 槽、决定读数)一变,旧键就过期。
|
|
17
|
+
* · 可用性(引擎的 brain 腿、端点、读腿逐条丢)计划判不了;决定性的 summarize 那一格由引擎的不变量兜。
|
|
18
|
+
*/
|
|
19
|
+
import { z } from "zod";
|
|
20
|
+
import { entryEnabledFlag, catalogManagesModelPlane } from "./catalog-manages.js";
|
|
21
|
+
import { parseDomainSalvaged, isDisclosableIdentity, resolveActiveTiers, withheldNameToken } from "./config-fns.js";
|
|
22
|
+
import { ENV_MODELS_DOCUMENT } from "./config-root-documents.js";
|
|
23
|
+
import { resolveCatalogDefaultModel } from "./default-model.js";
|
|
24
|
+
import { SUMMARIZE_MODEL_MISSING_CODE } from "./fleet.js";
|
|
25
|
+
import { landCatalogWord } from "./tier-words.js";
|
|
26
|
+
import { stableStringify } from "./hash.js";
|
|
27
|
+
import { captureJson, dataDescriptor, isolatedObject } from "./own-key-read.js";
|
|
28
|
+
import { ROSTER_CHEAP_ROLES, ROSTER_PRIMARY_ROLES } from "./resolve-roster.js";
|
|
29
|
+
/** env 模型表的键名(闭集)。 */
|
|
30
|
+
export const ENV_MODEL_TABLE_KEYS = { mainId: "MODEL_ID", cheapId: "MODEL_CHEAP_ID", verifier: "MODEL_VERIFIER", codeRoles: "MODEL_CODE_ROLES" };
|
|
31
|
+
/** env 表里主模型的别名键(角色底的 `default` 绑它;引擎 env 表的条目键 = 这个别名 + 主 id + cheap id)。 */
|
|
32
|
+
export const DEFAULT_MODEL_KEY = "default";
|
|
33
|
+
/** cheap 槽铸出来的 id(不铸 ⇒ `undefined`)。 */
|
|
34
|
+
export function mintedCheapId(slot) {
|
|
35
|
+
switch (slot.kind) {
|
|
36
|
+
case "minted":
|
|
37
|
+
return slot.id;
|
|
38
|
+
case "absent":
|
|
39
|
+
case "without_main":
|
|
40
|
+
case "same_as_main":
|
|
41
|
+
return undefined;
|
|
42
|
+
default: {
|
|
43
|
+
const unhandled = slot;
|
|
44
|
+
throw new Error(`mintedCheapId: unhandled slot ${JSON.stringify(unhandled)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* 包内(不进包根):把 env 模型表的键(缺省四把;调用方只要两槽时只拍两把)**读一次**进一份快照(下标读,同「读到即算」)。计划助手与一次性迁移在**要用的那一刻**拍、
|
|
50
|
+
* 此后的每个读法都答自同一份 —— 一只 getter 两次读答两个值时,判「env 表已声明」与列 env 模型不会各读到一个(那样结局与回执自相矛盾);用不到的键不读。
|
|
51
|
+
*/
|
|
52
|
+
export function snapshotEnvModelTable(env, keys = Object.values(ENV_MODEL_TABLE_KEYS)) {
|
|
53
|
+
const snapshot = isolatedObject(); // the package's copy container (no inherited members to read)
|
|
54
|
+
for (const key of keys)
|
|
55
|
+
Object.defineProperty(snapshot, key, dataDescriptor(env[key]));
|
|
56
|
+
return snapshot;
|
|
57
|
+
}
|
|
58
|
+
/** env 两槽的**唯一读口**(见文件头)。`mainId` = `MODEL_ID`(空串 = 没有主槽)。 */
|
|
59
|
+
export function readEnvModelSlots(env) {
|
|
60
|
+
const mainId = env[ENV_MODEL_TABLE_KEYS.mainId] ?? "";
|
|
61
|
+
const rawCheapId = env[ENV_MODEL_TABLE_KEYS.cheapId];
|
|
62
|
+
const cheapSlot = !rawCheapId
|
|
63
|
+
? { kind: "absent" }
|
|
64
|
+
: mainId === ""
|
|
65
|
+
? { kind: "without_main", id: rawCheapId }
|
|
66
|
+
: rawCheapId === mainId
|
|
67
|
+
? { kind: "same_as_main", id: rawCheapId }
|
|
68
|
+
: { kind: "minted", id: rawCheapId };
|
|
69
|
+
return { mainId, cheapSlot };
|
|
70
|
+
}
|
|
71
|
+
/** **env 模型表已声明吗**(= `MODEL_ID` 在场且非空串;纯空白算在场)—— 引擎一侧的顶替闸(今天还不强制)与计划助手共用的那一只谓词。 */
|
|
72
|
+
export function envModelTableDeclared(env) {
|
|
73
|
+
return readEnvModelSlots(env).mainId !== "";
|
|
74
|
+
}
|
|
75
|
+
/** 逗号串:逐项 trim、丢空项。 */
|
|
76
|
+
const csv = (raw) => (raw ?? "")
|
|
77
|
+
.split(",")
|
|
78
|
+
.map((s) => s.trim())
|
|
79
|
+
.filter(Boolean);
|
|
80
|
+
/**
|
|
81
|
+
* env 模型表的**角色底**(纯数据):条目(主槽在 ⇒ 主;cheap 槽铸了 ⇒ cheap)、角色(verifier ⇒ `MODEL_VERIFIER` 指 env 表里的键时是它、否则 `default`;
|
|
82
|
+
* 主角色族(`ROSTER_PRIMARY_ROLES`)⇒ `default`;cheap 角色族(`ROSTER_CHEAP_ROLES`)⇒ cheap id,cheap 没铸 ⇒ `default`;`MODEL_CODE_ROLES` 点名的、角色底自己有的键
|
|
83
|
+
* 加开发模式提示词标记)。env 表键 = 别名 `default` + 主 id + cheap id(主槽不在 ⇒ 表空)。
|
|
84
|
+
*/
|
|
85
|
+
export function buildEnvModelBaseline(env) {
|
|
86
|
+
const { mainId, cheapSlot } = readEnvModelSlots(env);
|
|
87
|
+
const cheapId = mintedCheapId(cheapSlot);
|
|
88
|
+
const entries = mainId === "" ? [] : [{ id: mainId, slot: "main", env: ENV_MODEL_TABLE_KEYS.mainId }, ...(cheapId === undefined ? [] : [{ id: cheapId, slot: "cheap", env: ENV_MODEL_TABLE_KEYS.cheapId }])];
|
|
89
|
+
const tableKeys = new Set(mainId === "" ? [] : [DEFAULT_MODEL_KEY, mainId, ...(cheapId === undefined ? [] : [cheapId])]);
|
|
90
|
+
const rawVerifier = env[ENV_MODEL_TABLE_KEYS.verifier];
|
|
91
|
+
const verifier = !rawVerifier ? { kind: "unset" } : tableKeys.has(rawVerifier) ? { kind: "bound", ref: rawVerifier } : { kind: "rejected", raw: rawVerifier };
|
|
92
|
+
const roles = new Map([["verifier", verifier.kind === "bound" ? verifier.ref : DEFAULT_MODEL_KEY]]);
|
|
93
|
+
for (const r of ROSTER_PRIMARY_ROLES)
|
|
94
|
+
roles.set(r, DEFAULT_MODEL_KEY);
|
|
95
|
+
for (const r of ROSTER_CHEAP_ROLES)
|
|
96
|
+
roles.set(r, cheapId ?? DEFAULT_MODEL_KEY);
|
|
97
|
+
for (const name of csv(env[ENV_MODEL_TABLE_KEYS.codeRoles])) {
|
|
98
|
+
const role = name;
|
|
99
|
+
const cur = roles.get(role);
|
|
100
|
+
if (cur === undefined)
|
|
101
|
+
continue; // 只装饰角色底自己有的键(Map 没有原型上的名字)
|
|
102
|
+
roles.set(role, { model: typeof cur === "string" ? cur : cur.model, promptRole: "code" });
|
|
103
|
+
}
|
|
104
|
+
return { entries, roles: Object.fromEntries(roles), verifier };
|
|
105
|
+
}
|
|
106
|
+
const ENV_MODEL_TABLE_TARGETS = ["local", "remote", "none"];
|
|
107
|
+
const decisionKindOf = (decision) => {
|
|
108
|
+
const kind = decision?.kind;
|
|
109
|
+
switch (kind) {
|
|
110
|
+
case "absent":
|
|
111
|
+
case "replaced":
|
|
112
|
+
case "kept":
|
|
113
|
+
case "unreadable":
|
|
114
|
+
return kind;
|
|
115
|
+
default:
|
|
116
|
+
throw new TypeError(`planEnvModelTableWrite: decision must be a readEnvModelsDecision answer (got kind ${JSON.stringify(kind)})`);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
/** 候选按引擎本地读腿的视图读(逐条抢救;整份读不进 ⇒ 抛 TypeError —— 引擎读不进的文档,本助手不替它算回执,先修文档)。 */
|
|
120
|
+
function candidateView(candidate) {
|
|
121
|
+
try {
|
|
122
|
+
return parseDomainSalvaged("models", candidate);
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
const first = e instanceof z.ZodError ? e.issues[0] : undefined;
|
|
126
|
+
const why = first === undefined ? (e instanceof Error ? e.message : String(e)) : `${first.path.join(".") || "(document)"}: ${first.message}`;
|
|
127
|
+
throw new TypeError(`planEnvModelTableWrite: the candidate does not read as a models document — fix it first (${why})`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function buildReceipt(baseline, view, decision) {
|
|
131
|
+
const enabled = view.models.filter((m) => entryEnabledFlag(m) !== false).map((m) => ({ name: m.name, id: m.id }));
|
|
132
|
+
const byName = new Map(enabled.map((e) => [e.name, e]));
|
|
133
|
+
const tiers = resolveActiveTiers(view);
|
|
134
|
+
/** 一个名采纳后落到哪条:经与引擎同一张档位展开(目录键先赢;env 底继承来的 `flash` 一类档位词按活跃组解)。 */
|
|
135
|
+
const land = (word) => {
|
|
136
|
+
const name = landCatalogWord(word, (n) => byName.has(n), tiers);
|
|
137
|
+
const e = name === undefined ? undefined : byName.get(name);
|
|
138
|
+
return e === undefined ? undefined : { name: e.name, id: e.id };
|
|
139
|
+
};
|
|
140
|
+
const main = baseline.entries.find((e) => e.slot === "main");
|
|
141
|
+
const idOf = (ref) => (ref === DEFAULT_MODEL_KEY ? (main?.id ?? null) : (baseline.entries.find((e) => e.id === ref)?.id ?? null));
|
|
142
|
+
/** 目录缺省的落点 —— 恒是 `resolveCatalogDefaultModel` 的答案(与引擎的 stage 同一只解析器;它的第一级就是候选自己的 `roles.default`,点名停用条目时它落到下一级)。
|
|
143
|
+
* `bound` = 候选自己绑了 `default`:解析器用的正是那一级 ⇒ `candidate_binds`,否则 `catalog_default`。 */
|
|
144
|
+
const viaCatalogDefault = (bound) => {
|
|
145
|
+
const fallback = enabled[0];
|
|
146
|
+
if (fallback === undefined)
|
|
147
|
+
return { kind: "dangles", landing: null };
|
|
148
|
+
const d = resolveCatalogDefaultModel(view, (n) => byName.has(n), fallback.name);
|
|
149
|
+
if (d.kind === "select")
|
|
150
|
+
return { kind: bound ? "candidate_binds" : "catalog_default", landing: null };
|
|
151
|
+
const l = land(d.name);
|
|
152
|
+
if (l === undefined)
|
|
153
|
+
return { kind: "dangles", landing: null };
|
|
154
|
+
return { kind: bound && d.source === "roles.default" ? "candidate_binds" : "catalog_default", landing: l };
|
|
155
|
+
};
|
|
156
|
+
const afterOf = (role, ref) => {
|
|
157
|
+
const bound = Object.hasOwn(view.roles, role) ? view.roles[role] : undefined;
|
|
158
|
+
if (role === DEFAULT_MODEL_KEY)
|
|
159
|
+
return viaCatalogDefault(bound !== undefined); // the default role lands where the catalog default resolves, bound or not
|
|
160
|
+
if (bound !== undefined) {
|
|
161
|
+
if (!("model" in bound))
|
|
162
|
+
return { kind: "candidate_binds", landing: null };
|
|
163
|
+
const l = land(bound.model);
|
|
164
|
+
return l === undefined ? { kind: "dangles", landing: null } : { kind: "candidate_binds", landing: l };
|
|
165
|
+
}
|
|
166
|
+
if (ref === DEFAULT_MODEL_KEY)
|
|
167
|
+
return viaCatalogDefault(false);
|
|
168
|
+
const l = land(ref);
|
|
169
|
+
return l === undefined ? { kind: "dangles", landing: null } : { kind: "candidate_entry", landing: l };
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
decision,
|
|
173
|
+
envModels: baseline.entries.map((e) => ({ id: e.id, slot: e.slot, env: e.env, servedAs: enabled.filter((c) => c.id === e.id).map((c) => c.name) })),
|
|
174
|
+
roles: Object.entries(baseline.roles).map(([role, spec]) => {
|
|
175
|
+
const ref = typeof spec === "string" ? spec : spec.model;
|
|
176
|
+
const promptRole = typeof spec === "string" ? undefined : spec.promptRole;
|
|
177
|
+
return { role, before: { ref, id: idOf(ref) }, ...(promptRole === undefined ? {} : { promptRole }), after: afterOf(role, ref) };
|
|
178
|
+
}),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* 写者在写 `config.d/models.json` 之前调的**计划**(判序见文件头)。纯函数:不读环境、不读盘、不改入参。
|
|
183
|
+
* 目标腿 / 决定读数不在闭集 ⇒ `TypeError`;候选读不出(撤销的 Proxy 等)或整份读不进 models 域(重名、悬空的角色名 …)⇒ `TypeError`(先修文档)。
|
|
184
|
+
*/
|
|
185
|
+
export function planEnvModelTableWrite(input) {
|
|
186
|
+
const decision = decisionKindOf(input.decision);
|
|
187
|
+
const target = input.target;
|
|
188
|
+
if (!ENV_MODEL_TABLE_TARGETS.includes(target))
|
|
189
|
+
throw new TypeError(`planEnvModelTableWrite: target must be one of ${ENV_MODEL_TABLE_TARGETS.join(" / ")} (got ${JSON.stringify(target)})`);
|
|
190
|
+
switch (target) {
|
|
191
|
+
case "remote":
|
|
192
|
+
case "none":
|
|
193
|
+
return { kind: "write", why: "not_read" };
|
|
194
|
+
case "local":
|
|
195
|
+
break;
|
|
196
|
+
default: {
|
|
197
|
+
const unhandled = target;
|
|
198
|
+
throw new TypeError(`planEnvModelTableWrite: unhandled target ${JSON.stringify(unhandled)}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const copy = captureJson(input.candidate);
|
|
202
|
+
if (!copy.ok)
|
|
203
|
+
throw new TypeError(`planEnvModelTableWrite: the candidate could not be read — ${copy.sentence}`);
|
|
204
|
+
if (!catalogManagesModelPlane(copy.value))
|
|
205
|
+
return { kind: "write", why: "not_managing" };
|
|
206
|
+
const env = snapshotEnvModelTable(input.env); // read once: the receipt and the env-table rule answer from the same reading
|
|
207
|
+
const receipt = buildReceipt(buildEnvModelBaseline(env), candidateView(copy.value), decision);
|
|
208
|
+
if (receipt.roles.find((r) => r.role === "summarize")?.after.kind === "dangles")
|
|
209
|
+
return { kind: "fix_first", code: SUMMARIZE_MODEL_MISSING_CODE, receipt };
|
|
210
|
+
if (!envModelTableDeclared(env))
|
|
211
|
+
return { kind: "write", why: "env_table_empty" };
|
|
212
|
+
if (decision === "replaced")
|
|
213
|
+
return { kind: "write", why: "acknowledged", receipt };
|
|
214
|
+
const confirmKey = stableStringify(receipt);
|
|
215
|
+
const intent = input.intent;
|
|
216
|
+
if (intent !== undefined && intent.kind === "replace" && intent.confirmKey === confirmKey)
|
|
217
|
+
return { kind: "write_with_decision", receipt };
|
|
218
|
+
return { kind: "confirm", receipt, confirmKey, intent: intent === undefined ? "absent" : "stale" };
|
|
219
|
+
}
|
|
220
|
+
// ── 拒句投影 ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
221
|
+
/** 句子里的一个名 / id:可披露 ⇒ 原样(加引号);否则 ⇒ 不可逆记号(秘密形不出门)。 */
|
|
222
|
+
const shown = (v) => (isDisclosableIdentity(v) ? JSON.stringify(v) : withheldNameToken(v));
|
|
223
|
+
const landingText = (after) => {
|
|
224
|
+
if (after.landing !== null)
|
|
225
|
+
return `${shown(after.landing.name)} (id ${shown(after.landing.id)})`;
|
|
226
|
+
return after.kind === "dangles" ? "nothing (no enabled catalog entry)" : "an entry the engine selects ({select})";
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* 计划停下来的那两种结局(`confirm` / `fix_first`)的**一句话**(三端同一句;只用回执里的 env 键名、目录名与模型 id —— 可披露的原样,秘密形的打成记号;凭据与别的 env 值从不进句子)。
|
|
230
|
+
* 写类结局不是拒 ⇒ `TypeError`。
|
|
231
|
+
*/
|
|
232
|
+
export function envModelTableWriteRefusal(plan) {
|
|
233
|
+
const file = `${ENV_MODELS_DOCUMENT}.json`;
|
|
234
|
+
const kind = plan.kind;
|
|
235
|
+
switch (kind) {
|
|
236
|
+
case "confirm": {
|
|
237
|
+
const r = plan.receipt;
|
|
238
|
+
const models = r.envModels.map((e) => `${e.env} ${shown(e.id)} (${e.slot} slot) ${e.servedAs.length > 0 ? `stays served as ${e.servedAs.map(shown).join(", ")}` : "stops being served"}`).join("; ");
|
|
239
|
+
const roles = r.roles.map((row) => `${row.role} → ${landingText(row.after)}`).join(", ");
|
|
240
|
+
const decision = r.decision === "unreadable"
|
|
241
|
+
? ` The config root's ${file} is unreadable; confirming overwrites it as a whole.`
|
|
242
|
+
: r.decision === "kept"
|
|
243
|
+
? ` The config root's ${file} says "kept"; confirming overwrites it.`
|
|
244
|
+
: "";
|
|
245
|
+
return (`writing this models document replaces the env model table on the local config leg: ${models}; after adoption ${roles}.${decision} ` +
|
|
246
|
+
`To let the catalog replace them, confirm — the writer records ${file} = {"envModels":"replaced"} in the config root first, then writes the models document; to keep the env models, do not write it.`);
|
|
247
|
+
}
|
|
248
|
+
case "fix_first": {
|
|
249
|
+
const row = plan.receipt.roles.find((x) => x.role === "summarize");
|
|
250
|
+
const ref = row === undefined ? "" : ` (env binding ${shown(row.before.ref)})`;
|
|
251
|
+
return (`this models document leaves the summarize role unresolved${ref}: it lands on no enabled catalog entry, so every task would be refused after adoption (${SUMMARIZE_MODEL_MISSING_CODE}). ` +
|
|
252
|
+
"Bind roles.summarize to an enabled entry of the document, or add an enabled entry under the name it points at, then plan the write again.");
|
|
253
|
+
}
|
|
254
|
+
default:
|
|
255
|
+
throw new TypeError(`envModelTableWriteRefusal: only a "confirm" or "fix_first" plan is a refusal (got ${JSON.stringify(kind)})`);
|
|
256
|
+
}
|
|
257
|
+
}
|
package/dist/file-edit.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { mkdir } from "node:fs/promises";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { writeFileAtomic } from "./atomic-write.js";
|
|
11
|
+
import { emptyDocument, guardedSafeParse } from "./own-key-read.js";
|
|
11
12
|
import { collectEnvRefs } from "./secret-refs.js";
|
|
12
13
|
import { REMOTE_EXEC_DOCUMENT, RemoteExecSpec } from "./remote-exec.js";
|
|
13
14
|
import { configDDocumentText } from "./config-d-file.js";
|
|
@@ -33,28 +34,23 @@ const DOMAINS_LIST = Object.keys(DOMAIN_SCHEMAS).join(", ");
|
|
|
33
34
|
* (clear message) if `domain` is not a real domain name (a misuse, not invalid user data). */
|
|
34
35
|
export function validateDomain(domain, raw) {
|
|
35
36
|
assertDomainName(domain);
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
catch {
|
|
37
|
+
// explicit null is corrupt, not defaults (R4); the first read of `error` sits in the same guard as `safeParse` (zod 4.6 builds the sentences on that read)
|
|
38
|
+
const schema = DOMAIN_SCHEMAS[domain];
|
|
39
|
+
const judged = guardedSafeParse(() => schema.safeParse(raw === undefined ? emptyDocument() : raw), sanitizeZodError);
|
|
40
|
+
if (!judged.ok) {
|
|
41
41
|
// a hostile getter/Proxy trap threw during traversal — never surface its (secret-bearing) message. R13.
|
|
42
42
|
return { ok: false, issues: [{ code: "custom", ...buildQuotedIssue `invalid "${fixed(domain)}" config (a value could not be read during validation)`, path: [] }] };
|
|
43
43
|
}
|
|
44
|
-
if (
|
|
45
|
-
return { ok: true, value:
|
|
46
|
-
return { ok: false, issues:
|
|
44
|
+
if (judged.value.success)
|
|
45
|
+
return { ok: true, value: judged.value.data };
|
|
46
|
+
return { ok: false, issues: judged.value.error.issues }; // redacted inside the guard (R13/R14) — no secret echo
|
|
47
47
|
}
|
|
48
48
|
/** Same, for the remote-exec contract (a standalone contract, not a DOMAIN_SCHEMAS member). */
|
|
49
49
|
export function validateRemoteExec(raw) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
r = RemoteExecSpec.safeParse(raw);
|
|
53
|
-
}
|
|
54
|
-
catch {
|
|
50
|
+
const judged = guardedSafeParse(() => RemoteExecSpec.safeParse(raw), sanitizeZodError);
|
|
51
|
+
if (!judged.ok)
|
|
55
52
|
return { ok: false, issues: [{ code: "custom", ...buildQuotedIssue `invalid remoteExec (a value could not be read during validation)`, path: [] }] };
|
|
56
|
-
}
|
|
57
|
-
return r.success ? { ok: true, value: r.data } : { ok: false, issues: sanitizeZodError(r.error).issues }; // no secret echo (R14)
|
|
53
|
+
return judged.value.success ? { ok: true, value: judged.value.data } : { ok: false, issues: judged.value.error.issues }; // redacted inside the guard (R14)
|
|
58
54
|
}
|
|
59
55
|
/** Absolute path to a domain's JSON file under a config root (mirrors FileConfigStore.domainPath). Guards the
|
|
60
56
|
* name (no `../../secret` traversal even if a caller bypasses the DomainName type). */
|
package/dist/fleet.d.ts
CHANGED
|
@@ -163,9 +163,15 @@ export type RestartSignal = z.infer<typeof RestartSignal>;
|
|
|
163
163
|
* `config.model_refused_name_minted` — a refused entry's name would be re-minted onto another entry by the
|
|
164
164
|
* tier / alias expansion (or the dropped-entry carrier cannot prove it would not be);
|
|
165
165
|
* `config.model_route_endpoint_missing` — an openai-compatible route of the candidate resolves to no endpoint;
|
|
166
|
-
* `config.gateway_fallback_without_primary` — fallback gateways are declared without a primary
|
|
167
|
-
*
|
|
168
|
-
*
|
|
166
|
+
* `config.gateway_fallback_without_primary` — fallback gateways are declared without a primary;
|
|
167
|
+
* `config.catalog_replaces_env_models` ({@link CATALOG_REPLACES_ENV_MODELS_CODE}) — on the LOCAL config leg, a candidate that
|
|
168
|
+
* manages the model plane would replace a declared env model table (`MODEL_ID` …) while the config root's decision document
|
|
169
|
+
* (`config.env-models.json`, read by `readEnvModelsDecision`) does not say `replaced` — the env models keep serving;
|
|
170
|
+
* `config.summarize_model_missing` ({@link SUMMARIZE_MODEL_MISSING_CODE}) — the summarize role, which every task resolves,
|
|
171
|
+
* would land on no servable entry (every task would be refused after adoption).
|
|
172
|
+
* Source (engine): the first two in `src/model-entry-refusal.ts`, the next two in `src/model-route-endpoint.ts:40,42`
|
|
173
|
+
* (invariant `model-route-endpoint`, judged by the model-plane stage in `src/config-center/apply-effective.ts:664-665`); the
|
|
174
|
+
* last two are named HERE first (the code is a contract word: this package names it and exports the two constants for the engine to read; the engine does not mint them today).
|
|
169
175
|
*/
|
|
170
176
|
export declare const MODEL_ENTRY_REFUSAL_CODES: readonly ["config.model_no_brain", "config.model_entry_invalid"];
|
|
171
177
|
export declare const ModelEntryRefusalCode: z.ZodEnum<{
|
|
@@ -173,8 +179,14 @@ export declare const ModelEntryRefusalCode: z.ZodEnum<{
|
|
|
173
179
|
"config.model_entry_invalid": "config.model_entry_invalid";
|
|
174
180
|
}>;
|
|
175
181
|
export type ModelEntryRefusalCode = z.infer<typeof ModelEntryRefusalCode>;
|
|
176
|
-
|
|
182
|
+
/** The env-model-table confirmation gate's group-refusal code (see {@link MODEL_PLANE_REFUSAL_CODES}). */
|
|
183
|
+
export declare const CATALOG_REPLACES_ENV_MODELS_CODE: "config.catalog_replaces_env_models";
|
|
184
|
+
/** The summarize invariant's group-refusal code (see {@link MODEL_PLANE_REFUSAL_CODES}); a writer's plan answers `fix_first` with it. */
|
|
185
|
+
export declare const SUMMARIZE_MODEL_MISSING_CODE: "config.summarize_model_missing";
|
|
186
|
+
export declare const MODEL_PLANE_REFUSAL_CODES: readonly ["config.default_model_missing", "config.model_refused_name_minted", "config.model_route_endpoint_missing", "config.gateway_fallback_without_primary", "config.catalog_replaces_env_models", "config.summarize_model_missing"];
|
|
177
187
|
export declare const ModelPlaneRefusalCode: z.ZodEnum<{
|
|
188
|
+
"config.catalog_replaces_env_models": "config.catalog_replaces_env_models";
|
|
189
|
+
"config.summarize_model_missing": "config.summarize_model_missing";
|
|
178
190
|
"config.default_model_missing": "config.default_model_missing";
|
|
179
191
|
"config.model_refused_name_minted": "config.model_refused_name_minted";
|
|
180
192
|
"config.model_route_endpoint_missing": "config.model_route_endpoint_missing";
|
|
@@ -548,6 +560,17 @@ export declare const FLEET_GENERATION_ENV = "FLEET_GENERATION";
|
|
|
548
560
|
* {@link FLEET_GENERATION_ENV} it is a reserved worker env name (`WorkerSpec.env` / `secretEnv` cannot set it).
|
|
549
561
|
*/
|
|
550
562
|
export declare const FLEET_SCOPE_ENV = "FLEET_SCOPE";
|
|
563
|
+
/**
|
|
564
|
+
* The env name a fleet member's advertise address travels under — the base URL this instance tells the control plane it can be
|
|
565
|
+
* reached at; the control plane appends request paths to it. The operator or the config renderer sets it (on Kubernetes the renderer
|
|
566
|
+
* writes a `$(POD_IP)` placeholder in the value and the kubelet expands it), and the engine reads it back. Both sides import THIS
|
|
567
|
+
* constant; neither spells the name. The address is a base that request paths are appended to, so a reader judges a present, non-empty
|
|
568
|
+
* value with `pathBaseUrlFlaws` (main barrel) — the one judge of every such seat, which forbids a credential, a
|
|
569
|
+
* query and a fragment there and allows a path prefix; judge the value AFTER the deployment has expanded its placeholders. Absent or
|
|
570
|
+
* the empty string means "no address declared" (the `${NAME:-}` idiom of a compose file), and that reading is the caller's, not the
|
|
571
|
+
* judge's.
|
|
572
|
+
*/
|
|
573
|
+
export declare const FLEET_ADVERTISE_ADDRESS_ENV = "FLEET_ADVERTISE_ADDRESS";
|
|
551
574
|
/**
|
|
552
575
|
* The serving lease — the ONE fence between a superseded generation and the traffic it must stop taking. A fleet-member
|
|
553
576
|
* engine renews it with web-admin; web-admin answers per the lifecycle object it holds for that instance. Three constants
|
|
@@ -555,9 +578,15 @@ export declare const FLEET_SCOPE_ENV = "FLEET_SCOPE";
|
|
|
555
578
|
* - {@link SERVING_LEASE_TTL_DEFAULT_MS} — the lease length when the operator sets none (10 min).
|
|
556
579
|
* - {@link SERVING_LEASE_TTL_FLOOR_MS} — the shortest lease there is: a configured TTL below it is treated as it (1 min),
|
|
557
580
|
* the same way {@link DRAIN_GRACE_FLOOR_MS} protects a fat-fingered grace.
|
|
558
|
-
* - {@link SERVING_LEASE_DRIFT_MARGIN_MS} —
|
|
559
|
-
*
|
|
560
|
-
*
|
|
581
|
+
* - {@link SERVING_LEASE_DRIFT_MARGIN_MS} — the clock-skew allowance for reading a lease deadline an engine REPORTS. The engine
|
|
582
|
+
* says `EngineSignal.leaseUntil` (epoch ms: its own computation from the granted `ttlMs`, on its own clock); a reader that asks
|
|
583
|
+
* "does this member's lease still hold?" compares that instant with ITS OWN now — two clocks that need not agree — so it counts
|
|
584
|
+
* the lease as held only while `leaseUntil >= now + SERVING_LEASE_DRIFT_MARGIN_MS`. That is the conservative direction: a skew
|
|
585
|
+
* up to the margin can make a lease that still holds read as short, never one that is over read as held. It is an allowance for
|
|
586
|
+
* reading a reported deadline, not part of the lease clock: the engine enforces the lease on its own monotonic clock and
|
|
587
|
+
* web-admin keeps its own expiry from its store time (see {@link ServingLeaseResponse}) — the two sides never compare clocks
|
|
588
|
+
* to enforce. It is DERIVED from {@link DEFAULT_HEARTBEAT_MS} (one cadence, one number), never a second constant that could
|
|
589
|
+
* drift from it.
|
|
561
590
|
*/
|
|
562
591
|
export declare const SERVING_LEASE_TTL_DEFAULT_MS = 600000;
|
|
563
592
|
export declare const SERVING_LEASE_TTL_FLOOR_MS = 60000;
|
package/dist/fleet.js
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { INTEGER } from "./integer.js";
|
|
22
22
|
import { fnv1a64 } from "./hash.js";
|
|
23
|
+
import { parseDeclared } from "./declared-capture.js";
|
|
24
|
+
import { readDeclaredKey, ownValueOf } from "./own-key-read.js";
|
|
23
25
|
import { isPlainJsonObject } from "./plain-json-object.js";
|
|
24
26
|
import { buildQuotedIssue } from "./quoted-issue.js";
|
|
25
27
|
// ── Web-admin's fleet endpoints (paths, one spelling) ─────────────────────────────────────────────────────
|
|
@@ -164,24 +166,41 @@ export const RestartSignal = z.object({
|
|
|
164
166
|
since: EpochMs,
|
|
165
167
|
withheld: z.boolean().optional(),
|
|
166
168
|
});
|
|
167
|
-
|
|
168
|
-
* The ONE guarded body judgement behind every wire-shape read in this file: `pick(body)` — one key of the body, or the body itself —
|
|
169
|
-
* is judged against that shape's one schema, and the verdict says WHY when it is not a read: `not-an-object` (the body is null, an
|
|
170
|
-
* array or a scalar) or `invalid` (the shape rejected it, or an accessor threw). It never throws (a probe must not die on one odd
|
|
171
|
-
* body). Almost every reader wants only "a read or nothing" and goes through {@link readSignalBody}; the one reader whose
|
|
172
|
-
* failure direction matters ({@link readCapabilityReading}) takes the reason. A second hand-written guard anywhere else in this
|
|
173
|
-
* package is the defect, not a convenience.
|
|
174
|
-
*/
|
|
169
|
+
const WHOLE_BODY = Symbol("whole body");
|
|
175
170
|
function judgeSignalBody(body, pick, schema) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
171
|
+
if (!isPlainJsonObject(body))
|
|
172
|
+
return { kind: "not-an-object" };
|
|
173
|
+
// the carrier key is read from the body's OWN keys (a polluted `Object.prototype.engineSignal` is not a signal the body carries) with the SAME descriptor check as a declared key:
|
|
174
|
+
// an own key that cannot be read as plain data (hidden — non-enumerable —, or an accessor that throws) is no readable signal, never "absent"
|
|
175
|
+
let target = body;
|
|
176
|
+
if (pick !== WHOLE_BODY) {
|
|
177
|
+
const carried = readDeclaredKey(body, pick);
|
|
178
|
+
switch (carried.presence) {
|
|
179
|
+
case "own":
|
|
180
|
+
target = carried.value;
|
|
181
|
+
break;
|
|
182
|
+
case "inherited":
|
|
183
|
+
case "absent":
|
|
184
|
+
target = undefined;
|
|
185
|
+
break;
|
|
186
|
+
case "hidden":
|
|
187
|
+
case "unreadable":
|
|
188
|
+
return { kind: "invalid" };
|
|
189
|
+
default: {
|
|
190
|
+
const unhandled = carried;
|
|
191
|
+
throw new Error(`judgeSignalBody: unhandled presence ${String(unhandled.presence)}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
181
194
|
}
|
|
182
|
-
|
|
195
|
+
// the validator reads members with `[[Get]]`, required keys included: a key the body does not carry would be filled from a polluted `Object.prototype` (a granted lease from an empty reply, a
|
|
196
|
+
// ready signal from `{}`, a "declared" capability from nothing) — so it never sees the caller's object: it reads an own-key snapshot of exactly the keys it declares (for a union: of
|
|
197
|
+
// the arm it will try), on the kernel's isolated prototype `COPY_PROTOTYPE` (no inherited members; it still answers `toString` / `valueOf`), and a value that cannot be captured
|
|
198
|
+
// (an accessor — throwing or not —, a hidden own key, a trap that throws, a cycle, not plain data) is `invalid`. `parseDeclared` admits only a closed allow-list of schema shapes, each
|
|
199
|
+
// reasoned and pinned against zod's own verdict and data; any other shape is a loud throw — the package chose the wrong reader, not a caller value that cannot be read
|
|
200
|
+
const parsed = parseDeclared(schema, target);
|
|
201
|
+
if (!parsed.ok)
|
|
183
202
|
return { kind: "invalid" };
|
|
184
|
-
}
|
|
203
|
+
return { kind: "read", value: parsed.data };
|
|
185
204
|
}
|
|
186
205
|
/**
|
|
187
206
|
* The reader behind every wire-shape read in this file that has no use for a reason: the engine-owned signals on /health
|
|
@@ -193,8 +212,6 @@ function readSignalBody(body, pick, schema) {
|
|
|
193
212
|
const judged = judgeSignalBody(body, pick, schema);
|
|
194
213
|
return judged.kind === "read" ? judged.value : undefined;
|
|
195
214
|
}
|
|
196
|
-
/** `pick` for a shape whose keys sit flat on the body itself (as opposed to under one key of it). */
|
|
197
|
-
const wholeBody = (body) => body;
|
|
198
215
|
/**
|
|
199
216
|
* The model-refusal cause codes (S-768) — CLOSED sets, owned HERE, never a hand-copied second list. Adding a code is
|
|
200
217
|
* one edit in this file; who goes red at compile time: a reader that switches on the code exhaustively (a web-admin), and
|
|
@@ -214,17 +231,29 @@ const wholeBody = (body) => body;
|
|
|
214
231
|
* `config.model_refused_name_minted` — a refused entry's name would be re-minted onto another entry by the
|
|
215
232
|
* tier / alias expansion (or the dropped-entry carrier cannot prove it would not be);
|
|
216
233
|
* `config.model_route_endpoint_missing` — an openai-compatible route of the candidate resolves to no endpoint;
|
|
217
|
-
* `config.gateway_fallback_without_primary` — fallback gateways are declared without a primary
|
|
218
|
-
*
|
|
219
|
-
*
|
|
234
|
+
* `config.gateway_fallback_without_primary` — fallback gateways are declared without a primary;
|
|
235
|
+
* `config.catalog_replaces_env_models` ({@link CATALOG_REPLACES_ENV_MODELS_CODE}) — on the LOCAL config leg, a candidate that
|
|
236
|
+
* manages the model plane would replace a declared env model table (`MODEL_ID` …) while the config root's decision document
|
|
237
|
+
* (`config.env-models.json`, read by `readEnvModelsDecision`) does not say `replaced` — the env models keep serving;
|
|
238
|
+
* `config.summarize_model_missing` ({@link SUMMARIZE_MODEL_MISSING_CODE}) — the summarize role, which every task resolves,
|
|
239
|
+
* would land on no servable entry (every task would be refused after adoption).
|
|
240
|
+
* Source (engine): the first two in `src/model-entry-refusal.ts`, the next two in `src/model-route-endpoint.ts:40,42`
|
|
241
|
+
* (invariant `model-route-endpoint`, judged by the model-plane stage in `src/config-center/apply-effective.ts:664-665`); the
|
|
242
|
+
* last two are named HERE first (the code is a contract word: this package names it and exports the two constants for the engine to read; the engine does not mint them today).
|
|
220
243
|
*/
|
|
221
244
|
export const MODEL_ENTRY_REFUSAL_CODES = ["config.model_no_brain", "config.model_entry_invalid"];
|
|
222
245
|
export const ModelEntryRefusalCode = z.enum(MODEL_ENTRY_REFUSAL_CODES);
|
|
246
|
+
/** The env-model-table confirmation gate's group-refusal code (see {@link MODEL_PLANE_REFUSAL_CODES}). */
|
|
247
|
+
export const CATALOG_REPLACES_ENV_MODELS_CODE = "config.catalog_replaces_env_models";
|
|
248
|
+
/** The summarize invariant's group-refusal code (see {@link MODEL_PLANE_REFUSAL_CODES}); a writer's plan answers `fix_first` with it. */
|
|
249
|
+
export const SUMMARIZE_MODEL_MISSING_CODE = "config.summarize_model_missing";
|
|
223
250
|
export const MODEL_PLANE_REFUSAL_CODES = [
|
|
224
251
|
"config.default_model_missing",
|
|
225
252
|
"config.model_refused_name_minted",
|
|
226
253
|
"config.model_route_endpoint_missing",
|
|
227
254
|
"config.gateway_fallback_without_primary",
|
|
255
|
+
CATALOG_REPLACES_ENV_MODELS_CODE,
|
|
256
|
+
SUMMARIZE_MODEL_MISSING_CODE,
|
|
228
257
|
];
|
|
229
258
|
export const ModelPlaneRefusalCode = z.enum(MODEL_PLANE_REFUSAL_CODES);
|
|
230
259
|
/**
|
|
@@ -285,11 +314,14 @@ const isModelPlaneRefusalCode = (c) => MODEL_PLANE_REFUSAL_CODES.includes(c);
|
|
|
285
314
|
* at all — it answers the key only and never judges the engine's version (see {@link InstanceAnnouncement}`.modelRefusals`).
|
|
286
315
|
*/
|
|
287
316
|
export function readModelRefusals(body) {
|
|
288
|
-
const wire = readSignalBody(body,
|
|
317
|
+
const wire = readSignalBody(body, "modelRefusals", ModelRefusalsWire);
|
|
289
318
|
if (wire === undefined)
|
|
290
319
|
return undefined;
|
|
291
320
|
const entries = Object.fromEntries(Object.entries(wire.entries).filter((e) => isModelEntryRefusalCode(e[1])));
|
|
292
|
-
|
|
321
|
+
// the validated output is an ordinary object: an absent optional `plane` stays absent (own key only), never read off a polluted `Object.prototype`
|
|
322
|
+
const planeRead = ownValueOf(wire, "plane");
|
|
323
|
+
const planeWord = planeRead.ok ? planeRead.value : undefined;
|
|
324
|
+
const plane = typeof planeWord === "string" && isModelPlaneRefusalCode(planeWord) ? planeWord : undefined;
|
|
293
325
|
if (Object.keys(entries).length === 0 && plane === undefined)
|
|
294
326
|
return undefined;
|
|
295
327
|
return plane === undefined ? { entries } : { entries, plane };
|
|
@@ -363,7 +395,7 @@ export const ENGINE_SIGNAL_KEY = "engineSignal";
|
|
|
363
395
|
* dialect; the carrier is the one key).
|
|
364
396
|
*/
|
|
365
397
|
export function readEngineSignal(body) {
|
|
366
|
-
return readSignalBody(body,
|
|
398
|
+
return readSignalBody(body, ENGINE_SIGNAL_KEY, EngineSignal);
|
|
367
399
|
}
|
|
368
400
|
/** The `/health` self-description every orchestrator reads. Liveness and readiness are deliberately SPLIT: `status` stays ok while
|
|
369
401
|
* draining (the process is alive); readiness is {@link judgeReadiness} over the engine signal the body carries under
|
|
@@ -556,6 +588,17 @@ export const FLEET_GENERATION_ENV = "FLEET_GENERATION";
|
|
|
556
588
|
* {@link FLEET_GENERATION_ENV} it is a reserved worker env name (`WorkerSpec.env` / `secretEnv` cannot set it).
|
|
557
589
|
*/
|
|
558
590
|
export const FLEET_SCOPE_ENV = "FLEET_SCOPE";
|
|
591
|
+
/**
|
|
592
|
+
* The env name a fleet member's advertise address travels under — the base URL this instance tells the control plane it can be
|
|
593
|
+
* reached at; the control plane appends request paths to it. The operator or the config renderer sets it (on Kubernetes the renderer
|
|
594
|
+
* writes a `$(POD_IP)` placeholder in the value and the kubelet expands it), and the engine reads it back. Both sides import THIS
|
|
595
|
+
* constant; neither spells the name. The address is a base that request paths are appended to, so a reader judges a present, non-empty
|
|
596
|
+
* value with `pathBaseUrlFlaws` (main barrel) — the one judge of every such seat, which forbids a credential, a
|
|
597
|
+
* query and a fragment there and allows a path prefix; judge the value AFTER the deployment has expanded its placeholders. Absent or
|
|
598
|
+
* the empty string means "no address declared" (the `${NAME:-}` idiom of a compose file), and that reading is the caller's, not the
|
|
599
|
+
* judge's.
|
|
600
|
+
*/
|
|
601
|
+
export const FLEET_ADVERTISE_ADDRESS_ENV = "FLEET_ADVERTISE_ADDRESS";
|
|
559
602
|
/**
|
|
560
603
|
* The serving lease — the ONE fence between a superseded generation and the traffic it must stop taking. A fleet-member
|
|
561
604
|
* engine renews it with web-admin; web-admin answers per the lifecycle object it holds for that instance. Three constants
|
|
@@ -563,9 +606,15 @@ export const FLEET_SCOPE_ENV = "FLEET_SCOPE";
|
|
|
563
606
|
* - {@link SERVING_LEASE_TTL_DEFAULT_MS} — the lease length when the operator sets none (10 min).
|
|
564
607
|
* - {@link SERVING_LEASE_TTL_FLOOR_MS} — the shortest lease there is: a configured TTL below it is treated as it (1 min),
|
|
565
608
|
* the same way {@link DRAIN_GRACE_FLOOR_MS} protects a fat-fingered grace.
|
|
566
|
-
* - {@link SERVING_LEASE_DRIFT_MARGIN_MS} —
|
|
567
|
-
*
|
|
568
|
-
*
|
|
609
|
+
* - {@link SERVING_LEASE_DRIFT_MARGIN_MS} — the clock-skew allowance for reading a lease deadline an engine REPORTS. The engine
|
|
610
|
+
* says `EngineSignal.leaseUntil` (epoch ms: its own computation from the granted `ttlMs`, on its own clock); a reader that asks
|
|
611
|
+
* "does this member's lease still hold?" compares that instant with ITS OWN now — two clocks that need not agree — so it counts
|
|
612
|
+
* the lease as held only while `leaseUntil >= now + SERVING_LEASE_DRIFT_MARGIN_MS`. That is the conservative direction: a skew
|
|
613
|
+
* up to the margin can make a lease that still holds read as short, never one that is over read as held. It is an allowance for
|
|
614
|
+
* reading a reported deadline, not part of the lease clock: the engine enforces the lease on its own monotonic clock and
|
|
615
|
+
* web-admin keeps its own expiry from its store time (see {@link ServingLeaseResponse}) — the two sides never compare clocks
|
|
616
|
+
* to enforce. It is DERIVED from {@link DEFAULT_HEARTBEAT_MS} (one cadence, one number), never a second constant that could
|
|
617
|
+
* drift from it.
|
|
569
618
|
*/
|
|
570
619
|
export const SERVING_LEASE_TTL_DEFAULT_MS = 600_000;
|
|
571
620
|
export const SERVING_LEASE_TTL_FLOOR_MS = 60_000;
|
|
@@ -647,14 +696,14 @@ export const SERVING_LEASE_VERDICTS = ["granted", "denied", "unknown"];
|
|
|
647
696
|
/** Web-admin side: the request body of a serving-lease renewal. Shape mismatch / non-object / throwing accessor ⇒ `undefined`,
|
|
648
697
|
* never throws (same guarded reader as every other read in this file). */
|
|
649
698
|
export function readServingLeaseRequest(body) {
|
|
650
|
-
return readSignalBody(body,
|
|
699
|
+
return readSignalBody(body, WHOLE_BODY, ServingLeaseRequest);
|
|
651
700
|
}
|
|
652
701
|
/** Engine side: the reply to a renewal, judged by its `verdict` key: `granted` needs a valid `ttlMs`, `denied` is `denied`
|
|
653
702
|
* whatever its `reason` (see {@link ServingLeaseReading}), `unknown` is `unknown`. A non-object, a throwing accessor, a
|
|
654
703
|
* `verdict` outside the three words or a `granted` without a valid `ttlMs` ⇒ `undefined`, never throws — and `undefined`
|
|
655
704
|
* is read like an unreachable web-admin, never like `denied` and never like `granted`. */
|
|
656
705
|
export function readServingLeaseResponse(body) {
|
|
657
|
-
return readSignalBody(body,
|
|
706
|
+
return readSignalBody(body, WHOLE_BODY, ServingLeaseReadFace);
|
|
658
707
|
}
|
|
659
708
|
/**
|
|
660
709
|
* What web-admin says it can do: capability bits an engine reads before it starts using a web-admin-side endpoint.
|
|
@@ -704,14 +753,16 @@ const CapabilitiesCarrier = z.object({ [HEARTBEAT_REPLY_CAPABILITIES_KEY]: WebAd
|
|
|
704
753
|
* other reader here.
|
|
705
754
|
*/
|
|
706
755
|
export function readCapabilityReading(reply) {
|
|
707
|
-
const judged = judgeSignalBody(reply,
|
|
756
|
+
const judged = judgeSignalBody(reply, WHOLE_BODY, CapabilitiesCarrier);
|
|
708
757
|
switch (judged.kind) {
|
|
709
758
|
case "not-an-object":
|
|
710
759
|
return { kind: "unreadable", why: "not-an-object" };
|
|
711
760
|
case "invalid":
|
|
712
761
|
return { kind: "unreadable", why: "invalid-capabilities" };
|
|
713
762
|
case "read": {
|
|
714
|
-
|
|
763
|
+
// the validated output is an ordinary object: an absent optional key must stay absent, not be read off a polluted `Object.prototype` — own key only (it was parsed from a snapshot, so it cannot be unreadable)
|
|
764
|
+
const carried = ownValueOf(judged.value, HEARTBEAT_REPLY_CAPABILITIES_KEY);
|
|
765
|
+
const capabilities = carried.ok ? carried.value : undefined;
|
|
715
766
|
return capabilities === undefined ? { kind: "undeclared" } : { kind: "declared", capabilities };
|
|
716
767
|
}
|
|
717
768
|
default: {
|
|
@@ -755,11 +806,11 @@ export const DrainReceiptState = z.enum(DRAIN_RECEIPT_STATES);
|
|
|
755
806
|
export const DrainReceipt = EngineDraining.extend({ state: DrainReceiptState });
|
|
756
807
|
/** Engine side: the request body of the drain endpoint. Shape mismatch / non-object / throwing accessor ⇒ `undefined`, never throws. */
|
|
757
808
|
export function readDrainRequest(body) {
|
|
758
|
-
return readSignalBody(body,
|
|
809
|
+
return readSignalBody(body, WHOLE_BODY, DrainRequest);
|
|
759
810
|
}
|
|
760
811
|
/** Caller side: the drain endpoint's answer. Shape mismatch / non-object / throwing accessor ⇒ `undefined`, never throws. */
|
|
761
812
|
export function readDrainReceipt(body) {
|
|
762
|
-
return readSignalBody(body,
|
|
813
|
+
return readSignalBody(body, WHOLE_BODY, DrainReceipt);
|
|
763
814
|
}
|
|
764
815
|
// ── Domain 3: dispatch policy (traffic + failover + LB — three domains, one gateway) ──────────────────────
|
|
765
816
|
export const DISPATCH_STRATEGIES = ["round-robin", "least-inflight", "consistent-hash"];
|
package/dist/hash.d.ts
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tiny stable content hash — shared by `workerSpecHash` (reconciler drift detection) and the per-worker
|
|
3
|
-
* `/effective` ETag (so an unrelated roster change doesn't churn a worker's cache). Zero-dep, deterministic.
|
|
4
|
-
*
|
|
5
|
-
* FNV-1a **64-bit** (BigInt) → 16 hex. NOT cryptographic; it's a change-detector. 64-bit pushes the
|
|
6
|
-
* birthday-collision boundary to ~5B distinct inputs — a collision would silently SUPPRESS a needed
|
|
7
|
-
* container restart (drift) or serve a stale 304 (ETag), so we don't want the 32-bit ~77k boundary here
|
|
8
|
-
* (council finding). Output length isn't a contract (values are only compared for equality before/after).
|
|
9
|
-
*/
|
|
10
1
|
/** Max nesting stableStringify will hash. A real EffectiveConfig nests ~10-15 deep; this generous cap turns a
|
|
11
2
|
* pathologically deep OR cyclic value (e.g. a poisoned `model.extraBody` = z.unknown()) into a DETERMINISTIC
|
|
12
3
|
* throw instead of a `RangeError: Maximum call stack` that would crash every scoped-worker `/effective` poll
|