@sema-agent/settings-schema 11.1.0 → 11.3.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 +151 -0
- package/README.md +14 -1
- package/bin/settings-schema.mjs +5 -9
- package/conformance/VERSION +1 -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/dist/api/feedback.js +2 -3
- package/dist/api/scopes.js +2 -1
- package/dist/atomic-write.d.ts +7 -1
- package/dist/atomic-write.js +43 -5
- package/dist/cli.d.ts +34 -6
- package/dist/cli.js +80 -130
- package/dist/config-d-file.d.ts +11 -0
- package/dist/config-d-file.js +35 -1
- package/dist/config-dir-io.d.ts +2 -0
- package/dist/config-dir-io.js +14 -0
- package/dist/config-dir-migration.d.ts +67 -0
- package/dist/config-dir-migration.js +181 -0
- 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/index.d.ts +1 -0
- package/dist/index.js +1 -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 +3 -2
- package/dist/node.d.ts +2 -0
- package/dist/node.js +3 -0
- package/dist/one-time-migrations.d.ts +5 -3
- package/dist/one-time-migrations.js +21 -7
- package/dist/previous-web-admin-env.d.ts +15 -0
- package/dist/previous-web-admin-env.js +20 -0
- 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/retired-envs.js +13 -2
- package/dist/retired-form.d.ts +6 -1
- package/dist/retired-form.js +10 -2
- package/dist/rfc3339.d.ts +10 -0
- package/dist/rfc3339.js +12 -0
- package/dist/runtime-lift-targets.d.ts +7 -0
- package/dist/runtime-lift-targets.js +7 -0
- package/dist/shell-quote.d.ts +1 -0
- package/dist/shell-quote.js +14 -0
- package/dist/types.d.ts +140 -20
- package/dist/types.js +224 -114
- package/dist/web-admin-credential.d.ts +2 -2
- package/dist/web-admin-credential.js +5 -10
- package/dist/web-admin-env.d.ts +2 -1
- package/dist/web-admin-env.js +2 -1
- package/dist/web-admin-session-url.d.ts +17 -0
- package/dist/web-admin-session-url.js +64 -0
- package/package.json +1 -1
package/dist/types.js
CHANGED
|
@@ -24,11 +24,15 @@ import { isLoopbackAuthHost } from "./loopback.js";
|
|
|
24
24
|
import { httpUrlFlaws, httpUrlRule } from "./url-shape.js"; // 11.0.0: THE URL shape judge // 11.0.0: the one narrow「本机」predicate (auth relaxation)
|
|
25
25
|
import { secretScanForms } from "./secret-scan.js"; // package-private normalization — the one set of forms both scanners read (not in the barrel)
|
|
26
26
|
import { WEB_ADMIN_ENV } from "./web-admin-env.js"; // the three connection keys the config renderer writes are reserved worker env
|
|
27
|
+
import { PREVIOUS_WEB_ADMIN_ENV } from "./previous-web-admin-env.js"; // the previous generation's names of those three (the renderer still writes them for engines that read them)
|
|
27
28
|
import { retiredEnvRefusal } from "./retired-envs.js"; // the worker env write gate asks the retired-env tombstone table too (one table, one refusal sentence)
|
|
28
29
|
import { Sha256Digest } from "./sha256.js"; // the one `sha256:<hex>` digest form
|
|
29
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)
|
|
30
31
|
import { LANE_ENV_OWNER, isMigratableEnvKey, laneEnvSeat, laneRowOf } from "./execution-lane-index.js"; // package-private indexes derived from that table (not in the barrel)
|
|
31
32
|
import { migrationSignpost, retiredFormSentence, retiredKey } from "./retired-form.js"; // 11.0.0: the ONE retired-form sentence + key tombstone (package-private)
|
|
33
|
+
import { buildQuotedIssue, fixed } from "./quoted-issue.js"; // THE minting point of custom issues (package-private)
|
|
34
|
+
import { AbsoluteHostPath } from "./host-path.js"; // the one host-path shape judge (a task's cwd and a project's folder / file entries)
|
|
35
|
+
import { RFC3339_DATE_TIME } from "./rfc3339.js"; // the package's one RFC 3339 date-time judge (package-private)
|
|
32
36
|
// COARSE, best-effort literal-secret SHAPE detector (NOT exhaustive — the real boundary is the env-NAME schema
|
|
33
37
|
// on structured fields). Rejected in free-form value fields (worker `env`, model `extraBody`), URL components,
|
|
34
38
|
// 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-
|
|
@@ -55,7 +59,7 @@ export const ENV_NAME = z
|
|
|
55
59
|
// lowercase — the charset above rejects them). The ONE secret shape that IS a valid all-caps env-NAME charset
|
|
56
60
|
// is an AWS access-key id (AKIA…) — block exactly that, WITHOUT false-rejecting legit names like
|
|
57
61
|
// GITHUB_PAT_TOKEN / GHU_TOKEN (which the full SECRET_LITERAL_RE would). Round-5.
|
|
58
|
-
.refine((s) => !/^AKIA[0-9A-Z]{12,}$/.test(s),
|
|
62
|
+
.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)`);
|
|
59
63
|
function scanForSecret(s) {
|
|
60
64
|
// A private-key PEM header ANYWHERE in the string (the one regex the display redactor reads too). The header carries spaces, so
|
|
61
65
|
// neither the start-anchored SECRET_LITERAL_RE below nor the tokenizer at the bottom can see it behind a prefix / newline. 10.0.0.
|
|
@@ -93,30 +97,11 @@ function scanForSecret(s) {
|
|
|
93
97
|
* fs path — so the worker dies at exec/boot, not at config edit. A NUL is never legitimate in a command / path
|
|
94
98
|
* / host / image, so reject it on every exec-bound string field (mcp stdio + the whole remoteExec contract). R20. */
|
|
95
99
|
export const hasNul = (s) => s.includes("\0");
|
|
96
|
-
/** The
|
|
97
|
-
*
|
|
98
|
-
export
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
const fixed = (text) => ({ fixedText: text });
|
|
102
|
-
/** Mint a custom issue's `message` + `params` from a tagged template (`buildQuotedIssue\`duplicate model "${name}"\``): every
|
|
103
|
-
* interpolation is an operand unless wrapped in {@link fixed} (then it is template text). The ONE minting point of quoted
|
|
104
|
-
* sentences in this file — a template literal written straight into `message` would put a value into a sentence with no record
|
|
105
|
-
* of where it is (a source gate pins that none is left). */
|
|
106
|
-
function buildQuotedIssue(strings, ...parts) {
|
|
107
|
-
const template = [strings[0]];
|
|
108
|
-
const operands = [];
|
|
109
|
-
parts.forEach((part, i) => {
|
|
110
|
-
if (typeof part === "object")
|
|
111
|
-
template[template.length - 1] += part.fixedText + strings[i + 1];
|
|
112
|
-
else {
|
|
113
|
-
operands.push(part);
|
|
114
|
-
template.push(strings[i + 1]);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
const params = { template, operands };
|
|
118
|
-
return { message: renderQuotedSentence(params), params };
|
|
119
|
-
}
|
|
100
|
+
/** The params of a custom issue's sentence and their one rendering live in the package-private minting module `./quoted-issue`
|
|
101
|
+
* (one minting point for the whole package); re-exported here unchanged. */
|
|
102
|
+
export { renderQuotedSentence } from "./quoted-issue.js";
|
|
103
|
+
/** The one host-path shape judge (a task's `cwd`, a project's `folder` / `file` entries) lives in its leaf module `./host-path`. */
|
|
104
|
+
export { ABSOLUTE_HOST_PATH_MAX, AbsoluteHostPath, isAbsoluteHostPath } from "./host-path.js";
|
|
120
105
|
/** Does `s` contain a secret-shaped token? —— **the one scanner** (11.0.0, S-1274): the write side (this detector, behind every
|
|
121
106
|
* free-text write port — NON_SECRET_URL, mcp stdio, execution coords, ssh login, worker env, extraBody, …) and the display side
|
|
122
107
|
* (`redactSecrets`) read the SAME normalized forms ({@link secretScanForms} in the package-private `./secret-scan`: the raw string,
|
|
@@ -168,49 +153,52 @@ export function urlQueryFragmentHasSecret(u) {
|
|
|
168
153
|
* after it is dropped with it — the engine and server refuse a model route root that carries one, so a stored one only fails
|
|
169
154
|
* later, on the worker). A query is a legitimate request parameter (`?api-version=…`) and stays. */
|
|
170
155
|
const NON_SECRET_URL_FORBIDS = ["userinfo", "fragment"];
|
|
171
|
-
/** The refusal
|
|
172
|
-
* can carry from before 11.0.0 (a fragment) at a seat the one-time migration
|
|
173
|
-
* it, joined the way the other signposted refusals join it (`<rule>; <signpost>`).
|
|
174
|
-
* a compile error here. */
|
|
175
|
-
function
|
|
156
|
+
/** The refusal of one flaw at a {@link NON_SECRET_URL} seat (a {@link QuotedIssue} with no operands — the sentence quotes no value): the
|
|
157
|
+
* 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
|
|
158
|
+
* reaches — the signpost to the migration that strips it, joined the way the other signposted refusals join it (`<rule>; <signpost>`).
|
|
159
|
+
* Closed over the judge's flaw words: a new word is a compile error here. */
|
|
160
|
+
function nonSecretUrlIssue(flaw, reachedByMigration) {
|
|
176
161
|
switch (flaw) {
|
|
177
162
|
case "fragment":
|
|
178
|
-
return reachedByMigration ? `${httpUrlRule(flaw)}; ${migrationSignpost("SS-M7", "11.0.0")}` : httpUrlRule(flaw)
|
|
163
|
+
return reachedByMigration ? buildQuotedIssue `${fixed(httpUrlRule(flaw))}; ${fixed(migrationSignpost("SS-M7", "11.0.0"))}` : buildQuotedIssue `${fixed(httpUrlRule(flaw))}`;
|
|
179
164
|
case "not-http-url":
|
|
180
165
|
case "userinfo":
|
|
181
166
|
case "query":
|
|
182
|
-
return httpUrlRule(flaw)
|
|
167
|
+
return buildQuotedIssue `${fixed(httpUrlRule(flaw))}`;
|
|
183
168
|
default: {
|
|
184
169
|
const unhandled = flaw;
|
|
185
|
-
throw new Error("
|
|
170
|
+
throw new Error("nonSecretUrlIssue: unhandled flaw word", { cause: unhandled });
|
|
186
171
|
}
|
|
187
172
|
}
|
|
188
173
|
}
|
|
189
174
|
/** One judge, two seat kinds — the only difference is the fragment refusal's migration signpost. `reachedByMigration` = the seat sits in a
|
|
190
|
-
* stored
|
|
175
|
+
* stored document the one-time migration SS-M7 rewrites (it finds its seats by this schema's identity, so "carries the
|
|
191
176
|
* signpost" and "is rewritten by SS-M7" cannot drift apart). */
|
|
192
177
|
function createNonSecretUrl(reachedByMigration) {
|
|
193
178
|
return z.string().superRefine((s, ctx) => {
|
|
194
179
|
const flaws = httpUrlFlaws(s, NON_SECRET_URL_FORBIDS);
|
|
195
|
-
for (const flaw of flaws)
|
|
196
|
-
|
|
180
|
+
for (const flaw of flaws) {
|
|
181
|
+
const refusal = nonSecretUrlIssue(flaw, reachedByMigration);
|
|
182
|
+
ctx.addIssue({ code: "custom", ...refusal, params: { ...refusal.params, flaw } }); // `flaw` = the judge's rule word, beside the sentence's params
|
|
183
|
+
}
|
|
197
184
|
if (flaws.length === 0 && urlQueryFragmentHasSecret(new URL(s))) {
|
|
198
|
-
ctx.addIssue({ code: "custom",
|
|
185
|
+
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` });
|
|
199
186
|
}
|
|
200
187
|
});
|
|
201
188
|
}
|
|
202
189
|
/** A URL handed to `fetch` that carries no credential: judged by THE URL shape judge ({@link httpUrlFlaws}, `./url-shape`) — a
|
|
203
190
|
* hierarchical http(s) URL with neither userinfo nor a fragment (an empty `#` included, 11.0.0) — and with no secret-shaped value
|
|
204
191
|
* in its path or query ({@link urlQueryFragmentHasSecret}). One issue per flaw, its message the judge's rule sentence (no value
|
|
205
|
-
* echoed; a fragment's also names its migration, {@link
|
|
192
|
+
* echoed; a fragment's also names its migration, {@link nonSecretUrlIssue}); `params` carries the sentence (`template` / `operands`,
|
|
193
|
+
* no operands) and `flaw`, the rule word.
|
|
206
194
|
*
|
|
207
|
-
* This is the seat of every URL in a
|
|
208
|
-
* `gitApiBaseUrl`)
|
|
209
|
-
* `issuer
|
|
195
|
+
* This is the seat of every URL in a STORED document the one-time migration rewrites: the domain documents (models `baseUrl`, mcp `url`,
|
|
196
|
+
* `sessionMirror.engineUrl`, workers `gitApiBaseUrl`) and `config.d/remote-exec.json` (k8s `apiUrl`, `s3Snapshot.endpoint`). A URL
|
|
197
|
+
* that is not stored data — the web-admin credential envelope's `issuer`, a wire input — uses {@link NON_SECRET_URL_NO_MIGRATION}.
|
|
210
198
|
*
|
|
211
199
|
* The value is kept EXACTLY as written: nothing here trims or normalizes it (zod's own `.url()` would hand back a rewritten string). */
|
|
212
200
|
export const NON_SECRET_URL = createNonSecretUrl(true);
|
|
213
|
-
/** {@link NON_SECRET_URL}'s judge for a seat no one-time migration reaches (not
|
|
201
|
+
/** {@link NON_SECRET_URL}'s judge for a seat no one-time migration reaches (not stored data): identical rules and rule
|
|
214
202
|
* sentences, but a fragment's refusal carries no migration signpost — pointing at a migration that cannot rewrite the value would
|
|
215
203
|
* send the operator to a command that does nothing for it. */
|
|
216
204
|
export const NON_SECRET_URL_NO_MIGRATION = createNonSecretUrl(false);
|
|
@@ -298,19 +286,19 @@ function vetFreeFormJson(v, field) {
|
|
|
298
286
|
rawUnsafe = extraBodyUnsafe(v);
|
|
299
287
|
}
|
|
300
288
|
catch {
|
|
301
|
-
return { ok: false,
|
|
289
|
+
return { ok: false, issue: buildQuotedIssue `${fixed(field)} could not be validated (reflective access threw)` }; // R11
|
|
302
290
|
}
|
|
303
291
|
if (rawUnsafe)
|
|
304
|
-
return { ok: false,
|
|
292
|
+
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` };
|
|
305
293
|
let clone;
|
|
306
294
|
try {
|
|
307
295
|
clone = JSON.parse(JSON.stringify(v)); // the ONLY serialization — its result is what we scan AND persist
|
|
308
296
|
}
|
|
309
297
|
catch {
|
|
310
|
-
return { ok: false,
|
|
298
|
+
return { ok: false, issue: buildQuotedIssue `${fixed(field)} is not JSON-serializable` };
|
|
311
299
|
}
|
|
312
300
|
if (extraBodyUnsafe(clone))
|
|
313
|
-
return { ok: false,
|
|
301
|
+
return { ok: false, issue: buildQuotedIssue `${fixed(field)}: its serialized form carries a secret` };
|
|
314
302
|
return { ok: true, value: clone };
|
|
315
303
|
}
|
|
316
304
|
/** The transform of every **parse-transparent free-form** key (`z.unknown().optional().transform(createFreeFormTransform(field))`:
|
|
@@ -323,7 +311,7 @@ function createFreeFormTransform(field) {
|
|
|
323
311
|
return undefined;
|
|
324
312
|
const vetted = vetFreeFormJson(v, field);
|
|
325
313
|
if (!vetted.ok) {
|
|
326
|
-
ctx.addIssue({ code: "custom",
|
|
314
|
+
ctx.addIssue({ code: "custom", ...vetted.issue });
|
|
327
315
|
return z.NEVER;
|
|
328
316
|
}
|
|
329
317
|
return vetted.value;
|
|
@@ -503,12 +491,12 @@ export const ModelEntry = z.object({
|
|
|
503
491
|
return undefined;
|
|
504
492
|
const proto = typeof v === "object" && v !== null ? Object.getPrototypeOf(v) : undefined;
|
|
505
493
|
if (typeof v !== "object" || v === null || Array.isArray(v) || (proto !== Object.prototype && proto !== null)) {
|
|
506
|
-
ctx.addIssue({ code: "custom",
|
|
494
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `extraBody must be a plain JSON object` });
|
|
507
495
|
return z.NEVER;
|
|
508
496
|
}
|
|
509
497
|
const vetted = vetFreeFormJson(v, "extraBody");
|
|
510
498
|
if (!vetted.ok) {
|
|
511
|
-
ctx.addIssue({ code: "custom",
|
|
499
|
+
ctx.addIssue({ code: "custom", ...vetted.issue });
|
|
512
500
|
return z.NEVER;
|
|
513
501
|
}
|
|
514
502
|
return vetted.value;
|
|
@@ -807,7 +795,7 @@ export const SystemEntry = z.object({
|
|
|
807
795
|
// worker that isn't provisioned yet has no Secret to label-check, so a name-level reservation is
|
|
808
796
|
// the only thing closing the "squat on a future worker's credential surface" timing hole (S3 设计
|
|
809
797
|
// §4.4, council DESIGN-6/13). Cost: callers can't use a secret that happens to end in -env.
|
|
810
|
-
.refine((n) => !n.endsWith("-env"),
|
|
798
|
+
.refine((n) => !n.endsWith("-env"), buildQuotedIssue `secret names ending in "-env" are reserved for orchestrator worker-credential projections (红线1)`),
|
|
811
799
|
key: z.string().regex(/^[-._a-zA-Z0-9]+$/, "k8s secret data key").max(253),
|
|
812
800
|
})
|
|
813
801
|
.optional(),
|
|
@@ -974,13 +962,23 @@ export const LimitsConfig = z.object({
|
|
|
974
962
|
// The governance trio an operator tunes via publish: autonomy ladder + command guardrails + approval-required
|
|
975
963
|
// tools. NOT下沉 — gets its own admin tab (E5 form; E1 ships a transitional generic editor). Same `.optional()`
|
|
976
964
|
// no-default discipline as the six gates: ABSENT = web-admin 不管这项(keep env / 消费者默认),present 才生效。
|
|
965
|
+
/** The autonomy ladder —— the closed set as a RUNTIME VALUE (same form as `REF_CHECKED_DOMAINS`): {@link AutonomyMode} (the zod enum)
|
|
966
|
+
* 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
|
|
967
|
+
* it instead of reaching into `GovernanceConfig.shape.autonomy.unwrap().options` or writing the four words again. */
|
|
968
|
+
export const AUTONOMY_MODES = ["read-only", "ask", "plan", "auto"];
|
|
969
|
+
/** `governance.autonomy`'s enum, derived from {@link AUTONOMY_MODES}. */
|
|
970
|
+
export const AutonomyMode = z.enum(AUTONOMY_MODES);
|
|
971
|
+
/** A `commandPolicy[]` rule's decision —— the closed set as a RUNTIME VALUE; {@link CommandDecision} and its type are DERIVED from it. */
|
|
972
|
+
export const COMMAND_DECISIONS = ["allow", "ask", "deny"];
|
|
973
|
+
/** `commandPolicy[].decision`'s enum, derived from {@link COMMAND_DECISIONS}. */
|
|
974
|
+
export const CommandDecision = z.enum(COMMAND_DECISIONS);
|
|
977
975
|
export const GovernanceConfig = z.object({
|
|
978
976
|
// autonomy:一个跨 knob 的安全总开关 / 放权梯度(普通用户敢用 + 专业用户放权)。
|
|
979
977
|
// read-only(只读规划,不落改)| ask(每个放权动作问)| plan(plan-review 门)| auto(放开)。
|
|
980
978
|
// ⚠️ 消费侧(service)把它**展开**到 core 已有三原语(handsReadOnly / shellGate+onAsk / plan_review),
|
|
981
979
|
// 经 core 的收紧-only `tightenTaskSpec` 合并。core 裁决:不在 core 加并行 autonomy 枚举(双实现陷阱),
|
|
982
980
|
// 不静默覆盖调用者已设安全字段(auto 模式尤其不能静默清门控)。详见 web-admin docs/CONFIG-RESEARCH §10。
|
|
983
|
-
autonomy:
|
|
981
|
+
autonomy: AutonomyMode.optional(),
|
|
984
982
|
// commandPolicy:命令级 allow/ask/deny 护栏(remoteExec 管"在哪跑",这管"能跑什么命令")。每条 = 一个
|
|
985
983
|
// **精确 argv[0] 命令名** → 决策;**DENY-WINS**(冲突 deny 赢,与 core `combinePolicies` 一致,NOT last-match-wins)。
|
|
986
984
|
// ⚠️ EXACT,不是 glob(service 裁决①,2026-06-22):core 1.114.0 shipped 的 `createCoarseCommandNamePolicy`
|
|
@@ -1002,7 +1000,7 @@ export const GovernanceConfig = z.object({
|
|
|
1002
1000
|
.min(1)
|
|
1003
1001
|
.max(128)
|
|
1004
1002
|
.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"),
|
|
1005
|
-
decision:
|
|
1003
|
+
decision: CommandDecision,
|
|
1006
1004
|
}))
|
|
1007
1005
|
.optional(),
|
|
1008
1006
|
/** Tool names needing operator approval (F4). ABSENT = web-admin 不管;显式 `[]` = 一个都不需要审批。 */
|
|
@@ -1182,7 +1180,7 @@ export const EntitlementTier = z.object({
|
|
|
1182
1180
|
/** 🔴 B1 违宪修复(EXPERT-REDESIGN §4-1):tokenQuota **不挂 tier** —— 只挂 defaults/subject/groupBinding。
|
|
1183
1181
|
* fail-loud 而非 zod 默认的静默剥除(admin 挂上去以为生效才是最糟的)。 */
|
|
1184
1182
|
tokenQuota: z
|
|
1185
|
-
.custom(() => false,
|
|
1183
|
+
.custom(() => false, buildQuotedIssue `tokenQuota does not mount on a tier — mount it on defaults / subjects[] / groupBindings[] (拍板:配额挂用户/组,非访问级别)`)
|
|
1186
1184
|
.optional(),
|
|
1187
1185
|
});
|
|
1188
1186
|
/** A time-bounded, audited per-principal override (escape hatch above the tier/role defaults). */
|
|
@@ -1204,7 +1202,7 @@ export const EntitlementOverride = z
|
|
|
1204
1202
|
// by env-NAME, never inline. (Budget overrides are numbers/enums, so this never fires on the v1 use.)
|
|
1205
1203
|
const s = typeof o.value === "string" ? o.value : o.value == null ? "" : JSON.stringify(o.value);
|
|
1206
1204
|
if (s && containsSecretToken(s))
|
|
1207
|
-
ctx.addIssue({ code: "custom",
|
|
1205
|
+
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"] });
|
|
1208
1206
|
});
|
|
1209
1207
|
export const EntitlementSubject = z.object({
|
|
1210
1208
|
principal: z.string().min(1),
|
|
@@ -1367,7 +1365,7 @@ export const SESSION_MIRROR_ENGINE_URL = z.string().trim().pipe(NON_SECRET_URL).
|
|
|
1367
1365
|
catch {
|
|
1368
1366
|
return true; // already passed NON_SECRET_URL; unreachable in practice
|
|
1369
1367
|
}
|
|
1370
|
-
},
|
|
1368
|
+
}, 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 同款传输闸)`);
|
|
1371
1369
|
/** Org 默认会话镜像目标(WEB-SETUP-RECON 附B,产品拍板两层,0.10.1)。**收编进 execution 域**(非独立
|
|
1372
1370
|
* 域)——142-S3 草案同款尺量:①同一治理轴(org 对「会话数据必须落到受控引擎」的 scope 级管控,与
|
|
1373
1371
|
* 「任务必须跑在受控 lane」同族;`required` 语义与 {@link ExecutionPolicy}.required 完全同构=壳锁定、
|
|
@@ -1452,41 +1450,72 @@ export const ExecutionPolicy = z.object({
|
|
|
1452
1450
|
* flags:本常量刻意**不带** `i`,因为它是登记簿 Record 的**键**,大小写不同的同一个 UUID 会变成两条登记行
|
|
1453
1451
|
* (同一 project 双注册);引擎侧是识别语义(读 marker / 找回身份),放宽合理。 */
|
|
1454
1452
|
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}$/;
|
|
1453
|
+
/** The bounds of a project registration's free-form fields —— the ONE source of these numbers: the schema below reads them, and so do
|
|
1454
|
+
* the server's project routes and the admin / client forms (they import this, never copy a number). The path bound of `folder` / `file`
|
|
1455
|
+
* entries is the host-path judge's own ({@link ABSOLUTE_HOST_PATH_MAX}). */
|
|
1456
|
+
export const PROJECT_LIMITS = {
|
|
1457
|
+
displayNameMax: 200,
|
|
1458
|
+
goalMax: 2000,
|
|
1459
|
+
contextItemsMax: 64,
|
|
1460
|
+
noteMax: 4000,
|
|
1461
|
+
};
|
|
1462
|
+
/** A project context item's kind —— the closed set as a RUNTIME VALUE: `folder` / `file` = a host path (judged by the same
|
|
1463
|
+
* {@link isAbsoluteHostPath} as a task's `cwd`), `note` = free text. */
|
|
1464
|
+
export const PROJECT_CONTEXT_ITEM_KINDS = ["folder", "file", "note"];
|
|
1465
|
+
/** A project's icon: a token (lowercase letter first, then lowercase letters / digits / `-`, at most 64). Absent = the default icon. */
|
|
1466
|
+
export const PROJECT_ICON_REGEX = /^[a-z][a-z0-9-]{0,63}$/;
|
|
1467
|
+
/** A project's colour: `#` + six lowercase hex digits. Absent = the default colour. */
|
|
1468
|
+
export const PROJECT_COLOR_REGEX = /^#[0-9a-f]{6}$/;
|
|
1469
|
+
/** One context item of a project. */
|
|
1470
|
+
export const ProjectContextItem = z.discriminatedUnion("kind", [
|
|
1471
|
+
z.object({ kind: z.literal("folder"), value: AbsoluteHostPath }),
|
|
1472
|
+
z.object({ kind: z.literal("file"), value: AbsoluteHostPath }),
|
|
1473
|
+
z.object({ kind: z.literal("note"), value: z.string().max(PROJECT_LIMITS.noteMax) }),
|
|
1474
|
+
]);
|
|
1475
|
+
// 每个字段的 schema 只写一次:登记形({@link ProjectRegistration})与 PATCH 形({@link ProjectRegistrationPatch})都由它们组成。
|
|
1476
|
+
const ProjectDisplayName = z.string().min(1).max(PROJECT_LIMITS.displayNameMax);
|
|
1477
|
+
/** 归一化 git remotes(`host/path` 形态,design/142 §1.1 归一化规范)。归一化动作发生在 scaffold/调用方,本契约只验底线形状:非空、无空白、
|
|
1478
|
+
* 不含 "://"(出现即证据未归一化,fail-loud)。无仓 project = 空数组合法。`.max(16)` 防登记簿被单条撑爆。 */
|
|
1479
|
+
const ProjectGitRemotes = z
|
|
1480
|
+
.array(z
|
|
1481
|
+
.string()
|
|
1482
|
+
.min(1)
|
|
1483
|
+
.max(512)
|
|
1484
|
+
.refine((s) => !/\s/.test(s) && !s.includes("://"), buildQuotedIssue `must be a NORMALIZED remote (host/path — no scheme, no whitespace)`))
|
|
1485
|
+
.max(16);
|
|
1486
|
+
/** 该项目会话的默认记忆 scope 词表。OPEN value domain — z.string(),不 enum(纪律:加词不得 BREAKING)。方向(S4 细化):消费方=service
|
|
1487
|
+
* memoryScope 派生(作 TaskSpec.memory.scopes 种子),形态=v2 scope 键字符串(过 core parseScopeKey 校验;known 词根:`proj` / `userproj`)。
|
|
1488
|
+
* 未知串=消费者自定义;空数组=不下发默认。本域只登记不解释,零耦合。 */
|
|
1489
|
+
const ProjectDefaultScopes = z.array(z.string().min(1));
|
|
1490
|
+
const ProjectGoal = z.string().max(PROJECT_LIMITS.goalMax);
|
|
1491
|
+
const ProjectContextItems = z.array(ProjectContextItem).max(PROJECT_LIMITS.contextItemsMax);
|
|
1492
|
+
const ProjectIcon = z.string().regex(PROJECT_ICON_REGEX, "an icon is a token: a lowercase letter, then lowercase letters / digits / -, at most 64");
|
|
1493
|
+
const ProjectColor = z.string().regex(PROJECT_COLOR_REGEX, "a colour is # followed by six lowercase hex digits");
|
|
1455
1494
|
export const ProjectRegistration = z.object({
|
|
1456
|
-
/** 人读名(展示/管理面用;不是身份,改名自由——身份=Record 键 projectId)。 */
|
|
1457
|
-
displayName:
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
gitRemotes: z
|
|
1462
|
-
.array(z
|
|
1463
|
-
.string()
|
|
1464
|
-
.min(1)
|
|
1465
|
-
.max(512)
|
|
1466
|
-
.refine((s) => !/\s/.test(s) && !s.includes("://"), "must be a NORMALIZED remote (host/path — no scheme, no whitespace)"))
|
|
1467
|
-
.max(16)
|
|
1468
|
-
.prefault([]),
|
|
1469
|
-
/** 该项目会话的默认记忆 scope 词表。OPEN value domain — z.string(),不 enum(纪律:加词
|
|
1470
|
-
* 不得 BREAKING)。方向(S4 细化):消费方=service memoryScope 派生(作 TaskSpec.
|
|
1471
|
-
* memory.scopes 种子),形态=v2 scope 键字符串(过 core parseScopeKey 校验;known 词根:`proj` /
|
|
1472
|
-
* `userproj`)。未知串=消费者自定义;空数组=不下发默认。本域只登记不解释,零耦合。 */
|
|
1473
|
-
defaultScopes: z.array(z.string().min(1)).prefault([]),
|
|
1474
|
-
/** 登记人 principal(审计锚;web-admin 写面自动填 caller,不信客户端提交)。 */
|
|
1495
|
+
/** 人读名(展示/管理面用;不是身份,改名自由——身份=Record 键 projectId)。上界 {@link PROJECT_LIMITS}`.displayNameMax`(200;此前 120)。 */
|
|
1496
|
+
displayName: ProjectDisplayName,
|
|
1497
|
+
gitRemotes: ProjectGitRemotes.prefault([]),
|
|
1498
|
+
defaultScopes: ProjectDefaultScopes.prefault([]),
|
|
1499
|
+
/** 登记人 principal(审计锚;web-admin 写面自动填 caller,不信客户端提交)。PATCH 改不了它。 */
|
|
1475
1500
|
registeredBy: z.string().min(1),
|
|
1476
|
-
/** 登记时刻(ISO)。web-admin 写面 server 端盖章。 */
|
|
1501
|
+
/** 登记时刻(ISO)。web-admin 写面 server 端盖章。PATCH 改不了它。 */
|
|
1477
1502
|
registeredAt: z.string().min(1),
|
|
1503
|
+
/** 自由备注(与 `goal` 不是一件事)。 */
|
|
1478
1504
|
notes: z.string().optional(),
|
|
1479
|
-
})
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
})
|
|
1486
|
-
.
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1505
|
+
/** 项目目标(≤ {@link PROJECT_LIMITS}`.goalMax`)。 */
|
|
1506
|
+
goal: ProjectGoal.optional(),
|
|
1507
|
+
/** 项目上下文条目(≤ {@link PROJECT_LIMITS}`.contextItemsMax` 条,见 {@link ProjectContextItem})。 */
|
|
1508
|
+
contextItems: ProjectContextItems.optional(),
|
|
1509
|
+
/** 归档时刻(RFC 3339)。在场 = 已归档;没有硬删(会话行引用项目)。 */
|
|
1510
|
+
archivedAt: RFC3339_DATE_TIME.optional(),
|
|
1511
|
+
/** 图标 token({@link PROJECT_ICON_REGEX})。 */
|
|
1512
|
+
icon: ProjectIcon.optional(),
|
|
1513
|
+
/** 颜色({@link PROJECT_COLOR_REGEX})。 */
|
|
1514
|
+
color: ProjectColor.optional(),
|
|
1515
|
+
});
|
|
1516
|
+
/** 同 scope 内一条 remote 只许属于一个 project——「remote → projectId」找回阶梯(design/142 §1.5 ③)与 scaffold 防重铸查询要求该映射在
|
|
1517
|
+
* 租户内确定;撞了=写入 fail-loud(合法双登记诉求出现时再放开,先紧后松 additive)。按文档键序走:后出现的那一条被拒、句子点名先占的项目。 */
|
|
1518
|
+
function remoteUniquenessIssues(cfg, ctx) {
|
|
1490
1519
|
const seen = new Map();
|
|
1491
1520
|
for (const [pid, reg] of Object.entries(cfg.projects)) {
|
|
1492
1521
|
for (const [i, r] of reg.gitRemotes.entries()) {
|
|
@@ -1497,7 +1526,66 @@ export const ProjectsConfig = z
|
|
|
1497
1526
|
seen.set(r, pid);
|
|
1498
1527
|
}
|
|
1499
1528
|
}
|
|
1500
|
-
}
|
|
1529
|
+
}
|
|
1530
|
+
export const ProjectsConfig = z
|
|
1531
|
+
.object({
|
|
1532
|
+
/** 键=projectId({@link PROJECT_ID_REGEX},generic lowercase UUID——宽读);tenant 半边=web-admin
|
|
1533
|
+
* scope 轴(不进域文档,见域头注)。 */
|
|
1534
|
+
projects: z.record(z.string().regex(PROJECT_ID_REGEX, "projectId must be a lowercase canonical UUID"), ProjectRegistration).prefault({}),
|
|
1535
|
+
})
|
|
1536
|
+
.superRefine(remoteUniquenessIssues);
|
|
1537
|
+
/** The PATCH form of one registration: every key a PATCH may change, all optional; `registeredBy` / `registeredAt` (and any key
|
|
1538
|
+
* the registration does not have) are refused (`.strict()`). An optional key given `null` is cleared (merge-patch: `archivedAt: null`
|
|
1539
|
+
* un-archives). Each field is the registration's own schema — one bound, one shape. */
|
|
1540
|
+
export const ProjectRegistrationPatch = z
|
|
1541
|
+
.object({
|
|
1542
|
+
displayName: ProjectDisplayName.optional(),
|
|
1543
|
+
gitRemotes: ProjectGitRemotes.optional(),
|
|
1544
|
+
defaultScopes: ProjectDefaultScopes.optional(),
|
|
1545
|
+
notes: z.string().nullable().optional(),
|
|
1546
|
+
goal: ProjectGoal.nullable().optional(),
|
|
1547
|
+
contextItems: ProjectContextItems.nullable().optional(),
|
|
1548
|
+
archivedAt: RFC3339_DATE_TIME.nullable().optional(),
|
|
1549
|
+
icon: ProjectIcon.nullable().optional(),
|
|
1550
|
+
color: ProjectColor.nullable().optional(),
|
|
1551
|
+
})
|
|
1552
|
+
.strict();
|
|
1553
|
+
/**
|
|
1554
|
+
* Apply a PATCH to one registration and judge the whole document with **the same** {@link ProjectsConfig} (the same remote-uniqueness rule a
|
|
1555
|
+
* full write meets — a PATCH has no rule of its own). The patch is judged here by {@link ProjectRegistrationPatch} first — whatever the caller
|
|
1556
|
+
* did before (a request body handed over as is cannot rewrite `registeredBy` / `registeredAt` or carry an unknown key) — and only its parsed
|
|
1557
|
+
* keys are merged. The patched project is judged LAST, so a remote another project already holds is refused on the patched project and the
|
|
1558
|
+
* sentence names the holder, whatever the document's key order; a successful result keeps the document's key order. Every issue path is a
|
|
1559
|
+
* document coordinate (`["projects", <projectId>, …]`), the patch's own included. `projectId` must be registered (the caller answers "no such
|
|
1560
|
+
* project" first) —— otherwise this throws.
|
|
1561
|
+
*/
|
|
1562
|
+
export function applyProjectPatch(doc, projectId, patch) {
|
|
1563
|
+
const current = Object.prototype.hasOwnProperty.call(doc.projects, projectId) ? doc.projects[projectId] : undefined;
|
|
1564
|
+
if (current === undefined)
|
|
1565
|
+
throw new Error(`applyProjectPatch: project "${projectId}" is not registered in this document`);
|
|
1566
|
+
const parsed = ProjectRegistrationPatch.safeParse(patch);
|
|
1567
|
+
if (!parsed.success) {
|
|
1568
|
+
const issues = parsed.error.issues.map((i) => ({ ...i, path: ["projects", projectId, ...i.path] }));
|
|
1569
|
+
return { success: false, error: new z.ZodError(issues) };
|
|
1570
|
+
}
|
|
1571
|
+
const next = { ...current };
|
|
1572
|
+
for (const [k, v] of Object.entries(parsed.data)) {
|
|
1573
|
+
if (v === undefined)
|
|
1574
|
+
continue;
|
|
1575
|
+
if (v === null)
|
|
1576
|
+
delete next[k];
|
|
1577
|
+
else
|
|
1578
|
+
next[k] = v;
|
|
1579
|
+
}
|
|
1580
|
+
const { [projectId]: _patched, ...others } = doc.projects;
|
|
1581
|
+
const judged = ProjectsConfig.safeParse({ ...doc, projects: { ...others, [projectId]: next } });
|
|
1582
|
+
if (!judged.success)
|
|
1583
|
+
return judged;
|
|
1584
|
+
const ordered = {};
|
|
1585
|
+
for (const pid of Object.keys(doc.projects))
|
|
1586
|
+
ordered[pid] = judged.data.projects[pid];
|
|
1587
|
+
return { ...judged, data: { ...judged.data, projects: ordered } };
|
|
1588
|
+
}
|
|
1501
1589
|
// ── Domain: collab (协作模式 — multi-model collaboration templates, 0.7.0) ─────
|
|
1502
1590
|
// Replaces the deleted `teams` domain (EXPERT-REDESIGN §8/§9, 五拍裁定 T1-T5).
|
|
1503
1591
|
// Unified shape = slots × topology × rounds × finalizer. COMPLETELY separate from rosters (拍板): a slot
|
|
@@ -1570,7 +1658,7 @@ const CollabTemplate = z.object({
|
|
|
1570
1658
|
});
|
|
1571
1659
|
if (t.finalizer.type === "none") {
|
|
1572
1660
|
if (t.finalizer.slot !== undefined)
|
|
1573
|
-
ctx.addIssue({ code: "custom",
|
|
1661
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `finalizer type "none" must not anchor a slot`, path: ["finalizer", "slot"] });
|
|
1574
1662
|
}
|
|
1575
1663
|
else if (t.finalizer.slot !== undefined && !slotIds.has(t.finalizer.slot)) {
|
|
1576
1664
|
ctx.addIssue({ code: "custom", ...buildQuotedIssue `finalizer.slot "${t.finalizer.slot}" is not a slot id of this template`, path: ["finalizer", "slot"] });
|
|
@@ -1645,13 +1733,22 @@ const WorkerSecretRefs = z.object({
|
|
|
1645
1733
|
* · **删** `MEMORY_BACKEND`(引擎从不读的死名,渲染器不再产出)与 `TIDB_*` 族(引擎的墓碑名,见之拒启;结构字段不再产出它们)。
|
|
1646
1734
|
* MYSQL_* / PG_* 两族归 `runtime.store.db` + `secretRefs.dbPasswordEnv`。
|
|
1647
1735
|
*
|
|
1648
|
-
* `env` / `secretEnv`
|
|
1649
|
-
*
|
|
1736
|
+
* `env` / `secretEnv` 的写闸问**两张表**,各答一个问题,互相独立:
|
|
1737
|
+
* · 本表答「规格作者可不可以自己设这个名」—— 不可以:它归某个结构字段,或归渲染器 / 编排器写,或与渲染器 / 编排器写的名互斥(拒句指路到那个写位);
|
|
1738
|
+
* · 墓碑表 `RETIRED_ENVS`(`./retired-envs`)答「带着墓碑表的引擎还读不读这个名」—— 不读、见之拒启(拒句就是墓碑句 `retiredEnvRefusal` 本身,引擎起服、壳、web-admin 读同一张表同一句)。
|
|
1739
|
+
* 两个问题的读者不同,答案可以同时为「是」。换名窗里就是这样:更老的引擎内带更老的契约、看不见墓碑表,仍读上一代名,渲染器因此对它们仍写上一代的 URL / WORKER / TOKEN
|
|
1740
|
+
* (TOKEN_FILE 与 TOKEN 互斥,同组同入)—— 这四名对第一个问题答「不可以自设」(本表),对第二个问题答「不读」(墓碑表),写闸交两句:墓碑句在前、保留句在后,各说各的事。
|
|
1741
|
+
* 这是**窗期**形。窗关 —— 渲染器不再对任何目标引擎写上一代名、此后没有引擎要读它们 —— 这四名离开本表,两表回到零交集,那才是终态;窗期交集是个写死的闭集,
|
|
1742
|
+
* 有钉盯着,窗关时那一钉翻回零交集。
|
|
1650
1743
|
*
|
|
1651
|
-
* 11.0.0
|
|
1652
|
-
*
|
|
1653
|
-
*
|
|
1654
|
-
*
|
|
1744
|
+
* 11.0.0:`ANTHROPIC_BASEURL`(墓碑名)换成现名 `ANTHROPIC_BASE_URL`;渲染器写的 web-admin 连接三键入表,同 fleet 成员身份两键的「渲染器写、不可配」。
|
|
1745
|
+
* **规则一条:渲染器写的名字是保留名**(运维不可在 `env` / `secretEnv` 里自写)。改名窗里渲染器按目标引擎版本写名,对还读上一代名的引擎写的是
|
|
1746
|
+
* 上一代的 URL / WORKER / TOKEN 三名({@link PREVIOUS_WEB_ADMIN_ENV}),所以这三名与现名三键同一组、保留句同一句(指路同一处)。
|
|
1747
|
+
* 上一代八名都是墓碑(引擎换读现名之后,新引擎见之拒启);其中上面几名因窗期而同时留在本表,另外几名(LIVE_URL / WEB_URL / DRY_RUN / ADMIN_TOKEN)没有谁写、也不与谁互斥,只是墓碑。
|
|
1748
|
+
*
|
|
1749
|
+
* 两把凭证的文件键与断言令牌值键也是保留名(同一条规则):断言令牌(`wat_`)的值键归编排器注入;两个文件键(主凭证 `tokenFile`、断言令牌 `assertionTokenFile`)
|
|
1750
|
+
* 只给不经编排器的手接引擎、由运维直设,不经 worker 规格。文件键与同一份凭证的值键互斥(`judgeWebAdminCredentialDeclaration`:一对里两个都设 ⇒ 拒启),
|
|
1751
|
+
* 规格里写了任一个就与注入撞,拒句点名各自的出路({@link WEB_ADMIN_ASSERTION_SEAT}、{@link WEB_ADMIN_OPERATOR_FILE_SEAT});上一代的 `tokenFile` 与老三名同组同入。
|
|
1655
1752
|
*/
|
|
1656
1753
|
/** web-admin 连接三键的唯一写位(渲染器;没有 WorkerSpec 字段)—— 现名与上一代名指路同一句。 */
|
|
1657
1754
|
const WEB_ADMIN_CONNECTION_SEAT = {
|
|
@@ -1659,6 +1756,12 @@ const WEB_ADMIN_CONNECTION_SEAT = {
|
|
|
1659
1756
|
worker: "(set by the config renderer from the worker's name — not configurable)",
|
|
1660
1757
|
token: "(set by the config renderer from the worker's pull token — not configurable)",
|
|
1661
1758
|
};
|
|
1759
|
+
/** web-admin 凭证的出路:值键由编排器注入;文件键由运维直设(只给不经编排器的手接引擎),不进 worker 规格。没有 WorkerSpec 字段能写它们。
|
|
1760
|
+
* 两把凭证(主凭证 `token` / `tokenFile`、断言令牌 `assertionToken` / `assertionTokenFile`)同一条规则 —— 文件键与同一份凭证的值键互斥,所以文件键同一句出路。 */
|
|
1761
|
+
const WEB_ADMIN_ASSERTION_SEAT = {
|
|
1762
|
+
token: "(injected by the orchestrator from the worker's assertion token — not configurable)",
|
|
1763
|
+
};
|
|
1764
|
+
const WEB_ADMIN_OPERATOR_FILE_SEAT = "(set directly by the operator on a hand-wired engine that is not managed by the orchestrator — never in a worker spec — not configurable)";
|
|
1662
1765
|
const RESERVED_ENV_FIELD = Object.freeze({
|
|
1663
1766
|
PORT: "(fixed at 8090 — not configurable)", MODEL_PROVIDER: "model.provider", MODEL_ID: "model.modelId",
|
|
1664
1767
|
MODEL_REASONING: "model.reasoning", MODEL_CHEAP_ID: "model.cheapId", MODEL_MAX_TOKENS: "model.maxTokens",
|
|
@@ -1682,9 +1785,14 @@ const RESERVED_ENV_FIELD = Object.freeze({
|
|
|
1682
1785
|
[FLEET_SCOPE_ENV]: "(set by the config renderer from the worker's scope — not configurable)",
|
|
1683
1786
|
// ── web-admin 连接三键:渲染器给每台 worker 写 web-admin 根 URL、worker 名与该 worker 的拉取令牌 —— 没有 WorkerSpec 字段能写它们;
|
|
1684
1787
|
// 现名与渲染器对老引擎写的上一代名同一组(见表头注)
|
|
1685
|
-
[WEB_ADMIN_ENV.url]: WEB_ADMIN_CONNECTION_SEAT.url,
|
|
1686
|
-
[WEB_ADMIN_ENV.worker]: WEB_ADMIN_CONNECTION_SEAT.worker,
|
|
1687
|
-
[WEB_ADMIN_ENV.token]: WEB_ADMIN_CONNECTION_SEAT.token,
|
|
1788
|
+
[WEB_ADMIN_ENV.url]: WEB_ADMIN_CONNECTION_SEAT.url, [PREVIOUS_WEB_ADMIN_ENV.url]: WEB_ADMIN_CONNECTION_SEAT.url,
|
|
1789
|
+
[WEB_ADMIN_ENV.worker]: WEB_ADMIN_CONNECTION_SEAT.worker, [PREVIOUS_WEB_ADMIN_ENV.worker]: WEB_ADMIN_CONNECTION_SEAT.worker,
|
|
1790
|
+
[WEB_ADMIN_ENV.token]: WEB_ADMIN_CONNECTION_SEAT.token, [PREVIOUS_WEB_ADMIN_ENV.token]: WEB_ADMIN_CONNECTION_SEAT.token,
|
|
1791
|
+
// ── web-admin 凭证的文件键与断言令牌值键:文件键由运维直设、与同一份凭证的值键互斥 —— 规格里写任一个都与注入撞(主凭证与断言令牌同一规则)。
|
|
1792
|
+
// 上一代的 TOKEN_FILE 与老三名同组同入(换名窗里还读上一代名的引擎读它)。
|
|
1793
|
+
[WEB_ADMIN_ENV.tokenFile]: WEB_ADMIN_OPERATOR_FILE_SEAT, [PREVIOUS_WEB_ADMIN_ENV.tokenFile]: WEB_ADMIN_OPERATOR_FILE_SEAT,
|
|
1794
|
+
[WEB_ADMIN_ENV.assertionToken]: WEB_ADMIN_ASSERTION_SEAT.token,
|
|
1795
|
+
[WEB_ADMIN_ENV.assertionTokenFile]: WEB_ADMIN_OPERATOR_FILE_SEAT,
|
|
1688
1796
|
REQUIRE_EXECUTION_LANE: "runtime.execution.lane (the renderer emits REQUIRE_EXECUTION_LANE=true for every declared lane)",
|
|
1689
1797
|
...Object.fromEntries([...LANE_ENV_OWNER].map(([name, owner]) => [name, laneEnvSeat(name, owner)])),
|
|
1690
1798
|
});
|
|
@@ -1714,7 +1822,7 @@ const nonSecretLiteral = (minLength) => z
|
|
|
1714
1822
|
.string()
|
|
1715
1823
|
.min(minLength)
|
|
1716
1824
|
.max(2048)
|
|
1717
|
-
.refine((s) => !containsSecretToken(s),
|
|
1825
|
+
.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`);
|
|
1718
1826
|
/**
|
|
1719
1827
|
* `runtime.execution` —— 这台 worker 的执行车道(渲染:`REMOTE_EXEC=<lane>` + `REQUIRE_EXECUTION_LANE=true` + coords / secretRefs 逐键)。
|
|
1720
1828
|
* - `lane`:{@link EXECUTION_LANE_WORDS} 之一。没有「无手」词。
|
|
@@ -1835,7 +1943,7 @@ const WorkerSpecObject = z.object({
|
|
|
1835
1943
|
/** Target average CPU utilization %, vs resources.requests.cpu. Absent → backend default 80. */
|
|
1836
1944
|
targetCpuPercent: INTEGER.min(10).max(95).optional(),
|
|
1837
1945
|
})
|
|
1838
|
-
.refine((a) => a.maxReplicas >= a.minReplicas,
|
|
1946
|
+
.refine((a) => a.maxReplicas >= a.minReplicas, buildQuotedIssue `maxReplicas must be >= minReplicas`)
|
|
1839
1947
|
.optional(),
|
|
1840
1948
|
/** Per-worker resource requests/limits (k8s backend). Optional → the backend's modest default
|
|
1841
1949
|
* (requests cpu 50m/mem 128Mi, no limits). Hash-INCLUDED → a change rolls the pods (resources are
|
|
@@ -2069,7 +2177,7 @@ export const WorkersConfig = z
|
|
|
2069
2177
|
// replicas + autoscaling = two owners of the same knob (the orchestrator's SSA vs the HPA controller
|
|
2070
2178
|
// would endlessly fight over spec.replicas) → reject at save, not at reconcile.
|
|
2071
2179
|
if (w.replicas !== undefined && w.autoscaling) {
|
|
2072
|
-
ctx.addIssue({ code: "custom",
|
|
2180
|
+
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"] });
|
|
2073
2181
|
}
|
|
2074
2182
|
});
|
|
2075
2183
|
});
|
|
@@ -2230,7 +2338,7 @@ const McpEnvRefs = z.record(ENV_NAME, ENV_NAME); // target ENV key → ENV_NAME
|
|
|
2230
2338
|
const HTTP_HEADER_NAME = z
|
|
2231
2339
|
.string()
|
|
2232
2340
|
.regex(/^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/, "must be an HTTP header NAME")
|
|
2233
|
-
.refine((s) => !containsSecretToken(s),
|
|
2341
|
+
.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`);
|
|
2234
2342
|
// Header NAME → ENV_NAME holding its value. Header names are tokens (RFC 7230) — not env-name-shaped.
|
|
2235
2343
|
const McpHeaderRefs = z.record(HTTP_HEADER_NAME, ENV_NAME);
|
|
2236
2344
|
export const McpTransport = z.discriminatedUnion("kind", [
|
|
@@ -2243,13 +2351,13 @@ export const McpTransport = z.discriminatedUnion("kind", [
|
|
|
2243
2351
|
command: z
|
|
2244
2352
|
.string()
|
|
2245
2353
|
.min(1)
|
|
2246
|
-
.refine((s) => !containsSecretToken(s),
|
|
2247
|
-
.refine((s) => !hasNul(s),
|
|
2354
|
+
.refine((s) => !containsSecretToken(s), buildQuotedIssue `mcp stdio command must not contain a secret literal — put credentials in envRefs (an env-NAME)`)
|
|
2355
|
+
.refine((s) => !hasNul(s), buildQuotedIssue `mcp stdio command must not contain a NUL byte (Node cannot spawn it)`),
|
|
2248
2356
|
args: z
|
|
2249
2357
|
.array(z
|
|
2250
2358
|
.string()
|
|
2251
|
-
.refine((s) => !containsSecretToken(s),
|
|
2252
|
-
.refine((s) => !hasNul(s),
|
|
2359
|
+
.refine((s) => !containsSecretToken(s), buildQuotedIssue `mcp stdio arg must not contain a secret literal — put credentials in envRefs (an env-NAME)`)
|
|
2360
|
+
.refine((s) => !hasNul(s), buildQuotedIssue `mcp stdio arg must not contain a NUL byte (Node cannot spawn it)`))
|
|
2253
2361
|
.optional(),
|
|
2254
2362
|
/** target ENV key → ENV_NAME in the service env (never a secret value). */
|
|
2255
2363
|
envRefs: McpEnvRefs.optional(),
|
|
@@ -2353,18 +2461,18 @@ export const McpConfig = z
|
|
|
2353
2461
|
export const PLUGIN_ORIGIN = z
|
|
2354
2462
|
.string()
|
|
2355
2463
|
.min(1)
|
|
2356
|
-
.refine((s) => !containsSecretToken(s),
|
|
2357
|
-
.refine((s) => !hasNul(s),
|
|
2464
|
+
.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`)
|
|
2465
|
+
.refine((s) => !hasNul(s), buildQuotedIssue `plugin source must not contain a NUL byte`)
|
|
2358
2466
|
.superRefine((s, ctx) => {
|
|
2359
2467
|
// The shape is THE URL judge's ({@link httpUrlFlaws}); this seat adds two rules of its own: https only, and spelled in full
|
|
2360
2468
|
// (`https://…` as written — git reads the raw string, and WHATWG's repair of `https:host/x` into `https://host/x` is not git's).
|
|
2361
2469
|
const flaws = httpUrlFlaws(s, ["userinfo"]);
|
|
2362
2470
|
if (flaws.includes("not-http-url") || new URL(s).protocol !== "https:" || s.slice(0, 8).toLowerCase() !== "https://") {
|
|
2363
|
-
ctx.addIssue({ code: "custom",
|
|
2471
|
+
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)` });
|
|
2364
2472
|
return;
|
|
2365
2473
|
}
|
|
2366
2474
|
if (flaws.includes("userinfo") || urlQueryFragmentHasSecret(new URL(s))) {
|
|
2367
|
-
ctx.addIssue({ code: "custom",
|
|
2475
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `an https plugin source must not embed credentials (userinfo) or a secret-bearing query/fragment` });
|
|
2368
2476
|
}
|
|
2369
2477
|
});
|
|
2370
2478
|
/** A full 40-hex git commit SHA — the ONLY pin form the supply-side treats as 钉死 (a branch/tag `version`
|
|
@@ -2413,7 +2521,9 @@ export const PluginsConfig = z
|
|
|
2413
2521
|
// 消费投影在 server(声明数组逐字喂 core typed stableBlocks 钩子),schema 恒在本仓(零第二份)。
|
|
2414
2522
|
/** 提示声明的槽位闭集(core `PromptTextDeclaration.slot` 同形):identity = 角色卡、scenario = 场景说明、behavior = 行为守则。
|
|
2415
2523
|
* declaration 轴与 catalog 轴({@link PromptArtifactSection})共用这一只。 */
|
|
2416
|
-
export const
|
|
2524
|
+
export const PROMPT_SLOTS = ["identity", "scenario", "behavior"];
|
|
2525
|
+
/** The slot enum, DERIVED from {@link PROMPT_SLOTS} (the runtime member table —— same form as `REF_CHECKED_DOMAINS`). */
|
|
2526
|
+
export const PromptSlot = z.enum(PROMPT_SLOTS);
|
|
2417
2527
|
/** core `PromptTextDeclaration` 的契约层同形(core prompts/default.d.ts 逐字:id/slot 闭集/text)。
|
|
2418
2528
|
* slot ↔ 三卡:identity=角色卡、scenario=场景说明、behavior=行为守则([1051]①)。 */
|
|
2419
2529
|
export const PromptTextDeclaration = z.object({
|
|
@@ -2444,11 +2554,11 @@ export const PromptBinding = z.object({
|
|
|
2444
2554
|
notes: z.string().optional(),
|
|
2445
2555
|
}).superRefine((b, ctx) => {
|
|
2446
2556
|
if (b.level === "worker" && !b.worker)
|
|
2447
|
-
ctx.addIssue({ code: "custom",
|
|
2557
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `level=worker requires \`worker\`` });
|
|
2448
2558
|
if (b.level === "scenario" && !b.scenario)
|
|
2449
|
-
ctx.addIssue({ code: "custom",
|
|
2559
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `level=scenario requires \`scenario\`` });
|
|
2450
2560
|
if (b.level === "global" && (b.worker || b.scenario))
|
|
2451
|
-
ctx.addIssue({ code: "custom",
|
|
2561
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `level=global must not carry a selector` });
|
|
2452
2562
|
});
|
|
2453
2563
|
// ═══ prompts 域 catalog 轴(0.10.20;[ref]a/[1242]/[1248] 过轮定稿;core design/148 S1 seam)═══
|
|
2454
2564
|
// 形 = core 1.336+ 的 `PublishedPromptArtifactEnvelope`(sema-core src/prompt-assembly/artifact.ts,
|
|
@@ -2544,7 +2654,7 @@ export const EffectivePromptsWire = z
|
|
|
2544
2654
|
declarations: EffectivePrompts.optional(),
|
|
2545
2655
|
catalog: PublishedPromptArtifactEnvelope.optional(),
|
|
2546
2656
|
})
|
|
2547
|
-
.refine((w) => w.declarations !== undefined || w.catalog !== undefined,
|
|
2657
|
+
.refine((w) => w.declarations !== undefined || w.catalog !== undefined, buildQuotedIssue `at least one axis (declarations|catalog) required`);
|
|
2548
2658
|
export const DOMAIN_SCHEMAS = {
|
|
2549
2659
|
models: ModelsConfig,
|
|
2550
2660
|
rosters: RostersConfig,
|