@sema-agent/settings-schema 11.2.0 → 12.0.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 +112 -0
- package/README.md +2 -1
- package/conformance/VERSION +1 -1
- package/conformance/cases/c40-s712-identity-only-what-reads-out/expected.json +6 -3
- package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +4 -2
- package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +4 -2
- package/conformance/cases/c46-s811-host-name-url-userinfo-withheld/expected.json +2 -1
- package/conformance/cases/c73-projects-extended-registration-reads/expected.json +150 -0
- package/conformance/cases/c73-projects-extended-registration-reads/input.json +42 -0
- package/conformance/cases/c73-projects-extended-registration-reads/meta.json +6 -0
- package/conformance/cases/c74-projects-over-bound-refused/expected.json +89 -0
- package/conformance/cases/c74-projects-over-bound-refused/input.json +29 -0
- package/conformance/cases/c74-projects-over-bound-refused/meta.json +5 -0
- package/conformance/cases/c75-projects-patch-remote-uniqueness/expected.json +89 -0
- package/conformance/cases/c75-projects-patch-remote-uniqueness/input.json +35 -0
- package/conformance/cases/c75-projects-patch-remote-uniqueness/meta.json +5 -0
- package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +4 -2
- package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +4 -2
- package/dist/api/feedback.js +2 -3
- package/dist/api/scopes.js +2 -1
- package/dist/atomic-write.d.ts +6 -0
- package/dist/atomic-write.js +23 -6
- package/dist/cli.d.ts +8 -4
- package/dist/cli.js +55 -10
- package/dist/config-d-file.d.ts +12 -2
- package/dist/config-d-file.js +36 -9
- package/dist/config-dir-io.js +4 -3
- package/dist/config-dir-migration.d.ts +5 -3
- package/dist/config-dir-migration.js +19 -9
- package/dist/config-fns.d.ts +14 -2
- package/dist/config-fns.js +13 -1
- package/dist/cross-domain.d.ts +8 -3
- package/dist/cross-domain.js +23 -13
- package/dist/file-edit.js +7 -5
- package/dist/file-store.js +2 -1
- package/dist/fleet.js +3 -4
- package/dist/hooks.d.ts +14 -1
- package/dist/hooks.js +61 -16
- package/dist/host-path.d.ts +17 -0
- package/dist/host-path.js +22 -0
- package/dist/integer.js +2 -1
- package/dist/local-load.d.ts +1 -1
- package/dist/local-load.js +9 -31
- package/dist/migration-receipt.d.ts +4 -3
- package/dist/migration-receipt.js +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/one-time-migrations.d.ts +6 -4
- package/dist/one-time-migrations.js +73 -6
- package/dist/quoted-issue.d.ts +41 -0
- package/dist/quoted-issue.js +35 -0
- package/dist/remote-exec.d.ts +3 -0
- package/dist/remote-exec.js +12 -7
- package/dist/resolve-roster.d.ts +1 -1
- package/dist/resolve-roster.js +3 -3
- package/dist/retired-role-words.d.ts +5 -0
- package/dist/retired-role-words.js +25 -0
- package/dist/rfc3339.d.ts +10 -0
- package/dist/rfc3339.js +12 -0
- package/dist/safety-merge-spec.d.ts +2 -2
- package/dist/types.d.ts +140 -32
- package/dist/types.js +215 -114
- package/dist/web-admin-credential.d.ts +2 -2
- package/dist/web-admin-credential.js +5 -10
- package/dist/write-failure.d.ts +16 -0
- package/dist/write-failure.js +43 -0
- package/package.json +1 -1
package/dist/types.js
CHANGED
|
@@ -30,6 +30,10 @@ import { Sha256Digest } from "./sha256.js"; // the one `sha256:<hex>` digest for
|
|
|
30
30
|
import { EXECUTION_LANES, EXECUTION_LANE_WORDS, MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS, retiredLaneWordRefusal } from "./execution-lanes.js"; // 7.0.0 M-R2: the lane / store vocabulary (one exported table)
|
|
31
31
|
import { LANE_ENV_OWNER, isMigratableEnvKey, laneEnvSeat, laneRowOf } from "./execution-lane-index.js"; // package-private indexes derived from that table (not in the barrel)
|
|
32
32
|
import { migrationSignpost, retiredFormSentence, retiredKey } from "./retired-form.js"; // 11.0.0: the ONE retired-form sentence + key tombstone (package-private)
|
|
33
|
+
import { retiredRoleWordsRefusal } from "./retired-role-words.js"; // 12.0.0: the retired role-word tombstone (package-private; SS-M8)
|
|
34
|
+
import { buildQuotedIssue, fixed } from "./quoted-issue.js"; // THE minting point of custom issues (package-private)
|
|
35
|
+
import { AbsoluteHostPath } from "./host-path.js"; // the one host-path shape judge (a task's cwd and a project's folder / file entries)
|
|
36
|
+
import { RFC3339_DATE_TIME } from "./rfc3339.js"; // the package's one RFC 3339 date-time judge (package-private)
|
|
33
37
|
// COARSE, best-effort literal-secret SHAPE detector (NOT exhaustive — the real boundary is the env-NAME schema
|
|
34
38
|
// on structured fields). Rejected in free-form value fields (worker `env`, model `extraBody`), URL components,
|
|
35
39
|
// 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-
|
|
@@ -56,7 +60,7 @@ export const ENV_NAME = z
|
|
|
56
60
|
// lowercase — the charset above rejects them). The ONE secret shape that IS a valid all-caps env-NAME charset
|
|
57
61
|
// is an AWS access-key id (AKIA…) — block exactly that, WITHOUT false-rejecting legit names like
|
|
58
62
|
// GITHUB_PAT_TOKEN / GHU_TOKEN (which the full SECRET_LITERAL_RE would). Round-5.
|
|
59
|
-
.refine((s) => !/^AKIA[0-9A-Z]{12,}$/.test(s),
|
|
63
|
+
.refine((s) => !/^AKIA[0-9A-Z]{12,}$/.test(s), buildQuotedIssue `looks like an AWS access-key id, not an env-NAME — put the NAME of the env var here (the value lives in .env)`);
|
|
60
64
|
function scanForSecret(s) {
|
|
61
65
|
// A private-key PEM header ANYWHERE in the string (the one regex the display redactor reads too). The header carries spaces, so
|
|
62
66
|
// neither the start-anchored SECRET_LITERAL_RE below nor the tokenizer at the bottom can see it behind a prefix / newline. 10.0.0.
|
|
@@ -94,30 +98,11 @@ function scanForSecret(s) {
|
|
|
94
98
|
* fs path — so the worker dies at exec/boot, not at config edit. A NUL is never legitimate in a command / path
|
|
95
99
|
* / host / image, so reject it on every exec-bound string field (mcp stdio + the whole remoteExec contract). R20. */
|
|
96
100
|
export const hasNul = (s) => s.includes("\0");
|
|
97
|
-
/** The
|
|
98
|
-
*
|
|
99
|
-
export
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
const fixed = (text) => ({ fixedText: text });
|
|
103
|
-
/** Mint a custom issue's `message` + `params` from a tagged template (`buildQuotedIssue\`duplicate model "${name}"\``): every
|
|
104
|
-
* interpolation is an operand unless wrapped in {@link fixed} (then it is template text). The ONE minting point of quoted
|
|
105
|
-
* sentences in this file — a template literal written straight into `message` would put a value into a sentence with no record
|
|
106
|
-
* of where it is (a source gate pins that none is left). */
|
|
107
|
-
function buildQuotedIssue(strings, ...parts) {
|
|
108
|
-
const template = [strings[0]];
|
|
109
|
-
const operands = [];
|
|
110
|
-
parts.forEach((part, i) => {
|
|
111
|
-
if (typeof part === "object")
|
|
112
|
-
template[template.length - 1] += part.fixedText + strings[i + 1];
|
|
113
|
-
else {
|
|
114
|
-
operands.push(part);
|
|
115
|
-
template.push(strings[i + 1]);
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
const params = { template, operands };
|
|
119
|
-
return { message: renderQuotedSentence(params), params };
|
|
120
|
-
}
|
|
101
|
+
/** The params of a custom issue's sentence and their one rendering live in the package-private minting module `./quoted-issue`
|
|
102
|
+
* (one minting point for the whole package); re-exported here unchanged. */
|
|
103
|
+
export { renderQuotedSentence } from "./quoted-issue.js";
|
|
104
|
+
/** The one host-path shape judge (a task's `cwd`, a project's `folder` / `file` entries) lives in its leaf module `./host-path`. */
|
|
105
|
+
export { ABSOLUTE_HOST_PATH_MAX, AbsoluteHostPath, isAbsoluteHostPath } from "./host-path.js";
|
|
121
106
|
/** Does `s` contain a secret-shaped token? —— **the one scanner** (11.0.0, S-1274): the write side (this detector, behind every
|
|
122
107
|
* free-text write port — NON_SECRET_URL, mcp stdio, execution coords, ssh login, worker env, extraBody, …) and the display side
|
|
123
108
|
* (`redactSecrets`) read the SAME normalized forms ({@link secretScanForms} in the package-private `./secret-scan`: the raw string,
|
|
@@ -169,49 +154,52 @@ export function urlQueryFragmentHasSecret(u) {
|
|
|
169
154
|
* after it is dropped with it — the engine and server refuse a model route root that carries one, so a stored one only fails
|
|
170
155
|
* later, on the worker). A query is a legitimate request parameter (`?api-version=…`) and stays. */
|
|
171
156
|
const NON_SECRET_URL_FORBIDS = ["userinfo", "fragment"];
|
|
172
|
-
/** The refusal
|
|
173
|
-
* can carry from before 11.0.0 (a fragment) at a seat the one-time migration
|
|
174
|
-
* it, joined the way the other signposted refusals join it (`<rule>; <signpost>`).
|
|
175
|
-
* a compile error here. */
|
|
176
|
-
function
|
|
157
|
+
/** The refusal of one flaw at a {@link NON_SECRET_URL} seat (a {@link QuotedIssue} with no operands — the sentence quotes no value): the
|
|
158
|
+
* judge's rule sentence, and — for the one flaw that stored data can carry from before 11.0.0 (a fragment) at a seat the one-time migration
|
|
159
|
+
* reaches — the signpost to the migration that strips it, joined the way the other signposted refusals join it (`<rule>; <signpost>`).
|
|
160
|
+
* Closed over the judge's flaw words: a new word is a compile error here. */
|
|
161
|
+
function nonSecretUrlIssue(flaw, reachedByMigration) {
|
|
177
162
|
switch (flaw) {
|
|
178
163
|
case "fragment":
|
|
179
|
-
return reachedByMigration ? `${httpUrlRule(flaw)}; ${migrationSignpost("SS-M7", "11.0.0")}` : httpUrlRule(flaw)
|
|
164
|
+
return reachedByMigration ? buildQuotedIssue `${fixed(httpUrlRule(flaw))}; ${fixed(migrationSignpost("SS-M7", "11.0.0"))}` : buildQuotedIssue `${fixed(httpUrlRule(flaw))}`;
|
|
180
165
|
case "not-http-url":
|
|
181
166
|
case "userinfo":
|
|
182
167
|
case "query":
|
|
183
|
-
return httpUrlRule(flaw)
|
|
168
|
+
return buildQuotedIssue `${fixed(httpUrlRule(flaw))}`;
|
|
184
169
|
default: {
|
|
185
170
|
const unhandled = flaw;
|
|
186
|
-
throw new Error("
|
|
171
|
+
throw new Error("nonSecretUrlIssue: unhandled flaw word", { cause: unhandled });
|
|
187
172
|
}
|
|
188
173
|
}
|
|
189
174
|
}
|
|
190
175
|
/** One judge, two seat kinds — the only difference is the fragment refusal's migration signpost. `reachedByMigration` = the seat sits in a
|
|
191
|
-
* stored
|
|
176
|
+
* stored document the one-time migration SS-M7 rewrites (it finds its seats by this schema's identity, so "carries the
|
|
192
177
|
* signpost" and "is rewritten by SS-M7" cannot drift apart). */
|
|
193
178
|
function createNonSecretUrl(reachedByMigration) {
|
|
194
179
|
return z.string().superRefine((s, ctx) => {
|
|
195
180
|
const flaws = httpUrlFlaws(s, NON_SECRET_URL_FORBIDS);
|
|
196
|
-
for (const flaw of flaws)
|
|
197
|
-
|
|
181
|
+
for (const flaw of flaws) {
|
|
182
|
+
const refusal = nonSecretUrlIssue(flaw, reachedByMigration);
|
|
183
|
+
ctx.addIssue({ code: "custom", ...refusal, params: { ...refusal.params, flaw } }); // `flaw` = the judge's rule word, beside the sentence's params
|
|
184
|
+
}
|
|
198
185
|
if (flaws.length === 0 && urlQueryFragmentHasSecret(new URL(s))) {
|
|
199
|
-
ctx.addIssue({ code: "custom",
|
|
186
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `must not carry a secret-shaped value in its path or query — secrets go in the *Env (env-NAME) fields` });
|
|
200
187
|
}
|
|
201
188
|
});
|
|
202
189
|
}
|
|
203
190
|
/** A URL handed to `fetch` that carries no credential: judged by THE URL shape judge ({@link httpUrlFlaws}, `./url-shape`) — a
|
|
204
191
|
* hierarchical http(s) URL with neither userinfo nor a fragment (an empty `#` included, 11.0.0) — and with no secret-shaped value
|
|
205
192
|
* in its path or query ({@link urlQueryFragmentHasSecret}). One issue per flaw, its message the judge's rule sentence (no value
|
|
206
|
-
* echoed; a fragment's also names its migration, {@link
|
|
193
|
+
* echoed; a fragment's also names its migration, {@link nonSecretUrlIssue}); `params` carries the sentence (`template` / `operands`,
|
|
194
|
+
* no operands) and `flaw`, the rule word.
|
|
207
195
|
*
|
|
208
|
-
* This is the seat of every URL in a
|
|
209
|
-
* `gitApiBaseUrl`)
|
|
210
|
-
* `issuer
|
|
196
|
+
* This is the seat of every URL in a STORED document the one-time migration rewrites: the domain documents (models `baseUrl`, mcp `url`,
|
|
197
|
+
* `sessionMirror.engineUrl`, workers `gitApiBaseUrl`) and `config.d/remote-exec.json` (k8s `apiUrl`, `s3Snapshot.endpoint`). A URL
|
|
198
|
+
* that is not stored data — the web-admin credential envelope's `issuer`, a wire input — uses {@link NON_SECRET_URL_NO_MIGRATION}.
|
|
211
199
|
*
|
|
212
200
|
* The value is kept EXACTLY as written: nothing here trims or normalizes it (zod's own `.url()` would hand back a rewritten string). */
|
|
213
201
|
export const NON_SECRET_URL = createNonSecretUrl(true);
|
|
214
|
-
/** {@link NON_SECRET_URL}'s judge for a seat no one-time migration reaches (not
|
|
202
|
+
/** {@link NON_SECRET_URL}'s judge for a seat no one-time migration reaches (not stored data): identical rules and rule
|
|
215
203
|
* sentences, but a fragment's refusal carries no migration signpost — pointing at a migration that cannot rewrite the value would
|
|
216
204
|
* send the operator to a command that does nothing for it. */
|
|
217
205
|
export const NON_SECRET_URL_NO_MIGRATION = createNonSecretUrl(false);
|
|
@@ -299,19 +287,19 @@ function vetFreeFormJson(v, field) {
|
|
|
299
287
|
rawUnsafe = extraBodyUnsafe(v);
|
|
300
288
|
}
|
|
301
289
|
catch {
|
|
302
|
-
return { ok: false,
|
|
290
|
+
return { ok: false, issue: buildQuotedIssue `${fixed(field)} could not be validated (reflective access threw)` }; // R11
|
|
303
291
|
}
|
|
304
292
|
if (rawUnsafe)
|
|
305
|
-
return { ok: false,
|
|
293
|
+
return { ok: false, issue: buildQuotedIssue `${fixed(field)}: a key/value is a secret shape, non-JSON (Infinity/function/Map), an accessor, cyclic, or too deep` };
|
|
306
294
|
let clone;
|
|
307
295
|
try {
|
|
308
296
|
clone = JSON.parse(JSON.stringify(v)); // the ONLY serialization — its result is what we scan AND persist
|
|
309
297
|
}
|
|
310
298
|
catch {
|
|
311
|
-
return { ok: false,
|
|
299
|
+
return { ok: false, issue: buildQuotedIssue `${fixed(field)} is not JSON-serializable` };
|
|
312
300
|
}
|
|
313
301
|
if (extraBodyUnsafe(clone))
|
|
314
|
-
return { ok: false,
|
|
302
|
+
return { ok: false, issue: buildQuotedIssue `${fixed(field)}: its serialized form carries a secret` };
|
|
315
303
|
return { ok: true, value: clone };
|
|
316
304
|
}
|
|
317
305
|
/** The transform of every **parse-transparent free-form** key (`z.unknown().optional().transform(createFreeFormTransform(field))`:
|
|
@@ -324,7 +312,7 @@ function createFreeFormTransform(field) {
|
|
|
324
312
|
return undefined;
|
|
325
313
|
const vetted = vetFreeFormJson(v, field);
|
|
326
314
|
if (!vetted.ok) {
|
|
327
|
-
ctx.addIssue({ code: "custom",
|
|
315
|
+
ctx.addIssue({ code: "custom", ...vetted.issue });
|
|
328
316
|
return z.NEVER;
|
|
329
317
|
}
|
|
330
318
|
return vetted.value;
|
|
@@ -504,12 +492,12 @@ export const ModelEntry = z.object({
|
|
|
504
492
|
return undefined;
|
|
505
493
|
const proto = typeof v === "object" && v !== null ? Object.getPrototypeOf(v) : undefined;
|
|
506
494
|
if (typeof v !== "object" || v === null || Array.isArray(v) || (proto !== Object.prototype && proto !== null)) {
|
|
507
|
-
ctx.addIssue({ code: "custom",
|
|
495
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `extraBody must be a plain JSON object` });
|
|
508
496
|
return z.NEVER;
|
|
509
497
|
}
|
|
510
498
|
const vetted = vetFreeFormJson(v, "extraBody");
|
|
511
499
|
if (!vetted.ok) {
|
|
512
|
-
ctx.addIssue({ code: "custom",
|
|
500
|
+
ctx.addIssue({ code: "custom", ...vetted.issue });
|
|
513
501
|
return z.NEVER;
|
|
514
502
|
}
|
|
515
503
|
return vetted.value;
|
|
@@ -563,20 +551,31 @@ export const ModelEntry = z.object({
|
|
|
563
551
|
// `verifier` (2026-06-11, SERVICE-RESPONSE-…-PHASE2): real consumer = core's verification gate
|
|
564
552
|
// (runWithVerification/resumeWithVerification resolve the verifier sub-task model via this role). Fallback
|
|
565
553
|
// semantics per core: a roster that doesn't set `verifier` uses `default`.
|
|
566
|
-
// 1.8.0(消费方壳侧真案):对齐 core
|
|
567
|
-
// `classifier` = auto 模式权限分类器(core FALLBACK classifier→summarize→default;tier 缺省 flash)
|
|
568
|
-
// 写 `roles.classifier` 被本闭集拒,用户想单独钉分类器模型只能连带 summarize。
|
|
554
|
+
// 1.8.0(消费方壳侧真案):对齐 core 角色表里**有真消费方**的两个角色 ——
|
|
555
|
+
// `classifier` = auto 模式权限分类器(core FALLBACK classifier→summarize→default;tier 缺省 flash);
|
|
569
556
|
// `consolidate` = 记忆整理(core FALLBACK consolidate→summarize)。
|
|
570
557
|
// 两个词都**不进** roster 标记族(resolve-roster.ts):显式归 cheap 会让 server 把它钉死到 cheapRef,盖掉用户
|
|
571
558
|
// `models.tiers.flash` 的档位与 core 兜底链 —— 新角色「不派生、走 core 兜底、只由 settings 显式钉」。
|
|
572
|
-
//
|
|
573
|
-
//
|
|
574
|
-
|
|
559
|
+
// 12.0.0(硬破坏):`team` / `synthesize` 随 core 7.49.0 删掉(引擎对未知角色词响亮拒)—— 零别名、零双读;存着的这两个键由一次性迁移 SS-M8 删,
|
|
560
|
+
// 迁移前到达判官的旧词拒句点名它(墓碑在 `./retired-role-words`)。`advisor`(teacher mode)本包从未收(没有消费方)。
|
|
561
|
+
// 子集关系(本表 ⊆ 引擎角色表)由消费方服务端的型面对账钉住(本仓不依赖 core;dev 期的漂移门 ② 对拍 devDependency 那一版引擎)。
|
|
562
|
+
export const ModelRole = z.enum(["default", "summarize", "subagent", "verifier", "classifier", "consolidate"], {
|
|
563
|
+
error: (iss) => (iss.code === "invalid_value" ? retiredRoleWordsRefusal([iss.input]) : undefined),
|
|
564
|
+
});
|
|
575
565
|
/** A role resolves either to a model NAME, or by capability/price (core 1.25 `{ select }`). */
|
|
576
566
|
export const RoleTarget = z.union([
|
|
577
567
|
z.object({ model: z.string().min(1) }),
|
|
578
568
|
z.object({ select: z.object({ tier: ModelTier.optional(), needs: z.array(z.enum(["vision", "reasoning"])).optional(), prefer: z.enum(["cheapest", "premium"]).optional() }) }),
|
|
579
569
|
]);
|
|
570
|
+
/** 角色 → 目标的图(`models.roles` 与 `rosters[].roles` 同一只)。键不认识 ⇒ zod 的「未知键」句;键里有 12.0.0 退役的角色词 ⇒ 那一句点名
|
|
571
|
+
* 一次性迁移 SS-M8(同组别的未知键一并点名)。 */
|
|
572
|
+
const RoleMap = z.partialRecord(ModelRole, RoleTarget, {
|
|
573
|
+
// 枚举键的 record 在运行期以 `unrecognized_keys`(带 `keys`)报未知键,zod 的型面只列了 invalid_type / invalid_key —— 按运行期形读(钉:role-words-12.0.0)。
|
|
574
|
+
error: (iss) => {
|
|
575
|
+
const raw = iss;
|
|
576
|
+
return raw.code === "unrecognized_keys" && Array.isArray(raw.keys) ? retiredRoleWordsRefusal(raw.keys) : undefined;
|
|
577
|
+
},
|
|
578
|
+
});
|
|
580
579
|
/** {@link AtMention} 的模式闭集:`all` = 每个启用的模型都可 `@`;`none` = 一个都不许;`only` = 只许 `models` 里点名的那几个。 */
|
|
581
580
|
export const AT_MENTION_MODES = ["all", "none", "only"];
|
|
582
581
|
/** 「`@` 能点名哪些模型」(11.0.0,取代 `atModelAllowlist`)。义由 `mode` 说,不由数组空不空说:旧形一个数组靠「缺席 / 空 / 非空」
|
|
@@ -622,7 +621,7 @@ export const ModelsConfig = z
|
|
|
622
621
|
* the main task ran the first and the core `default` role resolved the second). Which catalog entry is the default is
|
|
623
622
|
* `resolveCatalogDefaultModel` (`./default-model`; a `{select}` default is handed back as-is for the engine to resolve). A stored
|
|
624
623
|
* `default` key is moved here by the one-time migration SS-M9. */
|
|
625
|
-
roles:
|
|
624
|
+
roles: RoleMap.prefault({}),
|
|
626
625
|
/** Which model names an end user may `@`-mention({@link AtMention});缺省 `{mode:"all"}`。 */
|
|
627
626
|
atMention: AtMention.prefault({ mode: "all" }),
|
|
628
627
|
/** retired 键墓碑(11.0.0):在场即拒,拒句点名一次性迁移 SS-M6。 */
|
|
@@ -688,7 +687,7 @@ export const RosterEntry = z.object({
|
|
|
688
687
|
models: z.array(z.string().min(1)).prefault([]),
|
|
689
688
|
/**
|
|
690
689
|
* 主模型角色标记(0.6.3, E3)——catalog model NAME。worker.model 废除后 service 从 roster
|
|
691
|
-
* 解析 boot 主模型的归宿位:解析语义(resolve-roster)= primary 喂 `default
|
|
690
|
+
* 解析 boot 主模型的归宿位:解析语义(resolve-roster)= primary 喂 `default`(12.0.0 起唯一;`team` / `synthesize` 随 core 7.49.0 删)、
|
|
692
691
|
* cheap 喂 `subagent`/`summarize`(service 现有 ModelRoles 口径;`verifier`/`classifier`/`consolidate` 不派生——unset 走 core 兜底链)。
|
|
693
692
|
* 显式 `roles` **逐角色**优先于标记派生;标记优先于全局 models.roles。additive:不设=行为与 0.6.2 全等。
|
|
694
693
|
*/
|
|
@@ -696,7 +695,7 @@ export const RosterEntry = z.object({
|
|
|
696
695
|
/** cheap 模型角色标记(同上;worker.model.cheapId 的迁移归宿,§3②)。 */
|
|
697
696
|
cheapModel: z.string().min(1).optional(),
|
|
698
697
|
/** Role map override; unset roles fall back to marker-derived roles, then the global models.roles. */
|
|
699
|
-
roles:
|
|
698
|
+
roles: RoleMap.optional(),
|
|
700
699
|
/** `@` 点名覆盖({@link AtMention});缺席 = 继承全局 `models.atMention`。 */
|
|
701
700
|
atMention: AtMention.optional(),
|
|
702
701
|
/** retired 键墓碑(11.0.0):在场即拒,拒句点名一次性迁移 SS-M6。 */
|
|
@@ -808,7 +807,7 @@ export const SystemEntry = z.object({
|
|
|
808
807
|
// worker that isn't provisioned yet has no Secret to label-check, so a name-level reservation is
|
|
809
808
|
// the only thing closing the "squat on a future worker's credential surface" timing hole (S3 设计
|
|
810
809
|
// §4.4, council DESIGN-6/13). Cost: callers can't use a secret that happens to end in -env.
|
|
811
|
-
.refine((n) => !n.endsWith("-env"),
|
|
810
|
+
.refine((n) => !n.endsWith("-env"), buildQuotedIssue `secret names ending in "-env" are reserved for orchestrator worker-credential projections (红线1)`),
|
|
812
811
|
key: z.string().regex(/^[-._a-zA-Z0-9]+$/, "k8s secret data key").max(253),
|
|
813
812
|
})
|
|
814
813
|
.optional(),
|
|
@@ -975,13 +974,23 @@ export const LimitsConfig = z.object({
|
|
|
975
974
|
// The governance trio an operator tunes via publish: autonomy ladder + command guardrails + approval-required
|
|
976
975
|
// tools. NOT下沉 — gets its own admin tab (E5 form; E1 ships a transitional generic editor). Same `.optional()`
|
|
977
976
|
// no-default discipline as the six gates: ABSENT = web-admin 不管这项(keep env / 消费者默认),present 才生效。
|
|
977
|
+
/** The autonomy ladder —— the closed set as a RUNTIME VALUE (same form as `REF_CHECKED_DOMAINS`): {@link AutonomyMode} (the zod enum)
|
|
978
|
+
* and its type are DERIVED from this table, so a consumer that needs the words at runtime (an env parser, a catalog's enum column) imports
|
|
979
|
+
* it instead of reaching into `GovernanceConfig.shape.autonomy.unwrap().options` or writing the four words again. */
|
|
980
|
+
export const AUTONOMY_MODES = ["read-only", "ask", "plan", "auto"];
|
|
981
|
+
/** `governance.autonomy`'s enum, derived from {@link AUTONOMY_MODES}. */
|
|
982
|
+
export const AutonomyMode = z.enum(AUTONOMY_MODES);
|
|
983
|
+
/** A `commandPolicy[]` rule's decision —— the closed set as a RUNTIME VALUE; {@link CommandDecision} and its type are DERIVED from it. */
|
|
984
|
+
export const COMMAND_DECISIONS = ["allow", "ask", "deny"];
|
|
985
|
+
/** `commandPolicy[].decision`'s enum, derived from {@link COMMAND_DECISIONS}. */
|
|
986
|
+
export const CommandDecision = z.enum(COMMAND_DECISIONS);
|
|
978
987
|
export const GovernanceConfig = z.object({
|
|
979
988
|
// autonomy:一个跨 knob 的安全总开关 / 放权梯度(普通用户敢用 + 专业用户放权)。
|
|
980
989
|
// read-only(只读规划,不落改)| ask(每个放权动作问)| plan(plan-review 门)| auto(放开)。
|
|
981
990
|
// ⚠️ 消费侧(service)把它**展开**到 core 已有三原语(handsReadOnly / shellGate+onAsk / plan_review),
|
|
982
991
|
// 经 core 的收紧-only `tightenTaskSpec` 合并。core 裁决:不在 core 加并行 autonomy 枚举(双实现陷阱),
|
|
983
992
|
// 不静默覆盖调用者已设安全字段(auto 模式尤其不能静默清门控)。详见 web-admin docs/CONFIG-RESEARCH §10。
|
|
984
|
-
autonomy:
|
|
993
|
+
autonomy: AutonomyMode.optional(),
|
|
985
994
|
// commandPolicy:命令级 allow/ask/deny 护栏(remoteExec 管"在哪跑",这管"能跑什么命令")。每条 = 一个
|
|
986
995
|
// **精确 argv[0] 命令名** → 决策;**DENY-WINS**(冲突 deny 赢,与 core `combinePolicies` 一致,NOT last-match-wins)。
|
|
987
996
|
// ⚠️ EXACT,不是 glob(service 裁决①,2026-06-22):core 1.114.0 shipped 的 `createCoarseCommandNamePolicy`
|
|
@@ -1003,7 +1012,7 @@ export const GovernanceConfig = z.object({
|
|
|
1003
1012
|
.min(1)
|
|
1004
1013
|
.max(128)
|
|
1005
1014
|
.regex(/^[A-Za-z0-9._+-]+$/, "exact argv[0] command NAME — letters/digits/._+- only; NO glob (*,?), path (/), spaces or shell fragments — e.g. rm, git, docker-compose"),
|
|
1006
|
-
decision:
|
|
1015
|
+
decision: CommandDecision,
|
|
1007
1016
|
}))
|
|
1008
1017
|
.optional(),
|
|
1009
1018
|
/** Tool names needing operator approval (F4). ABSENT = web-admin 不管;显式 `[]` = 一个都不需要审批。 */
|
|
@@ -1183,7 +1192,7 @@ export const EntitlementTier = z.object({
|
|
|
1183
1192
|
/** 🔴 B1 违宪修复(EXPERT-REDESIGN §4-1):tokenQuota **不挂 tier** —— 只挂 defaults/subject/groupBinding。
|
|
1184
1193
|
* fail-loud 而非 zod 默认的静默剥除(admin 挂上去以为生效才是最糟的)。 */
|
|
1185
1194
|
tokenQuota: z
|
|
1186
|
-
.custom(() => false,
|
|
1195
|
+
.custom(() => false, buildQuotedIssue `tokenQuota does not mount on a tier — mount it on defaults / subjects[] / groupBindings[] (拍板:配额挂用户/组,非访问级别)`)
|
|
1187
1196
|
.optional(),
|
|
1188
1197
|
});
|
|
1189
1198
|
/** A time-bounded, audited per-principal override (escape hatch above the tier/role defaults). */
|
|
@@ -1205,7 +1214,7 @@ export const EntitlementOverride = z
|
|
|
1205
1214
|
// by env-NAME, never inline. (Budget overrides are numbers/enums, so this never fires on the v1 use.)
|
|
1206
1215
|
const s = typeof o.value === "string" ? o.value : o.value == null ? "" : JSON.stringify(o.value);
|
|
1207
1216
|
if (s && containsSecretToken(s))
|
|
1208
|
-
ctx.addIssue({ code: "custom",
|
|
1217
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `override.value appears to contain a secret token — overrides are persisted + audited; reference secrets by env-NAME, never inline a value`, path: ["value"] });
|
|
1209
1218
|
});
|
|
1210
1219
|
export const EntitlementSubject = z.object({
|
|
1211
1220
|
principal: z.string().min(1),
|
|
@@ -1368,7 +1377,7 @@ export const SESSION_MIRROR_ENGINE_URL = z.string().trim().pipe(NON_SECRET_URL).
|
|
|
1368
1377
|
catch {
|
|
1369
1378
|
return true; // already passed NON_SECRET_URL; unreachable in practice
|
|
1370
1379
|
}
|
|
1371
|
-
},
|
|
1380
|
+
}, buildQuotedIssue `non-loopback sessionMirror.engineUrl must be https — the mirror carries session content + a Bearer credential; plain http across the network is rejected at WRITE time (shell evaluateEngineRouting 同款传输闸)`);
|
|
1372
1381
|
/** Org 默认会话镜像目标(WEB-SETUP-RECON 附B,产品拍板两层,0.10.1)。**收编进 execution 域**(非独立
|
|
1373
1382
|
* 域)——142-S3 草案同款尺量:①同一治理轴(org 对「会话数据必须落到受控引擎」的 scope 级管控,与
|
|
1374
1383
|
* 「任务必须跑在受控 lane」同族;`required` 语义与 {@link ExecutionPolicy}.required 完全同构=壳锁定、
|
|
@@ -1453,41 +1462,72 @@ export const ExecutionPolicy = z.object({
|
|
|
1453
1462
|
* flags:本常量刻意**不带** `i`,因为它是登记簿 Record 的**键**,大小写不同的同一个 UUID 会变成两条登记行
|
|
1454
1463
|
* (同一 project 双注册);引擎侧是识别语义(读 marker / 找回身份),放宽合理。 */
|
|
1455
1464
|
export const PROJECT_ID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
1465
|
+
/** The bounds of a project registration's free-form fields —— the ONE source of these numbers: the schema below reads them, and so do
|
|
1466
|
+
* the server's project routes and the admin / client forms (they import this, never copy a number). The path bound of `folder` / `file`
|
|
1467
|
+
* entries is the host-path judge's own ({@link ABSOLUTE_HOST_PATH_MAX}). */
|
|
1468
|
+
export const PROJECT_LIMITS = {
|
|
1469
|
+
displayNameMax: 200,
|
|
1470
|
+
goalMax: 2000,
|
|
1471
|
+
contextItemsMax: 64,
|
|
1472
|
+
noteMax: 4000,
|
|
1473
|
+
};
|
|
1474
|
+
/** A project context item's kind —— the closed set as a RUNTIME VALUE: `folder` / `file` = a host path (judged by the same
|
|
1475
|
+
* {@link isAbsoluteHostPath} as a task's `cwd`), `note` = free text. */
|
|
1476
|
+
export const PROJECT_CONTEXT_ITEM_KINDS = ["folder", "file", "note"];
|
|
1477
|
+
/** A project's icon: a token (lowercase letter first, then lowercase letters / digits / `-`, at most 64). Absent = the default icon. */
|
|
1478
|
+
export const PROJECT_ICON_REGEX = /^[a-z][a-z0-9-]{0,63}$/;
|
|
1479
|
+
/** A project's colour: `#` + six lowercase hex digits. Absent = the default colour. */
|
|
1480
|
+
export const PROJECT_COLOR_REGEX = /^#[0-9a-f]{6}$/;
|
|
1481
|
+
/** One context item of a project. */
|
|
1482
|
+
export const ProjectContextItem = z.discriminatedUnion("kind", [
|
|
1483
|
+
z.object({ kind: z.literal("folder"), value: AbsoluteHostPath }),
|
|
1484
|
+
z.object({ kind: z.literal("file"), value: AbsoluteHostPath }),
|
|
1485
|
+
z.object({ kind: z.literal("note"), value: z.string().max(PROJECT_LIMITS.noteMax) }),
|
|
1486
|
+
]);
|
|
1487
|
+
// 每个字段的 schema 只写一次:登记形({@link ProjectRegistration})与 PATCH 形({@link ProjectRegistrationPatch})都由它们组成。
|
|
1488
|
+
const ProjectDisplayName = z.string().min(1).max(PROJECT_LIMITS.displayNameMax);
|
|
1489
|
+
/** 归一化 git remotes(`host/path` 形态,design/142 §1.1 归一化规范)。归一化动作发生在 scaffold/调用方,本契约只验底线形状:非空、无空白、
|
|
1490
|
+
* 不含 "://"(出现即证据未归一化,fail-loud)。无仓 project = 空数组合法。`.max(16)` 防登记簿被单条撑爆。 */
|
|
1491
|
+
const ProjectGitRemotes = z
|
|
1492
|
+
.array(z
|
|
1493
|
+
.string()
|
|
1494
|
+
.min(1)
|
|
1495
|
+
.max(512)
|
|
1496
|
+
.refine((s) => !/\s/.test(s) && !s.includes("://"), buildQuotedIssue `must be a NORMALIZED remote (host/path — no scheme, no whitespace)`))
|
|
1497
|
+
.max(16);
|
|
1498
|
+
/** 该项目会话的默认记忆 scope 词表。OPEN value domain — z.string(),不 enum(纪律:加词不得 BREAKING)。方向(S4 细化):消费方=service
|
|
1499
|
+
* memoryScope 派生(作 TaskSpec.memory.scopes 种子),形态=v2 scope 键字符串(过 core parseScopeKey 校验;known 词根:`proj` / `userproj`)。
|
|
1500
|
+
* 未知串=消费者自定义;空数组=不下发默认。本域只登记不解释,零耦合。 */
|
|
1501
|
+
const ProjectDefaultScopes = z.array(z.string().min(1));
|
|
1502
|
+
const ProjectGoal = z.string().max(PROJECT_LIMITS.goalMax);
|
|
1503
|
+
const ProjectContextItems = z.array(ProjectContextItem).max(PROJECT_LIMITS.contextItemsMax);
|
|
1504
|
+
const ProjectIcon = z.string().regex(PROJECT_ICON_REGEX, "an icon is a token: a lowercase letter, then lowercase letters / digits / -, at most 64");
|
|
1505
|
+
const ProjectColor = z.string().regex(PROJECT_COLOR_REGEX, "a colour is # followed by six lowercase hex digits");
|
|
1456
1506
|
export const ProjectRegistration = z.object({
|
|
1457
|
-
/** 人读名(展示/管理面用;不是身份,改名自由——身份=Record 键 projectId)。 */
|
|
1458
|
-
displayName:
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
gitRemotes: z
|
|
1463
|
-
.array(z
|
|
1464
|
-
.string()
|
|
1465
|
-
.min(1)
|
|
1466
|
-
.max(512)
|
|
1467
|
-
.refine((s) => !/\s/.test(s) && !s.includes("://"), "must be a NORMALIZED remote (host/path — no scheme, no whitespace)"))
|
|
1468
|
-
.max(16)
|
|
1469
|
-
.prefault([]),
|
|
1470
|
-
/** 该项目会话的默认记忆 scope 词表。OPEN value domain — z.string(),不 enum(纪律:加词
|
|
1471
|
-
* 不得 BREAKING)。方向(S4 细化):消费方=service memoryScope 派生(作 TaskSpec.
|
|
1472
|
-
* memory.scopes 种子),形态=v2 scope 键字符串(过 core parseScopeKey 校验;known 词根:`proj` /
|
|
1473
|
-
* `userproj`)。未知串=消费者自定义;空数组=不下发默认。本域只登记不解释,零耦合。 */
|
|
1474
|
-
defaultScopes: z.array(z.string().min(1)).prefault([]),
|
|
1475
|
-
/** 登记人 principal(审计锚;web-admin 写面自动填 caller,不信客户端提交)。 */
|
|
1507
|
+
/** 人读名(展示/管理面用;不是身份,改名自由——身份=Record 键 projectId)。上界 {@link PROJECT_LIMITS}`.displayNameMax`(200;此前 120)。 */
|
|
1508
|
+
displayName: ProjectDisplayName,
|
|
1509
|
+
gitRemotes: ProjectGitRemotes.prefault([]),
|
|
1510
|
+
defaultScopes: ProjectDefaultScopes.prefault([]),
|
|
1511
|
+
/** 登记人 principal(审计锚;web-admin 写面自动填 caller,不信客户端提交)。PATCH 改不了它。 */
|
|
1476
1512
|
registeredBy: z.string().min(1),
|
|
1477
|
-
/** 登记时刻(ISO)。web-admin 写面 server 端盖章。 */
|
|
1513
|
+
/** 登记时刻(ISO)。web-admin 写面 server 端盖章。PATCH 改不了它。 */
|
|
1478
1514
|
registeredAt: z.string().min(1),
|
|
1515
|
+
/** 自由备注(与 `goal` 不是一件事)。 */
|
|
1479
1516
|
notes: z.string().optional(),
|
|
1480
|
-
})
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
})
|
|
1487
|
-
.
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1517
|
+
/** 项目目标(≤ {@link PROJECT_LIMITS}`.goalMax`)。 */
|
|
1518
|
+
goal: ProjectGoal.optional(),
|
|
1519
|
+
/** 项目上下文条目(≤ {@link PROJECT_LIMITS}`.contextItemsMax` 条,见 {@link ProjectContextItem})。 */
|
|
1520
|
+
contextItems: ProjectContextItems.optional(),
|
|
1521
|
+
/** 归档时刻(RFC 3339)。在场 = 已归档;没有硬删(会话行引用项目)。 */
|
|
1522
|
+
archivedAt: RFC3339_DATE_TIME.optional(),
|
|
1523
|
+
/** 图标 token({@link PROJECT_ICON_REGEX})。 */
|
|
1524
|
+
icon: ProjectIcon.optional(),
|
|
1525
|
+
/** 颜色({@link PROJECT_COLOR_REGEX})。 */
|
|
1526
|
+
color: ProjectColor.optional(),
|
|
1527
|
+
});
|
|
1528
|
+
/** 同 scope 内一条 remote 只许属于一个 project——「remote → projectId」找回阶梯(design/142 §1.5 ③)与 scaffold 防重铸查询要求该映射在
|
|
1529
|
+
* 租户内确定;撞了=写入 fail-loud(合法双登记诉求出现时再放开,先紧后松 additive)。按文档键序走:后出现的那一条被拒、句子点名先占的项目。 */
|
|
1530
|
+
function remoteUniquenessIssues(cfg, ctx) {
|
|
1491
1531
|
const seen = new Map();
|
|
1492
1532
|
for (const [pid, reg] of Object.entries(cfg.projects)) {
|
|
1493
1533
|
for (const [i, r] of reg.gitRemotes.entries()) {
|
|
@@ -1498,7 +1538,66 @@ export const ProjectsConfig = z
|
|
|
1498
1538
|
seen.set(r, pid);
|
|
1499
1539
|
}
|
|
1500
1540
|
}
|
|
1501
|
-
}
|
|
1541
|
+
}
|
|
1542
|
+
export const ProjectsConfig = z
|
|
1543
|
+
.object({
|
|
1544
|
+
/** 键=projectId({@link PROJECT_ID_REGEX},generic lowercase UUID——宽读);tenant 半边=web-admin
|
|
1545
|
+
* scope 轴(不进域文档,见域头注)。 */
|
|
1546
|
+
projects: z.record(z.string().regex(PROJECT_ID_REGEX, "projectId must be a lowercase canonical UUID"), ProjectRegistration).prefault({}),
|
|
1547
|
+
})
|
|
1548
|
+
.superRefine(remoteUniquenessIssues);
|
|
1549
|
+
/** The PATCH form of one registration: every key a PATCH may change, all optional; `registeredBy` / `registeredAt` (and any key
|
|
1550
|
+
* the registration does not have) are refused (`.strict()`). An optional key given `null` is cleared (merge-patch: `archivedAt: null`
|
|
1551
|
+
* un-archives). Each field is the registration's own schema — one bound, one shape. */
|
|
1552
|
+
export const ProjectRegistrationPatch = z
|
|
1553
|
+
.object({
|
|
1554
|
+
displayName: ProjectDisplayName.optional(),
|
|
1555
|
+
gitRemotes: ProjectGitRemotes.optional(),
|
|
1556
|
+
defaultScopes: ProjectDefaultScopes.optional(),
|
|
1557
|
+
notes: z.string().nullable().optional(),
|
|
1558
|
+
goal: ProjectGoal.nullable().optional(),
|
|
1559
|
+
contextItems: ProjectContextItems.nullable().optional(),
|
|
1560
|
+
archivedAt: RFC3339_DATE_TIME.nullable().optional(),
|
|
1561
|
+
icon: ProjectIcon.nullable().optional(),
|
|
1562
|
+
color: ProjectColor.nullable().optional(),
|
|
1563
|
+
})
|
|
1564
|
+
.strict();
|
|
1565
|
+
/**
|
|
1566
|
+
* Apply a PATCH to one registration and judge the whole document with **the same** {@link ProjectsConfig} (the same remote-uniqueness rule a
|
|
1567
|
+
* full write meets — a PATCH has no rule of its own). The patch is judged here by {@link ProjectRegistrationPatch} first — whatever the caller
|
|
1568
|
+
* did before (a request body handed over as is cannot rewrite `registeredBy` / `registeredAt` or carry an unknown key) — and only its parsed
|
|
1569
|
+
* keys are merged. The patched project is judged LAST, so a remote another project already holds is refused on the patched project and the
|
|
1570
|
+
* sentence names the holder, whatever the document's key order; a successful result keeps the document's key order. Every issue path is a
|
|
1571
|
+
* document coordinate (`["projects", <projectId>, …]`), the patch's own included. `projectId` must be registered (the caller answers "no such
|
|
1572
|
+
* project" first) —— otherwise this throws.
|
|
1573
|
+
*/
|
|
1574
|
+
export function applyProjectPatch(doc, projectId, patch) {
|
|
1575
|
+
const current = Object.prototype.hasOwnProperty.call(doc.projects, projectId) ? doc.projects[projectId] : undefined;
|
|
1576
|
+
if (current === undefined)
|
|
1577
|
+
throw new Error(`applyProjectPatch: project "${projectId}" is not registered in this document`);
|
|
1578
|
+
const parsed = ProjectRegistrationPatch.safeParse(patch);
|
|
1579
|
+
if (!parsed.success) {
|
|
1580
|
+
const issues = parsed.error.issues.map((i) => ({ ...i, path: ["projects", projectId, ...i.path] }));
|
|
1581
|
+
return { success: false, error: new z.ZodError(issues) };
|
|
1582
|
+
}
|
|
1583
|
+
const next = { ...current };
|
|
1584
|
+
for (const [k, v] of Object.entries(parsed.data)) {
|
|
1585
|
+
if (v === undefined)
|
|
1586
|
+
continue;
|
|
1587
|
+
if (v === null)
|
|
1588
|
+
delete next[k];
|
|
1589
|
+
else
|
|
1590
|
+
next[k] = v;
|
|
1591
|
+
}
|
|
1592
|
+
const { [projectId]: _patched, ...others } = doc.projects;
|
|
1593
|
+
const judged = ProjectsConfig.safeParse({ ...doc, projects: { ...others, [projectId]: next } });
|
|
1594
|
+
if (!judged.success)
|
|
1595
|
+
return judged;
|
|
1596
|
+
const ordered = {};
|
|
1597
|
+
for (const pid of Object.keys(doc.projects))
|
|
1598
|
+
ordered[pid] = judged.data.projects[pid];
|
|
1599
|
+
return { ...judged, data: { ...judged.data, projects: ordered } };
|
|
1600
|
+
}
|
|
1502
1601
|
// ── Domain: collab (协作模式 — multi-model collaboration templates, 0.7.0) ─────
|
|
1503
1602
|
// Replaces the deleted `teams` domain (EXPERT-REDESIGN §8/§9, 五拍裁定 T1-T5).
|
|
1504
1603
|
// Unified shape = slots × topology × rounds × finalizer. COMPLETELY separate from rosters (拍板): a slot
|
|
@@ -1571,7 +1670,7 @@ const CollabTemplate = z.object({
|
|
|
1571
1670
|
});
|
|
1572
1671
|
if (t.finalizer.type === "none") {
|
|
1573
1672
|
if (t.finalizer.slot !== undefined)
|
|
1574
|
-
ctx.addIssue({ code: "custom",
|
|
1673
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `finalizer type "none" must not anchor a slot`, path: ["finalizer", "slot"] });
|
|
1575
1674
|
}
|
|
1576
1675
|
else if (t.finalizer.slot !== undefined && !slotIds.has(t.finalizer.slot)) {
|
|
1577
1676
|
ctx.addIssue({ code: "custom", ...buildQuotedIssue `finalizer.slot "${t.finalizer.slot}" is not a slot id of this template`, path: ["finalizer", "slot"] });
|
|
@@ -1735,7 +1834,7 @@ const nonSecretLiteral = (minLength) => z
|
|
|
1735
1834
|
.string()
|
|
1736
1835
|
.min(minLength)
|
|
1737
1836
|
.max(2048)
|
|
1738
|
-
.refine((s) => !containsSecretToken(s),
|
|
1837
|
+
.refine((s) => !containsSecretToken(s), buildQuotedIssue `looks like a literal secret — a credential goes in a secretRefs field as an env-NAME, never a value here`);
|
|
1739
1838
|
/**
|
|
1740
1839
|
* `runtime.execution` —— 这台 worker 的执行车道(渲染:`REMOTE_EXEC=<lane>` + `REQUIRE_EXECUTION_LANE=true` + coords / secretRefs 逐键)。
|
|
1741
1840
|
* - `lane`:{@link EXECUTION_LANE_WORDS} 之一。没有「无手」词。
|
|
@@ -1856,7 +1955,7 @@ const WorkerSpecObject = z.object({
|
|
|
1856
1955
|
/** Target average CPU utilization %, vs resources.requests.cpu. Absent → backend default 80. */
|
|
1857
1956
|
targetCpuPercent: INTEGER.min(10).max(95).optional(),
|
|
1858
1957
|
})
|
|
1859
|
-
.refine((a) => a.maxReplicas >= a.minReplicas,
|
|
1958
|
+
.refine((a) => a.maxReplicas >= a.minReplicas, buildQuotedIssue `maxReplicas must be >= minReplicas`)
|
|
1860
1959
|
.optional(),
|
|
1861
1960
|
/** Per-worker resource requests/limits (k8s backend). Optional → the backend's modest default
|
|
1862
1961
|
* (requests cpu 50m/mem 128Mi, no limits). Hash-INCLUDED → a change rolls the pods (resources are
|
|
@@ -2090,7 +2189,7 @@ export const WorkersConfig = z
|
|
|
2090
2189
|
// replicas + autoscaling = two owners of the same knob (the orchestrator's SSA vs the HPA controller
|
|
2091
2190
|
// would endlessly fight over spec.replicas) → reject at save, not at reconcile.
|
|
2092
2191
|
if (w.replicas !== undefined && w.autoscaling) {
|
|
2093
|
-
ctx.addIssue({ code: "custom",
|
|
2192
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `replicas and autoscaling are mutually exclusive — the HPA owns the replica count when autoscaling is set`, path: ["workers", i, "autoscaling"] });
|
|
2094
2193
|
}
|
|
2095
2194
|
});
|
|
2096
2195
|
});
|
|
@@ -2251,7 +2350,7 @@ const McpEnvRefs = z.record(ENV_NAME, ENV_NAME); // target ENV key → ENV_NAME
|
|
|
2251
2350
|
const HTTP_HEADER_NAME = z
|
|
2252
2351
|
.string()
|
|
2253
2352
|
.regex(/^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/, "must be an HTTP header NAME")
|
|
2254
|
-
.refine((s) => !containsSecretToken(s),
|
|
2353
|
+
.refine((s) => !containsSecretToken(s), buildQuotedIssue `HTTP header name must not contain a secret literal — credentials go in the env-NAME value, never the header name`);
|
|
2255
2354
|
// Header NAME → ENV_NAME holding its value. Header names are tokens (RFC 7230) — not env-name-shaped.
|
|
2256
2355
|
const McpHeaderRefs = z.record(HTTP_HEADER_NAME, ENV_NAME);
|
|
2257
2356
|
export const McpTransport = z.discriminatedUnion("kind", [
|
|
@@ -2264,13 +2363,13 @@ export const McpTransport = z.discriminatedUnion("kind", [
|
|
|
2264
2363
|
command: z
|
|
2265
2364
|
.string()
|
|
2266
2365
|
.min(1)
|
|
2267
|
-
.refine((s) => !containsSecretToken(s),
|
|
2268
|
-
.refine((s) => !hasNul(s),
|
|
2366
|
+
.refine((s) => !containsSecretToken(s), buildQuotedIssue `mcp stdio command must not contain a secret literal — put credentials in envRefs (an env-NAME)`)
|
|
2367
|
+
.refine((s) => !hasNul(s), buildQuotedIssue `mcp stdio command must not contain a NUL byte (Node cannot spawn it)`),
|
|
2269
2368
|
args: z
|
|
2270
2369
|
.array(z
|
|
2271
2370
|
.string()
|
|
2272
|
-
.refine((s) => !containsSecretToken(s),
|
|
2273
|
-
.refine((s) => !hasNul(s),
|
|
2371
|
+
.refine((s) => !containsSecretToken(s), buildQuotedIssue `mcp stdio arg must not contain a secret literal — put credentials in envRefs (an env-NAME)`)
|
|
2372
|
+
.refine((s) => !hasNul(s), buildQuotedIssue `mcp stdio arg must not contain a NUL byte (Node cannot spawn it)`))
|
|
2274
2373
|
.optional(),
|
|
2275
2374
|
/** target ENV key → ENV_NAME in the service env (never a secret value). */
|
|
2276
2375
|
envRefs: McpEnvRefs.optional(),
|
|
@@ -2374,18 +2473,18 @@ export const McpConfig = z
|
|
|
2374
2473
|
export const PLUGIN_ORIGIN = z
|
|
2375
2474
|
.string()
|
|
2376
2475
|
.min(1)
|
|
2377
|
-
.refine((s) => !containsSecretToken(s),
|
|
2378
|
-
.refine((s) => !hasNul(s),
|
|
2476
|
+
.refine((s) => !containsSecretToken(s), buildQuotedIssue `plugin source must not contain a secret literal — private remotes authenticate via the consuming machine's git credentials, never an inline token`)
|
|
2477
|
+
.refine((s) => !hasNul(s), buildQuotedIssue `plugin source must not contain a NUL byte`)
|
|
2379
2478
|
.superRefine((s, ctx) => {
|
|
2380
2479
|
// The shape is THE URL judge's ({@link httpUrlFlaws}); this seat adds two rules of its own: https only, and spelled in full
|
|
2381
2480
|
// (`https://…` as written — git reads the raw string, and WHATWG's repair of `https:host/x` into `https://host/x` is not git's).
|
|
2382
2481
|
const flaws = httpUrlFlaws(s, ["userinfo"]);
|
|
2383
2482
|
if (flaws.includes("not-http-url") || new URL(s).protocol !== "https:" || s.slice(0, 8).toLowerCase() !== "https://") {
|
|
2384
|
-
ctx.addIssue({ code: "custom",
|
|
2483
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `plugin source must be an https:// URL (contract: https-only; ssh/scp/git/file/plain-http and owner/repo shorthand are not accepted — domestic mirrors ride an https mirror domain via PLUGINS_ALLOW_HOSTS)` });
|
|
2385
2484
|
return;
|
|
2386
2485
|
}
|
|
2387
2486
|
if (flaws.includes("userinfo") || urlQueryFragmentHasSecret(new URL(s))) {
|
|
2388
|
-
ctx.addIssue({ code: "custom",
|
|
2487
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `an https plugin source must not embed credentials (userinfo) or a secret-bearing query/fragment` });
|
|
2389
2488
|
}
|
|
2390
2489
|
});
|
|
2391
2490
|
/** A full 40-hex git commit SHA — the ONLY pin form the supply-side treats as 钉死 (a branch/tag `version`
|
|
@@ -2434,7 +2533,9 @@ export const PluginsConfig = z
|
|
|
2434
2533
|
// 消费投影在 server(声明数组逐字喂 core typed stableBlocks 钩子),schema 恒在本仓(零第二份)。
|
|
2435
2534
|
/** 提示声明的槽位闭集(core `PromptTextDeclaration.slot` 同形):identity = 角色卡、scenario = 场景说明、behavior = 行为守则。
|
|
2436
2535
|
* declaration 轴与 catalog 轴({@link PromptArtifactSection})共用这一只。 */
|
|
2437
|
-
export const
|
|
2536
|
+
export const PROMPT_SLOTS = ["identity", "scenario", "behavior"];
|
|
2537
|
+
/** The slot enum, DERIVED from {@link PROMPT_SLOTS} (the runtime member table —— same form as `REF_CHECKED_DOMAINS`). */
|
|
2538
|
+
export const PromptSlot = z.enum(PROMPT_SLOTS);
|
|
2438
2539
|
/** core `PromptTextDeclaration` 的契约层同形(core prompts/default.d.ts 逐字:id/slot 闭集/text)。
|
|
2439
2540
|
* slot ↔ 三卡:identity=角色卡、scenario=场景说明、behavior=行为守则([1051]①)。 */
|
|
2440
2541
|
export const PromptTextDeclaration = z.object({
|
|
@@ -2465,11 +2566,11 @@ export const PromptBinding = z.object({
|
|
|
2465
2566
|
notes: z.string().optional(),
|
|
2466
2567
|
}).superRefine((b, ctx) => {
|
|
2467
2568
|
if (b.level === "worker" && !b.worker)
|
|
2468
|
-
ctx.addIssue({ code: "custom",
|
|
2569
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `level=worker requires \`worker\`` });
|
|
2469
2570
|
if (b.level === "scenario" && !b.scenario)
|
|
2470
|
-
ctx.addIssue({ code: "custom",
|
|
2571
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `level=scenario requires \`scenario\`` });
|
|
2471
2572
|
if (b.level === "global" && (b.worker || b.scenario))
|
|
2472
|
-
ctx.addIssue({ code: "custom",
|
|
2573
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `level=global must not carry a selector` });
|
|
2473
2574
|
});
|
|
2474
2575
|
// ═══ prompts 域 catalog 轴(0.10.20;[ref]a/[1242]/[1248] 过轮定稿;core design/148 S1 seam)═══
|
|
2475
2576
|
// 形 = core 1.336+ 的 `PublishedPromptArtifactEnvelope`(sema-core src/prompt-assembly/artifact.ts,
|
|
@@ -2565,7 +2666,7 @@ export const EffectivePromptsWire = z
|
|
|
2565
2666
|
declarations: EffectivePrompts.optional(),
|
|
2566
2667
|
catalog: PublishedPromptArtifactEnvelope.optional(),
|
|
2567
2668
|
})
|
|
2568
|
-
.refine((w) => w.declarations !== undefined || w.catalog !== undefined,
|
|
2669
|
+
.refine((w) => w.declarations !== undefined || w.catalog !== undefined, buildQuotedIssue `at least one axis (declarations|catalog) required`);
|
|
2569
2670
|
export const DOMAIN_SCHEMAS = {
|
|
2570
2671
|
models: ModelsConfig,
|
|
2571
2672
|
rosters: RostersConfig,
|
|
@@ -32,8 +32,8 @@ export declare const WEB_ADMIN_CREDENTIAL_ENVELOPE_KIND: "user-session";
|
|
|
32
32
|
* 文件**取当前值,不用内存旧值(另一个写者可能刚轮换过)。
|
|
33
33
|
* · `expiresAt` —— 访问令牌到期时刻(RFC 3339)。
|
|
34
34
|
* · `issuer` —— 签发它的 web-admin 的**基础 URL**(与读方连 web-admin 的根 URL 同形,可带路径前缀):续签只向同源的 web-admin 发,
|
|
35
|
-
* 同不同源由读方比。URL
|
|
36
|
-
* query / fragment
|
|
35
|
+
* 同不同源由读方比。URL 形判据是包内 URL 判官的「非存量座位」那一只 {@link NON_SECRET_URL_NO_MIGRATION}(http(s)、有 host、userinfo 与
|
|
36
|
+
* query / fragment 里不许夹带凭据;issuer 是 wire 输入、不是存量,一次性迁移不碰它,fragment 拒句因此不指路)—— 本包不另写一份更窄的 URL 律。
|
|
37
37
|
*/
|
|
38
38
|
export declare const WebAdminCredentialEnvelope: z.ZodObject<{
|
|
39
39
|
kind: z.ZodLiteral<"user-session">;
|