@sema-agent/settings-schema 10.1.1 → 11.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 +414 -0
- package/README.md +141 -65
- package/bin/settings-schema.mjs +13 -6
- package/conformance/VERSION +1 -1
- package/conformance/baseline.json +3 -1
- package/conformance/cases/c01-entry-salvage-then-typo/expected.json +3 -1
- package/conformance/cases/c02-hosts-clamp-stacked/expected.json +12 -5
- package/conformance/cases/c02-hosts-clamp-stacked/meta.json +1 -1
- package/conformance/cases/c03-literal-dotted-top-key/expected.json +22 -2
- package/conformance/cases/c04-all-bad-collection-not-salvaged/expected.json +3 -1
- package/conformance/cases/c06-legacy-governance-lift/expected.json +11 -101
- package/conformance/cases/c06-legacy-governance-lift/meta.json +1 -1
- package/conformance/cases/c07-hooks-is-not-a-config-domain/expected.json +3 -1
- package/conformance/cases/c08-limits-unknown-carried-64/expected.json +203 -2
- package/conformance/cases/c09-limits-unknown-carried-65/expected.json +204 -2
- package/conformance/cases/c10-record-key-secret-redacted-twice/expected.json +3 -1
- package/conformance/cases/c11-unicode-unknown-keys/expected.json +20 -2
- package/conformance/cases/c12-prototype-name-top-keys/expected.json +3 -1
- package/conformance/cases/c13-models-field-null/expected.json +3 -1
- package/conformance/cases/c14-models-field-empty-array/expected.json +6 -1
- package/conformance/cases/c15-models-field-number/expected.json +3 -1
- package/conformance/cases/c16-deep-nesting-205/expected.json +14 -2
- package/conformance/cases/c17-empty-domain-file/expected.json +3 -1
- package/conformance/cases/c18-wire-envelope-not-an-object/expected.json +3 -1
- package/conformance/cases/c19-wire-envelope-no-version/expected.json +3 -1
- package/conformance/cases/c20-governance-absent-lifts/expected.json +11 -86
- package/conformance/cases/c20-governance-absent-lifts/meta.json +1 -1
- package/conformance/cases/c21-governance-empty-object-wins/expected.json +6 -5
- package/conformance/cases/c21-governance-empty-object-wins/input.json +1 -4
- package/conformance/cases/c21-governance-empty-object-wins/meta.json +1 -1
- package/conformance/cases/c23-mixed-validity-models/expected.json +3 -1
- package/conformance/cases/c24-valid-entries-with-unknown-keys/expected.json +22 -2
- package/conformance/cases/c25-published-roundtrip-zero-warnings/expected.json +6 -1
- package/conformance/cases/c26-hosts-array-1024/expected.json +3 -1
- package/conformance/cases/c27-hosts-array-1025/expected.json +6 -7175
- package/conformance/cases/c27-hosts-array-1025/meta.json +1 -1
- package/conformance/cases/c28-large-document-over-1mb/expected.json +14 -2
- package/conformance/cases/c29-salvage-then-domain-refine-fails/expected.json +3 -1
- package/conformance/cases/c30-version-triple-a-first/expected.json +6 -1
- package/conformance/cases/c31-version-triple-b-changed/expected.json +6 -1
- package/conformance/cases/c32-version-triple-a-restored/expected.json +6 -1
- package/conformance/cases/c33-limits-nested-carried-key/expected.json +18 -2
- package/conformance/cases/c34-skills-content-vs-manifest/expected.json +19 -3
- package/conformance/cases/c35-portable-domain-file-is-null/expected.json +3 -1
- package/conformance/cases/c36-skills-entry-without-content/expected.json +6 -2
- package/conformance/cases/c37-skills-field-null/expected.json +6 -2
- package/conformance/cases/c38-remote-exec-is-a-non-domain-file/expected.json +3 -1
- package/conformance/cases/c39-hidden-file-in-config-d/expected.json +3 -1
- package/conformance/cases/c40-s712-identity-only-what-reads-out/expected.json +11 -4
- package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +9 -3
- package/conformance/cases/c42-s792-unknown-key-secret-redacted/expected.json +39 -2
- package/conformance/cases/c43-s792-form-and-deep-encoded-pem/expected.json +4 -1
- package/conformance/cases/c44-s792-utf8-escape-glued-token/expected.json +3 -1
- package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +19 -10
- package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/meta.json +1 -1
- package/conformance/cases/{c46-s811-host-name-url-userinfo-characterization → c46-s811-host-name-url-userinfo-withheld}/expected.json +12 -5
- package/conformance/cases/c46-s811-host-name-url-userinfo-withheld/meta.json +9 -0
- package/conformance/cases/{c47-s811-roles-key-url-userinfo-characterization → c47-s811-roles-key-url-userinfo-withheld}/expected.json +23 -3
- package/conformance/cases/{c47-s811-roles-key-url-userinfo-characterization → c47-s811-roles-key-url-userinfo-withheld}/input.json +6 -0
- package/conformance/cases/c47-s811-roles-key-url-userinfo-withheld/meta.json +10 -0
- package/conformance/cases/c48-s935-model-headers-transported/expected.json +6 -1
- package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/expected.json +3 -1
- package/conformance/cases/c50-s784-lane-host-legal/expected.json +3 -1
- package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +3 -1
- package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +3 -1
- package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +3 -1
- package/conformance/cases/c54-s784-lane-adb-legal/expected.json +3 -1
- package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +3 -1
- package/conformance/cases/c56-s784-lane-device-legal/expected.json +3 -1
- package/conformance/cases/c57-s784-g1-enabled-no-lane/expected.json +3 -1
- package/conformance/cases/c58-s784-g2-multitenant-host/expected.json +3 -1
- package/conformance/cases/c59-s784-g3-k8s-missing-image/expected.json +3 -1
- package/conformance/cases/c60-s784-g3-e2b-missing-key/expected.json +3 -1
- package/conformance/cases/c61-s784-g3-other-lane-key/expected.json +3 -1
- package/conformance/cases/c62-s784-g7a-session-tidb/expected.json +3 -1
- package/conformance/cases/c63-s784-host-two-replicas/expected.json +3 -1
- package/conformance/cases/c64-s784-multitenant-local-store/expected.json +3 -1
- package/conformance/cases/c65-s784-pg-without-db/expected.json +3 -1
- package/conformance/cases/c66-s784-memory-backend-mismatch/expected.json +3 -1
- package/conformance/cases/c67-s784-retired-field-loud/expected.json +3 -1
- package/conformance/cases/c67-s784-retired-field-loud/meta.json +1 -1
- package/conformance/cases/c68-s784-device-local-store/expected.json +3 -1
- package/conformance/cases/c69-deep-encoded-token-past-every-reading/expected.json +3 -1
- package/conformance/cases/c70-rewritten-token-in-front-of-encoded-pem-header/expected.json +3 -1
- package/conformance/cases/c71-one-scanner-write-reads-display-forms/expected.json +119 -0
- package/conformance/cases/c71-one-scanner-write-reads-display-forms/input.json +24 -0
- package/conformance/cases/c71-one-scanner-write-reads-display-forms/meta.json +10 -0
- package/conformance/cases/c72-dangling-reference-write-seat-refuses/expected.json +111 -0
- package/conformance/cases/c72-dangling-reference-write-seat-refuses/input.json +26 -0
- package/conformance/cases/c72-dangling-reference-write-seat-refuses/meta.json +6 -0
- package/conformance/cases/cc01-mcp-toolfaces-non-object/expected.json +3 -1
- package/conformance/cases/cc02-mcp-source-key-is-transported/expected.json +3 -1
- package/conformance/cases/cc03-model-compat-unknown-subkey/expected.json +6 -1
- package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +11 -1
- package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +11 -1
- package/conformance/cases/cli03-credential-pasted-into-unknown-key/expected.json +16 -2
- package/conformance/cases/cli04-env-value-with-newline/expected.json +3 -1
- package/conformance/cases/cli05-long-and-prefix-colliding-names/expected.json +6 -1
- package/conformance/cases/t01-absent-domain-key-not-null/expected.json +3 -1
- package/conformance/cases/t03-prototype-name-key-not-swallowed/expected.json +29 -2
- package/conformance/cases/t04-secret-literal-never-in-warning-value/expected.json +3 -1
- package/conformance/cases/z01-every-domain-empty-object/expected.json +8 -4
- package/conformance/cases/z01-every-domain-empty-object/input.json +0 -1
- package/conformance/cases/z01-every-domain-empty-object/meta.json +1 -1
- package/conformance/cases/z02-every-domain-unknown-key/expected.json +163 -23
- package/conformance/cases/z02-every-domain-unknown-key/input.json +0 -3
- package/conformance/cases/z02-every-domain-unknown-key/meta.json +1 -1
- package/dist/api/auth-bridge.d.ts +51 -181
- package/dist/api/auth-bridge.js +58 -33
- package/dist/api/auth.d.ts +37 -146
- package/dist/api/auth.js +16 -26
- package/dist/api/feedback.d.ts +87 -0
- package/dist/api/feedback.js +66 -0
- package/dist/api/pull-token.d.ts +32 -0
- package/dist/api/pull-token.js +88 -0
- package/dist/api/scopes.d.ts +82 -174
- package/dist/api/scopes.js +23 -18
- package/dist/bundle.d.ts +15 -7
- package/dist/bundle.js +28 -27
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +132 -81
- package/dist/config-d-file.d.ts +14 -0
- package/dist/config-d-file.js +67 -0
- package/dist/config-fns.d.ts +87 -186
- package/dist/config-fns.js +207 -262
- package/dist/conformance/contract-adapter.d.ts +9 -2
- package/dist/conformance/contract-adapter.js +15 -7
- package/dist/conformance/corpus.d.ts +3 -0
- package/dist/conformance/corpus.js +5 -0
- package/dist/conformance/derive.d.ts +3 -0
- package/dist/conformance/index.d.ts +2 -2
- package/dist/conformance/index.js +2 -2
- package/dist/conformance/run.js +67 -22
- package/dist/conformance/types.d.ts +31 -15
- package/dist/conformance/types.js +5 -0
- package/dist/conformance/warnings.d.ts +1 -1
- package/dist/conformance/warnings.js +1 -4
- package/dist/cross-domain.d.ts +66 -22
- package/dist/cross-domain.js +164 -93
- package/dist/cross-session.d.ts +29 -11
- package/dist/default-model.d.ts +43 -0
- package/dist/default-model.js +34 -0
- package/dist/execution-lane-index.d.ts +4 -8
- package/dist/execution-lane-index.js +4 -8
- package/dist/execution-lanes.d.ts +31 -8
- package/dist/execution-lanes.js +23 -5
- package/dist/file-edit.js +5 -2
- package/dist/file-store.d.ts +7 -6
- package/dist/file-store.js +37 -50
- package/dist/fleet.d.ts +364 -762
- package/dist/fleet.js +277 -224
- package/dist/hooks.d.ts +1151 -4737
- package/dist/hooks.js +59 -15
- package/dist/index.d.ts +10 -4
- package/dist/index.js +13 -7
- package/dist/integer.d.ts +11 -0
- package/dist/integer.js +11 -0
- package/dist/issue-path.d.ts +10 -0
- package/dist/issue-path.js +10 -0
- package/dist/loopback.d.ts +26 -0
- package/dist/loopback.js +54 -0
- package/dist/migration-receipt.d.ts +42 -0
- package/dist/migration-receipt.js +12 -0
- package/dist/one-time-migrations.d.ts +56 -0
- package/dist/one-time-migrations.js +819 -0
- package/dist/prompts-digest.d.ts +1 -1
- package/dist/prompts-digest.js +3 -2
- package/dist/reader.d.ts +2 -2
- package/dist/remote-exec.d.ts +45 -187
- package/dist/remote-exec.js +13 -7
- package/dist/resolve-roster.d.ts +18 -3
- package/dist/resolve-roster.js +28 -25
- package/dist/retired-envs.d.ts +24 -0
- package/dist/retired-envs.js +114 -0
- package/dist/retired-form.d.ts +31 -0
- package/dist/retired-form.js +28 -0
- package/dist/safety-merge-spec.d.ts +131 -191
- package/dist/safety-merge-spec.js +18 -14
- package/dist/scheduler-lifetime.d.ts +5 -0
- package/dist/scheduler-lifetime.js +7 -0
- package/dist/scheduler-store-migration.d.ts +14 -0
- package/dist/scheduler-store-migration.js +14 -0
- package/dist/scheduler-store-node.d.ts +24 -9
- package/dist/scheduler-store-node.js +25 -15
- package/dist/scheduler-store.d.ts +47 -14
- package/dist/scheduler-store.js +32 -61
- package/dist/secret-refs.d.ts +3 -3
- package/dist/secret-refs.js +1 -1
- package/dist/secret-scan.d.ts +14 -24
- package/dist/secret-scan.js +45 -23
- package/dist/sha256-core.d.ts +16 -0
- package/dist/sha256-core.js +138 -0
- package/dist/sha256.d.ts +6 -1
- package/dist/sha256.js +8 -98
- package/dist/skills-manifest.js +1 -1
- package/dist/types.d.ts +1501 -13536
- package/dist/types.js +494 -438
- package/dist/url-shape.d.ts +34 -0
- package/dist/url-shape.js +61 -0
- package/dist/web-admin-credential.d.ts +96 -0
- package/dist/{center-credential.js → web-admin-credential.js} +46 -43
- package/dist/web-admin-env.d.ts +33 -0
- package/dist/web-admin-env.js +33 -0
- package/dist/worker-row-verdict.d.ts +16 -0
- package/dist/worker-row-verdict.js +31 -0
- package/dist/workers-rows.d.ts +34 -0
- package/dist/workers-rows.js +42 -0
- package/dist/zod-unwrap.d.ts +11 -0
- package/dist/zod-unwrap.js +25 -0
- package/package.json +17 -9
- package/conformance/cases/c46-s811-host-name-url-userinfo-characterization/meta.json +0 -6
- package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/meta.json +0 -6
- package/conformance/migrate-workers/m01-g5-k8s-env-lifted/expected.json +0 -49
- package/conformance/migrate-workers/m01-g5-k8s-env-lifted/input.json +0 -26
- package/conformance/migrate-workers/m01-g5-k8s-env-lifted/meta.json +0 -4
- package/conformance/migrate-workers/m02-g5-idempotent/expected.json +0 -38
- package/conformance/migrate-workers/m02-g5-idempotent/input.json +0 -33
- package/conformance/migrate-workers/m02-g5-idempotent/meta.json +0 -5
- package/conformance/migrate-workers/m03-g5-refused-no-lane/expected.json +0 -71
- package/conformance/migrate-workers/m03-g5-refused-no-lane/input.json +0 -42
- package/conformance/migrate-workers/m03-g5-refused-no-lane/meta.json +0 -4
- package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/expected.json +0 -52
- package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/input.json +0 -30
- package/conformance/migrate-workers/m04-g7b-session-tidb-lifted/meta.json +0 -4
- package/conformance/migrate-workers/m05-row-verdict-reserved-env/expected.json +0 -82
- package/conformance/migrate-workers/m05-row-verdict-reserved-env/input.json +0 -68
- package/conformance/migrate-workers/m05-row-verdict-reserved-env/meta.json +0 -4
- package/dist/center-credential.d.ts +0 -116
- package/dist/issue-text.d.ts +0 -10
- package/dist/issue-text.js +0 -8
- package/dist/migrate-workers.d.ts +0 -71
- package/dist/migrate-workers.js +0 -351
- package/dist/migrate.d.ts +0 -255
- package/dist/migrate.js +0 -542
- /package/conformance/cases/{c46-s811-host-name-url-userinfo-characterization → c46-s811-host-name-url-userinfo-withheld}/input.json +0 -0
package/dist/types.js
CHANGED
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Config schema — the single source of truth for what
|
|
2
|
+
* Config schema — the single source of truth for what web-admin manages.
|
|
3
3
|
*
|
|
4
4
|
* Organized into DOMAINS (one top-level menu each). The UI, the store, and the service-facing API all
|
|
5
5
|
* speak these types. Use zod both to validate UI input AND to derive the TS types (one definition).
|
|
6
6
|
*
|
|
7
7
|
* Detail level is deliberate (the user asked for "详略设计"): `models` is the MVP — fully specified —
|
|
8
|
-
* while `scenarios`
|
|
9
|
-
* (see CHANGELOG.md; `integrations`/`access` were deleted in 0.6.0, EXPERT-REDESIGN §9
|
|
8
|
+
* while `scenarios` carries a real-but-skeletal schema to be fleshed out in later phases
|
|
9
|
+
* (see CHANGELOG.md; `integrations`/`access` were deleted in 0.6.0, EXPERT-REDESIGN §9; `runtime` was retired in 11.0.0 —
|
|
10
|
+
* see {@link RETIRED_DOMAINS}). Adding a
|
|
10
11
|
* field is: extend the zod schema here → the form + the effective-config API pick it up.
|
|
11
12
|
*
|
|
12
|
-
* ⚠️ SECURITY BOUNDARY (see the service-integration design note):
|
|
13
|
+
* ⚠️ SECURITY BOUNDARY (see the service-integration design note): web-admin stores **non-secret**
|
|
13
14
|
* config only. A model's API key / a backend token is NEVER stored here as a value — we store the NAME
|
|
14
15
|
* of the env var that holds it (`apiKeyEnv`, `tokenEnv`), and the agent-service reads the real secret
|
|
15
16
|
* from its own environment. So a leak of this DB never leaks a credential. This is **enforced** by the
|
|
16
17
|
* `ENV_NAME` schema (regex) on every `*Env` field — a pasted secret value is rejected, not just discouraged.
|
|
17
18
|
*/
|
|
18
19
|
import { z } from "zod";
|
|
20
|
+
import { INTEGER } from "./integer.js";
|
|
19
21
|
import { MAX_EXTRA_BODY_DEPTH } from "./hash.js"; // reconcile the extraBody depth cap with the hash cap (R18)
|
|
20
22
|
import { DispatchPolicy, FLEET_GENERATION_ENV, FLEET_SCOPE_ENV } from "./fleet.js"; // WorkerSpec.fleet.dispatch (scheduling framework D3)
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
23
|
+
import { isLoopbackAuthHost } from "./loopback.js";
|
|
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
|
+
import { secretScanForms } from "./secret-scan.js"; // package-private normalization — the one set of forms both scanners read (not in the barrel)
|
|
26
|
+
import { WEB_ADMIN_ENV } from "./web-admin-env.js"; // the three connection keys the config renderer writes are reserved worker env
|
|
27
|
+
import { retiredEnvRefusal } from "./retired-envs.js"; // the worker env write gate asks the retired-env tombstone table too (one table, one refusal sentence)
|
|
28
|
+
import { Sha256Digest } from "./sha256.js"; // the one `sha256:<hex>` digest form
|
|
29
|
+
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
|
+
import { LANE_ENV_OWNER, isMigratableEnvKey, laneEnvSeat, laneRowOf } from "./execution-lane-index.js"; // package-private indexes derived from that table (not in the barrel)
|
|
31
|
+
import { migrationSignpost, retiredFormSentence, retiredKey } from "./retired-form.js"; // 11.0.0: the ONE retired-form sentence + key tombstone (package-private)
|
|
24
32
|
// COARSE, best-effort literal-secret SHAPE detector (NOT exhaustive — the real boundary is the env-NAME schema
|
|
25
33
|
// on structured fields). Rejected in free-form value fields (worker `env`, model `extraBody`), URL components,
|
|
26
34
|
// 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-
|
|
@@ -30,9 +38,8 @@ import { LANE_ENV_OWNER, MIGRATION_SIGNPOST, isMigratableEnvKey, laneEnvSeat, la
|
|
|
30
38
|
// 0.1.32: this is the root fix for the public-key false-positive — replaces the 0.1.31 name-based exemption,
|
|
31
39
|
// which review flagged as a bypass (a private key misnamed `*_PUBKEY` skipped scanning entirely).
|
|
32
40
|
/** A private-key PEM header (`-----BEGIN [RSA |EC |OPENSSH |ENCRYPTED ]PRIVATE KEY`), unanchored — the ONE definition
|
|
33
|
-
* of that shape, read ANYWHERE in a string by BOTH
|
|
34
|
-
* (`scanForSecret`,
|
|
35
|
-
* normalized form). {@link SECRET_LITERAL_RE} is also composed from it (anchored, as one of its branches — the exported per-token
|
|
41
|
+
* of that shape, read ANYWHERE in a string by BOTH sides of the one scanner: the validation detector {@link containsSecretToken}
|
|
42
|
+
* (`scanForSecret`, 10.0.0) and the display redactor `redactSecrets` (config-fns) — on the same normalized forms since 11.0.0. {@link SECRET_LITERAL_RE} is also composed from it (anchored, as one of its branches — the exported per-token
|
|
36
43
|
* literal shape). The header carries spaces, so the token-splitting passes can never isolate it; before 10.0.0 the validation side
|
|
37
44
|
* therefore saw it only at the very start of a string, and a PEM behind a newline, a `key: ` prefix or any other text rode past
|
|
38
45
|
* every free-text write port while the display side already withheld it (a `key=`-prefixed percent-encoded
|
|
@@ -63,6 +70,11 @@ function scanForSecret(s) {
|
|
|
63
70
|
// the token; the JWT dot is handled above). R12. */
|
|
64
71
|
if (/(?:^|[^A-Za-z0-9_-])(?:bearer|basic)[ \t]+\S{8,4096}/i.test(s))
|
|
65
72
|
return true;
|
|
73
|
+
// The same scheme followed by an ENCODED space (`+` form-encoding, `%20` percent-encoding) and any token at all (S-797, 11.0.0):
|
|
74
|
+
// an encoded space is the mark of an encoded header VALUE (`note:Basic+dTpw`, `x-Bearer%20abc123`), not prose, so the 8-char floor
|
|
75
|
+
// the plain-space arm keeps against prose (`Basic auth`) does not apply. The boundary admits `-` / `_` (`x-Bearer%20…`). Bounded.
|
|
76
|
+
if (/(?:^|[^A-Za-z0-9])(?:bearer|basic)(?:\+|%20)[^\s+%]{1,4096}/i.test(s))
|
|
77
|
+
return true;
|
|
66
78
|
// a JWT (3 dot-joined base64url segments, header starts `eyJ`) searched anywhere — so a mid-string one
|
|
67
79
|
// survives the tokenizer's dot-split. ReDoS-SAFE: a token-boundary prefix `(?:^|[^A-Za-z0-9_-])` (one start
|
|
68
80
|
// per word, so `"eyJ".repeat(n)` is a single anchored start = linear, not the quadratic an unanchored scan
|
|
@@ -81,18 +93,43 @@ function scanForSecret(s) {
|
|
|
81
93
|
* fs path — so the worker dies at exec/boot, not at config edit. A NUL is never legitimate in a command / path
|
|
82
94
|
* / host / image, so reject it on every exec-bound string field (mcp stdio + the whole remoteExec contract). R20. */
|
|
83
95
|
export const hasNul = (s) => s.includes("\0");
|
|
84
|
-
/**
|
|
85
|
-
*
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
96
|
+
/** The one rendering of a quoted sentence: `template[0] + show(operands[0]) + template[1] + … + template[n]`. `show` defaults to
|
|
97
|
+
* the value as written (that is how `message` is minted); a sanitizer / log boundary passes its own. */
|
|
98
|
+
export function renderQuotedSentence(p, show = String) {
|
|
99
|
+
return p.template.reduce((out, part, i) => (i === 0 ? part : `${out}${show(p.operands[i - 1])}${part}`), "");
|
|
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
|
+
}
|
|
120
|
+
/** Does `s` contain a secret-shaped token? —— **the one scanner** (11.0.0, S-1274): the write side (this detector, behind every
|
|
121
|
+
* free-text write port — NON_SECRET_URL, mcp stdio, execution coords, ssh login, worker env, extraBody, …) and the display side
|
|
122
|
+
* (`redactSecrets`) read the SAME normalized forms ({@link secretScanForms} in the package-private `./secret-scan`: the raw string,
|
|
123
|
+
* up to three tolerant percent-decoded layers, each layer's boundary reading, each of those read the form-encoded way with `+` as a
|
|
124
|
+
* space) through the SAME shape table ({@link scanForSecret}). A string whose normalization did not finish (a fourth layer would
|
|
125
|
+
* still decode something) counts as secret-shaped: what cannot be normalized is not judged clean. Before 11.0.0 this detector read
|
|
126
|
+
* only the percent layers, so a header spelled only in a deeper layer or with `+` for its spaces passed write validation while the
|
|
127
|
+
* display side withheld it. We do NOT fail-closed on a bare `%` — that would false-reject legit values like `100%` / `%s`. A secret
|
|
128
|
+
* glued to a malformed escape (`%ZZsk-`) is an accepted coarse-heuristic gap (the real guarantee is the env-NAME boundary on
|
|
129
|
+
* structured fields). R11/R13/R14. */
|
|
94
130
|
export function containsSecretToken(s) {
|
|
95
|
-
|
|
131
|
+
const scan = secretScanForms(s);
|
|
132
|
+
return scan.unresolved || scan.forms.some((form) => scanForSecret(form));
|
|
96
133
|
}
|
|
97
134
|
/** Scan a URL's PATH + query + fragment (KEYS and values, decoded, tokenized) for a secret-shaped literal.
|
|
98
135
|
* Userinfo is handled separately (NON_SECRET_URL bans it; the ssh username is scanned by `containsSecretToken`). Round-4/5. */
|
|
@@ -126,24 +163,25 @@ export function urlQueryFragmentHasSecret(u) {
|
|
|
126
163
|
}
|
|
127
164
|
return false;
|
|
128
165
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
166
|
+
/** The parts every {@link NON_SECRET_URL} seat forbids: userinfo (a credential in the URL — it would store a secret VALUE in the
|
|
167
|
+
* config and ship it on /effective, defeating the env-NAME boundary) and a fragment (never sent; anything a consumer appends
|
|
168
|
+
* 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
|
+
* later, on the worker). A query is a legitimate request parameter (`?api-version=…`) and stays. */
|
|
170
|
+
const NON_SECRET_URL_FORBIDS = ["userinfo", "fragment"];
|
|
171
|
+
/** A URL handed to `fetch` that carries no credential: judged by THE URL shape judge ({@link httpUrlFlaws}, `./url-shape`) — a
|
|
172
|
+
* hierarchical http(s) URL with neither userinfo nor a fragment (an empty `#` included, 11.0.0) — and with no secret-shaped value
|
|
173
|
+
* in its path or query ({@link urlQueryFragmentHasSecret}). One issue per flaw, its message the judge's rule sentence (no value
|
|
174
|
+
* echoed); `params.flaw` carries the rule word.
|
|
175
|
+
*
|
|
176
|
+
* The value is kept EXACTLY as written: nothing here trims or normalizes it (zod's own `.url()` would hand back a rewritten string). */
|
|
177
|
+
export const NON_SECRET_URL = z.string().superRefine((s, ctx) => {
|
|
178
|
+
const flaws = httpUrlFlaws(s, NON_SECRET_URL_FORBIDS);
|
|
179
|
+
for (const flaw of flaws)
|
|
180
|
+
ctx.addIssue({ code: "custom", message: httpUrlRule(flaw), params: { flaw } });
|
|
181
|
+
if (flaws.length === 0 && urlQueryFragmentHasSecret(new URL(s))) {
|
|
182
|
+
ctx.addIssue({ code: "custom", message: "must not carry a secret-shaped value in its path or query — secrets go in the *Env (env-NAME) fields" });
|
|
145
183
|
}
|
|
146
|
-
}
|
|
184
|
+
});
|
|
147
185
|
/** Validate a free-form `extraBody`: returns true = UNSAFE (reject). A GETTER-FREE, descriptor-based, bounded
|
|
148
186
|
* walk that rejects — WITHOUT ever invoking a getter (a malicious getter could throw a secret-bearing message,
|
|
149
187
|
* or return different values across the validate vs persist JSON.stringify calls) — anything that isn't plain
|
|
@@ -228,19 +266,19 @@ function vetFreeFormJson(v, field) {
|
|
|
228
266
|
rawUnsafe = extraBodyUnsafe(v);
|
|
229
267
|
}
|
|
230
268
|
catch {
|
|
231
|
-
return { ok: false,
|
|
269
|
+
return { ok: false, ...buildQuotedIssue `${fixed(field)} could not be validated (reflective access threw)` }; // R11
|
|
232
270
|
}
|
|
233
271
|
if (rawUnsafe)
|
|
234
|
-
return { ok: false,
|
|
272
|
+
return { ok: false, ...buildQuotedIssue `${fixed(field)}: a key/value is a secret shape, non-JSON (Infinity/function/Map), an accessor, cyclic, or too deep` };
|
|
235
273
|
let clone;
|
|
236
274
|
try {
|
|
237
275
|
clone = JSON.parse(JSON.stringify(v)); // the ONLY serialization — its result is what we scan AND persist
|
|
238
276
|
}
|
|
239
277
|
catch {
|
|
240
|
-
return { ok: false,
|
|
278
|
+
return { ok: false, ...buildQuotedIssue `${fixed(field)} is not JSON-serializable` };
|
|
241
279
|
}
|
|
242
280
|
if (extraBodyUnsafe(clone))
|
|
243
|
-
return { ok: false,
|
|
281
|
+
return { ok: false, ...buildQuotedIssue `${fixed(field)}: its serialized form carries a secret` };
|
|
244
282
|
return { ok: true, value: clone };
|
|
245
283
|
}
|
|
246
284
|
/** The transform of every **parse-transparent free-form** key (`z.unknown().optional().transform(createFreeFormTransform(field))`:
|
|
@@ -253,7 +291,7 @@ function createFreeFormTransform(field) {
|
|
|
253
291
|
return undefined;
|
|
254
292
|
const vetted = vetFreeFormJson(v, field);
|
|
255
293
|
if (!vetted.ok) {
|
|
256
|
-
ctx.addIssue({ code:
|
|
294
|
+
ctx.addIssue({ code: "custom", message: vetted.message, params: vetted.params });
|
|
257
295
|
return z.NEVER;
|
|
258
296
|
}
|
|
259
297
|
return vetted.value;
|
|
@@ -264,14 +302,14 @@ function createFreeFormTransform(field) {
|
|
|
264
302
|
export const K8S_QUANTITY = z.string().regex(/^\d+(\.\d+)?(m|[kKMGTPE]i?)?$/, 'must be a k8s quantity like "250m", "512Mi", "1Gi"');
|
|
265
303
|
/** A TCP port: 1–65535. ONE shared schema so every port field (workers.runtime.store.db.port, ssh.port, …) bounds
|
|
266
304
|
* the SAME way — a port of 65536/0/-1 is config corruption, reject it at validation, not at connect time. R16. */
|
|
267
|
-
export const TCP_PORT =
|
|
305
|
+
export const TCP_PORT = INTEGER.positive().max(65535);
|
|
268
306
|
/** Per-1M-token price ($/Mtok). Maps to the service's MODEL_COST_INPUT/OUTPUT/CACHE_READ/CACHE_WRITE
|
|
269
307
|
* (budget.ts buildPricing → per-principal cost gate). Lives on the models catalog (ModelEntry.cost) — the
|
|
270
308
|
* single pricing source (worker.model, the old second consumer, was deleted in 0.7.0). A free model declares
|
|
271
309
|
* 0s (core 1.26: missing cost ≠ cheapest). */
|
|
272
|
-
// `.finite()` on every $/Mtok field
|
|
273
|
-
// JSON.stringify's to `null` on write → a later read fails "expected number,
|
|
274
|
-
// WRONG time).
|
|
310
|
+
// `.finite()` on every $/Mtok field states the rule HERE rather than leaning on whether the library's `number()` admits
|
|
311
|
+
// Infinity (zod 3's did): a non-finite number JSON.stringify's to `null` on write → a later read fails "expected number,
|
|
312
|
+
// received null" (fail-closed at the WRONG time). Integer fields ({@link INTEGER}) are Infinity-safe by their own rule. Round-4.
|
|
275
313
|
export const ModelCost = z.object({
|
|
276
314
|
input: z.number().finite().nonnegative(),
|
|
277
315
|
output: z.number().finite().nonnegative(),
|
|
@@ -282,12 +320,12 @@ export const ModelCost = z.object({
|
|
|
282
320
|
// Maps 1:1 onto the worker service's config.models / config.roles / @-model allowlist (core 1.24 role
|
|
283
321
|
// map). `name` is the catalog key AND the `@name` handle; `id` is the gateway's model id.
|
|
284
322
|
// ── sealed-box 密钥托管(0.10.5,产品拍板铁律修订:「不存 secret 值」精化为「不存**可解明文**」)──
|
|
285
|
-
//
|
|
286
|
-
// XSalsa20-Poly1305)加密后提交,
|
|
287
|
-
// 明文 key 绝不出浏览器 =
|
|
323
|
+
// web-admin 可存"不可解密的密文":浏览器端用执行域公钥 libsodium sealed-box(crypto_box_seal,X25519+
|
|
324
|
+
// XSalsa20-Poly1305)加密后提交,web-admin 无私钥永远解不开;caps/config 原样下发,执行侧私钥解密注入 env。
|
|
325
|
+
// 明文 key 绝不出浏览器 = web-admin 服务端代码路径里绝不出现明文。
|
|
288
326
|
/** sealed-box 算法标识(v1 唯一取值)。z.literal 钉死:换算法=换值=显式契约演进,不许静默漂移。 */
|
|
289
327
|
export const SEALED_BOX_ALG = "libsodium-sealedbox-x25519";
|
|
290
|
-
/** base64 形状底线(标准字母表,可带 padding)。**只校验形状不校验内容**——
|
|
328
|
+
/** base64 形状底线(标准字母表,可带 padding)。**只校验形状不校验内容**——web-admin 是托管方不是消费方,
|
|
291
329
|
* 解密/解不开是执行侧运行时的事;这里只挡"根本不是 base64"的写入事故。 */
|
|
292
330
|
export const BASE64_SHAPE = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
293
331
|
/** 接入模型的 sealed-box 托管密文(默认模式「直接贴 key」的存储形态)。
|
|
@@ -295,19 +333,19 @@ export const BASE64_SHAPE = /^[A-Za-z0-9+/]+={0,2}$/;
|
|
|
295
333
|
* 真实 API key ≤ 几百字节;`.max(8192)` 只防 blob 滥用,不是格式承诺。
|
|
296
334
|
* - `publicKeyId`:加密时用的执行域公钥 id(指向 execution.publicKeys[].publicKeyId)——执行侧凭它选私钥;
|
|
297
335
|
* 轮换后旧密文仍带旧 id,可解性由执行侧留存的旧私钥决定。
|
|
298
|
-
* - **write-only 纪律(消费方约定)**:
|
|
336
|
+
* - **write-only 纪律(消费方约定)**:web-admin 的 GET 面永不回 ciphertext 本体(只回 publicKeyId+设置
|
|
299
337
|
* 时间);密文字段绝不进任何日志。schema 层不执法读面,这是 API 层的契约义务。
|
|
300
338
|
* - **与 `apiKeyEnv` 的互斥语义**:见 ModelEntry.sealedApiKey JSDoc(两者都设 = sealed 优先)。 */
|
|
301
339
|
export const SealedApiKey = z.object({
|
|
302
|
-
/** sealed-box 密文,base64(形状校验 only——内容对
|
|
340
|
+
/** sealed-box 密文,base64(形状校验 only——内容对 web-admin 永远不透明)。 */
|
|
303
341
|
ciphertext: z.string().min(1).max(8192).regex(BASE64_SHAPE, "ciphertext must be base64"),
|
|
304
342
|
/** 加密所用执行域公钥的 id(execution.publicKeys 登记表的键)。 */
|
|
305
343
|
publicKeyId: z.string().min(1).max(128),
|
|
306
344
|
/** 算法标识,v1 恒为 {@link SEALED_BOX_ALG}。 */
|
|
307
345
|
alg: z.literal(SEALED_BOX_ALG),
|
|
308
|
-
/** 密文设置时刻(ISO)。
|
|
346
|
+
/** 密文设置时刻(ISO)。web-admin 写面 server 端盖章(projects.registeredAt 同款纪律);write-only
|
|
309
347
|
* 的 masked GET 面靠它+publicKeyId 展示「已于 X 设置」——密文本体永不回读。optional:执行侧
|
|
310
|
-
* 解密不依赖它,非
|
|
348
|
+
* 解密不依赖它,非 web-admin 写面(本地 TOC 手写配置)缺省合法。 */
|
|
311
349
|
setAt: z.string().min(1).optional(),
|
|
312
350
|
});
|
|
313
351
|
/**
|
|
@@ -367,6 +405,10 @@ export const PARSE_TRANSPARENT_KEYS = [
|
|
|
367
405
|
why: "它的形由同一条 override 的 `key` 决定(一个 key 一种形),执法点在读那个 key 的消费方;在这里写死形等于把所有可覆盖键的形表搬进本包。",
|
|
368
406
|
},
|
|
369
407
|
];
|
|
408
|
+
/** A catalog model's price / capability tier — fuels core 1.25 `selectModel({ tier })` role resolution. ONE closed set for both
|
|
409
|
+
* places that name it (`ModelEntry.tier`, `RoleTarget.select.tier`). Not the tier-group ladder of {@link TierBindings}
|
|
410
|
+
* (`ultra` … `lite`), which binds model NAMES to rungs. */
|
|
411
|
+
export const ModelTier = z.enum(["cheap", "standard", "premium"]);
|
|
370
412
|
export const ModelEntry = z.object({
|
|
371
413
|
/** Catalog key + `@name` mention handle (stable, url/word-safe), e.g. "model-a". */
|
|
372
414
|
name: z.string().min(1).regex(/^[a-z0-9][a-z0-9._-]*$/i, "letters/digits/._- , no spaces"),
|
|
@@ -384,27 +426,26 @@ export const ModelEntry = z.object({
|
|
|
384
426
|
* 专家模式(env-NAME 引用,web 面折叠进「高级」);默认模式见 {@link ModelEntry}.sealedApiKey。 */
|
|
385
427
|
apiKeyEnv: ENV_NAME.optional(),
|
|
386
428
|
/** sealed-box 托管密文(0.10.5 默认模式「直接贴 key」;形状见 {@link SealedApiKey})。
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
* ABSENT = 未托管(纯 env-NAME 现状),零迁移。 */
|
|
429
|
+
* **二选一**(11.0.0):一个模型的钥匙只有一种写法 —— `sealedApiKey` 与 `apiKeyEnv` 同设 ⇒ 本条目被拒(读写同一只判据,见本
|
|
430
|
+
* schema 的 superRefine)。切换写法的写者负责清掉另一字段。11.0.0 之前存下的双设条目由一次性迁移 SS-M5 改写(删 `apiKeyEnv`
|
|
431
|
+
* = 当年的生效读法「sealed 赢」)。ABSENT = 未托管(纯 env-NAME 现状)。 */
|
|
391
432
|
sealedApiKey: SealedApiKey.optional(),
|
|
392
433
|
/** Price/capability tier — fuels core 1.25 `selectModel({ tier })` role resolution. */
|
|
393
|
-
tier:
|
|
434
|
+
tier: ModelTier.default("standard"),
|
|
394
435
|
reasoning: z.boolean().default(false),
|
|
395
436
|
vision: z.boolean().default(false),
|
|
396
|
-
contextWindow:
|
|
397
|
-
maxTokens:
|
|
398
|
-
/** Auto-compaction trigger threshold (tokens) for this model —
|
|
437
|
+
contextWindow: INTEGER.positive().optional(),
|
|
438
|
+
maxTokens: INTEGER.positive().optional(),
|
|
439
|
+
/** Auto-compaction trigger threshold (tokens) for this model — web-admin 显式下发时本地 FileConfigStore
|
|
399
440
|
* lane 必须原样保真(0.10.10:strip 导致 parse 期即丢)。消费方=壳/引擎的 compaction
|
|
400
441
|
* 配置(safety-merge autoCompaction seam);ABSENT = 消费方自身默认,零迁移。 */
|
|
401
|
-
autoCompactTokens:
|
|
442
|
+
autoCompactTokens: INTEGER.positive().optional(),
|
|
402
443
|
/** Chars-per-token estimate for local token estimation (may be fractional, e.g. 3.5;CJK-heavy 模型
|
|
403
444
|
* 比英文低)。与 autoCompactTokens 同班车(0.10.10 [830]④a);ABSENT = 消费方自身默认,零迁移。 */
|
|
404
445
|
charsPerToken: z.number().finite().positive().optional(),
|
|
405
446
|
/** Per-model prompt-guidance lines(装配 behavior 槽的 per-model 缓解句)。形对齐 core 消费面
|
|
406
447
|
* `model.promptGuidance?: string[]`(engine/llm types;prompt-assembly 战役起装配位固定于
|
|
407
|
-
* volatile 尾前)。web/
|
|
448
|
+
* volatile 尾前)。web/web-admin models 域先行小切片(不等 pack 大架构)。Non-secret model
|
|
408
449
|
* metadata(cost/tier 同类);ABSENT = 无 per-model 句,零迁移。 */
|
|
409
450
|
promptGuidance: z.array(z.string()).optional(),
|
|
410
451
|
/** $/Mtok. A genuinely-free model still declares 0s (core 1.26: missing cost ≠ cheapest). */
|
|
@@ -430,12 +471,12 @@ export const ModelEntry = z.object({
|
|
|
430
471
|
return undefined;
|
|
431
472
|
const proto = typeof v === "object" && v !== null ? Object.getPrototypeOf(v) : undefined;
|
|
432
473
|
if (typeof v !== "object" || v === null || Array.isArray(v) || (proto !== Object.prototype && proto !== null)) {
|
|
433
|
-
ctx.addIssue({ code:
|
|
474
|
+
ctx.addIssue({ code: "custom", message: "extraBody must be a plain JSON object" });
|
|
434
475
|
return z.NEVER;
|
|
435
476
|
}
|
|
436
477
|
const vetted = vetFreeFormJson(v, "extraBody");
|
|
437
478
|
if (!vetted.ok) {
|
|
438
|
-
ctx.addIssue({ code:
|
|
479
|
+
ctx.addIssue({ code: "custom", message: vetted.message, params: vetted.params });
|
|
439
480
|
return z.NEVER;
|
|
440
481
|
}
|
|
441
482
|
return vetted.value;
|
|
@@ -481,7 +522,10 @@ export const ModelEntry = z.object({
|
|
|
481
522
|
// suggested this; enforce it in the SCHEMA so a direct PUT / local TOC config can't ship a mis-paired model.
|
|
482
523
|
// (provider:"anthropic" must set api explicitly; gateway/vllm keep the openai-completions default.) Round-4.
|
|
483
524
|
if ((m.provider === "anthropic") !== (m.api === "anthropic-messages"))
|
|
484
|
-
ctx.addIssue({ code:
|
|
525
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `provider "${m.provider}" + api "${m.api}" mismatch — provider:"anthropic" ⟺ api:"anthropic-messages"`, path: ["api"] });
|
|
526
|
+
// 钥匙二选一(11.0.0):同设 = 两个答案回答「这个模型用哪把钥匙」。此前「sealed 压过 env」只活在注里与消费方三处优先级臂上。
|
|
527
|
+
if (m.sealedApiKey !== undefined && m.apiKeyEnv !== undefined)
|
|
528
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `sealedApiKey and apiKeyEnv are both set — a model's key has ONE form: keep sealedApiKey (a pasted key) or apiKeyEnv (an env-NAME), not both; ${fixed(migrationSignpost("SS-M5", "11.0.0"))}`, path: ["apiKeyEnv"] });
|
|
485
529
|
});
|
|
486
530
|
// `verifier` (2026-06-11, SERVICE-RESPONSE-…-PHASE2): real consumer = core's verification gate
|
|
487
531
|
// (runWithVerification/resumeWithVerification resolve the verifier sub-task model via this role). Fallback
|
|
@@ -498,11 +542,26 @@ export const ModelRole = z.enum(["default", "summarize", "subagent", "team", "sy
|
|
|
498
542
|
/** A role resolves either to a model NAME, or by capability/price (core 1.25 `{ select }`). */
|
|
499
543
|
export const RoleTarget = z.union([
|
|
500
544
|
z.object({ model: z.string().min(1) }),
|
|
501
|
-
z.object({ select: z.object({ tier:
|
|
545
|
+
z.object({ select: z.object({ tier: ModelTier.optional(), needs: z.array(z.enum(["vision", "reasoning"])).optional(), prefer: z.enum(["cheapest", "premium"]).optional() }) }),
|
|
546
|
+
]);
|
|
547
|
+
/** {@link AtMention} 的模式闭集:`all` = 每个启用的模型都可 `@`;`none` = 一个都不许;`only` = 只许 `models` 里点名的那几个。 */
|
|
548
|
+
export const AT_MENTION_MODES = ["all", "none", "only"];
|
|
549
|
+
/** 「`@` 能点名哪些模型」(11.0.0,取代 `atModelAllowlist`)。义由 `mode` 说,不由数组空不空说:旧形一个数组靠「缺席 / 空 / 非空」
|
|
550
|
+
* 三种在场性背三种义,「一个都不许」表达不了,三个消费方各写了一只绕行。`only` 的名单至少一个名(空名单的义就是 `none`,一个义一种写法)。
|
|
551
|
+
* 全局位(`models.atMention`)缺省 `{mode:"all"}`;roster 位(`rosters[].atMention`)缺席 = 继承全局。收窄到某台 worker 的模型集
|
|
552
|
+
* 用 {@link narrowAtMention}(`./resolve-roster`)—— 一只读法。 */
|
|
553
|
+
export const AtMention = z.discriminatedUnion("mode", [
|
|
554
|
+
z.object({ mode: z.literal("all") }),
|
|
555
|
+
z.object({ mode: z.literal("none") }),
|
|
556
|
+
z.object({ mode: z.literal("only"), models: z.array(z.string().min(1)).min(1) }),
|
|
502
557
|
]);
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
558
|
+
/** `models.default` 的墓碑(11.0.0,N-7):缺省模型只有 `roles.default` 一位。 */
|
|
559
|
+
const MODELS_DEFAULT_RETIRED = { retiredIn: "11.0.0", now: "roles.default ({model:<catalog name>}) — the one default-model seat", migration: "SS-M9" };
|
|
560
|
+
/** `atModelAllowlist` 的墓碑(11.0.0):全局位与 roster 位同一行。 */
|
|
561
|
+
const AT_MODEL_ALLOWLIST_RETIRED = { retiredIn: "11.0.0", now: "atMention ({mode:\"all\"} | {mode:\"none\"} | {mode:\"only\",models:[…]})", migration: "SS-M6" };
|
|
562
|
+
// ── sema 档位(tiers)与档位组(tier groups)──(web-admin 半场, core 1.265 档位车) ──────
|
|
563
|
+
// core 侧接缝 = `RunnerDeps.tiers`(档位词→模型绑定的**单表**, catalog 单点增广 expandTiers);web-admin 侧
|
|
564
|
+
// 把「多套命名预配置」存成 tierGroups,下发时只解析 ACTIVE 那一套成单表(组切换=web-admin 换表下发,零新
|
|
506
565
|
// 引擎机制)。与 `ModelEntry.tier`(cheap/standard/premium 价格分级, core 1.25 selectModel)是两回事——
|
|
507
566
|
// 那是能力/价格元数据,这是用户面的模型预配置绑定;词表也不同(max/pro/flash/lite + ultra 预留)。
|
|
508
567
|
/** 档位词→catalog 模型 `name` 的绑定。值=目录键(string 引用,不 enum——开放性照 execution
|
|
@@ -519,23 +578,25 @@ export const TierBindings = z.object({
|
|
|
519
578
|
export const TierGroup = z.object({
|
|
520
579
|
/** Group key (same charset as catalog names), e.g. "daily" / "heavy". */
|
|
521
580
|
name: z.string().min(1).regex(/^[a-z0-9][a-z0-9._-]*$/i, "letters/digits/._- , no spaces"),
|
|
522
|
-
tiers: TierBindings.
|
|
581
|
+
tiers: TierBindings.prefault({}),
|
|
523
582
|
notes: z.string().optional(),
|
|
524
583
|
});
|
|
525
584
|
export const ModelsConfig = z
|
|
526
585
|
.object({
|
|
527
|
-
models: z.array(ModelEntry).
|
|
528
|
-
/**
|
|
529
|
-
* (
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
586
|
+
models: z.array(ModelEntry).prefault([]),
|
|
587
|
+
/** role → target. Unset roles fall back to `default` (core fallback chain). `roles.default` is THE default model — the one seat
|
|
588
|
+
* (11.0.0, N-7: the structural `models.default` key is retired; it and `roles.default` used to answer the same question twice,
|
|
589
|
+
* the main task ran the first and the core `default` role resolved the second). Which catalog entry is the default is
|
|
590
|
+
* `resolveCatalogDefaultModel` (`./default-model`; a `{select}` default is handed back as-is for the engine to resolve). A stored
|
|
591
|
+
* `default` key is moved here by the one-time migration SS-M9. */
|
|
592
|
+
roles: z.partialRecord(ModelRole, RoleTarget).prefault({}),
|
|
593
|
+
/** Which model names an end user may `@`-mention({@link AtMention});缺省 `{mode:"all"}`。 */
|
|
594
|
+
atMention: AtMention.prefault({ mode: "all" }),
|
|
595
|
+
/** retired 键墓碑(11.0.0):在场即拒,拒句点名一次性迁移 SS-M6。 */
|
|
596
|
+
atModelAllowlist: retiredKey("atModelAllowlist", AT_MODEL_ALLOWLIST_RETIRED),
|
|
597
|
+
default: retiredKey("models.default", MODELS_DEFAULT_RETIRED),
|
|
537
598
|
/** 命名档位组预配置。只有 ACTIVE 组的绑定表会下发为引擎的 tiers 单表。 */
|
|
538
|
-
tierGroups: z.array(TierGroup).
|
|
599
|
+
tierGroups: z.array(TierGroup).prefault([]),
|
|
539
600
|
/** 当前生效的档位组 name。未设(或无组)→ 不下发 tiers,引擎档位层恒惰性。 */
|
|
540
601
|
activeTierGroup: z.string().optional(),
|
|
541
602
|
})
|
|
@@ -548,53 +609,50 @@ export const ModelsConfig = z
|
|
|
548
609
|
const seen = new Set();
|
|
549
610
|
cfg.models.forEach((m, i) => {
|
|
550
611
|
if (seen.has(m.name))
|
|
551
|
-
ctx.addIssue({ code:
|
|
612
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate model "${m.name}" — the catalog key (and @-handle) must be unique`, path: ["models", i, "name"] });
|
|
552
613
|
seen.add(m.name);
|
|
553
614
|
});
|
|
554
615
|
// Catalog self-references must resolve to a real model — a dangling global `roles.{role}.model` or an
|
|
555
|
-
// `
|
|
556
|
-
// directly, with no roster filter). This lived ONLY in refIntegrityIssues (
|
|
616
|
+
// `atMention.models` name not in `models` ships a broken wire (the global /effective path uses eff.models
|
|
617
|
+
// directly, with no roster filter). This lived ONLY in refIntegrityIssues (web-admin) — but the schema
|
|
557
618
|
// is the only guard for a local TOC config / a direct PUT. Round-4 (reproduced).
|
|
558
619
|
const names = new Set(cfg.models.map((m) => m.name));
|
|
559
620
|
for (const [role, t] of Object.entries(cfg.roles ?? {})) {
|
|
560
621
|
if (t && "model" in t && !names.has(t.model))
|
|
561
|
-
ctx.addIssue({ code:
|
|
622
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `role "${role}" → model "${t.model}" is not in the catalog`, path: ["roles", role, "model"] });
|
|
562
623
|
}
|
|
563
|
-
(cfg.
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
// 存量/本地 lane 仍 warn 降级——与 activeTierGroup 的「写面 fail-loud + 读面 graceful」双层姿势一致)。
|
|
569
|
-
if (cfg.default !== undefined && !names.has(cfg.default)) {
|
|
570
|
-
ctx.addIssue({ code: z.ZodIssueCode.custom, message: `default "${cfg.default}" is not in the catalog`, path: ["default"] });
|
|
624
|
+
if (cfg.atMention.mode === "only") {
|
|
625
|
+
cfg.atMention.models.forEach((n, i) => {
|
|
626
|
+
if (!names.has(n))
|
|
627
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `atMention.models["${n}"] is not in the catalog`, path: ["atMention", "models", i] });
|
|
628
|
+
});
|
|
571
629
|
}
|
|
572
630
|
// 档位组 ref 完整性(照上面 roles/allowlist 悬空检测同款):组名去重 + 绑定必须命中 catalog +
|
|
573
631
|
// activeTierGroup 必须命中某组(悬空 active = 下发面从「换表」静默滑成「无表」,写入时 fail-loud)。
|
|
574
632
|
const groupSeen = new Set();
|
|
575
633
|
cfg.tierGroups.forEach((g, i) => {
|
|
576
634
|
if (groupSeen.has(g.name))
|
|
577
|
-
ctx.addIssue({ code:
|
|
635
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate tier group "${g.name}"`, path: ["tierGroups", i, "name"] });
|
|
578
636
|
groupSeen.add(g.name);
|
|
579
637
|
for (const [tier, model] of Object.entries(g.tiers)) {
|
|
580
638
|
if (typeof model === "string" && !names.has(model))
|
|
581
|
-
ctx.addIssue({ code:
|
|
639
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `tier group "${g.name}" ${tier} → model "${model}" is not in the catalog`, path: ["tierGroups", i, "tiers", tier] });
|
|
582
640
|
}
|
|
583
641
|
});
|
|
584
642
|
if (cfg.activeTierGroup !== undefined && !groupSeen.has(cfg.activeTierGroup)) {
|
|
585
|
-
ctx.addIssue({ code:
|
|
643
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `activeTierGroup "${cfg.activeTierGroup}" is not a tier group name`, path: ["activeTierGroup"] });
|
|
586
644
|
}
|
|
587
645
|
});
|
|
588
646
|
// ── Domain: rosters (per-worker model roster — the multi-roster design note) ───────────
|
|
589
647
|
// A named VIEW over the global `models` catalog: which models a worker exposes + its role map + @-allowlist.
|
|
590
648
|
// A worker references one by name (WorkerSpec.roster); GET /effective?worker=<name> resolves it. Models stay
|
|
591
649
|
// single-sourced in the `models` domain — a roster only references them by name (cross-domain ref checked at
|
|
592
|
-
// resolution/PUT, see the multi-roster design note §6). This is what lets one
|
|
650
|
+
// resolution/PUT, see the multi-roster design note §6). This is what lets one web-admin serve heterogeneous
|
|
593
651
|
// instances (OA vs review 各持异构模型) instead of a single global roster.
|
|
594
652
|
export const RosterEntry = z.object({
|
|
595
653
|
name: z.string().min(1).regex(/^[a-z0-9][a-z0-9._-]*$/i, "letters/digits/._- , no spaces"),
|
|
596
654
|
/** Catalog model names this roster exposes (must exist in models.models[].name). Empty = the full catalog. */
|
|
597
|
-
models: z.array(z.string().min(1)).
|
|
655
|
+
models: z.array(z.string().min(1)).prefault([]),
|
|
598
656
|
/**
|
|
599
657
|
* 主模型角色标记(0.6.3, E3)——catalog model NAME。worker.model 废除后 service 从 roster
|
|
600
658
|
* 解析 boot 主模型的归宿位:解析语义(resolve-roster)= primary 喂 `default`/`team`/`synthesize` 三角色、
|
|
@@ -605,18 +663,20 @@ export const RosterEntry = z.object({
|
|
|
605
663
|
/** cheap 模型角色标记(同上;worker.model.cheapId 的迁移归宿,§3②)。 */
|
|
606
664
|
cheapModel: z.string().min(1).optional(),
|
|
607
665
|
/** Role map override; unset roles fall back to marker-derived roles, then the global models.roles. */
|
|
608
|
-
roles: z.
|
|
609
|
-
/**
|
|
610
|
-
|
|
666
|
+
roles: z.partialRecord(ModelRole, RoleTarget).optional(),
|
|
667
|
+
/** `@` 点名覆盖({@link AtMention});缺席 = 继承全局 `models.atMention`。 */
|
|
668
|
+
atMention: AtMention.optional(),
|
|
669
|
+
/** retired 键墓碑(11.0.0):在场即拒,拒句点名一次性迁移 SS-M6。 */
|
|
670
|
+
atModelAllowlist: retiredKey("atModelAllowlist", AT_MODEL_ALLOWLIST_RETIRED),
|
|
611
671
|
notes: z.string().optional(),
|
|
612
672
|
});
|
|
613
673
|
export const RostersConfig = z
|
|
614
|
-
.object({ rosters: z.array(RosterEntry).
|
|
674
|
+
.object({ rosters: z.array(RosterEntry).prefault([]) })
|
|
615
675
|
.superRefine((cfg, ctx) => {
|
|
616
676
|
const seen = new Set();
|
|
617
677
|
cfg.rosters.forEach((r, i) => {
|
|
618
678
|
if (seen.has(r.name))
|
|
619
|
-
ctx.addIssue({ code:
|
|
679
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate roster "${r.name}"`, path: ["rosters", i, "name"] });
|
|
620
680
|
seen.add(r.name);
|
|
621
681
|
// intra-roster consistency: a role target / an @-allowlist entry pinned to a model NAME must be one
|
|
622
682
|
// this roster exposes (when it's a subset). Cross-domain (the name exists in the catalog) is checked at
|
|
@@ -627,39 +687,41 @@ export const RostersConfig = z
|
|
|
627
687
|
// 0.6.3 markers: same subset-consistency line as roles/@-allowlist — a marker naming a model this
|
|
628
688
|
// roster doesn't expose would derive a dangling role (dropped at resolution, but reject loudly at PUT).
|
|
629
689
|
if (r.primaryModel && !allowed.has(r.primaryModel)) {
|
|
630
|
-
ctx.addIssue({ code:
|
|
690
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `roster "${r.name}" primaryModel → "${r.primaryModel}" not in roster.models`, path: ["rosters", i, "primaryModel"] });
|
|
631
691
|
}
|
|
632
692
|
if (r.cheapModel && !allowed.has(r.cheapModel)) {
|
|
633
|
-
ctx.addIssue({ code:
|
|
693
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `roster "${r.name}" cheapModel → "${r.cheapModel}" not in roster.models`, path: ["rosters", i, "cheapModel"] });
|
|
634
694
|
}
|
|
635
695
|
for (const [role, target] of Object.entries(r.roles ?? {})) {
|
|
636
696
|
if (target && "model" in target && !allowed.has(target.model)) {
|
|
637
|
-
ctx.addIssue({ code:
|
|
697
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `roster "${r.name}" role.${role} → "${target.model}" not in roster.models`, path: ["rosters", i, "roles", role] });
|
|
638
698
|
}
|
|
639
699
|
}
|
|
640
|
-
(r.
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
700
|
+
if (r.atMention?.mode === "only") {
|
|
701
|
+
r.atMention.models.forEach((name, j) => {
|
|
702
|
+
if (!allowed.has(name))
|
|
703
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `roster "${r.name}" atMention.models "${name}" not in roster.models`, path: ["rosters", i, "atMention", "models", j] });
|
|
704
|
+
});
|
|
705
|
+
}
|
|
644
706
|
}
|
|
645
707
|
});
|
|
646
708
|
});
|
|
647
709
|
// ── Domain: scenarios (场景组合配置化 — ASK-CENTER-2026-06-11-SCENARIOS, v1) ───────────────────────────
|
|
648
|
-
// "组合即配置、能力钉死在部署":
|
|
710
|
+
// "组合即配置、能力钉死在部署": web-admin COMPOSES scenarios from the toolsets the deployment registers —
|
|
649
711
|
// it can never mint a new capability (a new toolset = a service code change, deliberately expensive). The
|
|
650
712
|
// service overlays these onto its built-in scenario table at boot; SAME NAME OVERRIDES the built-in (产品拍板
|
|
651
713
|
// ①, the worker logs `sema_registry_scenarios.shadowsBuiltin` as the audit hook). Invalid entries are
|
|
652
714
|
// skipped+warned worker-side, never fail boot. restart-to-apply like skills/mcp (产品拍板 ③).
|
|
653
715
|
// The old skeletal roles/tools fields are gone (never stored anywhere, zero consumers, no migration).
|
|
654
716
|
export const ScenarioEntry = z.object({
|
|
655
|
-
/** Scenario name — may equal a built-in name (then
|
|
717
|
+
/** Scenario name — may equal a built-in name (then web-admin definition SHADOWS it). */
|
|
656
718
|
name: z.string().regex(/^[a-z][a-z0-9-]{1,31}$/, "lowercase slug, 2-32 chars (a-z, 0-9, -)"),
|
|
657
719
|
/** Toolset key from the SERVICE's registry (v1: "none" | "repo-readonly"). Deliberately a shaped string,
|
|
658
|
-
* not an enum: the worker registry is the authority (unknown → skip+warn there), so
|
|
720
|
+
* not an enum: the worker registry is the authority (unknown → skip+warn there), so web-admin never needs
|
|
659
721
|
* a release just to NAME a toolset a newer service ships. */
|
|
660
722
|
toolset: z.string().regex(/^[a-z][a-z0-9-]{0,63}$/, "toolset key (lowercase slug)"),
|
|
661
723
|
// ScenarioEntry.prompt 已废弃(产品裁定 2026-07-17,[ref]③a;第二评审独立同判「second
|
|
662
|
-
// registry field would become an ambiguous precedence channel」):配置域 prompt 单入口=
|
|
724
|
+
// registry field would become an ambiguous precedence channel」):配置域 prompt 单入口=web-admin
|
|
663
725
|
// prompts binding(scenario 维度);引擎 API 层 TaskSpec.systemPrompt 保留(不同层,勿混)。
|
|
664
726
|
// 现网无用户直删;z.object 非 strict ⇒ 存量数据带 prompt 被 strip,parse 不炸。
|
|
665
727
|
// 落日(2.0.0):本条是**纯墓碑**(零代码,靠 strip 生效),没有要删的兼容码;它留在这里的唯一作用
|
|
@@ -669,12 +731,12 @@ export const ScenarioEntry = z.object({
|
|
|
669
731
|
notes: z.string().optional(),
|
|
670
732
|
});
|
|
671
733
|
export const ScenarioConfig = z
|
|
672
|
-
.object({ scenarios: z.array(ScenarioEntry).
|
|
734
|
+
.object({ scenarios: z.array(ScenarioEntry).prefault([]) })
|
|
673
735
|
.superRefine((cfg, ctx) => {
|
|
674
736
|
const seen = new Set();
|
|
675
737
|
cfg.scenarios.forEach((sc, i) => {
|
|
676
738
|
if (seen.has(sc.name))
|
|
677
|
-
ctx.addIssue({ code:
|
|
739
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate scenario "${sc.name}"`, path: ["scenarios", i, "name"] });
|
|
678
740
|
seen.add(sc.name);
|
|
679
741
|
});
|
|
680
742
|
});
|
|
@@ -699,7 +761,7 @@ export const SystemEntry = z.object({
|
|
|
699
761
|
name: z.string().regex(/^[a-z][a-z0-9-]{1,31}$/, "lowercase slug, 2-32 chars (a-z, 0-9, -)"),
|
|
700
762
|
/** Workers this system may call — its token lands in each one's SERVICE_AUTH_TOKENS CSV (S2).
|
|
701
763
|
* Cross-domain ref-checked against the workers domain. */
|
|
702
|
-
callsWorkers: z.array(z.string().min(1)).
|
|
764
|
+
callsWorkers: z.array(z.string().min(1)).prefault([]),
|
|
703
765
|
/** Where the orchestrator delivers this system's credential (the CALLER's secret). v1: a BARE secret name
|
|
704
766
|
* in the orchestrator's OWN namespace (no "ns/name" — cross-namespace = an RBAC expansion, deferred until
|
|
705
767
|
* a real need). Optional: a system without deliverTo gets a minted token visible only via out-of-band ops. */
|
|
@@ -722,21 +784,21 @@ export const SystemEntry = z.object({
|
|
|
722
784
|
rotateNonce: z.string().max(64).optional(),
|
|
723
785
|
/** Overlap window (seconds) during rotation before the OLD token is retired. Absent → orchestrator default
|
|
724
786
|
* (service AI to confirm; S3). */
|
|
725
|
-
rotateGraceSec:
|
|
787
|
+
rotateGraceSec: INTEGER.positive().optional(),
|
|
726
788
|
enabled: z.boolean().default(true),
|
|
727
789
|
notes: z.string().optional(),
|
|
728
790
|
});
|
|
729
791
|
export const SystemsConfig = z
|
|
730
|
-
.object({ systems: z.array(SystemEntry).
|
|
792
|
+
.object({ systems: z.array(SystemEntry).prefault([]) })
|
|
731
793
|
.superRefine((cfg, ctx) => {
|
|
732
794
|
const seen = new Set();
|
|
733
795
|
const targets = new Map(); // "secret\0key" -> index of first ENABLED claimant
|
|
734
796
|
cfg.systems.forEach((sys, i) => {
|
|
735
797
|
if (seen.has(sys.name))
|
|
736
|
-
ctx.addIssue({ code:
|
|
798
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate system "${sys.name}"`, path: ["systems", i, "name"] });
|
|
737
799
|
seen.add(sys.name);
|
|
738
800
|
if (sys.deliverTo && DELIVER_DENYLIST.has(sys.deliverTo.k8sSecret)) {
|
|
739
|
-
ctx.addIssue({ code:
|
|
801
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `deliverTo.k8sSecret "${sys.deliverTo.k8sSecret}" is a PLATFORM credential surface — refused (红线1)`, path: ["systems", i, "deliverTo", "k8sSecret"] });
|
|
740
802
|
}
|
|
741
803
|
// Cross-system deliverTo collision (S3 设计 §4.5, council DESIGN-4): two enabled systems writing the
|
|
742
804
|
// same {secret,key} would silently overwrite each other (same SSA fieldManager). Save-time half; the
|
|
@@ -745,7 +807,7 @@ export const SystemsConfig = z
|
|
|
745
807
|
const t = `${sys.deliverTo.k8sSecret}\0${sys.deliverTo.key}`;
|
|
746
808
|
const prior = targets.get(t);
|
|
747
809
|
if (prior !== undefined) {
|
|
748
|
-
ctx.addIssue({ code:
|
|
810
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `deliverTo target "${sys.deliverTo.k8sSecret}/${sys.deliverTo.key}" already claimed by system "${cfg.systems[prior].name}" — one credential per delivery slot`, path: ["systems", i, "deliverTo"] });
|
|
749
811
|
}
|
|
750
812
|
else {
|
|
751
813
|
targets.set(t, i);
|
|
@@ -773,30 +835,28 @@ export const SystemStatusEntry = z.object({
|
|
|
773
835
|
reason: z.string().max(500).optional(),
|
|
774
836
|
updatedAt: z.string(),
|
|
775
837
|
});
|
|
776
|
-
// ──
|
|
777
|
-
// 0.6.0
|
|
778
|
-
//
|
|
779
|
-
//
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
costQuotaWindowSec: z.number().int().positive().optional(),
|
|
799
|
-
});
|
|
838
|
+
// ── retired 域(11.0.0)─────────────────────────────────────────────────────────────────────────────
|
|
839
|
+
// `runtime` 域retired:0.6.0 起它只剩五个限额键,而 `limits`(0.19.0)全覆盖那五个键 —— 同一个限额两处可发布、判据不同、「limits 赢」
|
|
840
|
+
// 只活在消费方的注里;0.5 期的 runtime 文档还带着治理三闸,读侧靠一只「governance 从未设过就抬升」的臂接住。11.0.0 两件都收:
|
|
841
|
+
// 五个限额键只在 `limits`,治理三闸只在 `governance`;存量由一次性迁移 SS-M1 改写(`./one-time-migrations`)。
|
|
842
|
+
/** retired 域的**墓碑表**:域名 → retired版本 / 事实今天住哪 / 改写存量的一次性迁移。读存储文档的每个读者同判(`parseDomain` 拒写、`buildEffective`
|
|
843
|
+
* 向店要retired 域、`readEffectiveWire` 看 wire、`FileConfigStore` 与 `readBundle` 读 `config.d/<名>.json`):文档**带事实**(在场且不是 `{}`)⇒ 响亮拒
|
|
844
|
+
* (`RetiredDomainError`,句子点名迁移 id);缺席或 `{}`(没有可丢的事实)⇒ 不理会。墓碑是拒,不是读法 —— 它不把旧文档读成任何值。 */
|
|
845
|
+
export const RETIRED_DOMAINS = {
|
|
846
|
+
runtime: {
|
|
847
|
+
retiredIn: "11.0.0",
|
|
848
|
+
now: "governance (autonomy / commandPolicy / approvalRequire) and limits (rateLimitPerMin / maxTaskCostUsd / maxTaskTokens / maxPrincipalCostUsd / costQuotaWindowSec)",
|
|
849
|
+
migration: "SS-M1",
|
|
850
|
+
},
|
|
851
|
+
};
|
|
852
|
+
/** 是不是一个retired 域名(墓碑表成员)。 */
|
|
853
|
+
export function isRetiredDomainName(x) {
|
|
854
|
+
return typeof x === "string" && Object.prototype.hasOwnProperty.call(RETIRED_DOMAINS, x);
|
|
855
|
+
}
|
|
856
|
+
/** 一个retired 域的拒句(读写两面同一句)。 */
|
|
857
|
+
export function retiredDomainSentence(domain) {
|
|
858
|
+
return retiredFormSentence(`the "${domain}" config domain`, RETIRED_DOMAINS[domain]);
|
|
859
|
+
}
|
|
800
860
|
// ── Domain: limits (cost/限流族的热更新发布位 — 0.19.0, 消费方 #322 批1) ──────
|
|
801
861
|
// 为什么是**新顶层域**而不是往 `runtime` 里加键(消费方亲验,不是推测):`RuntimeConfig` 是 zod 默认的
|
|
802
862
|
// `"strip"` 对象,`runtime` 文档里的未知键在两条腿上都被剥掉(实测 `{rateLimitPerMin:7,
|
|
@@ -805,10 +865,9 @@ export const RuntimeConfig = z.object({
|
|
|
805
865
|
// 域表是闭集)读不到**:`config.d/limits.json` 根本不进 effective,且零告警。本域 = 那个缺口的源头修复
|
|
806
866
|
// (CLI/桌面单机形正是热更新战役的主诉求形)。同族先例:governance 当年也是从 runtime 拆成独立顶层域。
|
|
807
867
|
//
|
|
808
|
-
//
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
// 折叠会让「旧位有值、新位撤键」这类过渡态在两个消费者之间产生两种答案。
|
|
868
|
+
// 五个限额键(rateLimitPerMin / maxTaskCostUsd / maxTaskTokens / maxPrincipalCostUsd / costQuotaWindowSec)11.0.0 起只住这里:
|
|
869
|
+
// 旧位 `runtime` 域已retired(墓碑见 {@link RETIRED_DOMAINS};存量由一次性迁移 SS-M1 搬进本域,本域已有该键则旧值丢弃 —— 那是当年消费方
|
|
870
|
+
// 「新位赢」的生效读法)。
|
|
812
871
|
//
|
|
813
872
|
// 判据分工(**宽进严出**,与既有域一致):本域只判**结构**——类型、有限数、数组/记录的元素类型。
|
|
814
873
|
// 业务域值(下限、`0` 关断哨兵、跨键不变量如「窗长必 > 0」「runStaleSec 必严格大于心跳」)的判据属主
|
|
@@ -828,7 +887,7 @@ export const InfraCostRates = z
|
|
|
828
887
|
})
|
|
829
888
|
.passthrough();
|
|
830
889
|
export const LimitsConfig = z.object({
|
|
831
|
-
// `.optional()`
|
|
890
|
+
// `.optional()` 无默认:ABSENT = 「web-admin 不管这一键」⇒ 消费方回自己的 env 底;
|
|
832
891
|
// 显式值(**包括 0**)= 发布了一个真值(0 多半是「关断哨兵」,语义归消费方)。给默认值 = 静默覆盖部署。
|
|
833
892
|
/** 每 principal 每分钟请求上限。 */
|
|
834
893
|
rateLimitPerMin: z.number().finite().optional(),
|
|
@@ -871,7 +930,7 @@ export const LimitsConfig = z.object({
|
|
|
871
930
|
// · 默认的 strip 会把这个窗口变成**键被静默剥掉 ⇒ 那条天花板悄悄回落到部署 env 底** —— 正是本域
|
|
872
931
|
// 要根治的那种病换了个位置复发。已知键照旧严判结构(坏值 ⇒ 本域 throw 档 ⇒ 消费方 fail-static
|
|
873
932
|
// 在上一份好配置);未知键原样承运,由消费方(键表在它手里)决定认或不认。
|
|
874
|
-
// · 附带收益:老编辑器(TOC)写回新
|
|
933
|
+
// · 附带收益:老编辑器(TOC)写回新 web-admin 的 limits 文档时不会把自己不认识的键写没了(strip 语义下
|
|
875
934
|
// writeDomainFile 会真的丢键)。
|
|
876
935
|
// · **承运 ≠ 沉默**(对抗复审 复审 round2 F2):未知成员会被 `"unknown-keys-carried"` 警告逐个点名
|
|
877
936
|
// (config-fns 的已知键表由本 schema 的 shape 单源派生)——拼错的键名于是有人说,而值仍然到得了
|
|
@@ -882,13 +941,13 @@ export const LimitsConfig = z.object({
|
|
|
882
941
|
// ── Domain: governance (治理三件 — split out of runtime, EXPERT-REDESIGN §5) ──────
|
|
883
942
|
// The governance trio an operator tunes via publish: autonomy ladder + command guardrails + approval-required
|
|
884
943
|
// tools. NOT下沉 — gets its own admin tab (E5 form; E1 ships a transitional generic editor). Same `.optional()`
|
|
885
|
-
// no-default discipline as the six gates: ABSENT =
|
|
944
|
+
// no-default discipline as the six gates: ABSENT = web-admin 不管这项(keep env / 消费者默认),present 才生效。
|
|
886
945
|
export const GovernanceConfig = z.object({
|
|
887
946
|
// autonomy:一个跨 knob 的安全总开关 / 放权梯度(普通用户敢用 + 专业用户放权)。
|
|
888
947
|
// read-only(只读规划,不落改)| ask(每个放权动作问)| plan(plan-review 门)| auto(放开)。
|
|
889
948
|
// ⚠️ 消费侧(service)把它**展开**到 core 已有三原语(handsReadOnly / shellGate+onAsk / plan_review),
|
|
890
949
|
// 经 core 的收紧-only `tightenTaskSpec` 合并。core 裁决:不在 core 加并行 autonomy 枚举(双实现陷阱),
|
|
891
|
-
// 不静默覆盖调用者已设安全字段(auto 模式尤其不能静默清门控)。详见
|
|
950
|
+
// 不静默覆盖调用者已设安全字段(auto 模式尤其不能静默清门控)。详见 web-admin docs/CONFIG-RESEARCH §10。
|
|
892
951
|
autonomy: z.enum(["read-only", "ask", "plan", "auto"]).optional(),
|
|
893
952
|
// commandPolicy:命令级 allow/ask/deny 护栏(remoteExec 管"在哪跑",这管"能跑什么命令")。每条 = 一个
|
|
894
953
|
// **精确 argv[0] 命令名** → 决策;**DENY-WINS**(冲突 deny 赢,与 core `combinePolicies` 一致,NOT last-match-wins)。
|
|
@@ -899,8 +958,7 @@ export const GovernanceConfig = z.object({
|
|
|
899
958
|
// ⚠️ COARSE / 非沙箱:只匹配 leading simple-command 名(经 core `parseLeadingCommandName` fail-closed 解析器);
|
|
900
959
|
// `$(...)` / `sh -c` / `env` / `xargs` / `find -exec` / `python -c` / 子命令都绕得过——是 egress/irreversibility
|
|
901
960
|
// 安全轴之下的**纵深防御,绝非唯一隔离**(真隔离靠 executionEnv sandbox)。消费侧(service)必须经
|
|
902
|
-
// `combinePolicies(部署默认, 本策略)` 编译进 `TaskSpec.toolPolicy
|
|
903
|
-
// "classify"` 下普通 ask 会被 irreversibility 提升为不可预算的 irreversible_ask。core 建
|
|
961
|
+
// `combinePolicies(部署默认, 本策略)` 编译进 `TaskSpec.toolPolicy`,绝不裸覆盖清掉部署级护栏。core 建
|
|
904
962
|
// `createCoarseCommandNamePolicy` 消费它(config-research §10)。
|
|
905
963
|
commandPolicy: z
|
|
906
964
|
.array(z.object({
|
|
@@ -915,13 +973,9 @@ export const GovernanceConfig = z.object({
|
|
|
915
973
|
decision: z.enum(["allow", "ask", "deny"]),
|
|
916
974
|
}))
|
|
917
975
|
.optional(),
|
|
918
|
-
/** Tool names needing operator approval (F4).
|
|
976
|
+
/** Tool names needing operator approval (F4). ABSENT = web-admin 不管;显式 `[]` = 一个都不需要审批。 */
|
|
919
977
|
approvalRequire: z.array(z.string()).optional(),
|
|
920
978
|
});
|
|
921
|
-
/** The governance trio keys. 0.7.1:写出方向的 runtime 双写镜像已撤(service 1.150.0 已切新位,
|
|
922
|
-
* 回执确认)——此常量今日唯一职责=READ 方向的 legacy lift(governance 域从未设时从旧 runtime 文档抬升,
|
|
923
|
-
* config-fns legacyGovernanceFromRuntime),单一键源防逐键漂移。 */
|
|
924
|
-
export const GOVERNANCE_MIRROR_KEYS = ["autonomy", "commandPolicy", "approvalRequire"];
|
|
925
979
|
// ── integrations domain: DELETED in 0.6.0 (EXPERT-REDESIGN §9 — 零消费者实锤:service 明确排除,OA/Gitea
|
|
926
980
|
// 实走 worker.env)。“按 scope connect GitHub” 是 backlog 的全新设计,不复用此形状。
|
|
927
981
|
// ── access domain: DELETED in 0.6.0 (EXPERT-REDESIGN §9 — 彻底删除不留兼容;唯一消费者 agent-client portal
|
|
@@ -933,25 +987,25 @@ export const GOVERNANCE_MIRROR_KEYS = ["autonomy", "commandPolicy", "approvalReq
|
|
|
933
987
|
// every principal's tier/budget to a worker), and `/effective?principal=` resolves the per-principal view
|
|
934
988
|
// (effective = worker-base ∩ tier⊕roles ∩ time-bounded overrides; intersection = either side's "deny" wins).
|
|
935
989
|
// Minimal records — never raw usage numbers (those live in the data plane / telemetry).
|
|
936
|
-
/** Token budget — LiteLLM-interop vocab +
|
|
990
|
+
/** Token budget — LiteLLM-interop vocab + web-admin-specific DEGRADE mode + a per-RUN admission gate. */
|
|
937
991
|
export const EntitlementBudget = z.object({
|
|
938
|
-
/** soft = alert only · hard = pre-call block · degrade = fall back to a cheaper model (
|
|
992
|
+
/** soft = alert only · hard = pre-call block · degrade = fall back to a cheaper model (web-admin-specific). */
|
|
939
993
|
mode: z.enum(["soft", "hard", "degrade"]).optional(),
|
|
940
994
|
// .finite() on every $/USD float: z.number().nonnegative() accepts Infinity, which JSON.stringify's to null on
|
|
941
995
|
// persist → a later strict reparse throws (fail-open on write, fail-hard on read). Same discipline as ModelCost.
|
|
942
996
|
maxBudgetUsd: z.number().finite().nonnegative().optional(),
|
|
943
997
|
budgetDuration: z.string().optional(), // e.g. "30d"
|
|
944
|
-
tpmLimit:
|
|
945
|
-
rpmLimit:
|
|
946
|
-
maxParallelRequests:
|
|
947
|
-
perModel: z.record(z.string(), z.object({ maxBudgetUsd: z.number().finite().nonnegative().optional(), tpmLimit:
|
|
998
|
+
tpmLimit: INTEGER.nonnegative().optional(),
|
|
999
|
+
rpmLimit: INTEGER.nonnegative().optional(),
|
|
1000
|
+
maxParallelRequests: INTEGER.nonnegative().optional(),
|
|
1001
|
+
perModel: z.record(z.string(), z.object({ maxBudgetUsd: z.number().finite().nonnegative().optional(), tpmLimit: INTEGER.nonnegative().optional(), rpmLimit: INTEGER.nonnegative().optional() })).optional(),
|
|
948
1002
|
/** Per-RUN admission gate (agentic-runaway guard, §3.3 🔴) — period budgets can't stop ONE runaway run. */
|
|
949
1003
|
maxRunCostUsd: z.number().finite().nonnegative().optional(),
|
|
950
|
-
maxIterations:
|
|
1004
|
+
maxIterations: INTEGER.positive().optional(),
|
|
951
1005
|
/** WHERE this budget is enforced (scheduling framework D4, 产品拍板 2026-07-04): `local` = worker-only
|
|
952
|
-
* (per-instance or shared-DB — today's posture) · `lease` = AP pre-authorized QuotaLease from
|
|
1006
|
+
* (per-instance or shared-DB — today's posture) · `lease` = AP pre-authorized QuotaLease from web-admin +
|
|
953
1007
|
* after-the-fact reconciliation (default for sema-managed fleets) · `central` = CP pre-call check against
|
|
954
|
-
*
|
|
1008
|
+
* web-admin (strict tenants; slow but exact). ABSENT = local. Orthogonal to `mode` (what happens on breach). */
|
|
955
1009
|
enforcement: z.enum(["local", "lease", "central"]).optional(),
|
|
956
1010
|
});
|
|
957
1011
|
/** Token 双窗额度(配额轴, EXPERT-REDESIGN §4 — Claude 式 5h 滚动窗 + 自然周窗),单位=加权 tokens
|
|
@@ -961,9 +1015,9 @@ export const EntitlementBudget = z.object({
|
|
|
961
1015
|
* 更严;tokenQuota 不参与 budget 的 min-of-budgets 归并。任一窗耗尽即拒(CC 式拒绝+恢复时间,E4)。 */
|
|
962
1016
|
export const TokenQuota = z.object({
|
|
963
1017
|
/** 5h 滚动窗额度(加权 tokens)。ABSENT = 此窗不设限。 */
|
|
964
|
-
fiveHourTokens:
|
|
1018
|
+
fiveHourTokens: INTEGER.nonnegative().optional(),
|
|
965
1019
|
/** 自然周窗额度(加权 tokens)。ABSENT = 此窗不设限。 */
|
|
966
|
-
weeklyTokens:
|
|
1020
|
+
weeklyTokens: INTEGER.nonnegative().optional(),
|
|
967
1021
|
});
|
|
968
1022
|
/** 预算组(共享钱池,§4-1)——与个人 budget 双轨并行:一笔花费计入个人轨 + 所有所属池;多池并存取最先
|
|
969
1023
|
* 耗尽;tokens 配额轴(TokenQuota)与它是两轨,先到顶者生效。成员条目带 `memberSince`:池只聚合入组时刻
|
|
@@ -982,60 +1036,60 @@ export const BudgetGroup = z.object({
|
|
|
982
1036
|
.object({
|
|
983
1037
|
/** instance-level group ids(registry `groups` 表——store 对象,zod 看不见,存在性同 groupBindings
|
|
984
1038
|
* 先例不做 schema 校验,dangling 由 UI 标红)。 */
|
|
985
|
-
groups: z.array(BudgetGroupMember.extend({ group: z.string().min(1) })).
|
|
986
|
-
principals: z.array(BudgetGroupMember.extend({ principal: z.string().min(1) })).
|
|
1039
|
+
groups: z.array(BudgetGroupMember.extend({ group: z.string().min(1) })).prefault([]),
|
|
1040
|
+
principals: z.array(BudgetGroupMember.extend({ principal: z.string().min(1) })).prefault([]),
|
|
987
1041
|
})
|
|
988
|
-
.
|
|
1042
|
+
.prefault({ groups: [], principals: [] }),
|
|
989
1043
|
});
|
|
990
1044
|
/** 生效场景分配(§7,与配额同一挂载心智):全局默认 + 按组 + 按用户三层;解析优先级=用户 > 组 > 全局默认
|
|
991
|
-
* > worker 级 runtime.scenario 兜底(引擎兜底行为待 ask② 确认)。`scenario` 是场景 NAME——可指
|
|
1045
|
+
* > worker 级 runtime.scenario 兜底(引擎兜底行为待 ask② 确认)。`scenario` 是场景 NAME——可指 web-admin
|
|
992
1046
|
* scenarios 域条目,也可指 service 内建场景名(内建不在域里,故**不做** cross-domain 存在性硬校验,
|
|
993
1047
|
* dangling 由 UI 标注)。 */
|
|
994
1048
|
export const ScenarioAssignments = z.object({
|
|
995
1049
|
/** 全局默认场景名。ABSENT = 不设全局默认(落到 worker 级 runtime.scenario / 引擎内建)。 */
|
|
996
1050
|
default: z.string().min(1).optional(),
|
|
997
|
-
groups: z.array(z.object({ group: z.string().min(1), scenario: z.string().min(1) })).
|
|
998
|
-
principals: z.array(z.object({ principal: z.string().min(1), scenario: z.string().min(1) })).
|
|
1051
|
+
groups: z.array(z.object({ group: z.string().min(1), scenario: z.string().min(1) })).prefault([]),
|
|
1052
|
+
principals: z.array(z.object({ principal: z.string().min(1), scenario: z.string().min(1) })).prefault([]),
|
|
999
1053
|
});
|
|
1000
1054
|
/** Org-forced-off runtime ceilings (gate②). ABSENT = no ceiling (local controls); `false` = org-disabled
|
|
1001
1055
|
* fleet-wide. Only ever pushes the "off" ceiling — never force-enables (monotone tighten-only). */
|
|
1002
1056
|
export const EntitlementRuntimeCaps = z.object({
|
|
1003
1057
|
allowBypassPermissions: z.boolean().optional(),
|
|
1004
1058
|
allowAutoMode: z.boolean().optional(),
|
|
1005
|
-
/** Per-principal governance of the workflow/ultracode capability (K-2, 产品拍板 2026-06-28).
|
|
1059
|
+
/** Per-principal governance of the workflow/ultracode capability (K-2, 产品拍板 2026-06-28). web-admin treats this as
|
|
1006
1060
|
* the "准不准" axis; the engine separately advertises "能不能" (Capabilities.workflows). The shell enables
|
|
1007
1061
|
* ultracode/workflows iff engine-capable ∧ principal-entitled. Like the others: ABSENT = no ceiling,
|
|
1008
1062
|
* `false` = org-disabled fleet-wide (tighten-only false-wins; never force-enables). Full server-side
|
|
1009
1063
|
* enforcement awaits core adding a per-principal authz axis that consumes this; until then it gates the shell UI. */
|
|
1010
1064
|
allowWorkflows: z.boolean().optional(),
|
|
1011
|
-
/** Per-principal governance of the Fork (fork-session) tool —
|
|
1065
|
+
/** Per-principal governance of the Fork (fork-session) tool — web-admin's "准不准" axis, mirroring allowWorkflows.
|
|
1012
1066
|
* The engine advertises "能不能" separately; a Fork is admitted iff engine-capable ∧ principal-entitled. Like the
|
|
1013
1067
|
* others: ABSENT = no ceiling (Fork allowed), `false` = org-disabled fleet-wide (tighten-only false-wins; never
|
|
1014
1068
|
* force-enables). core (RuntimeCaps.allowFork, prepare-task) consumes this per-principal: `allowFork === false`
|
|
1015
1069
|
* → Fork DENIED, else allowed. */
|
|
1016
1070
|
allowFork: z.boolean().optional(),
|
|
1017
1071
|
/** Per-principal governance of observer agents (core 1.270.0/1.273.0 OBS-2) —
|
|
1018
|
-
*
|
|
1072
|
+
* web-admin's "准不准" axis for the AgentDefinition.observer pairing surface. ⚠️ core's consumption polarity is
|
|
1019
1073
|
* INVERTED vs the other allow* caps: the engine is `=== true` explicit opt-in (default OFF — an undefined cap
|
|
1020
1074
|
* ships dark), so this key is the ENABLE line, not just a ceiling. Three-state semantics (ABSENT ≠ false):
|
|
1021
|
-
* ABSENT =
|
|
1075
|
+
* ABSENT = web-admin says nothing → key NOT emitted on the wire (core stays the SOLE default source; service's
|
|
1022
1076
|
* toCoreRuntimeCaps must not backfill a missing key into an explicit verdict — that would poison the
|
|
1023
1077
|
* env-baseline compose order); `true` = explicitly enable observer agents for this principal; `false` =
|
|
1024
|
-
* explicit org deny — overrides the node-level env baseline EXPERIMENTAL_OBSERVER_AGENTS (
|
|
1025
|
-
*
|
|
1078
|
+
* explicit org deny — overrides the node-level env baseline EXPERIMENTAL_OBSERVER_AGENTS (web-admin speaks →
|
|
1079
|
+
* web-admin wins). On compose across bundles, `false` (deny) still wins — see RUNTIME_CAP_STRICT; core's
|
|
1026
1080
|
* `=== true` opt-in check is orthogonal to that merge polarity. */
|
|
1027
1081
|
allowObservers: z.boolean().optional(),
|
|
1028
1082
|
/** Per-principal ARM switch for the auto-mode CLASSIFIER (core 1.276 批2 seam `RuntimeCaps.autoMode`, 板
|
|
1029
1083
|
* A surviving permission `ask` is routed to the small-model security classifier before any
|
|
1030
1084
|
* human/durable resolution). ⚠️ Same INVERTED consumption polarity as `allowObservers`: core is `=== true`
|
|
1031
1085
|
* explicit opt-in, default OFF — this key is the ENABLE line, not just a ceiling. Three-state semantics
|
|
1032
|
-
* (ABSENT ≠ false): ABSENT =
|
|
1086
|
+
* (ABSENT ≠ false): ABSENT = web-admin says nothing → key NOT emitted on the wire (core stays the SOLE default
|
|
1033
1087
|
* source — never backfill absence into an explicit verdict); `true` = explicitly arm the classifier for this
|
|
1034
1088
|
* principal (a no-op on a deployment without a wired classify leg — the capability gate lives in core);
|
|
1035
1089
|
* `false` = explicit org deny. On compose across bundles `false` (deny) wins — see RUNTIME_CAP_STRICT;
|
|
1036
1090
|
* core's `=== true` opt-in check is orthogonal to that merge polarity. DIVISION OF LABOR vs
|
|
1037
1091
|
* {@link allowAutoMode}: `allowAutoMode` is the 准入 may-I ceiling on the USER-facing auto permission mode
|
|
1038
|
-
* (shell/settings face, tighten-only); `autoMode` is
|
|
1092
|
+
* (shell/settings face, tighten-only); `autoMode` is web-admin's 部署 arm decision for the server-side
|
|
1039
1093
|
* classifier leg itself. */
|
|
1040
1094
|
autoMode: z.boolean().optional(),
|
|
1041
1095
|
allowUltracode: z.boolean().optional(),
|
|
@@ -1043,18 +1097,18 @@ export const EntitlementRuntimeCaps = z.object({
|
|
|
1043
1097
|
* (design/383 §3.1): may this principal declare `memory.capture: "off"` for a run? `false` = the deployment
|
|
1044
1098
|
* REQUIRES capture for this principal (compliance / knowledge retention) — the declaration is refused, terminal
|
|
1045
1099
|
* (`memory.capture_optout_denied`). `true` = explicitly no per-principal restriction. Three-state, ABSENT ≠ false:
|
|
1046
|
-
* ABSENT =
|
|
1100
|
+
* ABSENT = web-admin says nothing → key NOT emitted on the wire (never coined into a verdict). The consumer's
|
|
1047
1101
|
* fault-transit contract for THIS member runs opposite to the `{allowWorkflows:false}` degrade habit: a resolver
|
|
1048
1102
|
* FAULT must surface as ABSENT (or a throw), never as a stand-in `false` — on the privacy axis "deny" is the
|
|
1049
1103
|
* IRREVERSIBLE side (capturing a session the user said not to capture), so the default/`open` posture reads a
|
|
1050
1104
|
* fault as "opt-out honoured + named disclosure", and a `governed` posture reads it as "verdict missing ⇒
|
|
1051
|
-
* refuse the run". The consumer composes this key from its own local grant store FIRST and
|
|
1105
|
+
* refuse the run". The consumer composes this key from its own local grant store FIRST and web-admin value
|
|
1052
1106
|
* second. Compose polarity across bundles is the allow* family's: `false` (capture required) wins — see
|
|
1053
|
-
* RUNTIME_CAP_STRICT; the consumer's posture arms are orthogonal to that merge. Consumer-side:
|
|
1107
|
+
* RUNTIME_CAP_STRICT; the consumer's posture arms are orthogonal to that merge. Consumer-side: web-admin value
|
|
1054
1108
|
* reaches the engine caps only once the consumer's cap mapping picks this member up; until then its local grant
|
|
1055
1109
|
* store is the sole verdict source (the key parses and is carried on the wire from this version on). */
|
|
1056
1110
|
allowMemoryOptOut: z.boolean().optional(),
|
|
1057
|
-
/** Force this principal's run onto durable-approval (per-tool gate) —
|
|
1111
|
+
/** Force this principal's run onto durable-approval (per-tool gate) — web-admin's fleet-wide handle for CC-faithful
|
|
1058
1112
|
* per-tool interception (K-3, core confirmed it consumes this). Polarity is INVERTED vs the allow* caps:
|
|
1059
1113
|
* here `true` is the STRICT/tightening value (forcing the gate ON), so it wins on compose — see RUNTIME_CAP_STRICT.
|
|
1060
1114
|
* ABSENT = no fleet override (local/default resolution decides). Server-side enforcement = core's per-principal axis. */
|
|
@@ -1106,7 +1160,7 @@ export const EntitlementOverride = z
|
|
|
1106
1160
|
key: z.string().min(1),
|
|
1107
1161
|
/** The override's value — its SHAPE follows its `key`, so it is parse-transparent here and validated by
|
|
1108
1162
|
* whoever enforces that key: {@link PARSE_TRANSPARENT_KEYS} (`entitlement.subjects[].overrides[].value`). */
|
|
1109
|
-
value: z.unknown(),
|
|
1163
|
+
value: z.unknown().optional(),
|
|
1110
1164
|
effectiveFrom: z.string().optional(),
|
|
1111
1165
|
effectiveUntil: z.string().optional(),
|
|
1112
1166
|
approvedBy: z.string().min(1),
|
|
@@ -1118,15 +1172,15 @@ export const EntitlementOverride = z
|
|
|
1118
1172
|
// by env-NAME, never inline. (Budget overrides are numbers/enums, so this never fires on the v1 use.)
|
|
1119
1173
|
const s = typeof o.value === "string" ? o.value : o.value == null ? "" : JSON.stringify(o.value);
|
|
1120
1174
|
if (s && containsSecretToken(s))
|
|
1121
|
-
ctx.addIssue({ code:
|
|
1175
|
+
ctx.addIssue({ code: "custom", message: "override.value appears to contain a secret token — overrides are persisted + audited; reference secrets by env-NAME, never inline a value", path: ["value"] });
|
|
1122
1176
|
});
|
|
1123
1177
|
export const EntitlementSubject = z.object({
|
|
1124
1178
|
principal: z.string().min(1),
|
|
1125
1179
|
displayName: z.string().optional(),
|
|
1126
1180
|
tierId: z.string().optional(),
|
|
1127
1181
|
/** Additional tier ids composed on top of `tierId` (union of allows, min of budgets) — the "role combo". */
|
|
1128
|
-
roles: z.array(z.string()).
|
|
1129
|
-
overrides: z.array(EntitlementOverride).
|
|
1182
|
+
roles: z.array(z.string()).prefault([]),
|
|
1183
|
+
overrides: z.array(EntitlementOverride).prefault([]),
|
|
1130
1184
|
/** 用户层 token 双窗额度(最具体层,压过组与全局默认——极性见 {@link TokenQuota})。 */
|
|
1131
1185
|
tokenQuota: TokenQuota.optional(),
|
|
1132
1186
|
});
|
|
@@ -1141,7 +1195,7 @@ export const EntitlementGroupBinding = z.object({
|
|
|
1141
1195
|
group: z.string().min(1),
|
|
1142
1196
|
tierId: z.string().optional(),
|
|
1143
1197
|
/** Additional tier ids composed on top of `tierId` — same "role combo" shape as EntitlementSubject. */
|
|
1144
|
-
roles: z.array(z.string()).
|
|
1198
|
+
roles: z.array(z.string()).prefault([]),
|
|
1145
1199
|
/** 组层 token 双窗额度(同层多组命中取更严;被用户层压过——极性见 {@link TokenQuota})。 */
|
|
1146
1200
|
tokenQuota: TokenQuota.optional(),
|
|
1147
1201
|
});
|
|
@@ -1158,12 +1212,12 @@ export const EntitlementConfig = z
|
|
|
1158
1212
|
tokenQuota: TokenQuota.optional(),
|
|
1159
1213
|
})
|
|
1160
1214
|
.optional(),
|
|
1161
|
-
tiers: z.array(EntitlementTier).
|
|
1162
|
-
subjects: z.array(EntitlementSubject).
|
|
1215
|
+
tiers: z.array(EntitlementTier).prefault([]),
|
|
1216
|
+
subjects: z.array(EntitlementSubject).prefault([]),
|
|
1163
1217
|
/** Group bindings (W4, additive 0.4.0) — bind an instance-level GROUP to a tier ⊕ roles combo. */
|
|
1164
|
-
groupBindings: z.array(EntitlementGroupBinding).
|
|
1218
|
+
groupBindings: z.array(EntitlementGroupBinding).prefault([]),
|
|
1165
1219
|
/** 预算组(共享钱池,0.6.0 §4)——见 {@link BudgetGroup}。 */
|
|
1166
|
-
budgetGroups: z.array(BudgetGroup).
|
|
1220
|
+
budgetGroups: z.array(BudgetGroup).prefault([]),
|
|
1167
1221
|
/** 生效场景分配(0.6.0 §7)——见 {@link ScenarioAssignments}。 */
|
|
1168
1222
|
scenarioAssignments: ScenarioAssignments.optional(),
|
|
1169
1223
|
})
|
|
@@ -1171,53 +1225,53 @@ export const EntitlementConfig = z
|
|
|
1171
1225
|
const tierIds = new Set();
|
|
1172
1226
|
cfg.tiers.forEach((t, i) => {
|
|
1173
1227
|
if (tierIds.has(t.id))
|
|
1174
|
-
ctx.addIssue({ code:
|
|
1228
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate tier id "${t.id}"`, path: ["tiers", i, "id"] });
|
|
1175
1229
|
tierIds.add(t.id);
|
|
1176
1230
|
});
|
|
1177
1231
|
const seenPools = new Set();
|
|
1178
1232
|
cfg.budgetGroups.forEach((b, i) => {
|
|
1179
1233
|
if (seenPools.has(b.id))
|
|
1180
|
-
ctx.addIssue({ code:
|
|
1234
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate budget group id "${b.id}"`, path: ["budgetGroups", i, "id"] });
|
|
1181
1235
|
seenPools.add(b.id);
|
|
1182
1236
|
});
|
|
1183
1237
|
const seenGroups = new Set();
|
|
1184
1238
|
cfg.groupBindings.forEach((g, i) => {
|
|
1185
1239
|
if (seenGroups.has(g.group))
|
|
1186
|
-
ctx.addIssue({ code:
|
|
1240
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate group binding "${g.group}"`, path: ["groupBindings", i, "group"] });
|
|
1187
1241
|
seenGroups.add(g.group);
|
|
1188
1242
|
if (g.tierId && !tierIds.has(g.tierId))
|
|
1189
|
-
ctx.addIssue({ code:
|
|
1243
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `group binding "${g.group}" → unknown tier "${g.tierId}"`, path: ["groupBindings", i, "tierId"] });
|
|
1190
1244
|
g.roles.forEach((r, j) => { if (!tierIds.has(r))
|
|
1191
|
-
ctx.addIssue({ code:
|
|
1245
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `group binding "${g.group}" → unknown role/tier "${r}"`, path: ["groupBindings", i, "roles", j] }); });
|
|
1192
1246
|
});
|
|
1193
1247
|
const seen = new Set();
|
|
1194
1248
|
cfg.subjects.forEach((s, i) => {
|
|
1195
1249
|
if (seen.has(s.principal))
|
|
1196
|
-
ctx.addIssue({ code:
|
|
1250
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate subject principal "${s.principal}"`, path: ["subjects", i, "principal"] });
|
|
1197
1251
|
seen.add(s.principal);
|
|
1198
1252
|
if (s.tierId && !tierIds.has(s.tierId))
|
|
1199
|
-
ctx.addIssue({ code:
|
|
1253
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `subject "${s.principal}" → unknown tier "${s.tierId}"`, path: ["subjects", i, "tierId"] });
|
|
1200
1254
|
s.roles.forEach((r, j) => { if (!tierIds.has(r))
|
|
1201
|
-
ctx.addIssue({ code:
|
|
1255
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `subject "${s.principal}" → unknown role/tier "${r}"`, path: ["subjects", i, "roles", j] }); });
|
|
1202
1256
|
s.overrides.forEach((o, j) => { if (o.principal !== s.principal)
|
|
1203
|
-
ctx.addIssue({ code:
|
|
1257
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `override.principal "${o.principal}" must equal subject "${s.principal}"`, path: ["subjects", i, "overrides", j, "principal"] }); });
|
|
1204
1258
|
});
|
|
1205
1259
|
});
|
|
1206
|
-
/** design/170 件A:`?principal=` caps wire 上的 org 记忆解析段(
|
|
1207
|
-
* 段级三值语义:**键整体缺席**=
|
|
1260
|
+
/** design/170 件A:`?principal=` caps wire 上的 org 记忆解析段(web-admin 铸、server 消费,两侧共此同源锚)。
|
|
1261
|
+
* 段级三值语义:**键整体缺席**=web-admin 不支持该段(能力握手,消费方按「取不到」瞬时处理,不判终局);
|
|
1208
1262
|
* **`scopes: {}` 空表**=取数成功的负结果(该 principal 零 org 授权,终局);**有表**=逐条目判。
|
|
1209
1263
|
* `gen`=单调世代号(publish 模式=globalRevision,live 模式=store version;消费方拒退挂此,防线为
|
|
1210
|
-
* 进程生命周期内的 LB 陈旧副本——重启后无高水位=接受
|
|
1264
|
+
* 进程生命周期内的 LB 陈旧副本——重启后无高水位=接受 web-admin 当刻值,web-admin 是真源)。 */
|
|
1211
1265
|
export const PrincipalOrgMemoryWire = z
|
|
1212
1266
|
.object({
|
|
1213
|
-
gen:
|
|
1267
|
+
gen: INTEGER.nonnegative(),
|
|
1214
1268
|
scopes: z.record(z.string().regex(/^org:\S+$/, "orgMemory scope must be an `org:` scope string"), z.object({ write: z.boolean().optional() }).strict()),
|
|
1215
1269
|
})
|
|
1216
1270
|
.strict();
|
|
1217
|
-
/** `?principal=` caps wire **整体**的读边界 schema(
|
|
1271
|
+
/** `?principal=` caps wire **整体**的读边界 schema(web-admin 铸、server 消费同源锚;产品令 2026-08-05:
|
|
1218
1272
|
* 网络边界读入必 schema,禁裸 as-cast)。**逐面容错极性是裁定过的语义**,严格化会翻转极性的面在此
|
|
1219
1273
|
* 显式保持宽读(每面注明所依裁定),不许硬转严:
|
|
1220
|
-
* - `runtimeCaps`/`budget`(治理面,fail-closed):严格校验已知键 + **passthrough 未知键**——新
|
|
1274
|
+
* - `runtimeCaps`/`budget`(治理面,fail-closed):严格校验已知键 + **passthrough 未知键**——新 web-admin
|
|
1221
1275
|
* 的新 cap 键必须原样透传给 core(老 server 剥键=静默放松治理);值漂移 ⇒ 整体 parse 失败 ⇒ 消费端
|
|
1222
1276
|
* fail-close(「outage 绝不静默 GRANT」的既裁方向)。
|
|
1223
1277
|
* - `scenario`/`allowlist`/`execution`(能力面,fail-open,[565]/[580] 裁定):保持 `unknown` 宽读,
|
|
@@ -1268,18 +1322,15 @@ export const ActorAssertionWire = z
|
|
|
1268
1322
|
/** sessionMirror TARGET url 的底线传输校验(0.10.1;0.10.2 加 trim 前置):**https,或 loopback http**
|
|
1269
1323
|
* (本机第二引擎/dev)。与壳 `evaluateEngineRouting`(shell-host engineTarget.ts,S1 路由闸)语义对齐:
|
|
1270
1324
|
* 镜像上行带会话内容+Bearer 凭证,非 loopback 明文 http = 凭证与会话数据裸奔——**写侧 schema 层就拒**
|
|
1271
|
-
* (fail-loud 比读侧执法更早;
|
|
1272
|
-
* {@link NON_SECRET_URL}(禁 userinfo 凭证/禁 query/fragment 夹带 secret-shaped 值)。loopback
|
|
1273
|
-
*
|
|
1274
|
-
*
|
|
1325
|
+
* (fail-loud 比读侧执法更早;web-admin 编辑面存不进去,壳读侧闸只兜手改存储的漏网)。基线复用
|
|
1326
|
+
* {@link NON_SECRET_URL}(禁 userinfo 凭证/禁 query/fragment 夹带 secret-shaped 值)。loopback 判定 = 鉴权放宽那一只
|
|
1327
|
+
* {@link isLoopbackAuthHost}(11.0.0 起;明文 http 带凭据是放宽,窄是安全方向 —— 壳的同一判定改读同一只)。
|
|
1328
|
+
* **trim 前置(0.10.2,附B 三单合评审 M-1)**:前导/尾随空白先归一再验且存储值即 trim
|
|
1275
1329
|
* 后值——否则 " https://…" 写侧收(new URL 容忍前导空白)而壳读侧逐字符比对拒 = 全员镜像静默 degraded。 */
|
|
1276
1330
|
export const SESSION_MIRROR_ENGINE_URL = z.string().trim().pipe(NON_SECRET_URL).refine((s) => {
|
|
1277
1331
|
try {
|
|
1278
1332
|
const u = new URL(s);
|
|
1279
|
-
|
|
1280
|
-
return true;
|
|
1281
|
-
const h = u.hostname;
|
|
1282
|
-
return h === "127.0.0.1" || h === "localhost" || h === "::1" || h === "[::1]";
|
|
1333
|
+
return u.protocol === "https:" || isLoopbackAuthHost(u.hostname);
|
|
1283
1334
|
}
|
|
1284
1335
|
catch {
|
|
1285
1336
|
return true; // already passed NON_SECRET_URL; unreachable in practice
|
|
@@ -1298,7 +1349,7 @@ export const SESSION_MIRROR_ENGINE_URL = z.string().trim().pipe(NON_SECRET_URL).
|
|
|
1298
1349
|
* - `required:true` = **org 治理强制**:会话镜像默认开且不可关,且**本地覆盖不生效——既不能改目标
|
|
1299
1350
|
* 也不能关闭(org 压一切;附B 三单合评审 MAJOR-1 勘误:治理态下本地 --target 完全被忽略)**
|
|
1300
1351
|
* (审计场景;壳复用 CC managed-settings 锁定报错串——S3 execution 沙箱锁完全同构先例)。
|
|
1301
|
-
* 整个 sessionMirror **optional,缺省 INERT**(无下发=老
|
|
1352
|
+
* 整个 sessionMirror **optional,缺省 INERT**(无下发=老 web-admin/老壳/0.8-0.10 存量 execution 文档
|
|
1302
1353
|
* 零行为差、零迁移)。⚠️ ENFORCEMENT HONESTY 同域头注:required 的锁今天也是 CLIENT-SIDE 执法。
|
|
1303
1354
|
* 开放演化:v2 可能加 audit 字段类(保留窗/脱敏面等)——本对象刻意**不 .strict()**(zod 默认 strip
|
|
1304
1355
|
* 未知键:v2 文档被 v1 消费者读到=未知键剥除,forward-compatible;加字段恒 additive patch)。 */
|
|
@@ -1309,7 +1360,7 @@ export const SessionMirrorPolicy = z.object({
|
|
|
1309
1360
|
required: z.boolean().default(false),
|
|
1310
1361
|
});
|
|
1311
1362
|
/** 执行域 sealed-box 加密公钥登记(0.10.5,密钥托管线;挂 execution 域=「执行环境的属性」循
|
|
1312
|
-
* sessionMirror 收编先例,零新域)。
|
|
1363
|
+
* sessionMirror 收编先例,零新域)。web-admin **只登记公钥**——私钥由执行侧(service worker/引擎)
|
|
1313
1364
|
* 生成并持有,永不上行;浏览器端用这里的公钥 crypto_box_seal 加密模型 key(见 {@link SealedApiKey})。
|
|
1314
1365
|
* X25519 公钥恒 32 字节 → base64 恒 43 字符 + "=",regex 钉死(错长度=根本不是 X25519 公钥,写侧
|
|
1315
1366
|
* fail-loud 比执行侧解密失败早得多)。 */
|
|
@@ -1318,11 +1369,11 @@ export const ExecutionPublicKey = z.object({
|
|
|
1318
1369
|
publicKeyId: z.string().min(1).max(128),
|
|
1319
1370
|
/** X25519 公钥,base64(32 字节 → 恒 44 字符含 padding)。 */
|
|
1320
1371
|
publicKey: z.string().regex(/^[A-Za-z0-9+/]{43}=$/, "publicKey must be a base64 X25519 public key (32 bytes)"),
|
|
1321
|
-
/** 登记时刻(ISO)。
|
|
1372
|
+
/** 登记时刻(ISO)。web-admin 写面 server 端盖章(projects.registeredAt 同款纪律)。 */
|
|
1322
1373
|
createdAt: z.string().min(1),
|
|
1323
1374
|
notes: z.string().optional(),
|
|
1324
1375
|
/** 归属 worker 标识(0.10.9,service 合意)。**取值 = 上报方 worker 的
|
|
1325
|
-
* `
|
|
1376
|
+
* `SEMA_WEB_ADMIN_WORKER` env**(service 上报载荷带 `workerId`;该 env 未配 = 单 worker 部署,
|
|
1326
1377
|
* 上报省略本字段)。多 worker 部署下 seal 侧凭它按**目标 worker** 选钥(A worker 的密文只有
|
|
1327
1378
|
* A 的私钥能解——选错钥 = 执行侧解密失败,晚且难查);ABSENT = 未绑定,seal 侧 fallback
|
|
1328
1379
|
* newest(仅适合单 worker 部署)。optional additive:存量登记条目零迁移。 */
|
|
@@ -1336,8 +1387,8 @@ export const ExecutionPolicy = z.object({
|
|
|
1336
1387
|
* must not be a BREAKING schema change (promised to the service). The closed set of lanes a worker can
|
|
1337
1388
|
* declare is `EXECUTION_LANE_WORDS` (`execution-lanes.ts`, 7.0.0) — read it there, not from a copy in this
|
|
1338
1389
|
* comment. Unknown strings are consumer-defined. */
|
|
1339
|
-
allowedLanes: z.array(z.string().min(1)).
|
|
1340
|
-
/** Org 默认会话镜像目标(0.10.1,附B 两层)。ABSENT = INERT(
|
|
1390
|
+
allowedLanes: z.array(z.string().min(1)).prefault(["host"]),
|
|
1391
|
+
/** Org 默认会话镜像目标(0.10.1,附B 两层)。ABSENT = INERT(web-admin 未管镜像,壳解析序里本腿为空)。
|
|
1341
1392
|
* 读损档随域(DOMAIN_READ_FALLBACK.execution="throw" 已覆盖:损坏绝不静默退默认放权)。 */
|
|
1342
1393
|
sessionMirror: SessionMirrorPolicy.optional(),
|
|
1343
1394
|
/** 执行域 sealed-box 公钥登记表(0.10.5)。**optional 无 default,ABSENT = 未登记**(0.8–0.10 存量
|
|
@@ -1349,7 +1400,7 @@ export const ExecutionPolicy = z.object({
|
|
|
1349
1400
|
const seen = new Set();
|
|
1350
1401
|
(p.publicKeys ?? []).forEach((k, i) => {
|
|
1351
1402
|
if (seen.has(k.publicKeyId))
|
|
1352
|
-
ctx.addIssue({ code:
|
|
1403
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate publicKeyId "${k.publicKeyId}"`, path: ["publicKeys", i, "publicKeyId"] });
|
|
1353
1404
|
seen.add(k.publicKeyId);
|
|
1354
1405
|
});
|
|
1355
1406
|
});
|
|
@@ -1358,11 +1409,11 @@ export const ExecutionPolicy = z.object({
|
|
|
1358
1409
|
// 登记簿(索引/展示/审计)+ 无仓 project 的 projectId 铸造权威。铸造离线自由、使用时注册执法
|
|
1359
1410
|
// 佐证:执法位在 service worker(派生 `proj:*` 可读 scope 前查 `(tenant,projectId)`
|
|
1360
1411
|
// 已注册),本域只是它查的那张表——登记簿**不读 marker 文件、不镜像 marker schema**(已确认)。
|
|
1361
|
-
// 复合键 `(tenant, projectId)` 的 tenant 半边由
|
|
1412
|
+
// 复合键 `(tenant, projectId)` 的 tenant 半边由 web-admin 的 SCOPE 轴承载(api/scopes.ts:每 scope 一份
|
|
1362
1413
|
// 独立域文档),域内 Record 键=projectId only;fork 后同一 projectId 合法存在于两个 scope,互不相见。
|
|
1363
1414
|
/** projectId 形状:lowercase 规范 UUID。**宽读严写**(core 口径):读面/claim 收 generic
|
|
1364
1415
|
* UUID(外部/历史铸造的 v4 也认——marker 是身份不是格式声明);铸造侧(core formatProjectMarker /
|
|
1365
|
-
*
|
|
1416
|
+
* web-admin mint 端点)统一产 **uuidv7**。36 字符,天然满足 design/142 §1.2 段编码 64 上限。
|
|
1366
1417
|
* SINGLE-SOURCE 纪律:本常量是引擎那条正则的**自持镜像**(本仓不带引擎运行时依赖轴),对拍走 dev-only
|
|
1367
1418
|
* conformance(never a runtime import,EffectiveKey/coreTypes 先例)。
|
|
1368
1419
|
* 2.0.0:drift-lock 已建(`test/upstream-drift.test.ts` ③)—— pattern 必须与引擎逐字相同,唯一成文的差是
|
|
@@ -1382,23 +1433,23 @@ export const ProjectRegistration = z.object({
|
|
|
1382
1433
|
.max(512)
|
|
1383
1434
|
.refine((s) => !/\s/.test(s) && !s.includes("://"), "must be a NORMALIZED remote (host/path — no scheme, no whitespace)"))
|
|
1384
1435
|
.max(16)
|
|
1385
|
-
.
|
|
1436
|
+
.prefault([]),
|
|
1386
1437
|
/** 该项目会话的默认记忆 scope 词表。OPEN value domain — z.string(),不 enum(纪律:加词
|
|
1387
1438
|
* 不得 BREAKING)。方向(S4 细化):消费方=service memoryScope 派生(作 TaskSpec.
|
|
1388
1439
|
* memory.scopes 种子),形态=v2 scope 键字符串(过 core parseScopeKey 校验;known 词根:`proj` /
|
|
1389
1440
|
* `userproj`)。未知串=消费者自定义;空数组=不下发默认。本域只登记不解释,零耦合。 */
|
|
1390
|
-
defaultScopes: z.array(z.string().min(1)).
|
|
1391
|
-
/** 登记人 principal(审计锚;
|
|
1441
|
+
defaultScopes: z.array(z.string().min(1)).prefault([]),
|
|
1442
|
+
/** 登记人 principal(审计锚;web-admin 写面自动填 caller,不信客户端提交)。 */
|
|
1392
1443
|
registeredBy: z.string().min(1),
|
|
1393
|
-
/** 登记时刻(ISO)。
|
|
1444
|
+
/** 登记时刻(ISO)。web-admin 写面 server 端盖章。 */
|
|
1394
1445
|
registeredAt: z.string().min(1),
|
|
1395
1446
|
notes: z.string().optional(),
|
|
1396
1447
|
});
|
|
1397
1448
|
export const ProjectsConfig = z
|
|
1398
1449
|
.object({
|
|
1399
|
-
/** 键=projectId({@link PROJECT_ID_REGEX},generic lowercase UUID——宽读);tenant 半边=
|
|
1450
|
+
/** 键=projectId({@link PROJECT_ID_REGEX},generic lowercase UUID——宽读);tenant 半边=web-admin
|
|
1400
1451
|
* scope 轴(不进域文档,见域头注)。 */
|
|
1401
|
-
projects: z.record(z.string().regex(PROJECT_ID_REGEX, "projectId must be a lowercase canonical UUID"), ProjectRegistration).
|
|
1452
|
+
projects: z.record(z.string().regex(PROJECT_ID_REGEX, "projectId must be a lowercase canonical UUID"), ProjectRegistration).prefault({}),
|
|
1402
1453
|
})
|
|
1403
1454
|
.superRefine((cfg, ctx) => {
|
|
1404
1455
|
// 同 scope 内一条 remote 只许属于一个 project——「remote → projectId」找回阶梯(design/142
|
|
@@ -1409,7 +1460,7 @@ export const ProjectsConfig = z
|
|
|
1409
1460
|
for (const [i, r] of reg.gitRemotes.entries()) {
|
|
1410
1461
|
const prev = seen.get(r);
|
|
1411
1462
|
if (prev !== undefined && prev !== pid) {
|
|
1412
|
-
ctx.addIssue({ code:
|
|
1463
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `remote "${r}" already registered to project "${prev}"`, path: ["projects", pid, "gitRemotes", i] });
|
|
1413
1464
|
}
|
|
1414
1465
|
seen.set(r, pid);
|
|
1415
1466
|
}
|
|
@@ -1420,7 +1471,7 @@ export const ProjectsConfig = z
|
|
|
1420
1471
|
// Unified shape = slots × topology × rounds × finalizer. COMPLETELY separate from rosters (拍板): a slot
|
|
1421
1472
|
// member's `modelRef` points at the models CATALOG (`ModelEntry.name`, the key — NOT the gateway modelId,
|
|
1422
1473
|
// NOT a roster); roster churn never touches a template. Execution lives service-side ("collab spec →
|
|
1423
|
-
// workflow/team run" translation layer);
|
|
1474
|
+
// workflow/team run" translation layer); web-admin owns the template CONFIG only.
|
|
1424
1475
|
export const CollabTopology = z.enum(["star", "ring", "layered", "pair", "blackboard"]);
|
|
1425
1476
|
export const CollabFinalizerType = z.enum(["judge", "vote", "fuse", "verify", "summarize", "none"]);
|
|
1426
1477
|
export const CollabEarlyStop = z.enum(["consensus", "stability", "judge_call", "verify_pass"]);
|
|
@@ -1444,7 +1495,7 @@ const CollabSlot = z.object({
|
|
|
1444
1495
|
rolePrompt: z.string().max(2000).optional(),
|
|
1445
1496
|
/** 1 = exactly one member; "N" = the slot accepts multiple members (异构评审团). */
|
|
1446
1497
|
cardinality: z.union([z.literal(1), z.literal("N")]).default(1),
|
|
1447
|
-
members: z.array(CollabMember).
|
|
1498
|
+
members: z.array(CollabMember).prefault([]),
|
|
1448
1499
|
/** Tool names available to this slot's members (the old team `scenario` binding migrates here). */
|
|
1449
1500
|
tools: z.array(z.string().min(1)).optional(),
|
|
1450
1501
|
});
|
|
@@ -1457,7 +1508,7 @@ const CollabTemplate = z.object({
|
|
|
1457
1508
|
slots: z.array(CollabSlot).min(1),
|
|
1458
1509
|
/** `max` is ALWAYS the safety cap (blackboard topology: event/round cap — required, 防跑飞);
|
|
1459
1510
|
* `earlyStop: "verify_pass"` carries the architect pair's "verified → stop" loop. */
|
|
1460
|
-
rounds: z.object({ max:
|
|
1511
|
+
rounds: z.object({ max: INTEGER.positive(), earlyStop: CollabEarlyStop.optional() }),
|
|
1461
1512
|
finalizer: z.object({
|
|
1462
1513
|
type: CollabFinalizerType,
|
|
1463
1514
|
/** Slot id the finalizer anchors to (e.g. the judge slot). Meaningless for type "none". */
|
|
@@ -1468,10 +1519,10 @@ const CollabTemplate = z.object({
|
|
|
1468
1519
|
kind: z.enum(["transcript", "blackboard"]).default("transcript"),
|
|
1469
1520
|
persistence: CollabPersistence.default("session"),
|
|
1470
1521
|
})
|
|
1471
|
-
.
|
|
1522
|
+
.prefault({}),
|
|
1472
1523
|
/** 多模型烧钱必须有闸 (拍板): the budget object is REQUIRED and at least one axis must be set. */
|
|
1473
1524
|
budget: z.object({
|
|
1474
|
-
maxTokens:
|
|
1525
|
+
maxTokens: INTEGER.positive().optional(),
|
|
1475
1526
|
maxCost: z.number().finite().positive().optional(),
|
|
1476
1527
|
}),
|
|
1477
1528
|
enabled: z.boolean().default(true),
|
|
@@ -1480,34 +1531,34 @@ const CollabTemplate = z.object({
|
|
|
1480
1531
|
const slotIds = new Set();
|
|
1481
1532
|
t.slots.forEach((s, i) => {
|
|
1482
1533
|
if (slotIds.has(s.id))
|
|
1483
|
-
ctx.addIssue({ code:
|
|
1534
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate slot id "${s.id}"`, path: ["slots", i, "id"] });
|
|
1484
1535
|
slotIds.add(s.id);
|
|
1485
1536
|
if (s.cardinality === 1 && s.members.length > 1)
|
|
1486
|
-
ctx.addIssue({ code:
|
|
1537
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `slot "${s.id}" has cardinality 1 but ${s.members.length} members`, path: ["slots", i, "members"] });
|
|
1487
1538
|
});
|
|
1488
1539
|
if (t.finalizer.type === "none") {
|
|
1489
1540
|
if (t.finalizer.slot !== undefined)
|
|
1490
|
-
ctx.addIssue({ code:
|
|
1541
|
+
ctx.addIssue({ code: "custom", message: `finalizer type "none" must not anchor a slot`, path: ["finalizer", "slot"] });
|
|
1491
1542
|
}
|
|
1492
1543
|
else if (t.finalizer.slot !== undefined && !slotIds.has(t.finalizer.slot)) {
|
|
1493
|
-
ctx.addIssue({ code:
|
|
1544
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `finalizer.slot "${t.finalizer.slot}" is not a slot id of this template`, path: ["finalizer", "slot"] });
|
|
1494
1545
|
}
|
|
1495
1546
|
if (t.budget.maxTokens === undefined && t.budget.maxCost === undefined) {
|
|
1496
|
-
ctx.addIssue({ code:
|
|
1547
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `template "${t.id}": budget needs at least one of maxTokens/maxCost (多模型烧钱必须有闸)`, path: ["budget"] });
|
|
1497
1548
|
}
|
|
1498
1549
|
});
|
|
1499
|
-
export const CollabConfig = z.object({ templates: z.array(CollabTemplate).
|
|
1550
|
+
export const CollabConfig = z.object({ templates: z.array(CollabTemplate).prefault([]) }).superRefine((cfg, ctx) => {
|
|
1500
1551
|
// `id` is the template's reference key — dedup like every other keyed domain (two same-id templates
|
|
1501
1552
|
// shipping → the resolver silently takes the first; same failure class the old teams dedup fixed).
|
|
1502
1553
|
const seen = new Set();
|
|
1503
1554
|
cfg.templates.forEach((t, i) => {
|
|
1504
1555
|
if (seen.has(t.id))
|
|
1505
|
-
ctx.addIssue({ code:
|
|
1556
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate collab template "${t.id}" — the template key must be unique`, path: ["templates", i, "id"] });
|
|
1506
1557
|
seen.add(t.id);
|
|
1507
1558
|
});
|
|
1508
1559
|
});
|
|
1509
1560
|
// ── Domain: workers (声明式编排 — service design/22, the orchestrator design note) ──────
|
|
1510
|
-
// 每个 worker = 一个 worker 服务实例的声明(一份 env profile)。
|
|
1561
|
+
// 每个 worker = 一个 worker 服务实例的声明(一份 env profile)。web-admin 存 DESIRED(逻辑参数
|
|
1511
1562
|
// + secret 的 env-NAME 引用,**绝不存 secret 值**);一个 per-host reconciler 拉取后在宿主机本地解析
|
|
1512
1563
|
// secret → 渲染 docker compose → up/down 收敛。worker spec ↔ service env 的精确映射见 worker-env.ts。
|
|
1513
1564
|
//
|
|
@@ -1520,7 +1571,7 @@ const DURATION = z.string().regex(/^\d+(s|m|h|d)$/, 'duration like "30m", "2h",
|
|
|
1520
1571
|
// 生命周期 (design/25 §3 决策 3/4)。owner/purpose 用于配额 + 级联清理 + 审计;dependsOn 防级联雪崩。
|
|
1521
1572
|
export const WorkerLifecycle = z.object({
|
|
1522
1573
|
kind: z.enum(["persistent", "ephemeral"]).default("persistent"),
|
|
1523
|
-
/** ephemeral: 绝对存活时长,自
|
|
1574
|
+
/** ephemeral: 绝对存活时长,自 web-admin 首次见到起算(防时钟漂移),到期硬移除。 */
|
|
1524
1575
|
ttl: DURATION.optional(),
|
|
1525
1576
|
/** 无活动多久 → 告警(MVP 只告警,不自动 disable)。数据源 = 反代 metrics。 */
|
|
1526
1577
|
idleTimeout: DURATION.optional(),
|
|
@@ -1530,6 +1581,9 @@ export const WorkerLifecycle = z.object({
|
|
|
1530
1581
|
/** 依赖的其它 worker name —— sweeper 移除前查依赖,不孤儿化仍被依赖的 worker。 */
|
|
1531
1582
|
dependsOn: z.array(z.string().min(1)).optional(),
|
|
1532
1583
|
});
|
|
1584
|
+
/** 7.0.0 删除的 worker 字段(6.x 存储形)→ 它的新位。墓碑字段写在每个旧键原来所在的对象里({@link retiredKey}):在场即拒、拒句点名
|
|
1585
|
+
* 一次性迁移 SS-M4 —— 不让一行旧数据以「什么都没声明」的样子过闸(zod 会静默剥掉不认识的键)。 */
|
|
1586
|
+
const sixXField = (now) => ({ retiredIn: "7.0.0", now, migration: "SS-M4" });
|
|
1533
1587
|
const WorkerSecretRefs = z.object({
|
|
1534
1588
|
/** NAME of the host env var holding the model API key → service MODEL_API_KEY (或 anthropic 时 ANTHROPIC_API_KEY). */
|
|
1535
1589
|
apiKeyEnv: ENV_NAME,
|
|
@@ -1540,6 +1594,7 @@ const WorkerSecretRefs = z.object({
|
|
|
1540
1594
|
/** 7.0.0:SQL 店的口令 → `MYSQL_PASSWORD` / `PG_PASSWORD`(渲染器按 `runtime.store.backend` 选名族)。
|
|
1541
1595
|
* 在场 ⇔ `runtime.store.backend` ∈ {mysql, pg}(WorkerSpec 层判,与 `runtime.store.db` 同一条规则)。取代 6.x 的 `tidbPasswordEnv`。 */
|
|
1542
1596
|
dbPasswordEnv: ENV_NAME.optional(),
|
|
1597
|
+
tidbPasswordEnv: retiredKey("secretRefs.tidbPasswordEnv", sixXField("secretRefs.dbPasswordEnv")),
|
|
1543
1598
|
});
|
|
1544
1599
|
/**
|
|
1545
1600
|
* 每个**保留** env 名 → WorkerSpec 里产出它的那个结构字段(唯一写位)。`env` / `secretEnv` 逃生口拒这些名,拒因指路到这里写的字段
|
|
@@ -1552,12 +1607,26 @@ const WorkerSecretRefs = z.object({
|
|
|
1552
1607
|
* 7.0.0 增删(M-R2 结构块):
|
|
1553
1608
|
* · **加** `REMOTE_EXEC` / `REQUIRE_EXECUTION_LANE` + 车道表({@link EXECUTION_LANES})里**全部**车道 env 名 —— 车道有了结构位
|
|
1554
1609
|
* `runtime.execution`,逃生口再能设车道就是两把尺。这是对 0.1.30「执行基座键不保留」的**反转**:当年不保留是因为没有结构位
|
|
1555
|
-
* (活舰队 direct = k8s、canary = e2b 只能在 `worker.env` 里设);存量行由一次性迁移
|
|
1610
|
+
* (活舰队 direct = k8s、canary = e2b 只能在 `worker.env` 里设);存量行由一次性迁移 SS-M4 搬进结构块。
|
|
1556
1611
|
* 别的域也读的名(`MINIO_*`、`LOG_LEVEL`、`REMOTE_EXEC_HOME_DIR`)不在车道表里,照旧不保留。
|
|
1557
1612
|
* · **加** `DB_BACKEND` / `MEMORY_ENGINE` / `MEMORY_ENGINE_BACKEND` —— `runtime.store` 产出它们。
|
|
1558
|
-
* · **删** `MEMORY_BACKEND`(引擎从不读的死名,渲染器不再产出)与 `TIDB_*` 族(引擎的墓碑名,见之拒启;结构字段不再产出它们
|
|
1559
|
-
*
|
|
1613
|
+
* · **删** `MEMORY_BACKEND`(引擎从不读的死名,渲染器不再产出)与 `TIDB_*` 族(引擎的墓碑名,见之拒启;结构字段不再产出它们)。
|
|
1614
|
+
* MYSQL_* / PG_* 两族归 `runtime.store.db` + `secretRefs.dbPasswordEnv`。
|
|
1615
|
+
*
|
|
1616
|
+
* `env` / `secretEnv` 的写闸问**两张表**,各答一个问题:本表答「这个现名归哪个结构字段」(拒句指路到那个字段);墓碑表 `RETIRED_ENVS`
|
|
1617
|
+
* (`./retired-envs`)答「这个旧名已退役」(拒句就是墓碑句 `retiredEnvRefusal` 本身,引擎起服、壳、web-admin 读同一张表同一句)。墓碑名不进本表。
|
|
1618
|
+
*
|
|
1619
|
+
* 11.0.0:本表与 `RETIRED_ENVS` 的旧名集合零交集 —— `ANTHROPIC_BASEURL`(墓碑名)换成现名 `ANTHROPIC_BASE_URL`;渲染器写的 web-admin 连接三键
|
|
1620
|
+
* 入表,同 fleet 成员身份两键的「渲染器写、不可配」。**规则一条:渲染器写的名字是保留名**(运维不可在 `env` / `secretEnv` 里自写)。改名窗里渲染器
|
|
1621
|
+
* 按目标引擎版本写名,对还读上一代名的引擎写的是 `SEMA_REGISTRY_URL` / `SEMA_REGISTRY_WORKER` / `SEMA_REGISTRY_TOKEN`,所以这三名与现名三键同一组、
|
|
1622
|
+
* 同一句拒(指路同一处)。老三名在引擎换读现名的那一版随之搬去墓碑表(同一提交);保留表与墓碑表零交集的钉会逼这一步。
|
|
1560
1623
|
*/
|
|
1624
|
+
/** web-admin 连接三键的唯一写位(渲染器;没有 WorkerSpec 字段)—— 现名与上一代名指路同一句。 */
|
|
1625
|
+
const WEB_ADMIN_CONNECTION_SEAT = {
|
|
1626
|
+
url: "(set by the config renderer from the web-admin base URL — not configurable)",
|
|
1627
|
+
worker: "(set by the config renderer from the worker's name — not configurable)",
|
|
1628
|
+
token: "(set by the config renderer from the worker's pull token — not configurable)",
|
|
1629
|
+
};
|
|
1561
1630
|
const RESERVED_ENV_FIELD = Object.freeze({
|
|
1562
1631
|
PORT: "(fixed at 8090 — not configurable)", MODEL_PROVIDER: "model.provider", MODEL_ID: "model.modelId",
|
|
1563
1632
|
MODEL_REASONING: "model.reasoning", MODEL_CHEAP_ID: "model.cheapId", MODEL_MAX_TOKENS: "model.maxTokens",
|
|
@@ -1565,7 +1634,7 @@ const RESERVED_ENV_FIELD = Object.freeze({
|
|
|
1565
1634
|
MODEL_COST_CACHE_READ: "model.cost.cacheRead", MODEL_COST_CACHE_WRITE: "model.cost.cacheWrite",
|
|
1566
1635
|
MAX_PRINCIPAL_COST_USD: "runtime.maxPrincipalCostUsd", REQUIRE_PRINCIPAL: "runtime.requirePrincipal", RATE_LIMIT_RPM: "runtime.rateLimitRpm",
|
|
1567
1636
|
DEFAULT_SCENARIO: "runtime.scenario", GIT_API_BASEURL: "coords.gitApiBaseUrl", MODEL_GATEWAY_BASEURL: "model.gatewayBaseUrl",
|
|
1568
|
-
|
|
1637
|
+
ANTHROPIC_BASE_URL: "model.gatewayBaseUrl", MODEL_API_KEY: "secretRefs.apiKeyEnv", ANTHROPIC_API_KEY: "secretRefs.apiKeyEnv",
|
|
1569
1638
|
SERVICE_AUTH_TOKEN: "secretRefs.serviceAuthTokenEnv", GIT_API_TOKEN: "secretRefs.gitTokenEnv",
|
|
1570
1639
|
// ── runtime.store(7.0.0)
|
|
1571
1640
|
DB_BACKEND: "runtime.store.backend", SESSION_BACKEND: "runtime.store.session",
|
|
@@ -1579,6 +1648,11 @@ const RESERVED_ENV_FIELD = Object.freeze({
|
|
|
1579
1648
|
// ── fleet 成员身份两键:渲染器按该 worker 的代际写 generation(引擎读回并回声成 EngineSignal.generation)、按该 worker 令牌绑定的 scope 写 scope(引擎读作自身身份的 scope)—— 没有 WorkerSpec 字段能写它们
|
|
1580
1649
|
[FLEET_GENERATION_ENV]: "(set by the config renderer from the worker's generation — not configurable)",
|
|
1581
1650
|
[FLEET_SCOPE_ENV]: "(set by the config renderer from the worker's scope — not configurable)",
|
|
1651
|
+
// ── web-admin 连接三键:渲染器给每台 worker 写 web-admin 根 URL、worker 名与该 worker 的拉取令牌 —— 没有 WorkerSpec 字段能写它们;
|
|
1652
|
+
// 现名与渲染器对老引擎写的上一代名同一组(见表头注)
|
|
1653
|
+
[WEB_ADMIN_ENV.url]: WEB_ADMIN_CONNECTION_SEAT.url, SEMA_REGISTRY_URL: WEB_ADMIN_CONNECTION_SEAT.url,
|
|
1654
|
+
[WEB_ADMIN_ENV.worker]: WEB_ADMIN_CONNECTION_SEAT.worker, SEMA_REGISTRY_WORKER: WEB_ADMIN_CONNECTION_SEAT.worker,
|
|
1655
|
+
[WEB_ADMIN_ENV.token]: WEB_ADMIN_CONNECTION_SEAT.token, SEMA_REGISTRY_TOKEN: WEB_ADMIN_CONNECTION_SEAT.token,
|
|
1582
1656
|
REQUIRE_EXECUTION_LANE: "runtime.execution.lane (the renderer emits REQUIRE_EXECUTION_LANE=true for every declared lane)",
|
|
1583
1657
|
...Object.fromEntries([...LANE_ENV_OWNER].map(([name, owner]) => [name, laneEnvSeat(name, owner)])),
|
|
1584
1658
|
});
|
|
@@ -1588,14 +1662,20 @@ export const RESERVED_WORKER_ENV = new Set(Object.keys(RESERVED_ENV_FIELD));
|
|
|
1588
1662
|
/**
|
|
1589
1663
|
* 保留名被 `env` / `secretEnv` 逃生口设置时的拒因:点名键 + 它的唯一写位。句尾恒是「set it via <字段>」(下游按它抽归属字段,不动)。
|
|
1590
1664
|
* **迁移指路只有一条规则(7.0.1)**:这个键「旧形 ⇒ 迁移能搬」({@link isMigratableEnvKey},与迁移自己识别的键集同一只谓词)就在键名后
|
|
1591
|
-
*
|
|
1665
|
+
* 带同一句迁移指路(一次性迁移 SS-M4,`migrationSignpost`);搬不了的保留名(`PORT` / `MODEL_*` / `MYSQL_*` …)不带 —— 指一条跑了也没用的迁移比不指更糟。
|
|
1592
1666
|
*/
|
|
1593
|
-
function
|
|
1594
|
-
const signpost = isMigratableEnvKey(key) ?
|
|
1595
|
-
return `${slot} cannot set reserved key "${key}"${signpost} — set it via ${RESERVED_ENV_FIELD[key]}`;
|
|
1667
|
+
function reservedKeyIssue(slot, key) {
|
|
1668
|
+
const signpost = isMigratableEnvKey(key) ? " (" + migrationSignpost("SS-M4", "7.0.0") + ")" : "";
|
|
1669
|
+
return buildQuotedIssue `${fixed(slot)} cannot set reserved key "${key}"${fixed(signpost)} — set it via ${fixed(RESERVED_ENV_FIELD[key])}`;
|
|
1670
|
+
}
|
|
1671
|
+
/** 逃生口里有墓碑表的旧名 ⇒ 一条 issue,句子是墓碑句本身(`retiredEnvRefusal` —— 唯一铸点,本包不另拼;句里只有表里的名,不回显值)。 */
|
|
1672
|
+
function retiredEnvIssue(rec, ctx) {
|
|
1673
|
+
const refusal = retiredEnvRefusal(rec);
|
|
1674
|
+
if (refusal !== undefined)
|
|
1675
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `${fixed(refusal)}` });
|
|
1596
1676
|
}
|
|
1597
1677
|
// ── 7.0.0 M-R2:`runtime.execution` / `runtime.store` 两个结构块 ─────────────────────────────────────────────
|
|
1598
|
-
// 词表只有一个正本(`execution-lanes.ts` 的导出表):本包写闸(`WorkerSpec.safeParse`)
|
|
1678
|
+
// 词表只有一个正本(`execution-lanes.ts` 的导出表):本包写闸(`WorkerSpec.safeParse`)、web-admin 渲染器、引擎起服三处读同一张表。
|
|
1599
1679
|
// 拒因都在 zod issue 里(`path` 指到要改的字段、`message` 带出路)—— schema 本身是唯一判据,不另导出拒因函数。
|
|
1600
1680
|
/** 一个非密字面值(车道坐标 / SQL 坐标):≤2048,不像密钥(与 `env` 逃生口同一条判据 —— 密钥走 env-NAME 引用)。 */
|
|
1601
1681
|
const nonSecretLiteral = (minLength) => z
|
|
@@ -1613,47 +1693,47 @@ const nonSecretLiteral = (minLength) => z
|
|
|
1613
1693
|
*/
|
|
1614
1694
|
const ExecutionBlock = z
|
|
1615
1695
|
.object({
|
|
1616
|
-
lane: z.enum(EXECUTION_LANE_WORDS),
|
|
1617
|
-
coords: z.record(ENV_NAME, nonSecretLiteral(0)).
|
|
1618
|
-
secretRefs: z.record(ENV_NAME, ENV_NAME).
|
|
1696
|
+
lane: z.enum(EXECUTION_LANE_WORDS, { error: (iss) => retiredLaneWordRefusal(iss.input) }), // a retired lane word gets its way out
|
|
1697
|
+
coords: z.record(ENV_NAME, nonSecretLiteral(0)).prefault({}),
|
|
1698
|
+
secretRefs: z.record(ENV_NAME, ENV_NAME).prefault({}),
|
|
1619
1699
|
})
|
|
1620
1700
|
.superRefine((x, ctx) => {
|
|
1621
1701
|
const row = laneRowOf(x.lane);
|
|
1622
|
-
const issue = (slot, key,
|
|
1702
|
+
const issue = (slot, key, q) => ctx.addIssue({ code: "custom", path: [slot, key], ...q });
|
|
1623
1703
|
const owned = (secret) => (secret ? [...row.requiredSecretEnv, ...row.optionalSecretEnv] : [...row.requiredEnv, ...row.optionalEnv]);
|
|
1624
1704
|
const judgeKey = (slot, key) => {
|
|
1625
1705
|
const wantSecret = slot === "secretRefs";
|
|
1626
1706
|
const owner = LANE_ENV_OWNER.get(key);
|
|
1627
1707
|
if (owner === undefined) {
|
|
1628
1708
|
const allowed = owned(wantSecret);
|
|
1629
|
-
issue(slot, key, `${key} is not ${wantSecret ? "a secret" : "a coordinate"} of any execution lane — lane "${x.lane}" takes ${slot} ${allowed.length > 0 ? allowed.join(", ") : "(none)"}`);
|
|
1709
|
+
issue(slot, key, buildQuotedIssue `${key} is not ${fixed(wantSecret ? "a secret" : "a coordinate")} of any execution lane — lane "${x.lane}" takes ${fixed(slot)} ${fixed(allowed.length > 0 ? allowed.join(", ") : "(none)")}`);
|
|
1630
1710
|
return false;
|
|
1631
1711
|
}
|
|
1632
1712
|
if (owner.lane !== x.lane) {
|
|
1633
|
-
issue(slot, key, `${key} belongs to lane "${owner.lane}", not "${x.lane}" — remove it, or set runtime.execution.lane to "${owner.lane}"`);
|
|
1713
|
+
issue(slot, key, buildQuotedIssue `${key} belongs to lane "${fixed(owner.lane)}", not "${x.lane}" — remove it, or set runtime.execution.lane to "${fixed(owner.lane)}"`);
|
|
1634
1714
|
return false;
|
|
1635
1715
|
}
|
|
1636
1716
|
if (owner.secret !== wantSecret) {
|
|
1637
1717
|
issue(slot, key, owner.secret
|
|
1638
|
-
? `${key} is a secret of lane "${x.lane}" — put it in runtime.execution.secretRefs as an env-NAME (never a value in coords)`
|
|
1639
|
-
: `${key} is a non-secret coordinate of lane "${x.lane}" — write its value in runtime.execution.coords`);
|
|
1718
|
+
? buildQuotedIssue `${key} is a secret of lane "${x.lane}" — put it in runtime.execution.secretRefs as an env-NAME (never a value in coords)`
|
|
1719
|
+
: buildQuotedIssue `${key} is a non-secret coordinate of lane "${x.lane}" — write its value in runtime.execution.coords`);
|
|
1640
1720
|
return false;
|
|
1641
1721
|
}
|
|
1642
1722
|
return true;
|
|
1643
1723
|
};
|
|
1644
1724
|
for (const [key, value] of Object.entries(x.coords)) {
|
|
1645
1725
|
if (judgeKey("coords", key) && value === "" && LANE_ENV_OWNER.get(key)?.required === true) {
|
|
1646
|
-
issue("coords", key, `required coordinate ${key} of lane "${x.lane}" is empty — the engine reads an empty value as unset and refuses to start`);
|
|
1726
|
+
issue("coords", key, buildQuotedIssue `required coordinate ${key} of lane "${x.lane}" is empty — the engine reads an empty value as unset and refuses to start`);
|
|
1647
1727
|
}
|
|
1648
1728
|
}
|
|
1649
1729
|
for (const key of Object.keys(x.secretRefs))
|
|
1650
1730
|
judgeKey("secretRefs", key);
|
|
1651
1731
|
for (const key of row.requiredEnv)
|
|
1652
1732
|
if (!Object.hasOwn(x.coords, key))
|
|
1653
|
-
issue("coords", key, `lane "${x.lane}" requires coords.${key} (the engine refuses to start without it)`);
|
|
1733
|
+
issue("coords", key, buildQuotedIssue `lane "${x.lane}" requires coords.${key} (the engine refuses to start without it)`);
|
|
1654
1734
|
for (const key of row.requiredSecretEnv)
|
|
1655
1735
|
if (!Object.hasOwn(x.secretRefs, key))
|
|
1656
|
-
issue("secretRefs", key, `lane "${x.lane}" requires secretRefs.${key} — the host env-NAME holding it`);
|
|
1736
|
+
issue("secretRefs", key, buildQuotedIssue `lane "${x.lane}" requires secretRefs.${key} — the host env-NAME holding it`);
|
|
1657
1737
|
});
|
|
1658
1738
|
/** `MEMORY_ENGINE` 的两个词(引擎目录行的 `enumValues`;引擎还认 true/false/1/0… 两族同义词,结构位只收规范词)。 */
|
|
1659
1739
|
const MEMORY_ENGINE_SWITCH = ["on", "off"];
|
|
@@ -1675,7 +1755,7 @@ const StoreBlock = z.object({
|
|
|
1675
1755
|
engine: z.enum(MEMORY_ENGINE_SWITCH).default("on"),
|
|
1676
1756
|
backend: z.enum(MEMORY_ENGINE_BACKENDS).default("file"),
|
|
1677
1757
|
})
|
|
1678
|
-
.
|
|
1758
|
+
.prefault({}),
|
|
1679
1759
|
/** SQL 服务器坐标(中立,一份;名族由 `backend` 决定)。 */
|
|
1680
1760
|
db: z
|
|
1681
1761
|
.object({
|
|
@@ -1686,40 +1766,8 @@ const StoreBlock = z.object({
|
|
|
1686
1766
|
})
|
|
1687
1767
|
.optional(),
|
|
1688
1768
|
});
|
|
1689
|
-
/**
|
|
1690
|
-
*
|
|
1691
|
-
const RETIRED_WORKER_FIELDS = [
|
|
1692
|
-
{ parent: "runtime", key: "sessionBackend", now: "runtime.store.session" },
|
|
1693
|
-
{ parent: "runtime", key: "memoryBackend", now: "runtime.store.memory — the old field never reached the engine" },
|
|
1694
|
-
{ parent: "runtime", key: "db", now: "runtime.store.db.database" },
|
|
1695
|
-
{ parent: "coords", key: "tidbHost", now: "runtime.store.db.host" },
|
|
1696
|
-
{ parent: "coords", key: "tidbPort", now: "runtime.store.db.port" },
|
|
1697
|
-
{ parent: "coords", key: "tidbUser", now: "runtime.store.db.user" },
|
|
1698
|
-
{ parent: "secretRefs", key: "tidbPasswordEnv", now: "secretRefs.dbPasswordEnv" },
|
|
1699
|
-
];
|
|
1700
|
-
/** 一个对象的**自有数据属性**值(不触发访问器;不是普通对象 ⇒ undefined)。 */
|
|
1701
|
-
function ownDataValue(obj, key) {
|
|
1702
|
-
if (obj === null || typeof obj !== "object" || Array.isArray(obj))
|
|
1703
|
-
return undefined;
|
|
1704
|
-
const d = Object.getOwnPropertyDescriptor(obj, key);
|
|
1705
|
-
return d !== undefined && "value" in d ? d.value : undefined;
|
|
1706
|
-
}
|
|
1707
|
-
function retiredWorkerFieldIssues(raw, ctx) {
|
|
1708
|
-
for (const { parent, key, now } of RETIRED_WORKER_FIELDS) {
|
|
1709
|
-
const container = ownDataValue(raw, parent);
|
|
1710
|
-
if (container !== null && typeof container === "object" && !Array.isArray(container) && Object.hasOwn(container, key)) {
|
|
1711
|
-
ctx.addIssue({
|
|
1712
|
-
code: z.ZodIssueCode.custom,
|
|
1713
|
-
path: [parent, key],
|
|
1714
|
-
message: `${parent}.${key} was removed in 7.0.0 (now ${now}) — ${MIGRATION_SIGNPOST}`,
|
|
1715
|
-
});
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
return raw;
|
|
1719
|
-
}
|
|
1720
|
-
/** 一台 worker 的名字(唯一 → 容器名 / compose project / 缺省路由键)—— **唯一**的名字判据。中心在租约 / 用量 /
|
|
1721
|
-
* discovery `?worker=` 等面上校验外部传来的 worker 名时读它(不抄窄正则)。7.0.0 起单独导出:`WorkerSpec` 不再是普通
|
|
1722
|
-
* 对象 schema(预处理 + 跨字段判据),`WorkerSpec.shape.name` 不复存在。 */
|
|
1769
|
+
/** 一台 worker 的名字(唯一 → 容器名 / compose project / 缺省路由键)—— **唯一**的名字判据。web-admin 在租约 / 用量 /
|
|
1770
|
+
* discovery `?worker=` 等面上校验外部传来的 worker 名时读它(不抄窄正则)。 */
|
|
1723
1771
|
export const WORKER_NAME = z.string().min(1).regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters/digits/-, no spaces");
|
|
1724
1772
|
const WorkerSpecObject = z.object({
|
|
1725
1773
|
/** 唯一 → 容器名 / compose project. */
|
|
@@ -1739,7 +1787,7 @@ const WorkerSpecObject = z.object({
|
|
|
1739
1787
|
* Hash-INCLUDED → a change re-applies the Deployment (k8s scales IN PLACE via SSA, no pod destruction). The
|
|
1740
1788
|
* docker backend is single-container: it warns and runs 1 if this is >1. Capped at 100 (resource-exhaustion
|
|
1741
1789
|
* guard — a typo'd 1e6 mustn't try to schedule a million pods; raise the cap if a real workload needs it). */
|
|
1742
|
-
replicas:
|
|
1790
|
+
replicas: INTEGER.positive().max(100).optional(),
|
|
1743
1791
|
/** AUTOSCALING (k8s backend, HPA) — declare bounds and let the cluster's HorizontalPodAutoscaler own the
|
|
1744
1792
|
* replica count (autoscaling/v2, CPU utilization; prove-before-scale 解禁 2026-06-13). Mutually exclusive
|
|
1745
1793
|
* with `replicas` (WorkersConfig rejects both — two owners of the same knob WILL fight). The orchestrator
|
|
@@ -1750,10 +1798,10 @@ const WorkerSpecObject = z.object({
|
|
|
1750
1798
|
* single-container, warns + ignores. Hash-INCLUDED; absent → no HPA, today's behaviour (zero churn). */
|
|
1751
1799
|
autoscaling: z
|
|
1752
1800
|
.object({
|
|
1753
|
-
minReplicas:
|
|
1754
|
-
maxReplicas:
|
|
1801
|
+
minReplicas: INTEGER.positive().max(100),
|
|
1802
|
+
maxReplicas: INTEGER.positive().max(100),
|
|
1755
1803
|
/** Target average CPU utilization %, vs resources.requests.cpu. Absent → backend default 80. */
|
|
1756
|
-
targetCpuPercent:
|
|
1804
|
+
targetCpuPercent: INTEGER.min(10).max(95).optional(),
|
|
1757
1805
|
})
|
|
1758
1806
|
.refine((a) => a.maxReplicas >= a.minReplicas, { message: "maxReplicas must be >= minReplicas" })
|
|
1759
1807
|
.optional(),
|
|
@@ -1770,19 +1818,19 @@ const WorkerSpecObject = z.object({
|
|
|
1770
1818
|
* hash-INCLUDED like every spec field — absent → contract defaults (zero churn on existing fleets). */
|
|
1771
1819
|
fleet: z
|
|
1772
1820
|
.object({
|
|
1773
|
-
/** Per-worker dispatch policy consumed by
|
|
1821
|
+
/** Per-worker dispatch policy consumed by web-admin `/api/dispatch` (D3: the gateway hop is optional;
|
|
1774
1822
|
* only affects traffic routed through it). Absent → contract defaults (round-robin, affinity on). */
|
|
1775
1823
|
dispatch: DispatchPolicy.optional(),
|
|
1776
1824
|
/** D1 per-worker drain-grace override → delivered to the container as env `DRAIN_GRACE_MS`
|
|
1777
1825
|
* (worker-side default 600s, floor 10s). */
|
|
1778
|
-
drainGraceMs:
|
|
1826
|
+
drainGraceMs: INTEGER.positive().optional(),
|
|
1779
1827
|
})
|
|
1780
1828
|
.optional(),
|
|
1781
1829
|
// `model` DELETED in 0.7.0 (EXPERT-REDESIGN §3 拍板 + E3 迁移闭环 — a2 存量 5 worker 已迁移目录+roster
|
|
1782
1830
|
// 标记,E3):模型/网关配置单一真源=models 目录,worker 经 `roster` 引用;主/cheap boot env
|
|
1783
1831
|
// 归宿=roster.primaryModel/cheapModel 标记(0.6.3,service 已承接)。pre-0.7 存量 JSON 里的
|
|
1784
1832
|
// `model` 键被 parse 静默剥除(zod strips unknown)→ workerSpecHash 变化=一次性重建;迁移工具输入=
|
|
1785
|
-
//
|
|
1833
|
+
// 冻结的旧 worker.model 形(0.5 → 0.7 迁移工具,11.0.0 随 `./migrate` 出口删除),必须在升级 0.7 前跑完(a2 已闭环,E3)。
|
|
1786
1834
|
/** 运行时 (非 secret). 映射到 service REQUIRE_PRINCIPAL / RATE_LIMIT_RPM / … 与两个结构块(7.0.0)。 */
|
|
1787
1835
|
runtime: z.object({
|
|
1788
1836
|
/** 7.0.0 执行车道块(见 `ExecutionBlock`)。enabled 的 worker 必填(WorkerSpec 层判);停用的可缺。 */
|
|
@@ -1791,52 +1839,61 @@ const WorkerSpecObject = z.object({
|
|
|
1791
1839
|
store: StoreBlock.optional(),
|
|
1792
1840
|
scenario: z.string().optional(), // DEFAULT_SCENARIO
|
|
1793
1841
|
requirePrincipal: z.boolean().default(false),
|
|
1794
|
-
rateLimitRpm:
|
|
1842
|
+
rateLimitRpm: INTEGER.nonnegative().default(0),
|
|
1795
1843
|
/** Per-principal cumulative cost CAP (USD) → MAX_PRINCIPAL_COST_USD → the service's cost gate ENFORCEMENT
|
|
1796
1844
|
* (over-cap → 429). Pairs with model.cost (which only ATTRIBUTES cost). ABSENT → no env → the service
|
|
1797
1845
|
* defaults to 0 = off (today's behaviour); EXPLICIT 0 emits "0" = block-all and is HASH-DISTINCT from
|
|
1798
1846
|
* absent (so the guard is `!= null`, not `> 0` — the two carry different caller intent). Optional + NO
|
|
1799
1847
|
* default → absent never materialises → no spec-hash churn. Hash-included (read at boot → cap change recreates). */
|
|
1800
1848
|
maxPrincipalCostUsd: z.number().finite().nonnegative().optional(),
|
|
1849
|
+
sessionBackend: retiredKey("runtime.sessionBackend", sixXField("runtime.store.session")),
|
|
1850
|
+
memoryBackend: retiredKey("runtime.memoryBackend", sixXField("runtime.store.memory — the old field never reached the engine")),
|
|
1851
|
+
db: retiredKey("runtime.db", sixXField("runtime.store.db.database")),
|
|
1801
1852
|
}),
|
|
1802
|
-
/** 物理坐标 (非 secret). 映射到 GIT_API_BASEURL。7.0.0 起 SQL 坐标在 `runtime.store.db`(`tidb*`
|
|
1853
|
+
/** 物理坐标 (非 secret). 映射到 GIT_API_BASEURL。7.0.0 起 SQL 坐标在 `runtime.store.db`(`tidb*` 三键删除,墓碑在下)。 */
|
|
1803
1854
|
coords: z
|
|
1804
1855
|
.object({
|
|
1805
1856
|
gitApiBaseUrl: NON_SECRET_URL.optional().or(z.literal("")),
|
|
1857
|
+
tidbHost: retiredKey("coords.tidbHost", sixXField("runtime.store.db.host")),
|
|
1858
|
+
tidbPort: retiredKey("coords.tidbPort", sixXField("runtime.store.db.port")),
|
|
1859
|
+
tidbUser: retiredKey("coords.tidbUser", sixXField("runtime.store.db.user")),
|
|
1806
1860
|
})
|
|
1807
|
-
.
|
|
1861
|
+
.prefault({}),
|
|
1808
1862
|
secretRefs: WorkerSecretRefs,
|
|
1809
1863
|
/**
|
|
1810
1864
|
* 通用 env 透传(逃生口)——给**没有专属结构化字段**的 service env 用,让任意 worker 声明自己的 app 专属变量
|
|
1811
|
-
*
|
|
1865
|
+
* (如某个应用的 `MY_APP_BASEURL`),不必为每个变量改契约。值是**字面非密值**(≤2048,且拒绝明显的密钥串——密钥走
|
|
1812
1866
|
* `secretEnv`)。**只能加结构化字段没产生的键**:`RESERVED_WORKER_ENV` 里的键(PORT、MODEL_x、gateway、auth、
|
|
1813
|
-
* secret 名…)被拒,防 env 改路由/关鉴权/遮蔽密钥(council)
|
|
1867
|
+
* secret 名…)被拒,防 env 改路由/关鉴权/遮蔽密钥(council);墓碑表 `RETIRED_ENVS` 里的旧名被拒(引擎见之拒启;空串 = 未设,同引擎)。
|
|
1868
|
+
* 可覆盖非保留键(如 LOG_LEVEL)。hash-included(改它重建)。
|
|
1814
1869
|
*/
|
|
1815
1870
|
env: z
|
|
1816
1871
|
.record(ENV_NAME, z.string().max(2048))
|
|
1817
1872
|
.superRefine((rec, ctx) => {
|
|
1873
|
+
retiredEnvIssue(rec, ctx);
|
|
1818
1874
|
for (const [k, v] of Object.entries(rec)) {
|
|
1819
1875
|
if (RESERVED_WORKER_ENV.has(k))
|
|
1820
|
-
ctx.addIssue({ code: "custom",
|
|
1876
|
+
ctx.addIssue({ code: "custom", ...reservedKeyIssue("env", k) });
|
|
1821
1877
|
// 0.1.32: NO name exemption — a PUBLIC key is allowed because SECRET_LITERAL_RE now only flags PRIVATE keys
|
|
1822
1878
|
// (root fix), so a literal private key / token in env (ANY name, incl `*_PUBKEY`) is still rejected.
|
|
1823
1879
|
if (containsSecretToken(v))
|
|
1824
|
-
ctx.addIssue({ code: "custom",
|
|
1880
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `env["${k}"] looks like a literal secret — put secrets in secretEnv (an env-NAME), never a value in env` }); // tokenized (mid-string too). R8
|
|
1825
1881
|
}
|
|
1826
1882
|
})
|
|
1827
1883
|
.optional(),
|
|
1828
1884
|
/**
|
|
1829
1885
|
* 通用**密钥** env 透传:`{ service 的环境变量名 → 宿主机 env-NAME }`。编排器把宿主机 env-NAME 从它的 secret 源
|
|
1830
|
-
* 解析成真值、经 secretKeyRef 注入——**
|
|
1831
|
-
* 故无法粘贴真值)。给 app 专属令牌用(如 `
|
|
1832
|
-
* 故密钥永不被 `env` 同名键遮蔽。`RESERVED_WORKER_ENV` 里的键被拒(那些结构化 secret 走 `secretRefs
|
|
1886
|
+
* 解析成真值、经 secretKeyRef 注入——**web-admin 只存名、绝不存值**(同 `secretRefs` 边界,值也是 `ENV_NAME`
|
|
1887
|
+
* 故无法粘贴真值)。给 app 专属令牌用(如 `MY_APP_TOKEN` → 某 env-名)。在 reconciler 里于 `env` 之后解析,
|
|
1888
|
+
* 故密钥永不被 `env` 同名键遮蔽。`RESERVED_WORKER_ENV` 里的键被拒(那些结构化 secret 走 `secretRefs`);墓碑表 `RETIRED_ENVS` 里的旧名被拒。
|
|
1833
1889
|
*/
|
|
1834
1890
|
secretEnv: z
|
|
1835
1891
|
.record(ENV_NAME, ENV_NAME)
|
|
1836
1892
|
.superRefine((rec, ctx) => {
|
|
1893
|
+
retiredEnvIssue(rec, ctx);
|
|
1837
1894
|
for (const k of Object.keys(rec))
|
|
1838
1895
|
if (RESERVED_WORKER_ENV.has(k))
|
|
1839
|
-
ctx.addIssue({ code: "custom",
|
|
1896
|
+
ctx.addIssue({ code: "custom", ...reservedKeyIssue("secretEnv", k) });
|
|
1840
1897
|
})
|
|
1841
1898
|
.optional(),
|
|
1842
1899
|
/** Opaque recreate trigger (hash-INCLUDED): bump it to force the reconciler to recreate the worker — e.g. to
|
|
@@ -1845,19 +1902,19 @@ const WorkerSpecObject = z.object({
|
|
|
1845
1902
|
recreateNonce: z.string().max(64).optional(),
|
|
1846
1903
|
/**
|
|
1847
1904
|
* 生命周期 (v2②, design/25 §3)。可选——不填 = `persistent`(默认安全,绝不被 GC 误杀)。
|
|
1848
|
-
* **reconciler 不读这个字段**(保持纯收敛);TTL/idle 决策全在
|
|
1905
|
+
* **reconciler 不读这个字段**(保持纯收敛);TTL/idle 决策全在 web-admin 侧 sweeper:到期就把 worker
|
|
1849
1906
|
* 从 desired 移除,reconciler 声明式自然 teardown。`ttl`/`idleTimeout` 用时长串("30m"/"2h"/"7d")。
|
|
1850
1907
|
*/
|
|
1851
1908
|
lifecycle: WorkerLifecycle.optional(),
|
|
1852
1909
|
/**
|
|
1853
|
-
* 多主机调度
|
|
1854
|
-
*
|
|
1855
|
-
*
|
|
1910
|
+
* 多主机调度(可选)。`placement.auto` 判 `host` 是**钉**还是**提示**:`auto:false`(缺省)⇒ `host` 是钉 —— 运维指定的那一台,
|
|
1911
|
+
* 只在那台起;`auto:true` ⇒ web-admin 的排布按容量 / 标签约束挑一台写回 `host`,那是提示,排布可改挑。`requires` = host 必须具备的
|
|
1912
|
+
* 全部标签(hosts 域声明)。不加单独的「钉」字段:钉 / 提示已由 `placement.auto` 编码,再加一个字段 = 同一事实两处写。
|
|
1856
1913
|
*/
|
|
1857
1914
|
placement: z
|
|
1858
1915
|
.object({
|
|
1859
1916
|
auto: z.boolean().default(false),
|
|
1860
|
-
requires: z.array(z.string().min(1)).
|
|
1917
|
+
requires: z.array(z.string().min(1)).prefault([]),
|
|
1861
1918
|
/** SOFT anti-affinity group: auto workers sharing this key are spread across hosts when possible
|
|
1862
1919
|
* (preferred, never blocks placement — the scheduling design note). 不填 = 无 spread 约束。 */
|
|
1863
1920
|
spread: z.string().min(1).optional(),
|
|
@@ -1865,7 +1922,7 @@ const WorkerSpecObject = z.object({
|
|
|
1865
1922
|
.optional(),
|
|
1866
1923
|
/**
|
|
1867
1924
|
* per-worker roster 引用 (the multi-roster design note)。指向 `rosters` 域里一个命名 roster;该 worker 的 service 拉
|
|
1868
|
-
* `GET /api/config/effective?worker=<name>` 时,
|
|
1925
|
+
* `GET /api/config/effective?worker=<name>` 时,web-admin 用这个 roster 解析它专属的 models/roles/@白名单。
|
|
1869
1926
|
* 不填 → 该 worker 拿全局 models(向后兼容)。**不进 `workerSpecHash`**(改 roster 只让 service 重拉、不重建容器)。
|
|
1870
1927
|
*/
|
|
1871
1928
|
roster: z.string().min(1).optional(),
|
|
@@ -1875,29 +1932,26 @@ const WorkerSpecObject = z.object({
|
|
|
1875
1932
|
});
|
|
1876
1933
|
/** 跨字段判据(G1 / G2 / host∧多副本 / store 四条)。每条一个 issue,`path` 指到要改的字段。 */
|
|
1877
1934
|
function workerSpecIssues(w, ctx) {
|
|
1878
|
-
const issue = (path,
|
|
1935
|
+
const issue = (path, q) => ctx.addIssue({ code: "custom", path, ...q });
|
|
1879
1936
|
const { execution, store, requirePrincipal } = w.runtime;
|
|
1880
1937
|
// G1:enabled 的 worker 必须声明车道与店(停用的不渲染,可缺)。
|
|
1881
1938
|
if (w.enabled && execution === undefined) {
|
|
1882
|
-
issue(["runtime", "execution", "lane"], `an enabled worker must declare its execution lane — runtime.execution.lane: ${EXECUTION_LANE_WORDS.join(" | ")}. `
|
|
1883
|
-
`Without one it would boot with no hands and still report tasks completed; there is no "no hands" lane — disable the worker instead`);
|
|
1939
|
+
issue(["runtime", "execution", "lane"], buildQuotedIssue `an enabled worker must declare its execution lane — runtime.execution.lane: ${fixed(EXECUTION_LANE_WORDS.join(" | "))}. Without one it would boot with no hands and still report tasks completed; there is no "no hands" lane — disable the worker instead`);
|
|
1884
1940
|
}
|
|
1885
1941
|
if (w.enabled && store === undefined) {
|
|
1886
|
-
issue(["runtime", "store", "backend"], `an enabled worker must declare its store — runtime.store.backend: ${STORE_BACKENDS.join(" | ")}`);
|
|
1942
|
+
issue(["runtime", "store", "backend"], buildQuotedIssue `an enabled worker must declare its store — runtime.store.backend: ${fixed(STORE_BACKENDS.join(" | "))}`);
|
|
1887
1943
|
}
|
|
1888
1944
|
if (execution !== undefined) {
|
|
1889
1945
|
// G2:多租户姿态不许跑不分租户的车道;出路按表现算(不手写第五份词表)。
|
|
1890
1946
|
if (requirePrincipal && laneRowOf(execution.lane).tenantSeparation === "single-user-only") {
|
|
1891
1947
|
const outlets = EXECUTION_LANES.filter((r) => r.tenantSeparation === "supported").map((r) => r.lane);
|
|
1892
|
-
issue(["runtime", "execution", "lane"], `runtime.requirePrincipal=true (multi-tenant posture) with the "${execution.lane}" execution lane runs every tenant's Bash on one shared target — `
|
|
1893
|
-
`this lane does not separate tenants; set runtime.execution.lane to ${outlets.join(" / ")}, or set runtime.requirePrincipal=false for a single-user worker`);
|
|
1948
|
+
issue(["runtime", "execution", "lane"], buildQuotedIssue `runtime.requirePrincipal=true (multi-tenant posture) with the "${execution.lane}" execution lane runs every tenant's Bash on one shared target — this lane does not separate tenants; set runtime.execution.lane to ${fixed(outlets.join(" / "))}, or set runtime.requirePrincipal=false for a single-user worker`);
|
|
1894
1949
|
}
|
|
1895
1950
|
// host ∧ 多副本:副本数有两个旋钮(replicas / autoscaling.maxReplicas,二者互斥),一条规则看「最多几个副本」。
|
|
1896
1951
|
if (execution.lane === "host") {
|
|
1897
1952
|
const [path, replicas] = w.autoscaling !== undefined ? [["autoscaling", "maxReplicas"], w.autoscaling.maxReplicas] : [["replicas"], w.replicas ?? 1];
|
|
1898
1953
|
if (replicas > 1) {
|
|
1899
|
-
issue(path, `the "host" execution lane runs tools on each replica's own filesystem — ${replicas} replicas would hold ${replicas} different workspaces behind one route, `
|
|
1900
|
-
`so a session's next turn can land where its files are not. Keep a single replica, or choose a lane other than "host"`);
|
|
1954
|
+
issue(path, buildQuotedIssue `the "host" execution lane runs tools on each replica's own filesystem — ${replicas} replicas would hold ${replicas} different workspaces behind one route, so a session's next turn can land where its files are not. Keep a single replica, or choose a lane other than "host"`);
|
|
1901
1955
|
}
|
|
1902
1956
|
}
|
|
1903
1957
|
}
|
|
@@ -1913,46 +1967,42 @@ function workerSpecIssues(w, ctx) {
|
|
|
1913
1967
|
// 写闸判不了 —— 那一条只在起服拒。
|
|
1914
1968
|
if (execution?.lane === "device" && store !== undefined && !sqlPoolOpens) {
|
|
1915
1969
|
if (!sql) {
|
|
1916
|
-
issue(["runtime", "store", "backend"], `the "device" execution lane keeps its device registry, session↔device binding and audit trail in SQL — store.backend "${store.backend}" has none `
|
|
1917
|
-
`(the engine refuses to start: device-durable-store); set store.backend to mysql | pg`);
|
|
1970
|
+
issue(["runtime", "store", "backend"], buildQuotedIssue `the "device" execution lane keeps its device registry, session↔device binding and audit trail in SQL — store.backend "${store.backend}" has none (the engine refuses to start: device-durable-store); set store.backend to mysql | pg`);
|
|
1918
1971
|
}
|
|
1919
1972
|
else {
|
|
1920
|
-
issue(["runtime", "store", "session"], `the "device" execution lane needs the SQL store actually built — store.session "memory" builds no SQL coordinates `
|
|
1921
|
-
`(the engine refuses to start: device-durable-store); set store.session to auto | mysql`);
|
|
1973
|
+
issue(["runtime", "store", "session"], buildQuotedIssue `the "device" execution lane needs the SQL store actually built — store.session "memory" builds no SQL coordinates (the engine refuses to start: device-durable-store); set store.session to auto | mysql`);
|
|
1922
1974
|
}
|
|
1923
1975
|
}
|
|
1924
1976
|
if (store !== undefined) {
|
|
1925
1977
|
// ⑧ session mysql 要一个 durable 会话店,memory 后端一只都不建(`plugins/session-store.ts:17`「a DB session backend requires a store backend」)。
|
|
1926
1978
|
if (store.session === "mysql" && store.backend === "memory") {
|
|
1927
|
-
issue(["runtime", "store", "session"], `store.session "mysql" asks for a durable session store, but store.backend "memory" builds none (the engine refuses to start: a DB session backend requires a store backend); `
|
|
1928
|
-
`set store.session to auto | memory, or store.backend to mysql | pg | local`);
|
|
1979
|
+
issue(["runtime", "store", "session"], buildQuotedIssue `store.session "mysql" asks for a durable session store, but store.backend "memory" builds none (the engine refuses to start: a DB session backend requires a store backend); set store.session to auto | memory, or store.backend to mysql | pg | local`);
|
|
1929
1980
|
}
|
|
1930
1981
|
// ⑨ 记忆引擎开 ∧ 方言 pg / tidb ⇒ 引擎绑 SQL 池(`boot/stores.ts:209` / `:232` / `:300`);session memory 时池不开 ⇒ 起服拒。
|
|
1931
1982
|
if (store.memory.engine === "on" && MEMORY_BACKEND_NEEDS_STORE[store.memory.backend] !== undefined && sql && !sqlPoolOpens) {
|
|
1932
|
-
issue(["runtime", "store", "session"], `store.memory.backend "${store.memory.backend}" binds the SQL pool, but store.session "memory" never opens it (the engine refuses to start half-configured); `
|
|
1933
|
-
`set store.session to auto | mysql, or store.memory.backend to file, or store.memory.engine to off`);
|
|
1983
|
+
issue(["runtime", "store", "session"], buildQuotedIssue `store.memory.backend "${store.memory.backend}" binds the SQL pool, but store.session "memory" never opens it (the engine refuses to start half-configured); set store.session to auto | mysql, or store.memory.backend to file, or store.memory.engine to off`);
|
|
1934
1984
|
}
|
|
1935
1985
|
}
|
|
1936
1986
|
if (store !== undefined) {
|
|
1937
1987
|
if (sql && store.db === undefined)
|
|
1938
|
-
issue(["runtime", "store", "db"], `store.backend "${store.backend}" needs runtime.store.db {host, port, user, database}`);
|
|
1988
|
+
issue(["runtime", "store", "db"], buildQuotedIssue `store.backend "${store.backend}" needs runtime.store.db {host, port, user, database}`);
|
|
1939
1989
|
if (!sql && store.db !== undefined)
|
|
1940
|
-
issue(["runtime", "store", "db"], `store.backend "${store.backend}" uses no SQL server — remove runtime.store.db, or set store.backend to mysql | pg`);
|
|
1990
|
+
issue(["runtime", "store", "db"], buildQuotedIssue `store.backend "${store.backend}" uses no SQL server — remove runtime.store.db, or set store.backend to mysql | pg`);
|
|
1941
1991
|
const needs = MEMORY_BACKEND_NEEDS_STORE[store.memory.backend];
|
|
1942
1992
|
if (needs !== undefined && store.backend !== needs) {
|
|
1943
|
-
issue(["runtime", "store", "memory", "backend"], `store.memory.backend "${store.memory.backend}" keeps memory in the ${needs} store — it needs store.backend "${needs}" (got "${store.backend}")`);
|
|
1993
|
+
issue(["runtime", "store", "memory", "backend"], buildQuotedIssue `store.memory.backend "${store.memory.backend}" keeps memory in the ${fixed(needs)} store — it needs store.backend "${fixed(needs)}" (got "${store.backend}")`);
|
|
1944
1994
|
}
|
|
1945
1995
|
if (requirePrincipal && !sql) {
|
|
1946
|
-
issue(["runtime", "store", "backend"], `runtime.requirePrincipal=true (multi-tenant posture) needs a SQL store — store.backend "${store.backend}" has no tenant isolation; set store.backend to mysql | pg`);
|
|
1996
|
+
issue(["runtime", "store", "backend"], buildQuotedIssue `runtime.requirePrincipal=true (multi-tenant posture) needs a SQL store — store.backend "${store.backend}" has no tenant isolation; set store.backend to mysql | pg`);
|
|
1947
1997
|
}
|
|
1948
1998
|
}
|
|
1949
1999
|
if (sql && w.secretRefs.dbPasswordEnv === undefined)
|
|
1950
|
-
issue(["secretRefs", "dbPasswordEnv"], `a SQL store (runtime.store.backend mysql | pg) needs secretRefs.dbPasswordEnv — the host env-NAME holding the database password`);
|
|
2000
|
+
issue(["secretRefs", "dbPasswordEnv"], buildQuotedIssue `a SQL store (runtime.store.backend mysql | pg) needs secretRefs.dbPasswordEnv — the host env-NAME holding the database password`);
|
|
1951
2001
|
if (!sql && w.secretRefs.dbPasswordEnv !== undefined)
|
|
1952
|
-
issue(["secretRefs", "dbPasswordEnv"], `secretRefs.dbPasswordEnv is set but there is no SQL store (runtime.store.backend mysql | pg) — remove it`);
|
|
2002
|
+
issue(["secretRefs", "dbPasswordEnv"], buildQuotedIssue `secretRefs.dbPasswordEnv is set but there is no SQL store (runtime.store.backend mysql | pg) — remove it`);
|
|
1953
2003
|
}
|
|
1954
2004
|
/** 一台 worker 的声明。写闸 = `WorkerSpec.safeParse`(7.0.0 删除项读到即拒 + 两个结构块 + 跨字段判据)。 */
|
|
1955
|
-
export const WorkerSpec =
|
|
2005
|
+
export const WorkerSpec = WorkerSpecObject.superRefine(workerSpecIssues);
|
|
1956
2006
|
/** Effective route key for a worker = explicit routePrefix, else its name. */
|
|
1957
2007
|
export function workerRouteKey(w) {
|
|
1958
2008
|
return w.routePrefix ?? w.name;
|
|
@@ -1967,27 +2017,27 @@ export const WORKER_RESOURCE_PRESETS = {
|
|
|
1967
2017
|
L: { requests: { cpu: "1", memory: "2Gi" }, limits: { cpu: "4", memory: "4Gi" } },
|
|
1968
2018
|
};
|
|
1969
2019
|
export const WorkersConfig = z
|
|
1970
|
-
.object({ workers: z.array(WorkerSpec).
|
|
2020
|
+
.object({ workers: z.array(WorkerSpec).prefault([]) })
|
|
1971
2021
|
.superRefine((cfg, ctx) => {
|
|
1972
2022
|
// worker `name` and effective routePrefix must each be unique across the fleet (design/25 决策7:
|
|
1973
|
-
// routePrefix 唯一性
|
|
2023
|
+
// routePrefix 唯一性 web-admin 提交时校验). Dup name → container-name clash; dup route → ambiguous
|
|
1974
2024
|
// reverse-proxy routing.
|
|
1975
2025
|
const names = new Set();
|
|
1976
2026
|
const routes = new Set();
|
|
1977
2027
|
cfg.workers.forEach((w, i) => {
|
|
1978
2028
|
if (names.has(w.name))
|
|
1979
|
-
ctx.addIssue({ code:
|
|
2029
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate worker name "${w.name}"`, path: ["workers", i, "name"] });
|
|
1980
2030
|
names.add(w.name);
|
|
1981
2031
|
const route = workerRouteKey(w);
|
|
1982
2032
|
// point the error at the field that actually set the route (explicit routePrefix, else the name fallback).
|
|
1983
2033
|
const routeField = w.routePrefix !== undefined ? "routePrefix" : "name";
|
|
1984
2034
|
if (routes.has(route))
|
|
1985
|
-
ctx.addIssue({ code:
|
|
2035
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate routePrefix "${route}" (collides with another worker's routePrefix/name)`, path: ["workers", i, routeField] });
|
|
1986
2036
|
routes.add(route);
|
|
1987
2037
|
// replicas + autoscaling = two owners of the same knob (the orchestrator's SSA vs the HPA controller
|
|
1988
2038
|
// would endlessly fight over spec.replicas) → reject at save, not at reconcile.
|
|
1989
2039
|
if (w.replicas !== undefined && w.autoscaling) {
|
|
1990
|
-
ctx.addIssue({ code:
|
|
2040
|
+
ctx.addIssue({ code: "custom", message: "replicas and autoscaling are mutually exclusive — the HPA owns the replica count when autoscaling is set", path: ["workers", i, "autoscaling"] });
|
|
1991
2041
|
}
|
|
1992
2042
|
});
|
|
1993
2043
|
});
|
|
@@ -2004,9 +2054,9 @@ export const WorkerStatus = z.object({
|
|
|
2004
2054
|
name: z.string().min(1),
|
|
2005
2055
|
host: z.string().min(1), // 哪个 reconciler 上报的
|
|
2006
2056
|
phase: z.enum(["Pending", "Running", "Failed", "Unknown"]),
|
|
2007
|
-
conditions: z.array(WorkerCondition).
|
|
2057
|
+
conditions: z.array(WorkerCondition).prefault([]),
|
|
2008
2058
|
/** reconciler 处理时的 effective `version` — admin 改 spec 后 version 变,据此判断 status 是否陈旧. */
|
|
2009
|
-
observedVersion:
|
|
2059
|
+
observedVersion: INTEGER.nonnegative(),
|
|
2010
2060
|
/** reconciler 算的逻辑 spec hash (漂移检测 + 防振荡, design/22 失败模式#1). */
|
|
2011
2061
|
observedSpecHash: z.string().optional(),
|
|
2012
2062
|
message: z.string().optional(),
|
|
@@ -2018,20 +2068,22 @@ export const WorkerStatus = z.object({
|
|
|
2018
2068
|
// - `capacity` DELETED —— 可放性改由探测值折算(HostStatus.probes − Σ已排 worker requests;计数制→资源制
|
|
2019
2069
|
// 是探测数据齐备后的**独立开关**,E2 placement 承接,切换前沿用计数制兜底、不按票数)。
|
|
2020
2070
|
// - `approved` NEW —— 默认 false = 待批(不参与自动排布;enforcement 随 E2 placement 生效,E1 只落契约)。
|
|
2021
|
-
// 存量迁移 = grandfathering 批量回填 approved:true(migrate
|
|
2022
|
-
/** DNS-label bound on a host NAME (0.8.0 tightening; single source — schema
|
|
2023
|
-
*
|
|
2071
|
+
// 存量迁移 = grandfathering 批量回填 approved:true(0.5 → 0.6 迁移工具,11.0.0 随 `./migrate` 出口删除),存量机器不出现排布空窗(§11-9)。
|
|
2072
|
+
/** DNS-label bound on a host NAME (0.8.0 tightening; single source — the schema and the one-time migration SS-M2 that
|
|
2073
|
+
* rewrites 0.7-era stored data over it both read this constant). */
|
|
2024
2074
|
export const HOST_NAME_MAX = 63;
|
|
2025
2075
|
/** Ceiling on the hosts ARRAY (0.8.0 tightening; same single-source rule as {@link HOST_NAME_MAX}). */
|
|
2026
2076
|
export const HOSTS_ARRAY_MAX = 1024;
|
|
2077
|
+
/** 两条上界的拒句尾:0.8.0 之前存下的超界数据由一次性迁移 SS-M2 改写(11.0.0 起读侧不再夹紧)。 */
|
|
2078
|
+
const HOSTS_BOUND_SIGNPOST = migrationSignpost("SS-M2", "0.8.0");
|
|
2027
2079
|
export const HostEntry = z.object({
|
|
2028
|
-
// .max(HOST_NAME_MAX=63) = DNS-label bound (E2 F3③ 挂账收账, 0.8.0):
|
|
2029
|
-
name: z.string().min(1).max(HOST_NAME_MAX).regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters/digits/-, no spaces"),
|
|
2080
|
+
// .max(HOST_NAME_MAX=63) = DNS-label bound (E2 F3③ 挂账收账, 0.8.0): web-admin-side probe clamp stays as双保险.
|
|
2081
|
+
name: z.string().min(1).max(HOST_NAME_MAX, `host name longer than ${HOST_NAME_MAX} characters (a DNS label) — ${HOSTS_BOUND_SIGNPOST}`).regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters/digits/-, no spaces"),
|
|
2030
2082
|
/** 待批准入编状态机(§2-3):自注册/新登记默认 false = 待批,不参与自动排布;admin 批准(走 publish)→
|
|
2031
2083
|
* true 参排(enforcement 随 E2)。既有 worker 不受影响(reconciler 仍按具体 host 认领)。 */
|
|
2032
2084
|
approved: z.boolean().default(false),
|
|
2033
2085
|
/** 调度标签(如 "gpu"/"region-us"),worker 的 `placement.requires` 据此匹配。 */
|
|
2034
|
-
labels: z.array(z.string().min(1)).
|
|
2086
|
+
labels: z.array(z.string().min(1)).prefault([]),
|
|
2035
2087
|
/** 关闭 = 完全不可调度(既有 worker 仍由 reconciler 管,但 placement 视它不可用)。 */
|
|
2036
2088
|
enabled: z.boolean().default(true),
|
|
2037
2089
|
/** drain = 不再往这台**新**放 worker(既有的留着),用于下线前腾空。 */
|
|
@@ -2039,16 +2091,15 @@ export const HostEntry = z.object({
|
|
|
2039
2091
|
notes: z.string().optional(),
|
|
2040
2092
|
});
|
|
2041
2093
|
export const HostsConfig = z
|
|
2042
|
-
// .max(HOSTS_ARRAY_MAX=1024) hosts (E2 F3③ 挂账收账, 0.8.0) — a fleet registry, not a datacenter inventory;
|
|
2043
|
-
// an absurdly long array is a poisoned publish, fail-loud at save time.
|
|
2044
|
-
//
|
|
2045
|
-
|
|
2046
|
-
.object({ hosts: z.array(HostEntry).max(HOSTS_ARRAY_MAX).default([]) })
|
|
2094
|
+
// .max(HOSTS_ARRAY_MAX=1024) hosts (E2 F3③ 挂账收账, 0.8.0) — a fleet host registry, not a datacenter inventory;
|
|
2095
|
+
// an absurdly long array is a poisoned publish, fail-loud at save time. The read path judges with the same schema
|
|
2096
|
+
// (11.0.0: the 0.8.1 read-side clamp is gone; 0.7-era stored data over these bounds is rewritten once by SS-M2).
|
|
2097
|
+
.object({ hosts: z.array(HostEntry).max(HOSTS_ARRAY_MAX, `more than ${HOSTS_ARRAY_MAX} hosts — ${HOSTS_BOUND_SIGNPOST}`).prefault([]) })
|
|
2047
2098
|
.superRefine((cfg, ctx) => {
|
|
2048
2099
|
const seen = new Set();
|
|
2049
2100
|
cfg.hosts.forEach((h, i) => {
|
|
2050
2101
|
if (seen.has(h.name))
|
|
2051
|
-
ctx.addIssue({ code:
|
|
2102
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate host "${h.name}"`, path: ["hosts", i, "name"] });
|
|
2052
2103
|
seen.add(h.name);
|
|
2053
2104
|
});
|
|
2054
2105
|
});
|
|
@@ -2058,28 +2109,28 @@ export const HostsConfig = z
|
|
|
2058
2109
|
// (仅身份字段 {name, approved:false},幂等一条,来源 token 记审计——EXPERT-REDESIGN §2-1/§11-4)。E2
|
|
2059
2110
|
// placement 的资源制折算消费它(可分配 = probes − Σ已排 worker requests);从未上报的机器不参与自动排布。
|
|
2060
2111
|
// Sanity ceilings (E2 F3③ 挂账收账, 0.8.0): a forged/absurd probe (cpuCores=1e12) would suck every auto
|
|
2061
|
-
// worker onto one host. Ceilings MIRROR
|
|
2062
|
-
// PROBE_MEM_BYTES_CEILING=2^50=1PiB) — schema now fail-louds at report time;
|
|
2112
|
+
// worker onto one host. Ceilings MIRROR web-admin's probeCapacities clamp (PROBE_CPU_CORES_CEILING=4096 /
|
|
2113
|
+
// PROBE_MEM_BYTES_CEILING=2^50=1PiB) — schema now fail-louds at report time; web-admin's clamp stays 双保险.
|
|
2063
2114
|
export const PROBE_CPU_CORES_MAX = 4096;
|
|
2064
2115
|
export const PROBE_MEM_BYTES_MAX = 2 ** 50; // 1 PiB
|
|
2065
2116
|
export const HostProbes = z.object({
|
|
2066
2117
|
/** 探测到的 CPU 核数(docker info NCPU / k8s node allocatable cpu)。可为小数(k8s allocatable 如 3.9)。 */
|
|
2067
2118
|
cpuCores: z.number().finite().positive().max(PROBE_CPU_CORES_MAX),
|
|
2068
2119
|
/** 探测到的内存字节数(docker info MemTotal / k8s node allocatable memory)。 */
|
|
2069
|
-
memBytes:
|
|
2120
|
+
memBytes: INTEGER.positive().max(PROBE_MEM_BYTES_MAX),
|
|
2070
2121
|
/** 当前跑着的容器数(docker info ContainersRunning / k8s node 上本 fleet 的 pod 数)。 */
|
|
2071
|
-
containersRunning:
|
|
2122
|
+
containersRunning: INTEGER.nonnegative(),
|
|
2072
2123
|
/** k8s backend 补充:node 名 + allocatable(与顶层同单位——cpuCores 核数 / memBytes 字节)。docker 无此段。 */
|
|
2073
2124
|
k8sNode: z
|
|
2074
2125
|
.object({
|
|
2075
2126
|
name: z.string().min(1),
|
|
2076
|
-
allocatable: z.object({ cpuCores: z.number().finite().positive().max(PROBE_CPU_CORES_MAX).optional(), memBytes:
|
|
2127
|
+
allocatable: z.object({ cpuCores: z.number().finite().positive().max(PROBE_CPU_CORES_MAX).optional(), memBytes: INTEGER.positive().max(PROBE_MEM_BYTES_MAX).optional() }).optional(),
|
|
2077
2128
|
})
|
|
2078
2129
|
.optional(),
|
|
2079
2130
|
});
|
|
2080
2131
|
export const HostStatus = z.object({
|
|
2081
2132
|
/** host name(hosts 域 HostEntry.name / ORCH_HOST_ID 同字符集,同 .max(HOST_NAME_MAX=63) DNS-label bound)。
|
|
2082
|
-
* status 上报是 WRITE 路径 → fail-loud 保持(有意设计,S3 M-1 订正注记:
|
|
2133
|
+
* status 上报是 WRITE 路径 → fail-loud 保持(有意设计,S3 M-1 订正注记:web-admin hosts/status PUT 无独立
|
|
2083
2134
|
* probe 上限,schema 即闸;读路径 grandfather 只涉 config-hosts 域,不涉此处)。 */
|
|
2084
2135
|
host: z.string().min(1).max(HOST_NAME_MAX).regex(/^[a-z0-9][a-z0-9-]*$/, "lowercase letters/digits/-, no spaces"),
|
|
2085
2136
|
backend: z.enum(["docker", "k8s"]),
|
|
@@ -2090,7 +2141,7 @@ export const HostStatus = z.object({
|
|
|
2090
2141
|
// ── Domain: skills (中心化 skill 分发 — the mcp/skills design note, Roadmap B1) ─────────
|
|
2091
2142
|
// A skill is a prompt-level capability: frontmatter (name/description/scenarios) + markdown `content`
|
|
2092
2143
|
// the service feeds into the agent's model context when the skill is invoked (core progressive disclosure
|
|
2093
|
-
// — NOT the base system prompt). Still centrally-delivered agent instructions.
|
|
2144
|
+
// — NOT the base system prompt). Still centrally-delivered agent instructions. Web-admin stores the FULL skill
|
|
2094
2145
|
// (incl. content, admin-edited); /effective ships a MANIFEST (content → contentHash) and the service
|
|
2095
2146
|
// lazily pulls the body by hash (slice 2). Targeting is by `scenarios[]`, filtered service-side exactly
|
|
2096
2147
|
// like the in-image `skillsForScenario` (the mcp/skills design note §3.1, §6 Q2). Applied at service boot/reconcile,
|
|
@@ -2098,27 +2149,27 @@ export const HostStatus = z.object({
|
|
|
2098
2149
|
// (injected into the model on invoke) → writing this domain is gated to `publisher` + the publish gate
|
|
2099
2150
|
// (§3.3, DOMAIN_WRITE_ROLE in store.ts).
|
|
2100
2151
|
export const SkillSpec = z.object({
|
|
2101
|
-
/** Catalog key — must match the in-image SKILLS_DIR name to OVERRIDE it (§6 Q4
|
|
2152
|
+
/** Catalog key — must match the in-image SKILLS_DIR name to OVERRIDE it (§6 Q4 web-admin 按名覆盖). */
|
|
2102
2153
|
name: z.string().min(1).regex(/^[a-z0-9][a-z0-9._-]*$/i, "letters/digits/._- , no spaces"),
|
|
2103
2154
|
/** One-line summary (frontmatter `description`) — surfaced to the model for skill selection. */
|
|
2104
2155
|
description: z.string().default(""),
|
|
2105
2156
|
/** Scenarios this skill applies to; empty = all scenarios (service-side filter, same as skillsForScenario). */
|
|
2106
|
-
scenarios: z.array(z.string().min(1)).
|
|
2157
|
+
scenarios: z.array(z.string().min(1)).prefault([]),
|
|
2107
2158
|
/** Per-roster targeting (v2, the mcp/skills design note §6 Q2): roster NAMEs this skill is for. Empty = all rosters.
|
|
2108
2159
|
* Filtered at GET /effective?worker=<name> by the worker's roster; the unscoped global pull ships all. */
|
|
2109
|
-
rosters: z.array(z.string().min(1)).
|
|
2160
|
+
rosters: z.array(z.string().min(1)).prefault([]),
|
|
2110
2161
|
/** The markdown body injected into the model context on invoke (progressive disclosure). Stored in full here; shipped as a hash in /effective. */
|
|
2111
2162
|
content: z.string().default(""),
|
|
2112
2163
|
enabled: z.boolean().default(true),
|
|
2113
2164
|
notes: z.string().optional(),
|
|
2114
2165
|
});
|
|
2115
2166
|
export const SkillsConfig = z
|
|
2116
|
-
.object({ skills: z.array(SkillSpec).
|
|
2167
|
+
.object({ skills: z.array(SkillSpec).prefault([]) })
|
|
2117
2168
|
.superRefine((cfg, ctx) => {
|
|
2118
2169
|
const seen = new Set();
|
|
2119
2170
|
cfg.skills.forEach((s, i) => {
|
|
2120
2171
|
if (seen.has(s.name))
|
|
2121
|
-
ctx.addIssue({ code:
|
|
2172
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate skill "${s.name}"`, path: ["skills", i, "name"] });
|
|
2122
2173
|
seen.add(s.name);
|
|
2123
2174
|
});
|
|
2124
2175
|
});
|
|
@@ -2126,15 +2177,15 @@ export const SkillsConfig = z
|
|
|
2126
2177
|
* (`readEffectiveWire`). Derived from {@link SkillSpec} exactly like {@link SkillManifestEntry} is (omit
|
|
2127
2178
|
* `content` / require `contentHash`) so schema, entry type and interface can never drift apart. Only
|
|
2128
2179
|
* `min(1)` on contentHash: the hash is the service's PULL KEY for the content endpoint, its exact format
|
|
2129
|
-
* is
|
|
2180
|
+
* is web-admin's writing concern — re-pinning it here would make an old reader reject a newer web-admin's
|
|
2130
2181
|
* legal hash scheme. */
|
|
2131
2182
|
export const SkillsManifestSchema = z.object({
|
|
2132
|
-
skills: z.array(SkillSpec.omit({ content: true }).extend({ contentHash: z.string().min(1) })).
|
|
2183
|
+
skills: z.array(SkillSpec.omit({ content: true }).extend({ contentHash: z.string().min(1) })).prefault([]),
|
|
2133
2184
|
});
|
|
2134
2185
|
// ── Domain: mcp (MCP server 注册表 — the mcp/skills design note, Roadmap B1) ────────────
|
|
2135
|
-
//
|
|
2186
|
+
// web-admin = an MCP-server REGISTRY: a scenario mounts a subset by `scenarios[]`. Maps onto core's
|
|
2136
2187
|
// `McpServerSpec = { name, transport: {kind:"stdio",command,args?,env?} | {kind:"http",url,headers?}, allowTools?, contentOrigin?, source? }`.
|
|
2137
|
-
// SECRET BOUNDARY (§3.1, design/15): a transport's env/header VALUES may be secret →
|
|
2188
|
+
// SECRET BOUNDARY (§3.1, design/15): a transport's env/header VALUES may be secret → web-admin stores only
|
|
2138
2189
|
// env-NAME references (`envRefs`/`headerRefs`: target-key → an ENV_NAME in the SERVICE's env). The service's
|
|
2139
2190
|
// `resolveSpec` reads the real values from its own environment — this DB never holds a credential. Like
|
|
2140
2191
|
// skills, MCP is applied at boot/reconcile (not hot) and writing is gated to `publisher` + the publish gate.
|
|
@@ -2184,12 +2235,10 @@ export const McpTransport = z.discriminatedUnion("kind", [
|
|
|
2184
2235
|
}),
|
|
2185
2236
|
]);
|
|
2186
2237
|
/** The closed word table for {@link McpServerSpec}.contentOrigin — a MIRROR literal of the engine's
|
|
2187
|
-
* `ToolContentOrigin`. This package
|
|
2188
|
-
* PROMPT_ARTIFACT_LIMITS a mirror constant)
|
|
2189
|
-
* compile
|
|
2190
|
-
*
|
|
2191
|
-
* dependencies. Inside this repo the table is pinned as a literal only (a drift surfaces at the consumer's
|
|
2192
|
-
* pickup gate, never silently on the wire — an out-of-table word is rejected at parse on both sides). */
|
|
2238
|
+
* `ToolContentOrigin`. This package carries no engine RUNTIME dependency (the same ruling that made
|
|
2239
|
+
* PROMPT_ARTIFACT_LIMITS a mirror constant); the mirror is pinned HERE, by its owner: the upstream drift gate
|
|
2240
|
+
* (group ⑥) asserts at compile time that this union and the engine type are the same in both directions, against the
|
|
2241
|
+
* engine the devDependency pins. Consumers import this table — they keep no copy of their own. */
|
|
2193
2242
|
export const MCP_CONTENT_ORIGINS = ["local", "execution", "external"];
|
|
2194
2243
|
export const McpServerSpec = z.object({
|
|
2195
2244
|
name: z.string().min(1).regex(/^[a-z0-9][a-z0-9._-]*$/i, "letters/digits/._- , no spaces"),
|
|
@@ -2197,10 +2246,10 @@ export const McpServerSpec = z.object({
|
|
|
2197
2246
|
/** Allowlist of tool names exposed from this server (core `allowTools`). Empty/undefined = all. */
|
|
2198
2247
|
allowTools: z.array(z.string().min(1)).optional(),
|
|
2199
2248
|
/** Scenarios this server is mounted in; empty = all (service-side filter, same as skills). */
|
|
2200
|
-
scenarios: z.array(z.string().min(1)).
|
|
2249
|
+
scenarios: z.array(z.string().min(1)).prefault([]),
|
|
2201
2250
|
/** Per-roster targeting (v2): roster NAMEs this server is for. Empty = all rosters. Filtered at
|
|
2202
2251
|
* GET /effective?worker=<name> by the worker's roster; the unscoped global pull ships all. */
|
|
2203
|
-
rosters: z.array(z.string().min(1)).
|
|
2252
|
+
rosters: z.array(z.string().min(1)).prefault([]),
|
|
2204
2253
|
enabled: z.boolean().default(true),
|
|
2205
2254
|
/** Per-server opt-in for INBOUND MCP elicitation (core `McpServerSpec.elicitation`, design/99 §E23). Default
|
|
2206
2255
|
* OFF = fail-closed: only an opted-in server may prompt the user mid-tool-call. Service forwards this into the
|
|
@@ -2245,19 +2294,19 @@ export const McpServerSpec = z.object({
|
|
|
2245
2294
|
notes: z.string().optional(),
|
|
2246
2295
|
});
|
|
2247
2296
|
export const McpConfig = z
|
|
2248
|
-
.object({ servers: z.array(McpServerSpec).
|
|
2297
|
+
.object({ servers: z.array(McpServerSpec).prefault([]) })
|
|
2249
2298
|
.superRefine((cfg, ctx) => {
|
|
2250
2299
|
const seen = new Set();
|
|
2251
2300
|
cfg.servers.forEach((s, i) => {
|
|
2252
2301
|
if (seen.has(s.name))
|
|
2253
|
-
ctx.addIssue({ code:
|
|
2302
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate mcp server "${s.name}"`, path: ["servers", i, "name"] });
|
|
2254
2303
|
seen.add(s.name);
|
|
2255
2304
|
});
|
|
2256
2305
|
});
|
|
2257
2306
|
// ── Domain: plugins (plugin SOURCE REFERENCES — C2 plugins 上云, additive 0.5.0) ──────────────────────────
|
|
2258
2307
|
// The cloud stores WHERE a plugin comes from, NEVER its content (产品拍板): {name, source, version/sha pin}.
|
|
2259
2308
|
// The consuming shell materializes the plugin locally from the ref (git clone / marketplace install) — the
|
|
2260
|
-
//
|
|
2309
|
+
// web-admin never hosts executable plugin bytes, so there is nothing here to content-scan or lazily serve.
|
|
2261
2310
|
// Plugins are an EXECUTABLE surface (commands/hooks/MCP ride in) → write-gated to `publisher` like skills/
|
|
2262
2311
|
// mcp (DOMAIN_WRITE_ROLE) and capped per-principal by entitlement `visiblePlugins` (deny-wins, same
|
|
2263
2312
|
// mechanics as visibleMcp/visibleSkills — B1: a personal plugin ref is a net-new capability).
|
|
@@ -2274,18 +2323,18 @@ export const PLUGIN_ORIGIN = z
|
|
|
2274
2323
|
.min(1)
|
|
2275
2324
|
.refine((s) => !containsSecretToken(s), "plugin source must not contain a secret literal — private remotes authenticate via the consuming machine's git credentials, never an inline token")
|
|
2276
2325
|
.refine((s) => !hasNul(s), "plugin source must not contain a NUL byte")
|
|
2277
|
-
.
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
return
|
|
2326
|
+
.superRefine((s, ctx) => {
|
|
2327
|
+
// The shape is THE URL judge's ({@link httpUrlFlaws}); this seat adds two rules of its own: https only, and spelled in full
|
|
2328
|
+
// (`https://…` as written — git reads the raw string, and WHATWG's repair of `https:host/x` into `https://host/x` is not git's).
|
|
2329
|
+
const flaws = httpUrlFlaws(s, ["userinfo"]);
|
|
2330
|
+
if (flaws.includes("not-http-url") || new URL(s).protocol !== "https:" || s.slice(0, 8).toLowerCase() !== "https://") {
|
|
2331
|
+
ctx.addIssue({ code: "custom", message: "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)" });
|
|
2332
|
+
return;
|
|
2284
2333
|
}
|
|
2285
|
-
|
|
2286
|
-
|
|
2334
|
+
if (flaws.includes("userinfo") || urlQueryFragmentHasSecret(new URL(s))) {
|
|
2335
|
+
ctx.addIssue({ code: "custom", message: "an https plugin source must not embed credentials (userinfo) or a secret-bearing query/fragment" });
|
|
2287
2336
|
}
|
|
2288
|
-
}
|
|
2337
|
+
});
|
|
2289
2338
|
/** A full 40-hex git commit SHA — the ONLY pin form the supply-side treats as 钉死 (a branch/tag `version`
|
|
2290
2339
|
* is a moving ref; only a commit pin makes the materialized bytes reproducible). */
|
|
2291
2340
|
export const GIT_COMMIT_SHA = z.string().regex(/^[a-f0-9]{40}$/, "must be a full 40-character lowercase git commit SHA");
|
|
@@ -2316,37 +2365,40 @@ export const PluginSpec = z.object({
|
|
|
2316
2365
|
notes: z.string().optional(),
|
|
2317
2366
|
});
|
|
2318
2367
|
export const PluginsConfig = z
|
|
2319
|
-
.object({ plugins: z.array(PluginSpec).
|
|
2368
|
+
.object({ plugins: z.array(PluginSpec).prefault([]) })
|
|
2320
2369
|
.superRefine((cfg, ctx) => {
|
|
2321
2370
|
const seen = new Set();
|
|
2322
2371
|
cfg.plugins.forEach((p, i) => {
|
|
2323
2372
|
if (seen.has(p.name))
|
|
2324
|
-
ctx.addIssue({ code:
|
|
2373
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate plugin "${p.name}"`, path: ["plugins", i, "name"] });
|
|
2325
2374
|
seen.add(p.name);
|
|
2326
2375
|
});
|
|
2327
2376
|
});
|
|
2328
2377
|
// ── The domain registry ───────────────────────────────────────────────────────
|
|
2329
|
-
// ═══ prompts 域(
|
|
2378
|
+
// ═══ prompts 域(web-admin prompts 战役 v1;[ref] 五定夺/[1051] 产品终稿/[1057] M2 下发形)═══
|
|
2330
2379
|
// schema=编译目标非用户界面([1051]⓪):三卡(角色卡/场景说明/行为守则)是 web UI 编译层心智,
|
|
2331
|
-
// 本契约只承载编译产物(typed 声明)与 binding。单真源纪律:存储/解析在
|
|
2380
|
+
// 本契约只承载编译产物(typed 声明)与 binding。单真源纪律:存储/解析在 web-admin(sema-web),
|
|
2332
2381
|
// 消费投影在 server(声明数组逐字喂 core typed stableBlocks 钩子),schema 恒在本仓(零第二份)。
|
|
2382
|
+
/** 提示声明的槽位闭集(core `PromptTextDeclaration.slot` 同形):identity = 角色卡、scenario = 场景说明、behavior = 行为守则。
|
|
2383
|
+
* declaration 轴与 catalog 轴({@link PromptArtifactSection})共用这一只。 */
|
|
2384
|
+
export const PromptSlot = z.enum(["identity", "scenario", "behavior"]);
|
|
2333
2385
|
/** core `PromptTextDeclaration` 的契约层同形(core prompts/default.d.ts 逐字:id/slot 闭集/text)。
|
|
2334
2386
|
* slot ↔ 三卡:identity=角色卡、scenario=场景说明、behavior=行为守则([1051]①)。 */
|
|
2335
2387
|
export const PromptTextDeclaration = z.object({
|
|
2336
2388
|
id: z.string().min(1).max(128),
|
|
2337
|
-
slot:
|
|
2389
|
+
slot: PromptSlot,
|
|
2338
2390
|
text: z.string().max(65536),
|
|
2339
2391
|
/** core 1.315 opt-in 逐 section 对账锚([1062]①):声明方对 `text` 铸 sha256,core 校验后逐字
|
|
2340
2392
|
* 透传进 manifest `contentHash`(错值丢弃+warn,core 永不自算非盐化 digest)。缺省=退化为
|
|
2341
2393
|
* 整包 contentDigest 对账。契约层显式声明防非 strict parse strip 断锚([1062]③)。 */
|
|
2342
|
-
contentHash:
|
|
2394
|
+
contentHash: Sha256Digest.optional(),
|
|
2343
2395
|
});
|
|
2344
2396
|
/** 三维 binding([ref]①a:v1=global/worker/scenario,砍 model 维——per-model 微调走
|
|
2345
|
-
* ModelEntry.promptGuidance 专用通道)。covering 语义([1050]①b,解析只在
|
|
2397
|
+
* ModelEntry.promptGuidance 专用通道)。covering 语义([1050]①b,解析只在 web-admin 一处):
|
|
2346
2398
|
* 同 slot **层间整体替换**(胜出层该 slot 声明数组整体替换低层同 slot 全部条目;层内数组保序,
|
|
2347
2399
|
* 与 core assemble「declaration order preserved」无缝);层序 scenario > worker > global。
|
|
2348
2400
|
* 发布即物化([1051]②a):declarations 是 publish 时物化的不可变声明(意图层/编译器版本归
|
|
2349
|
-
*
|
|
2401
|
+
* web-admin 编辑面存储,非跨仓契约面)。 */
|
|
2350
2402
|
export const PromptBinding = z.object({
|
|
2351
2403
|
/** 绑定层。 */
|
|
2352
2404
|
level: z.enum(["global", "worker", "scenario"]),
|
|
@@ -2372,8 +2424,8 @@ export const PromptBinding = z.object({
|
|
|
2372
2424
|
// (overlays/profiles/modelPolicies/bindings)是后续 additive 切片,本轴到时 additive 扩,零迁移。
|
|
2373
2425
|
// 单真源纪律:server 候选门用 core 导出的 `verifyPromptArtifact`(digest 复算+上限+range 全管线),
|
|
2374
2426
|
// 本 zod 只是**传输门**(防 DoS/坏形早拒);上限=[[镜像常量]],见 PROMPT_ARTIFACT_LIMITS。
|
|
2375
|
-
/** core `ARTIFACT_LIMITS` 的镜像常量(
|
|
2376
|
-
*
|
|
2427
|
+
/** core `ARTIFACT_LIMITS` 的镜像常量(本包不加 core 运行期依赖轴 ⇒ 镜像)。对表钉住在**镜像的属主**这里:上游漂移门 ⑤ 逐键
|
|
2428
|
+
* === devDependency 钉住的那只 core 的导出 —— 改上限必须同版动两处、过钉。 */
|
|
2377
2429
|
export const PROMPT_ARTIFACT_LIMITS = {
|
|
2378
2430
|
maxSections: 64,
|
|
2379
2431
|
maxSectionBytes: 32_000,
|
|
@@ -2386,9 +2438,9 @@ export const PROMPT_ARTIFACT_LIMITS = {
|
|
|
2386
2438
|
* 为 UTF-16 码元数,core verify 按 UTF-8 字节复核——传输门宽、语义门准)。 */
|
|
2387
2439
|
export const PromptArtifactSection = z.object({
|
|
2388
2440
|
id: z.string().min(1).max(PROMPT_ARTIFACT_LIMITS.maxIdLength).regex(/^[a-z0-9][a-z0-9/._-]*$/),
|
|
2389
|
-
slot:
|
|
2441
|
+
slot: PromptSlot,
|
|
2390
2442
|
text: z.string().min(1).max(PROMPT_ARTIFACT_LIMITS.maxSectionBytes),
|
|
2391
|
-
contentHash:
|
|
2443
|
+
contentHash: Sha256Digest,
|
|
2392
2444
|
});
|
|
2393
2445
|
/** core `PromptCatalogSnapshot` 同形。packId=逻辑名(如 `center:acme@3`),历史身份恒在 digest。 */
|
|
2394
2446
|
export const PromptCatalogSnapshot = z.object({
|
|
@@ -2401,7 +2453,7 @@ export const PromptCatalogSnapshot = z.object({
|
|
|
2401
2453
|
export const PromptEpochArtifactPayload = z.object({
|
|
2402
2454
|
schemaVersion: z.literal(1),
|
|
2403
2455
|
assemblyApi: z.literal(1),
|
|
2404
|
-
catalogDigest:
|
|
2456
|
+
catalogDigest: Sha256Digest,
|
|
2405
2457
|
compatibleCore: z
|
|
2406
2458
|
.object({
|
|
2407
2459
|
minCore: z.string().regex(/^\d+\.\d+\.\d+$/).optional(),
|
|
@@ -2418,16 +2470,16 @@ export const PromptEpochArtifactPayload = z.object({
|
|
|
2418
2470
|
export const PublishedPromptArtifactEnvelope = z.object({
|
|
2419
2471
|
artifact: z.object({
|
|
2420
2472
|
payload: PromptEpochArtifactPayload,
|
|
2421
|
-
artifactDigest:
|
|
2473
|
+
artifactDigest: Sha256Digest,
|
|
2422
2474
|
}),
|
|
2423
2475
|
sourceRevision: z.string().min(1).max(256),
|
|
2424
2476
|
});
|
|
2425
|
-
/** prompts 域存储形 v1(
|
|
2477
|
+
/** prompts 域存储形 v1(web-admin 存储真形;[1043] PromptCatalogPayload 的 v1 最小面)。
|
|
2426
2478
|
* overlays/profiles/modelPolicies = 协议 §5.1 的后续 additive 面,留位不预设形(等 core S6
|
|
2427
2479
|
* catalog schema 班车,additive 补键零迁移)。 */
|
|
2428
2480
|
export const PromptsConfig = z.object({
|
|
2429
|
-
bindings: z.array(PromptBinding).max(256).
|
|
2430
|
-
/** catalog 轴存储位(0.10.20,[985]a):
|
|
2481
|
+
bindings: z.array(PromptBinding).max(256).prefault([]),
|
|
2482
|
+
/** catalog 轴存储位(0.10.20,[985]a):web-admin 发布门铸好的 immutable envelope 原样存(发布即
|
|
2431
2483
|
* 物化,店内零解析);local FileConfigStore lane 的 config.d/prompts.json 也可直接携带——catalog
|
|
2432
2484
|
* 免 covering 解析,两 lane 自此同形(结束 local lane prompts 不透传的过渡态,[1234]① 位)。 */
|
|
2433
2485
|
catalog: PublishedPromptArtifactEnvelope.optional(),
|
|
@@ -2438,17 +2490,22 @@ export const PromptsConfig = z.object({
|
|
|
2438
2490
|
* `promptsContentDigest`);sections=covering 已走完的基线终形(worker/core 零解析义务);
|
|
2439
2491
|
* scenarioOverrides[s]=该场景完整重算终形(整包替换非增量),无键=用基线;fail-closed=
|
|
2440
2492
|
* publish 闸关时整个 key 不出现。 */
|
|
2493
|
+
/** prompts 包身份 `packId` 的前缀(铸点 `promptsContentDigest` 与判官 {@link PromptPackId} 都引它)。值落在引擎的会话记录里、是
|
|
2494
|
+
* epoch 不误重 pin 的依据,所以值本身不随叫法归一改(换前缀 = 每条活会话的提示包身份翻一次,要引擎先读 epoch 判据)。 */
|
|
2495
|
+
export const PROMPT_PACK_ID_PREFIX = "center:";
|
|
2496
|
+
/** prompts 包身份的形:前缀 + 内容摘要的前 12 位小写 hex(纯内容寻址:同内容重发布 = 同 packId)。判它的每一处都引这一只。 */
|
|
2497
|
+
export const PromptPackId = z.string().regex(new RegExp(`^${PROMPT_PACK_ID_PREFIX}[0-9a-f]{12}$`));
|
|
2441
2498
|
export const EffectivePrompts = z.object({
|
|
2442
|
-
packId:
|
|
2443
|
-
contentDigest:
|
|
2499
|
+
packId: PromptPackId,
|
|
2500
|
+
contentDigest: Sha256Digest,
|
|
2444
2501
|
sections: z.array(PromptTextDeclaration),
|
|
2445
2502
|
scenarioOverrides: z.record(z.string(), z.array(PromptTextDeclaration)).optional(),
|
|
2446
2503
|
});
|
|
2447
2504
|
/** effective 下发形 v2(双轴,[985]a §1/[1242] Q1 裁:单 domain 单键、两轴是过渡窗内的两种表达)。
|
|
2448
2505
|
* declarations=现行 A 形(centerPromptProvider 继续消费);catalog=B 形(core S1 verify 管线的输入,
|
|
2449
2506
|
* server 原样交 core 不解释)。至少一轴必须在场(空对象=坏形);催收面语义照旧=publish 闸关时整个
|
|
2450
|
-
* `prompts` key 消失(fail-closed)
|
|
2451
|
-
* (
|
|
2507
|
+
* `prompts` key 消失(fail-closed)。引擎的候选门**只认本形**:没有 `schemaVersion` 的平铺 {@link EffectivePrompts} 体被响亮拒
|
|
2508
|
+
* (整个 prompts 面不采用,落最后一份好配置 / 内置)。 */
|
|
2452
2509
|
export const EffectivePromptsWire = z
|
|
2453
2510
|
.object({
|
|
2454
2511
|
schemaVersion: z.literal(1),
|
|
@@ -2467,18 +2524,17 @@ export const DOMAIN_SCHEMAS = {
|
|
|
2467
2524
|
collab: CollabConfig, // 0.7.0: replaces `teams` (EXPERT-REDESIGN §8/§9 同批换刀 — no editing gap)
|
|
2468
2525
|
workers: WorkersConfig,
|
|
2469
2526
|
hosts: HostsConfig,
|
|
2470
|
-
runtime: RuntimeConfig,
|
|
2471
2527
|
governance: GovernanceConfig,
|
|
2472
2528
|
entitlement: EntitlementConfig,
|
|
2473
2529
|
execution: ExecutionPolicy, // 0.8.0: S3 execution-lane policy (scope 级; client-advisory until service gate)
|
|
2474
2530
|
projects: ProjectsConfig, // 0.10.0: 142-S3 project identity ledger + 无仓铸造权威 (scope=tenant 轴; NOT portable — 租户身份数据)
|
|
2475
|
-
prompts: PromptsConfig, // 0.10.18:
|
|
2531
|
+
prompts: PromptsConfig, // 0.10.18: web-admin prompts 战役 v1([ref] 五定夺/产品终稿/M2 下发形;写门/store 携带解锁=[1064]②)
|
|
2476
2532
|
limits: LimitsConfig, // 0.19.0: cost/限流族热更新发布位(消费方 #322 批1;本地腿闭集缺口的源头修复)
|
|
2477
2533
|
// integrations + access: DELETED in 0.6.0 (EXPERT-REDESIGN §9) — parseDomain/effective/路由随之 404。
|
|
2478
2534
|
};
|
|
2479
2535
|
// ── Effective-config PROVENANCE (the unified shape TOC `sema config --effective` and TOB `/effective?principal=`
|
|
2480
2536
|
// step-5 trace both emit, so "why is this value what it is" renders identically across both seats). Settled
|
|
2481
|
-
// via
|
|
2537
|
+
// via web-admin control-plane design gate③ (see web-admin control-plane design note §R2-T/§10). A pure
|
|
2482
2538
|
// CONTRACT type — no runtime dep on the engine; the axis VALUES (enforcement/lockBehavior) mirror @sema-agent/
|
|
2483
2539
|
// core's safety vocabulary, validated by a dev-only conformance test (never a runtime import).
|
|
2484
2540
|
/** One provenance source in a key's resolution trail. Two orthogonal axes: the TOC file LAYER, or the TOB
|
|
@@ -2497,7 +2553,7 @@ export const SettingSource = z.discriminatedUnion("axis", [
|
|
|
2497
2553
|
* `lockBehavior` (BEHAVIOR / what the managed layer does) is orthogonal, only meaningful under managed enforcement. */
|
|
2498
2554
|
export const EffectiveKey = z.object({
|
|
2499
2555
|
key: z.string(),
|
|
2500
|
-
value: z.unknown(),
|
|
2556
|
+
value: z.unknown().optional(),
|
|
2501
2557
|
winningSource: SettingSource,
|
|
2502
2558
|
locked: z.boolean(),
|
|
2503
2559
|
enforcement: z.enum(["gate", "server", "resolver", "client"]),
|