@sema-agent/settings-schema 10.1.0 → 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 +419 -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/config-fns.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure config functions + data shapes — LIFTED verbatim out of
|
|
3
|
-
* depends only on `./types`, zero app/DB/next coupling).
|
|
2
|
+
* Pure config functions + data shapes — LIFTED verbatim out of web-admin's `store.ts` (the half that
|
|
3
|
+
* depends only on `./types`, zero app/DB/next coupling). web-admin re-exports these so its existing
|
|
4
4
|
* `@/lib/config` importers keep working unchanged (ONE copy of the code, here).
|
|
5
5
|
*
|
|
6
6
|
* Two DETERMINISM refactors vs. the original store.ts (so the package is wall-clock-free and a local file
|
|
@@ -10,15 +10,16 @@
|
|
|
10
10
|
* 2. `emptyEffective()` seeds the epoch (`new Date(0).toISOString()`) instead of the wall clock, matching
|
|
11
11
|
* the stores' existing "epoch sentinel" reduce seed.
|
|
12
12
|
*
|
|
13
|
-
* The fat `ConfigStore` interface itself STAYS in
|
|
13
|
+
* The fat `ConfigStore` interface itself STAYS in web-admin (app-only, ~32 publish/RBAC/worker-status
|
|
14
14
|
* methods); only the narrow {@link ConfigReader} (reader.ts) is carried here.
|
|
15
15
|
*/
|
|
16
16
|
// zod is a VALUE import here (not just a type): {@link salvageEntries} introspects a domain schema to find
|
|
17
17
|
// its entry collections. zod is this package's one runtime dependency, so this costs nothing new.
|
|
18
18
|
import { z } from "zod";
|
|
19
19
|
import { MODEL_ENTRY_REFUSAL_CODES, ModelRefusalsWire } from "./fleet.js";
|
|
20
|
-
import { carriesUserinfo, secretScanForms } from "./secret-scan.js"; // package-private
|
|
21
|
-
import {
|
|
20
|
+
import { carriesUserinfo, secretScanForms } from "./secret-scan.js"; // package-private normalization (both scanners read it) + the userinfo judgement (not in the barrel)
|
|
21
|
+
import { unwrapSchema } from "./zod-unwrap.js"; // package-private: the ONE reading of zod's wrapper layers (salvage + the one-time migrations share it)
|
|
22
|
+
import { DOMAIN_SCHEMAS, InfraCostRates, LimitsConfig, PRIVATE_KEY_PEM_HEADER, RETIRED_DOMAINS, SkillsManifestSchema, containsSecretToken, isDomainName, isRetiredDomainName, renderQuotedSentence, retiredDomainSentence } from "./types.js";
|
|
22
23
|
/** Redact secret-shaped tokens from a validation MESSAGE — a zod error echoes the offending value (e.g.
|
|
23
24
|
* `received 'sk-live-…'`), so a secret pasted into the WRONG field would leak via the error string. Redacts
|
|
24
25
|
* space-spanning auth credentials (`Bearer <token>`) FIRST (the token pass can't see across the space), then
|
|
@@ -26,9 +27,8 @@ import { DOMAIN_SCHEMAS, GOVERNANCE_MIRROR_KEYS, HOSTS_ARRAY_MAX, HOST_NAME_MAX,
|
|
|
26
27
|
* containsSecretToken). invariant ①: a secret VALUE never rides an error. R13/R14.
|
|
27
28
|
*
|
|
28
29
|
* **Output invariant** (S-712 verification pass N1 + codex review [high] ×2; 6.3.0): a string holding a private-key
|
|
29
|
-
* PEM header ANYWHERE ({@link PRIVATE_KEY_PEM_HEADER}) in ANY of its {@link secretScanForms} `forms` is withheld WHOLE,
|
|
30
|
-
*
|
|
31
|
-
* any of its forms.
|
|
30
|
+
* PEM header ANYWHERE ({@link PRIVATE_KEY_PEM_HEADER}) in ANY of its {@link secretScanForms} `forms` is withheld WHOLE, and no
|
|
31
|
+
* result trips `containsSecretToken` (which counts an unfinished normalization as secret-shaped, 11.0.0).
|
|
32
32
|
* The whole-string judgement is ONE rule — both detectors run over the normalized forms: the PEM header on every form of
|
|
33
33
|
* the ORIGINAL text, `containsSecretToken` on every form of the targeted result; what could not be normalized does not go
|
|
34
34
|
* out (6.3.0, codex r1 F1: `-----BEGIN+PRIVATE+KEY` form-encoded headers and four-layer encodings both rode out).
|
|
@@ -40,45 +40,97 @@ import { DOMAIN_SCHEMAS, GOVERNANCE_MIRROR_KEYS, HOSTS_ARRAY_MAX, HOST_NAME_MAX,
|
|
|
40
40
|
* key body after it would ride out untouched — codex verification round). A PEM header carries spaces, so the
|
|
41
41
|
* targeted passes can never isolate it. Every free-text warning face that goes through here (coordinates, error text)
|
|
42
42
|
* inherits the same guarantee; the entry identity face additionally has to be an identifier ({@link isDisclosableIdentity}).
|
|
43
|
-
* Write and display
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
|
|
48
|
-
*
|
|
43
|
+
* Write and display are ONE scanner since 11.0.0 (S-1274): `containsSecretToken` reads the same `secretScanForms` forms through the
|
|
44
|
+
* same shape table, so "withheld on display" and "refused on write" no longer disagree on any form. The redactor keeps its own PEM
|
|
45
|
+
* check on the ORIGINAL text's forms only because the token pass above can rewrite the header away before the scanner sees the
|
|
46
|
+
* result. */
|
|
47
|
+
/** What separates the candidate segments of a free text (a URL candidate is a run between these): whitespace only —— quotes,
|
|
48
|
+
* angle brackets and backticks can sit inside a URL password (codex r1), so a quoted URL goes out as one segment, quotes and all.
|
|
49
|
+
* Split (linear), not a scanning regex — a scanning `[^…]*@[^…]*` is quadratic on a long run without `@`. */
|
|
50
|
+
const OUTGOING_RUN_DELIMITERS = /(\s+)/;
|
|
49
51
|
export function redactSecrets(text) {
|
|
50
52
|
const targeted = text
|
|
51
53
|
.replace(/\b(bearer|basic)(\s+)\S+/gi, "$1$2[redacted-secret]")
|
|
54
|
+
.split(OUTGOING_RUN_DELIMITERS)
|
|
55
|
+
.map((run, i) => (i % 2 === 0 && carriesUserinfo(run) ? "[redacted-secret]" : run)) // a URL candidate segment carrying userinfo ⇒ the whole segment (S-811)
|
|
56
|
+
.join("")
|
|
52
57
|
.replace(/[A-Za-z0-9_./:+=%-]{6,}/g, (tok) => (containsSecretToken(tok) ? "[redacted-secret]" : tok));
|
|
58
|
+
// What could not be normalized does not go out: the scanner counts an unfinished normalization as secret-shaped, so such a run is
|
|
59
|
+
// either replaced by the targeted pass above or still in `targeted` — where `containsSecretToken` withholds the whole string.
|
|
60
|
+
// A credential pair only visible on a normalized form (percent-encoded `https%3A%2F%2Fu%3Apw%40h`) withholds the whole string too.
|
|
53
61
|
const scan = secretScanForms(text);
|
|
54
62
|
const targetedForms = targeted === text ? scan.forms : secretScanForms(targeted).forms; // 没替换过(最常见)⇒ 同一组形,不再归一化一遍
|
|
55
|
-
const whole = scan.
|
|
63
|
+
const whole = scan.forms.some((form) => PRIVATE_KEY_PEM_HEADER.test(form)) || containsSecretToken(targeted) || targetedForms.some((form) => carriesUserinfo(form));
|
|
56
64
|
return whole ? "[redacted-secret]" : targeted;
|
|
57
65
|
}
|
|
58
|
-
/** Sanitize a ZodError IN PLACE so no surfaced error (message /
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
66
|
+
/** Sanitize a ZodError IN PLACE so no surfaced error (message / any issue field / nested issues) echoes a pasted secret.
|
|
67
|
+
* Every string an issue carries is redacted, string path segments included, and the walk RECURSES into the issues zod nests
|
|
68
|
+
* inside an issue — `invalid_union.errors` (one issue list per union member, e.g. RoleTarget `{model}|{select}`) and
|
|
69
|
+
* `invalid_key` / `invalid_element`.issues — because a nested issue's message can echo the same value. Non-string path
|
|
70
|
+
* segments other than numbers (zod types a path as `PropertyKey[]`) leave as their redacted `String()` form, so a path read
|
|
71
|
+
* off a sanitized error is always `string | number`. The error's `message` is re-derived from the
|
|
72
|
+
* sanitized issues at the end (zod caches it on first read, so an earlier read would otherwise keep the raw text).
|
|
73
|
+
* The ONE sanitizer every validation entry point uses (parseDomain / validateDomain / validateRemoteExec / write*). R14. */
|
|
62
74
|
export function sanitizeZodError(err) {
|
|
63
|
-
for (const iss of err.issues)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const v = rec[k];
|
|
67
|
-
if (typeof v === "string")
|
|
68
|
-
rec[k] = redactSecrets(v);
|
|
69
|
-
else if (Array.isArray(v)) {
|
|
70
|
-
rec[k] = v.map((el) => {
|
|
71
|
-
if (typeof el === "string")
|
|
72
|
-
return redactSecrets(el);
|
|
73
|
-
if (el && typeof el === "object" && Array.isArray(el.issues))
|
|
74
|
-
return sanitizeZodError(el); // nested unionErrors
|
|
75
|
-
return el;
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
75
|
+
for (const iss of err.issues)
|
|
76
|
+
sanitizeIssueFields(iss, 0);
|
|
77
|
+
err.message = JSON.stringify(err.issues, null, 2);
|
|
80
78
|
return err;
|
|
81
79
|
}
|
|
80
|
+
/** Nesting bound for {@link sanitizeIssueFields}: zod nests one level per union / key / element boundary; anything deeper than
|
|
81
|
+
* this is not a shape zod produces, and a value the walk does not reach is withheld rather than passed through. */
|
|
82
|
+
const SANITIZE_MAX_DEPTH = 32;
|
|
83
|
+
function sanitizeIssueFields(rec, depth) {
|
|
84
|
+
if (typeof rec.code === "string" && typeof rec.message === "string")
|
|
85
|
+
boundIssueIdentifiers(rec);
|
|
86
|
+
for (const k of Object.keys(rec))
|
|
87
|
+
rec[k] = sanitizeIssueValue(rec[k], depth + 1);
|
|
88
|
+
}
|
|
89
|
+
/** S-815(11.0.0):一条 issue 里**用户写的标识串** —— 坐标的字符串段(`path`)、zod 回显的未知键(`keys`)、自定义问题句里的操作数
|
|
90
|
+
* (`params.operands`,{@link QuotedIssueParams})—— 出门前过告警坐标段同一道 {@link outgoingSegment}(长过 256 ⇒ 整段扣,否则脱敏),
|
|
91
|
+
* 句子由同一组出门形重铸:带 `params` 的按模板重渲染({@link renderQuotedSentence}),zod 自己的句子把每个改过的原串换成它的出门形
|
|
92
|
+
* (长的先换)。之后整条 issue 照旧过逐字段脱敏。改前只有告警坐标段有上界:同一个 300 字的坏键在坐标里读 `[redacted-secret]`、
|
|
93
|
+
* 在 `keys` / `message` 里原样 —— 一条告警两种读法。 */
|
|
94
|
+
function boundIssueIdentifiers(rec) {
|
|
95
|
+
const changed = new Map();
|
|
96
|
+
const bound = (v) => {
|
|
97
|
+
if (typeof v !== "string")
|
|
98
|
+
return v;
|
|
99
|
+
const out = outgoingSegment(v);
|
|
100
|
+
if (out !== v)
|
|
101
|
+
changed.set(v, out);
|
|
102
|
+
return out;
|
|
103
|
+
};
|
|
104
|
+
if (Array.isArray(rec.path))
|
|
105
|
+
rec.path = rec.path.map(bound);
|
|
106
|
+
if (Array.isArray(rec.keys))
|
|
107
|
+
rec.keys = rec.keys.map(bound);
|
|
108
|
+
const params = rec.params;
|
|
109
|
+
if (params !== undefined && Array.isArray(params.template) && Array.isArray(params.operands)) {
|
|
110
|
+
const quoted = { template: params.template, operands: params.operands.map((o) => bound(o)) };
|
|
111
|
+
rec.params = { ...params, ...quoted };
|
|
112
|
+
rec.message = renderQuotedSentence(quoted);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
let message = rec.message;
|
|
116
|
+
for (const [raw, out] of [...changed].sort((a, b) => b[0].length - a[0].length))
|
|
117
|
+
message = message.split(raw).join(out);
|
|
118
|
+
rec.message = message;
|
|
119
|
+
}
|
|
120
|
+
function sanitizeIssueValue(v, depth) {
|
|
121
|
+
if (typeof v === "string")
|
|
122
|
+
return redactSecrets(v);
|
|
123
|
+
if (typeof v === "symbol")
|
|
124
|
+
return redactSecrets(String(v));
|
|
125
|
+
if (v === null || typeof v !== "object")
|
|
126
|
+
return v;
|
|
127
|
+
if (depth > SANITIZE_MAX_DEPTH)
|
|
128
|
+
return "[redacted-secret]";
|
|
129
|
+
if (Array.isArray(v))
|
|
130
|
+
return v.map((el) => sanitizeIssueValue(el, depth + 1));
|
|
131
|
+
sanitizeIssueFields(v, depth);
|
|
132
|
+
return v;
|
|
133
|
+
}
|
|
82
134
|
export const ROLE_RANK = { viewer: 0, editor: 1, publisher: 2, admin: 3 };
|
|
83
135
|
export const USER_ROLES = ["viewer", "editor", "publisher", "admin"];
|
|
84
136
|
/** True if `role` is at least `min` in the hierarchy (e.g. publisher satisfies editor). */
|
|
@@ -107,21 +159,21 @@ export const DOMAIN_WRITE_ROLE = {
|
|
|
107
159
|
// systems drive CREDENTIAL minting/delivery/rotation (S2/S3) — the credential lifecycle is ≥ the skills tier.
|
|
108
160
|
systems: "publisher",
|
|
109
161
|
// access: DELETED in 0.6.0 (域整删,EXPERT-REDESIGN §9)。
|
|
110
|
-
// governance (治理三件,0.6.0 从 runtime 拆出): editor(EXPERT-REDESIGN §5 domainWriteRole
|
|
111
|
-
//
|
|
162
|
+
// governance (治理三件,0.6.0 从 runtime 拆出): editor(EXPERT-REDESIGN §5 domainWriteRole 同步;
|
|
163
|
+
// default 即 editor,故不列条目;此注释即该裁定的落点)。
|
|
112
164
|
// entitlement governs who-can-USE-what (models/skills/mcp/budget/runtime caps) per principal — a governance +
|
|
113
165
|
// security surface (budget ceilings, bypass kill-switch) on par with systems. Same raised gate.
|
|
114
166
|
entitlement: "publisher",
|
|
115
167
|
// prompts (0.10.19, [ref]③/②d): declaration text goes STRAIGHT into the system prompt of
|
|
116
168
|
// every worker×scenario the binding covers — the same injection surface (and rationale) as skills/mcp,
|
|
117
|
-
// so the same raised gate.
|
|
169
|
+
// so the same raised gate. Web-admin had pre-raised this locally per governance precedent; this row is the
|
|
118
170
|
// contract-side source of truth so every consumer of the shared write gate gets publisher, not editor.
|
|
119
171
|
prompts: "publisher",
|
|
120
172
|
// execution (0.8.0): editor — same档 as governance (治理旋钮同类;真安全边界在 service gate,
|
|
121
173
|
// client-advisory 面不值 publisher 档)。default 即 editor,故不列条目;此注释即该裁定的落点。
|
|
122
174
|
// projects (0.10.0, 142-S3): editor — 登记簿是身份索引不是注入面(无 prompt/凭据/可执行内容);
|
|
123
175
|
// mint/claim 动作端点与编辑面同档(草案 §3 "该 scope editor+")。default 即 editor,故不列条目。
|
|
124
|
-
// limits (0.19.0): editor ——
|
|
176
|
+
// limits (0.19.0): editor —— 与它的前身 runtime 域(11.0.0 retired)同档,刻意不抬 publisher:本域是纯数值
|
|
125
177
|
// 旋钮,无 prompt 注入面、无凭据、无可执行内容;真正的执法点在消费方的限额闸,发布闸(PUBLISH_MODE /
|
|
126
178
|
// 审批流)才是"要不要多一道人"的属主。default 即 editor,故不列条目;此注释即该裁定的落点。
|
|
127
179
|
};
|
|
@@ -135,8 +187,39 @@ export function domainWriteRole(domain) {
|
|
|
135
187
|
export function emptyEffective() {
|
|
136
188
|
return buildEffective(0, () => undefined, new Date(0).toISOString());
|
|
137
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* 一个**retired 域**的文档带着事实 —— 读存储文档的每个读者同一只拒(`parseDomain` 写、`buildEffective` 向店要、`readEffectiveWire` 看 wire、
|
|
192
|
+
* `FileConfigStore` 与 `readBundle` 读 `config.d/`)。`message` 是墓碑表那一行的整句(点名retired版本、事实今天住哪、改写存量的一次性迁移);
|
|
193
|
+
* `code` 是机读码,`domain` / `migration` 是那一行的两格。墓碑是拒,不是读法:它从不把旧文档读成任何值。
|
|
194
|
+
*/
|
|
195
|
+
export class RetiredDomainError extends Error {
|
|
196
|
+
domain;
|
|
197
|
+
code = "retired_domain";
|
|
198
|
+
migration;
|
|
199
|
+
constructor(domain) {
|
|
200
|
+
super(retiredDomainSentence(domain));
|
|
201
|
+
this.domain = domain;
|
|
202
|
+
this.name = "RetiredDomainError";
|
|
203
|
+
this.migration = RETIRED_DOMAINS[domain].migration;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** 一份retired 域文档**带不带事实**:缺席与 `{}` 不带(没有可丢的东西,读者不理会);其余一切(含 `null` / 非对象 / 有键的对象)都带 ⇒ 拒。 */
|
|
207
|
+
function carriesFacts(raw) {
|
|
208
|
+
if (raw === undefined)
|
|
209
|
+
return false;
|
|
210
|
+
return !(isPlainJsonObject(raw) && Object.keys(raw).length === 0);
|
|
211
|
+
}
|
|
212
|
+
/** 墓碑判一次:`raw` 是retired 域 `domain` 的文档,带事实 ⇒ 抛 {@link RetiredDomainError}。 */
|
|
213
|
+
export function refuseRetiredDomain(domain, raw) {
|
|
214
|
+
if (carriesFacts(raw))
|
|
215
|
+
throw new RetiredDomainError(domain);
|
|
216
|
+
}
|
|
138
217
|
/** Validate + fill defaults for a domain payload (or the schema's empty default when absent/invalid). */
|
|
139
218
|
export function parseDomain(domain, raw) {
|
|
219
|
+
// A retired domain is refused by its tombstone (one sentence naming the one-time migration), not as "not a domain":
|
|
220
|
+
// a writer still submitting it needs to know where its keys went.
|
|
221
|
+
if (isRetiredDomainName(domain))
|
|
222
|
+
throw new RetiredDomainError(domain);
|
|
140
223
|
// A name that is not a domain is refused for what it is — before, `DOMAIN_SCHEMAS[name]` was undefined and the
|
|
141
224
|
// TypeError surfaced through the guard below as "a value could not be read", which names the wrong failure. The
|
|
142
225
|
// strict-write face hands this function every key of a submitted document set, so the name is user text: it leaves
|
|
@@ -155,41 +238,7 @@ export function parseDomain(domain, raw) {
|
|
|
155
238
|
}
|
|
156
239
|
if (r.success)
|
|
157
240
|
return r.data;
|
|
158
|
-
throw sanitizeZodError(r.error); // recursive redaction (message/
|
|
159
|
-
}
|
|
160
|
-
// ── 逐条容错律(2.0.0,S-144 按类修)───────────────────────────────────────────────────────────────
|
|
161
|
-
//
|
|
162
|
-
// 病:域解析整域 all-or-nothing —— 13 个模型里坏一条,整张目录退成 schema 默认 = 一条都不剩。缓解一直
|
|
163
|
-
// 是**逐键**打补丁(把新键改成 `z.unknown()` 让它别拒),那是给每个新键加特判,规则只会越来越多;
|
|
164
|
-
// 而本仓的调度存储早就有成熟的逐条容错形(坏记录逐条丢 + 逐条上报,其余全存活),只是没抬到域面。
|
|
165
|
-
//
|
|
166
|
-
// 本节把那条判据提成**域无关的单铸点**:`salvageEntries(domain, raw)` 从域 schema 自身派生出「哪些字段
|
|
167
|
-
// 是条目集合」(数组 / Record),逐条 parse,坏的丢掉并点名。域面唯一的调用点是 `buildEffective` 的
|
|
168
|
-
// `parseOr`,且**只在整域解析已经失败之后**才跑 —— 好配置的解析路径一字不动(与 0.8.1 的 grandfather
|
|
169
|
-
// 同姿势:先精确、后通用、成功路径 byte 不变)。
|
|
170
|
-
//
|
|
171
|
-
// 🔴 不新开档位:容错只发生在 {@link DOMAIN_READ_FALLBACK} 判 `"default"` 的域。gate 域(rosters /
|
|
172
|
-
// governance / entitlement / execution / workers / runtime / limits / projects)坏条目仍然 throw ——
|
|
173
|
-
// 悄悄丢掉一条 roster 就是那个 worker 静默拿到全量目录,与整域降级是同一条「放权,不许」的理由,所以
|
|
174
|
-
// 共用同一张表,不为容错另立一张。
|
|
175
|
-
/** 展开 zod 的包装层(`.optional()` / `.default()` / `.nullable()` / `.catch()` / `.transform()` /
|
|
176
|
-
* `.superRefine()`),拿到里面真正的那个 schema。包装不改变「条目能不能单独解析」这件事。 */
|
|
177
|
-
function unwrapSchema(schema) {
|
|
178
|
-
let cur = schema;
|
|
179
|
-
// 有界:包装层数是 schema 作者写出来的常数,8 层远超实际(防被畸形 schema 拖住)。
|
|
180
|
-
for (let i = 0; i < 8; i++) {
|
|
181
|
-
if (cur instanceof z.ZodEffects)
|
|
182
|
-
cur = cur.innerType();
|
|
183
|
-
else if (cur instanceof z.ZodOptional || cur instanceof z.ZodNullable)
|
|
184
|
-
cur = cur.unwrap();
|
|
185
|
-
else if (cur instanceof z.ZodDefault)
|
|
186
|
-
cur = cur.removeDefault();
|
|
187
|
-
else if (cur instanceof z.ZodCatch)
|
|
188
|
-
cur = cur.removeCatch();
|
|
189
|
-
else
|
|
190
|
-
return cur;
|
|
191
|
-
}
|
|
192
|
-
return cur;
|
|
241
|
+
throw sanitizeZodError(r.error); // recursive redaction (message / every issue field / nested issues) — no secret echo. R13/R14.
|
|
193
242
|
}
|
|
194
243
|
/** 一个**逐字出门**的字符串段的样子 —— 一条判据,不是一串特例:**逐字写出去之后读回来只能是它自己**。
|
|
195
244
|
* 所以四类字符串段必须改走引号形:空串、含 `.`(段分隔符)、含 `"`(引号形自身)或控制字符的,以及
|
|
@@ -207,17 +256,19 @@ const PLAIN_PATH_SEGMENT = /^(?!(?:0|[1-9]\d*)$)[^."\u0000-\u001f]+$/;
|
|
|
207
256
|
export function formatConfigPath(p) {
|
|
208
257
|
return p.map((seg) => (typeof seg === "number" ? String(seg) : PLAIN_PATH_SEGMENT.test(seg) ? seg : JSON.stringify(seg))).join(".");
|
|
209
258
|
}
|
|
210
|
-
/**
|
|
259
|
+
/** 带用户写的键段的坐标出门前的**唯一**一道(6.3.0,codex r1 F2):字符串段过 {@link outgoingSegment} —— 与 `error` 文案
|
|
211
260
|
* ({@link sanitizeZodError})、身份否决({@link isDisclosableIdentity})**同一只**脱敏器,不另设判据;数字段(下标)原样。
|
|
212
261
|
* 键段是用户写的,完全可能就是一条粘错位置的凭据:被丢 Record 条目的键、未知键告警(`unknown-keys-dropped` /
|
|
213
262
|
* `-carried`)的键名都一样(后者 0.19.0 起从不过脱敏器,`sk-…` 形的未知键名原样进告警与投影)。schema 已知的字段名
|
|
214
263
|
* 脱敏器原样放行,所以坐标里「它是哪个字段」照旧可读。经它的坐标就是出门形 —— {@link droppedKeys} /
|
|
215
|
-
* {@link carriedUnknownKeys}
|
|
216
|
-
*
|
|
217
|
-
|
|
264
|
+
* {@link carriedUnknownKeys} 这两只扫描交给告警的 `keys` 同样是它的产物。
|
|
265
|
+
*
|
|
266
|
+
* 11.0.0 起导出:读告警之外还有两处铸坐标的面 —— 一次性迁移的收据(`./one-time-migrations`)与 workers 的逐行终判(`readWorkersRows`
|
|
267
|
+
* 的 `held[].field`),它们与告警走同一道,不另抄一份判据;消费方自己拼坐标出门(日志 / 界面)时也调它。 */
|
|
268
|
+
export function outgoingPath(p) {
|
|
218
269
|
return p.map((seg) => (typeof seg === "string" ? outgoingSegment(seg) : seg));
|
|
219
270
|
}
|
|
220
|
-
/** 一段用户写的串(
|
|
271
|
+
/** 一段用户写的串(坐标键段、域名)出门前的那一道:长过 {@link DISCLOSABLE_IDENTITY_MAX}(与身份白名单同一只上界)
|
|
221
272
|
* ⇒ 不扫描、整段 `[redacted-secret]`(标识符不会那么长;fail-closed 且 O(1) —— opus 独立腿 M1:6.3.0 首稿让每个未知键名
|
|
222
273
|
* 不论多长都过脱敏器,64 个 16 KB 的键名把一次读拖到 20 s 以上);否则过 {@link redactSecrets}。 */
|
|
223
274
|
function outgoingSegment(seg) {
|
|
@@ -233,8 +284,7 @@ function outgoingSegment(seg) {
|
|
|
233
284
|
* 为什么是词而不是 `error` 句:句子随 zod 小版本改字、可经脱敏,拿它当判据就是拿实现当契约;wire 上也不许有自由文本。
|
|
234
285
|
* 这张表由 `test/dropped-entry-reason-code-s1295.test.ts` 的两格咬住:穷举 `switch`(给表加词而不补臂 ⇒ `tsc` 红,门 = `npm run typecheck`)与
|
|
235
286
|
* 满射格(每个词至少有一个真丢弃点触发它 = 没有死词;每个被丢条目的词都在表里 = 没有无词的丢弃点)。
|
|
236
|
-
*
|
|
237
|
-
* (`hosts-grandfathered`,主机名超长)走它自己的告警臂,不是 `DroppedEntry`,不在本表。
|
|
287
|
+
* 读路径上剔条目的只有 `salvageEntries` 这一处(11.0.0 起 hosts 的读侧夹紧臂已retired),所以本表覆盖读路径上每一条被丢的条目。
|
|
238
288
|
*/
|
|
239
289
|
export const DROPPED_ENTRY_REASON_CODES = ["entry-schema-invalid", "entry-key-invalid", "entry-unreadable"];
|
|
240
290
|
/** 可披露身份与出门坐标段的**长度上界**(一只常量,两处用:{@link DISCLOSABLE_IDENTITY_RE} 的上限、{@link outgoingSegment}
|
|
@@ -254,8 +304,8 @@ export const DISCLOSABLE_IDENTITY_RE = new RegExp(`^[A-Za-z0-9._:/@+-]{1,${DISCL
|
|
|
254
304
|
* ② **否决**(「无密钥形」):坐标脱敏器 {@link redactSecrets} 原样放行 —— 集内照样有密钥形(`sk-…` / `ghp_…` /
|
|
255
305
|
* `AKIA…` / 三段 JWT 的字符全在集内),而且判它的必须是坐标段、`error` 文案({@link sanitizeZodError})用的**同一只**
|
|
256
306
|
* 脱敏器:两只判据会在同一条告警里给出两种答案(对抗轮 #2 真复现;集内的残例 = `@` 之后的单点 `eyJ…` 串,裸探测器
|
|
257
|
-
* 判阴、脱敏器会改,语料 c41 钉着)
|
|
258
|
-
* (
|
|
307
|
+
* 判阴、脱敏器会改,语料 c41 钉着)。带 userinfo 的 URL(opus 腿 L1:`postgres://svc:pw@h` 的字符全在集内)由同一只脱敏器扣下
|
|
308
|
+
* (11.0.0 S-811 起它对 URL 候选段问 `carriesUserinfo`),不再单列一条否决。
|
|
259
309
|
* 入参是 `string`、返回 `boolean`,**刻意不是**类型谓词(`v is string`):它对一些字符串答 false,谓词形会让调用方的
|
|
260
310
|
* 否定分支把一个 `string` 收窄成 `never`;「是不是字符串」由调用点自己判(运行期收到非字符串照答 false、不抛)。
|
|
261
311
|
* 不可披露的串按**无名**处置(不带,而不是打码:打过码的串不再是身份)。
|
|
@@ -263,7 +313,7 @@ export const DISCLOSABLE_IDENTITY_RE = new RegExp(`^[A-Za-z0-9._:/@+-]{1,${DISCL
|
|
|
263
313
|
* (经 {@link isReportableRefusalName},S-768);引擎自己的日志 / 拒因句里要不要写出一个名字,同样问这一只。 */
|
|
264
314
|
export function isDisclosableIdentity(v) {
|
|
265
315
|
// 非字符串(JS / any 调用方)⇒ false,不抛(秘密轴 fail-closed)
|
|
266
|
-
return typeof v === "string" && DISCLOSABLE_IDENTITY_RE.test(v) && redactSecrets(v) === v
|
|
316
|
+
return typeof v === "string" && DISCLOSABLE_IDENTITY_RE.test(v) && redactSecrets(v) === v;
|
|
267
317
|
}
|
|
268
318
|
/** S-768 —— 「一个目录条目名能不能当 {@link ModelRefusalsWire} `entries` 的键」的**唯一**判据;引擎铸键前只调这一只,
|
|
269
319
|
* 不过 ⇒ 按无名处置(不进键、不计数)。两条律的合取,不手抄任何一条:
|
|
@@ -291,8 +341,8 @@ function readEntryIdentity(entry) {
|
|
|
291
341
|
const id = own("id");
|
|
292
342
|
const enabled = own("enabled");
|
|
293
343
|
const identity = {
|
|
294
|
-
...(typeof name === "string"
|
|
295
|
-
...(typeof id === "string"
|
|
344
|
+
...(typeof name === "string" ? (isDisclosableIdentity(name) ? { name } : { nameWithheld: true }) : {}),
|
|
345
|
+
...(typeof id === "string" ? (isDisclosableIdentity(id) ? { id } : { idWithheld: true }) : {}),
|
|
296
346
|
...(typeof enabled === "boolean" ? { enabled } : {}),
|
|
297
347
|
};
|
|
298
348
|
return Object.keys(identity).length > 0 ? identity : undefined;
|
|
@@ -409,8 +459,8 @@ function salvageEntriesUnsafe(schema, raw) {
|
|
|
409
459
|
}
|
|
410
460
|
}
|
|
411
461
|
else if (collection instanceof z.ZodRecord && isPlainJsonObject(value)) {
|
|
412
|
-
const keySchema = collection.
|
|
413
|
-
const valueSchema = collection.
|
|
462
|
+
const keySchema = collection.keyType;
|
|
463
|
+
const valueSchema = collection.valueType;
|
|
414
464
|
const bad = [];
|
|
415
465
|
const kept = {};
|
|
416
466
|
for (const [key, entry] of Object.entries(value)) {
|
|
@@ -434,41 +484,21 @@ function salvageEntriesUnsafe(schema, raw) {
|
|
|
434
484
|
* Shared GC predicate for worker-status rows (F10) — keeps memory/sqlite/tidb identical. Given the rows'
|
|
435
485
|
* `{name, updatedAt}` and the GC options, returns the names to prune (A: not in `keepNames`; B: older than
|
|
436
486
|
* `ttlMs`). Pass neither selector → prunes nothing (guards against a wipe-all). See `ConfigStore.gcWorkerStatuses`.
|
|
487
|
+
*
|
|
488
|
+
* `now` is REQUIRED (11.0.0): this package never reads the wall clock (the determinism rule in the file header) — the
|
|
489
|
+
* TTL arm used to fall back to `new Date()`, the one argless clock left in the package. The caller owns the time.
|
|
437
490
|
*/
|
|
438
491
|
export function statusRowsToPrune(rows, opts) {
|
|
439
492
|
if (opts.keepNames === undefined && opts.ttlMs === undefined)
|
|
440
493
|
return [];
|
|
494
|
+
if (!(opts.now instanceof Date) || Number.isNaN(opts.now.getTime()))
|
|
495
|
+
throw new Error("statusRowsToPrune: opts.now must be a valid Date (the caller owns the clock)");
|
|
441
496
|
const keep = opts.keepNames ? new Set(opts.keepNames) : undefined;
|
|
442
|
-
const cutoff = opts.ttlMs !== undefined ?
|
|
497
|
+
const cutoff = opts.ttlMs !== undefined ? opts.now.getTime() - opts.ttlMs : undefined;
|
|
443
498
|
return rows
|
|
444
499
|
.filter((r) => (keep !== undefined && !keep.has(r.name)) || (cutoff !== undefined && Date.parse(r.updatedAt) < cutoff))
|
|
445
500
|
.map((r) => r.name);
|
|
446
501
|
}
|
|
447
|
-
/**
|
|
448
|
-
* Merge every domain into the effective config a consumer pulls. Shared by all store impls.
|
|
449
|
-
* `updatedAt` is REQUIRED (the determinism refactor — the original store.ts defaulted it to the wall clock,
|
|
450
|
-
* which made `emptyEffective`/the memory store non-deterministic). Every caller now supplies it explicitly.
|
|
451
|
-
*/
|
|
452
|
-
/** Lift the governance trio out of a LEGACY runtime doc (pre-0.6 stored runtime carried autonomy/commandPolicy/
|
|
453
|
-
* approvalRequire). Used as the governance domain's read fallback when governance was NEVER set — so a 0.5
|
|
454
|
-
* store upgrades with zero data migration and zero gate loss. An explicitly-set governance domain WINS
|
|
455
|
-
* (its absence-of-a-key then means "center not managing", never "fall back to legacy runtime").
|
|
456
|
-
*
|
|
457
|
-
* SUNSET (2.0.0): removal is a **persistent-data** break — a store still carrying the 0.5 shape would lose
|
|
458
|
-
* its guardrails silently, so this arm cannot simply be deleted at a minor. It goes at the next MAJOR, with
|
|
459
|
-
* the standard persistent-data treatment (a loud refusal naming the domain + a one-time migration on the
|
|
460
|
-
* writer side), and the closing condition is observable rather than a promise: `buildEffective` now emits
|
|
461
|
-
* `legacy-governance-lifted` every time the arm actually fires, so a deployment can prove it no longer does. */
|
|
462
|
-
export function legacyGovernanceFromRuntime(rawRuntime) {
|
|
463
|
-
if (rawRuntime === null || typeof rawRuntime !== "object")
|
|
464
|
-
return undefined;
|
|
465
|
-
const r = rawRuntime;
|
|
466
|
-
const lifted = {};
|
|
467
|
-
for (const k of GOVERNANCE_MIRROR_KEYS)
|
|
468
|
-
if (r[k] !== undefined)
|
|
469
|
-
lifted[k] = r[k];
|
|
470
|
-
return Object.keys(lifted).length ? lifted : undefined;
|
|
471
|
-
}
|
|
472
502
|
/** {@link EffectiveReadWarning} 的 `kind` **闭集**(运行期值)。一致性 kit 的覆盖律按它计数,消费方要遍历
|
|
473
503
|
* 「所有告警种类」也用它 —— 属主在告警联合自己的模块里,`./conformance` 只再导出(4.0.0 前它住在 kit 里,
|
|
474
504
|
* 那边是联合的下游,却握着联合的闭集)。
|
|
@@ -480,11 +510,9 @@ export function legacyGovernanceFromRuntime(rawRuntime) {
|
|
|
480
510
|
* 进本表」这半条缝,由 `test/warning-projection-derived-4.0.0.test.ts` 的闭集格看着(本表 == 从 AST 解出的
|
|
481
511
|
* 臂集,逐字同集同序)。 */
|
|
482
512
|
export const WARNING_KINDS = [
|
|
483
|
-
"hosts-grandfathered",
|
|
484
513
|
"domain-defaulted",
|
|
485
514
|
"unknown-keys-dropped",
|
|
486
515
|
"unknown-keys-carried",
|
|
487
|
-
"legacy-governance-lifted",
|
|
488
516
|
"entry-dropped",
|
|
489
517
|
"unread-config-file",
|
|
490
518
|
];
|
|
@@ -497,8 +525,6 @@ export const WARNING_KINDS = [
|
|
|
497
525
|
*/
|
|
498
526
|
export function projectWarning(w) {
|
|
499
527
|
switch (w.kind) {
|
|
500
|
-
case "hosts-grandfathered":
|
|
501
|
-
return { kind: w.kind, domain: w.domain, droppedNames: w.droppedNames, ...(w.droppedTotal === undefined ? {} : { droppedTotal: w.droppedTotal }), ...(w.truncatedFrom === undefined ? {} : { truncatedFrom: w.truncatedFrom }) };
|
|
502
528
|
case "domain-defaulted":
|
|
503
529
|
return { kind: w.kind, domain: w.domain };
|
|
504
530
|
// 两臂字段相同但**不合并**:合并后 `kind` 塌成两员的联合,TS 就再也认不出它属于投影联合的哪一员
|
|
@@ -507,8 +533,6 @@ export function projectWarning(w) {
|
|
|
507
533
|
return { kind: w.kind, domain: w.domain, keys: w.keys, ...(w.truncatedFrom === undefined ? {} : { truncatedFrom: w.truncatedFrom }) };
|
|
508
534
|
case "unknown-keys-carried":
|
|
509
535
|
return { kind: w.kind, domain: w.domain, keys: w.keys, ...(w.truncatedFrom === undefined ? {} : { truncatedFrom: w.truncatedFrom }) };
|
|
510
|
-
case "legacy-governance-lifted":
|
|
511
|
-
return { kind: w.kind, domain: w.domain, keys: w.keys };
|
|
512
536
|
case "entry-dropped":
|
|
513
537
|
return { kind: w.kind, domain: w.domain, path: w.path, ...(w.identity === undefined ? {} : { identity: w.identity }), ...(w.reasonCode === undefined ? {} : { reasonCode: w.reasonCode }) };
|
|
514
538
|
case "unread-config-file":
|
|
@@ -537,19 +561,18 @@ export function projectWarning(w) {
|
|
|
537
561
|
* **闭集穷举**:`switch` 覆盖 {@link EffectiveReadWarning} 的每个 `kind`,新 kind 不声明处置 = 编译红
|
|
538
562
|
* (#157 的闭集纪律)。只分两类,没有第三类:**带坐标的回写,不带坐标的原样返回**。
|
|
539
563
|
*
|
|
540
|
-
* 为什么 `entry-dropped`
|
|
541
|
-
* 说的就是这件事。{@link salvageEntries} 铸 `entry-dropped`
|
|
542
|
-
*
|
|
543
|
-
*
|
|
564
|
+
* 为什么 `entry-dropped` 也有回写臂:它**不是**「恒为原坐标」,只是不经**铸它那一层自己**的表 —— 上面那条律
|
|
565
|
+
* 说的就是这件事。{@link salvageEntries} 铸 `entry-dropped` 时用的是它**入参**那份文档的坐标;若那份文档本身被外层
|
|
566
|
+
* 压缩过,外层照样要回写它一次。今天读路径上只有一个压缩点(11.0.0 起 hosts 的读侧夹紧臂已retired),所以这一臂当下
|
|
567
|
+
* 只对 `keptIndex` 为空的表恒等;把「不动」写成一条臂的性质,就是把「哪一层」这件事错记在了「哪个 kind」上。
|
|
544
568
|
*
|
|
545
569
|
* **恒等纪律**:没有任何一条路径真的变了(含 `keptIndex` 为空)⇒ 返回**同一个对象引用**,连 `keys`
|
|
546
|
-
* 数组也不复制(
|
|
570
|
+
* 数组也不复制(好数据穿过读路径 byte 不变)。
|
|
547
571
|
*/
|
|
548
572
|
export function rebaseWarningPaths(w, keptIndex) {
|
|
549
573
|
switch (w.kind) {
|
|
550
574
|
case "unknown-keys-dropped":
|
|
551
|
-
case "unknown-keys-carried":
|
|
552
|
-
case "legacy-governance-lifted": {
|
|
575
|
+
case "unknown-keys-carried": {
|
|
553
576
|
const keys = rebasePaths(w.keys, keptIndex);
|
|
554
577
|
return keys === w.keys ? w : { ...w, keys };
|
|
555
578
|
}
|
|
@@ -557,8 +580,7 @@ export function rebaseWarningPaths(w, keptIndex) {
|
|
|
557
580
|
const path = rebasePath(w.path, keptIndex);
|
|
558
581
|
return path === w.path ? w : { ...w, path };
|
|
559
582
|
}
|
|
560
|
-
//
|
|
561
|
-
case "hosts-grandfathered":
|
|
583
|
+
// 以下两臂不带任何坐标:点名的是错误 / 文件名。
|
|
562
584
|
case "domain-defaulted":
|
|
563
585
|
case "unread-config-file":
|
|
564
586
|
return w;
|
|
@@ -654,22 +676,20 @@ function isPlainJsonObject(v) {
|
|
|
654
676
|
const proto = Object.getPrototypeOf(v);
|
|
655
677
|
return proto === Object.prototype || proto === null;
|
|
656
678
|
}
|
|
657
|
-
function collectDropped(domain, raw, parsed, prefix, depth,
|
|
679
|
+
function collectDropped(domain, raw, parsed, prefix, depth, acc) {
|
|
658
680
|
if (depth > DROP_SCAN_MAX_DEPTH)
|
|
659
681
|
return;
|
|
660
682
|
if (Array.isArray(raw) && Array.isArray(parsed)) {
|
|
661
|
-
// 长度不同 =
|
|
683
|
+
// 长度不同 = 整条元素被丢(`entry-dropped` 有自己的警告),不在本判据内,别造重复噪声。
|
|
662
684
|
if (raw.length !== parsed.length)
|
|
663
685
|
return;
|
|
664
686
|
for (let i = 0; i < raw.length; i++)
|
|
665
|
-
collectDropped(domain, raw[i], parsed[i], [...prefix, i], depth + 1,
|
|
687
|
+
collectDropped(domain, raw[i], parsed[i], [...prefix, i], depth + 1, acc);
|
|
666
688
|
return;
|
|
667
689
|
}
|
|
668
690
|
if (!isPlainJsonObject(raw) || !isPlainJsonObject(parsed))
|
|
669
691
|
return;
|
|
670
692
|
for (const k of Object.keys(raw)) {
|
|
671
|
-
if (depth === 0 && spared.includes(k))
|
|
672
|
-
continue;
|
|
673
693
|
if (!Object.prototype.hasOwnProperty.call(parsed, k)) {
|
|
674
694
|
if (isReservedExtensionKey(domain, prefix, k))
|
|
675
695
|
continue; // 扩展命名空间:按契约放的键,不是未知键
|
|
@@ -678,38 +698,36 @@ function collectDropped(domain, raw, parsed, prefix, depth, spared, acc) {
|
|
|
678
698
|
acc.keys.push(outgoingPath([...prefix, k]));
|
|
679
699
|
continue;
|
|
680
700
|
}
|
|
681
|
-
collectDropped(domain, raw[k], parsed[k], [...prefix, k], depth + 1,
|
|
701
|
+
collectDropped(domain, raw[k], parsed[k], [...prefix, k], depth + 1, acc);
|
|
682
702
|
}
|
|
683
703
|
}
|
|
684
704
|
/**
|
|
685
705
|
* 一份域文档里被域 schema **丢掉的键**(zod 对象默认 `"strip"`),段坐标形({@link ConfigPath})。
|
|
686
706
|
*
|
|
687
|
-
* 成案(消费方 #322 真事故)
|
|
707
|
+
* 成案(消费方 #322 真事故):往一份域文档里加一个不属于该域的键,两条腿都会把它剥掉,
|
|
688
708
|
* `domainErrors` 空、解析"成功"——配置写了、值没到、零告警。剥除行为本身是**刻意的前向兼容**
|
|
689
|
-
* (新
|
|
709
|
+
* (新 web-admin 的新键不该炸老消费方),所以修法不是收紧成 `.strict()`,而是让它**响亮**:
|
|
690
710
|
* {@link buildEffective} 每域解析成功后调用本函数,非空即发 `"unknown-keys-dropped"` 警告。
|
|
691
711
|
*
|
|
692
712
|
* **嵌套也扫**(对抗复审 复审 F2 真 finding):`{infraCostRates:{toolCalMicroUsd:10}}` 这种**拼错的嵌套
|
|
693
713
|
* 键**在顶层看不见(`infraCostRates` 键还在),只看顶层等于对计费面的错键完全失明。故按 raw↔parsed
|
|
694
714
|
* 的键集逐层对比(对象逐键、数组同长时逐下标),不依赖任何 zod 内省。
|
|
695
715
|
*
|
|
696
|
-
* `runtime`
|
|
697
|
-
* 成文的抬升位(本文件的 governance 抬升函数会把它们抬进 governance 域),报出来是噪声。豁免只在顶层。
|
|
716
|
+
* 零豁免(11.0.0):此前 `runtime` 域顶层的治理三件不算被丢(读侧抬升臂的成文位);抬升臂与 `runtime` 域一起retired,豁免随之删。
|
|
698
717
|
*
|
|
699
|
-
* 返回 {@link UnknownKeyScan}:`keys` 是**有界**的点名列表,`total`
|
|
700
|
-
*
|
|
701
|
-
* 数拼出来的,**一个铸点**:消费方自铸的 `truncatedFrom` 与告警里那个恒等。
|
|
718
|
+
* 返回 {@link UnknownKeyScan}:`keys` 是**有界**的点名列表,`total` 是截断前的真实总数;告警(`unknown-keys-dropped`)
|
|
719
|
+
* 的 `keys` / `truncatedFrom` 就是拿这两个数拼出来的(模块私有,见 {@link UnknownKeyScan})。
|
|
702
720
|
*
|
|
703
721
|
* 边界(如实登记):① 深度上限 {@link DROP_SCAN_MAX_DEPTH}、键数上限 {@link DROP_SCAN_MAX_KEYS}(告警
|
|
704
|
-
* 载荷有界,超出即少报**点名**——`total` 不少报);② 数组长度不同就整段跳过(
|
|
722
|
+
* 载荷有界,超出即少报**点名**——`total` 不少报);② 数组长度不同就整段跳过(元素级剔除由 `entry-dropped`
|
|
705
723
|
* 负责);③ 只比对纯 JSON 对象——raw 若是类实例/Proxy 这类形状不扫;④ 读 raw 的值可能触发访问器,整段包
|
|
706
724
|
* try/catch 且**绝不**让其错误外泄(错误文案可能带 secret,与 parseDomain 的 R13 同纪律),扫描失败即当作
|
|
707
725
|
* "没看见"(`{ keys: [], total: 0 }`),不影响解析。
|
|
708
726
|
*/
|
|
709
|
-
|
|
727
|
+
function droppedKeys(domain, raw, parsed) {
|
|
710
728
|
const acc = { keys: [], total: 0 };
|
|
711
729
|
try {
|
|
712
|
-
collectDropped(domain, raw, parsed, [], 0,
|
|
730
|
+
collectDropped(domain, raw, parsed, [], 0, acc);
|
|
713
731
|
}
|
|
714
732
|
catch {
|
|
715
733
|
return { keys: [], total: 0 }; // 反射/访问器抛错:告警是尽力而为的审计线,绝不能把解析拖下水,更不能回显其错误文案
|
|
@@ -737,14 +755,13 @@ const OPEN_WORLD_KNOWN_KEYS = {
|
|
|
737
755
|
* 一份已解析的 open-world 域文档里,**本包不认识但已承运**的键(段坐标)。非 open-world 域恒空
|
|
738
756
|
* (它们的未知键走 {@link droppedKeys} 那条更重的「被丢了」告警)。
|
|
739
757
|
*
|
|
740
|
-
* 返回 {@link UnknownKeyScan}(
|
|
741
|
-
*
|
|
742
|
-
* 「这个域没有承运未知键」与「这个域不参与承运」在消费方那里是同一个处置。
|
|
758
|
+
* 返回 {@link UnknownKeyScan}(与 {@link droppedKeys} 同形同律:`keys` 有界、`total` 是截断前的真实总数;模块私有)。
|
|
759
|
+
* 非 open-world 域返回的也是这个形(`{ keys: [], total: 0 }`):「这个域没有承运未知键」与「这个域不参与承运」是同一个处置。
|
|
743
760
|
*
|
|
744
761
|
* 边界(如实):只看 {@link OPEN_WORLD_KNOWN_KEYS} 登记了的层级 —— 未登记层级(如某个未知键自己的
|
|
745
762
|
* 子树)不再下探,那本就是消费方自定义的内容,逐层猜没有意义。
|
|
746
763
|
*/
|
|
747
|
-
|
|
764
|
+
function carriedUnknownKeys(domain, parsed) {
|
|
748
765
|
const table = OPEN_WORLD_KNOWN_KEYS[domain];
|
|
749
766
|
const acc = { keys: [], total: 0 };
|
|
750
767
|
if (table === undefined)
|
|
@@ -790,14 +807,12 @@ export function parseDomainLoud(domain, raw, onWarning) {
|
|
|
790
807
|
* this call explicitly (compile error until it's classified). Per-domain rationale:
|
|
791
808
|
* - models/skills/mcp/plugins/scenarios/systems/collab: additive capability catalogs — empty = the
|
|
792
809
|
* capability disappears (fail-closed), no gate is lost.
|
|
793
|
-
* - hosts: empty registry = no auto-placement targets (availability-only); self-registration re-lands
|
|
794
|
-
* entries as approved:false = 待批 (fail-closed).
|
|
810
|
+
* - hosts: empty host registry = no auto-placement targets (availability-only); self-registration re-lands
|
|
811
|
+
* entries as approved:false = 待批 (fail-closed).
|
|
795
812
|
* - rosters: THROW — a lost roster makes resolveEffectiveForWorker fall back to the FULL models catalog
|
|
796
813
|
* ("global(fallback)"), silently WIDENING every restricted worker's model access. 放权,不许.
|
|
797
814
|
* - workers: THROW — an empty fleet spec would make the reconciler RETIRE every managed worker
|
|
798
815
|
* (destructive apply — far worse than a read outage).
|
|
799
|
-
* - runtime: THROW — defaulting drops centrally-published rate/cost ceilings = unbounded spend (fail-open
|
|
800
|
-
* on the budget axis).
|
|
801
816
|
* - governance: THROW — defaulting drops autonomy/commandPolicy/approvalRequire = the consumer falls back
|
|
802
817
|
* to env/defaults with the operator's guardrails silently gone. 放权,不许.
|
|
803
818
|
* - entitlement: THROW — defaulting drops the org kill-switch/budget ceilings/tier restrictions for every
|
|
@@ -821,7 +836,6 @@ export const DOMAIN_READ_FALLBACK = {
|
|
|
821
836
|
collab: "default",
|
|
822
837
|
workers: "throw",
|
|
823
838
|
hosts: "default",
|
|
824
|
-
runtime: "throw",
|
|
825
839
|
governance: "throw",
|
|
826
840
|
entitlement: "throw",
|
|
827
841
|
execution: "throw",
|
|
@@ -830,80 +844,33 @@ export const DOMAIN_READ_FALLBACK = {
|
|
|
830
844
|
// backfill-empty-default 同口径);fail-closed 由 PUBLISH_MODE 发布闸承担,default 不扩权
|
|
831
845
|
// (空 bindings=不下发任何内容)。
|
|
832
846
|
prompts: "default",
|
|
833
|
-
// limits(0.19.0):THROW ——
|
|
847
|
+
// limits(0.19.0):THROW —— 本域是成本 / 限流天花板的唯一发布位(11.0.0 起 runtime 域retired)。坏形
|
|
834
848
|
// 降级成空域 = 中央发布的成本/限流天花板整片蒸发,消费方回自己的 env 底(可能宽得多)= 预算轴放权。
|
|
835
849
|
// 消费方 fail-static 在上一份好配置严格更好。
|
|
836
850
|
limits: "throw",
|
|
837
851
|
};
|
|
838
|
-
/**
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
*
|
|
852
|
+
/**
|
|
853
|
+
* Merge every domain into the effective config a consumer pulls. Shared by all store impls.
|
|
854
|
+
* `updatedAt` is REQUIRED (the determinism refactor — the original store.ts defaulted it to the wall clock,
|
|
855
|
+
* which made `emptyEffective`/the memory store non-deterministic). Every caller now supplies it explicitly.
|
|
842
856
|
*
|
|
843
|
-
*
|
|
844
|
-
*
|
|
845
|
-
|
|
846
|
-
export function grandfatherHostsRead(raw) {
|
|
847
|
-
if (raw === null || typeof raw !== "object" || !Array.isArray(raw.hosts)) {
|
|
848
|
-
return { raw, droppedNames: [], droppedTotal: 0, keptIndex: NO_ENTRIES_DROPPED }; // not the expected shape — let parseDomain rule on it
|
|
849
|
-
}
|
|
850
|
-
const original = raw.hosts;
|
|
851
|
-
const droppedNames = [];
|
|
852
|
-
let droppedTotal = 0;
|
|
853
|
-
const keptFrom = [];
|
|
854
|
-
let hosts = original.filter((h, i) => {
|
|
855
|
-
const name = h !== null && typeof h === "object" ? h.name : undefined;
|
|
856
|
-
if (typeof name === "string" && name.length > HOST_NAME_MAX) {
|
|
857
|
-
droppedTotal += 1;
|
|
858
|
-
// 先钳后扫(S-813):只给前 DROP_SCAN_MAX_KEYS 个铸标签,其余只计数 —— 出门那一道每个名都要归一化一遍,不能随文档条数放大。
|
|
859
|
-
if (droppedNames.length >= DROP_SCAN_MAX_KEYS)
|
|
860
|
-
return false;
|
|
861
|
-
// 出门前先过同一道(6.3.0,opus 腿 M2):用户写的主机名完全可能就是一条粘错位置的凭据,夹紧只管载荷上界、不管秘密 ——
|
|
862
|
-
// 此前 `sk-live-…` 形 / 编码 PEM 形以 63 字原文前缀出门。判在夹紧**之前**(截断会把秘密形截成探测器认不出的半截)。
|
|
863
|
-
const out = outgoingSegment(name);
|
|
864
|
-
droppedNames.push(out.length > HOST_NAME_MAX ? out.slice(0, HOST_NAME_MAX) + "…" : out); // audit label — clipped so the warning itself stays bounded
|
|
865
|
-
return false;
|
|
866
|
-
}
|
|
867
|
-
keptFrom.push(i); // 夹紧后第 keptFrom.length-1 条 = 原文档第 i 条
|
|
868
|
-
return true;
|
|
869
|
-
});
|
|
870
|
-
let truncatedFrom;
|
|
871
|
-
if (hosts.length > HOSTS_ARRAY_MAX) {
|
|
872
|
-
truncatedFrom = hosts.length;
|
|
873
|
-
hosts = hosts.slice(0, HOSTS_ARRAY_MAX);
|
|
874
|
-
}
|
|
875
|
-
if (droppedTotal === 0 && truncatedFrom === undefined)
|
|
876
|
-
return { raw, droppedNames, droppedTotal, keptIndex: NO_ENTRIES_DROPPED }; // identity — byte-stable
|
|
877
|
-
// 位移**只**来自按名剔条目;纯截断留下的是原数组前缀,幸存者下标不动 ⇒ 不登记(见 EntryKeptIndex)。
|
|
878
|
-
const keptIndex = droppedTotal > 0 ? { hosts: keptFrom.slice(0, hosts.length) } : NO_ENTRIES_DROPPED;
|
|
879
|
-
return { raw: { ...raw, hosts }, droppedNames, droppedTotal, truncatedFrom, keptIndex };
|
|
880
|
-
}
|
|
857
|
+
* 11.0.0:`getRaw` 也会被问到墓碑表里的retired 域(今天只有 `runtime`)。店里还留着一份**带事实**的retired 域文档 ⇒ 整份 effective 抛
|
|
858
|
+
* {@link RetiredDomainError}(拒句点名改写它的一次性迁移);缺席或 `{}` ⇒ 不理会。读写两面同一只 schema:读路径上不再有任何抬升臂。
|
|
859
|
+
*/
|
|
881
860
|
export function buildEffective(version, getRaw, updatedAt, onWarning) {
|
|
861
|
+
// 墓碑先判:一份没迁的retired 域文档意味着这个 scope 的存量还没过一次性迁移 —— 拿剩下的域拼一份 effective 等于静默丢掉它的事实。
|
|
862
|
+
for (const retired of Object.keys(RETIRED_DOMAINS))
|
|
863
|
+
refuseRetiredDomain(retired, getRaw(retired));
|
|
882
864
|
// 0.19.0 静默剥键/静默承运门:解析**成功**的域,把 schema 丢掉的键、以及 open-world 域里承运但本包
|
|
883
|
-
// 不认识的键(
|
|
865
|
+
// 不认识的键(都含嵌套,段坐标)点名(行为不变,只是不再无声)。判据与 `getDomain` 同源
|
|
884
866
|
// ({@link parseDomainLoud})。只在成功路径上报——降级路径已经有 domain-defaulted 那条更重的警告。
|
|
885
867
|
const parseLoud = (domain, raw) => parseDomainLoud(domain, raw, onWarning);
|
|
886
|
-
/**
|
|
887
|
-
* 一层读的**告警出口**,外面裹了那一层的坐标换算(S-323b 统一律)。
|
|
888
|
-
*
|
|
889
|
-
* 读路径上有**两个压缩点**:{@link grandfatherHostsRead}(parse 之前按名剔 host 条目)与
|
|
890
|
-
* {@link salvageEntries}(整域 parse 失败后剔坏条目)。律只有一条:**一条告警用「铸它的那一层所见
|
|
891
|
-
* 文档」的坐标表达;哪一层压缩了文档,哪一层就把从它下方上来的告警回写一次。** 组合因此是自然的
|
|
892
|
-
* ——内层先换算、外层后换算,不重不漏,而每一层都只需要知道**自己**那本账。
|
|
893
|
-
*
|
|
894
|
-
* 位移表**空**(这一层这次什么都没压缩)⇒ 原样返回 `onWarning`,连一层闭包都不套 —— 与
|
|
895
|
-
* {@link rebaseWarningPaths} 的恒等纪律同一条律:没发生的事不留痕迹。
|
|
896
|
-
*/
|
|
897
|
-
const exitOf = (keptIndex) => onWarning === undefined || Object.keys(keptIndex).length === 0 ? onWarning : (w) => onWarning(rebaseWarningPaths(w, keptIndex));
|
|
898
868
|
// Generic READ guardrail (0.8.1): a domain that no longer parses degrades to its schema default + a warning
|
|
899
869
|
// — but ONLY where DOMAIN_READ_FALLBACK says the default is safe; gate-bearing domains keep throwing (决策
|
|
900
870
|
// 表与逐域理由见 DOMAIN_READ_FALLBACK). The success path is UNCHANGED (same parseDomain, same bytes).
|
|
901
|
-
|
|
902
|
-
// `emit` = 这一腿的告警出口(默认 = 本函数的 `onWarning`)。**刻意不收「外层压缩了多少」这种入参**:
|
|
903
|
-
// parseOr 不必知道自己上面还有没有一层,它只管把告警交给给它的那个出口 —— 换算是出口的事。
|
|
904
|
-
const parseOr = (domain, raw, emit = onWarning) => {
|
|
871
|
+
const parseOr = (domain, raw) => {
|
|
905
872
|
try {
|
|
906
|
-
return parseDomainLoud(domain, raw,
|
|
873
|
+
return parseDomainLoud(domain, raw, onWarning);
|
|
907
874
|
}
|
|
908
875
|
catch (e) {
|
|
909
876
|
if (DOMAIN_READ_FALLBACK[domain] === "throw")
|
|
@@ -916,47 +883,23 @@ export function buildEffective(version, getRaw, updatedAt, onWarning) {
|
|
|
916
883
|
try {
|
|
917
884
|
const buffered = [];
|
|
918
885
|
const parsed = parseDomainLoud(domain, salvage.raw, (w) => buffered.push(w));
|
|
919
|
-
//
|
|
920
|
-
//
|
|
921
|
-
//
|
|
886
|
+
// 坐标律(S-323b):一条告警用「铸它的那一层所见文档」的坐标表达;哪一层压缩了文档,哪一层就把从它下方上来的告警回写一次。
|
|
887
|
+
// `entry-dropped` 是本层(salvage)铸的,用的就是本层入参那份文档的坐标 ⇒ 原样出门;`buffered` 里的是对本层**压缩后**
|
|
888
|
+
// 文档的诊断 ⇒ 先过本层的位移表。读路径上只有这一个压缩点(11.0.0 起 hosts 的读侧夹紧臂已retired)。
|
|
922
889
|
for (const d of salvage.dropped)
|
|
923
|
-
|
|
890
|
+
onWarning?.(entryDroppedWarning(domain, d));
|
|
924
891
|
for (const w of buffered)
|
|
925
|
-
|
|
892
|
+
onWarning?.(rebaseWarningPaths(w, salvage.keptIndex));
|
|
926
893
|
return parsed;
|
|
927
894
|
}
|
|
928
895
|
catch {
|
|
929
896
|
// 条目之外还有毛病(重名 / 悬空 ref / 域级判据)—— 丢条目救不了它,照常整域降级。
|
|
930
897
|
}
|
|
931
898
|
}
|
|
932
|
-
|
|
899
|
+
onWarning?.({ domain, kind: "domain-defaulted", error: e });
|
|
933
900
|
return parseDomain(domain, undefined); // schema default — always valid
|
|
934
901
|
}
|
|
935
902
|
};
|
|
936
|
-
// governance TRUTH: the domain itself; if it was never set, lift the trio from a legacy runtime doc
|
|
937
|
-
// (0.5→0.6 upgrade path — a2/local stores keep their gates without a data migration).
|
|
938
|
-
const governanceRaw = getRaw("governance");
|
|
939
|
-
let governanceSource = governanceRaw;
|
|
940
|
-
if (governanceRaw === undefined) {
|
|
941
|
-
const lifted = legacyGovernanceFromRuntime(getRaw("runtime"));
|
|
942
|
-
// 兼容臂真生效时出声(2.0.0):行为一字不变,但窗要能关就得有人看得见它还开着。
|
|
943
|
-
if (lifted !== undefined)
|
|
944
|
-
onWarning?.({ domain: "governance", kind: "legacy-governance-lifted", keys: Object.keys(lifted).map((k) => [k]) });
|
|
945
|
-
governanceSource = lifted;
|
|
946
|
-
}
|
|
947
|
-
const governance = parseLoud("governance", governanceSource);
|
|
948
|
-
// runtime slot = limit residue ONLY. 0.7.1(BREAKING,service 回执收账):E1 的 governance→runtime
|
|
949
|
-
// 六闸双写镜像已撤——service 1.150.0 已切优先读 eff.governance 并回帖确认(撤双写以其确认为门槛,
|
|
950
|
-
// ask⑤ 顺序契约兑现)。READ 方向的 legacy lift(governance 域从未设时从旧 runtime 文档抬升三闸,
|
|
951
|
-
// 见 legacyGovernanceFromRuntime)保留——那是 0.5→0.6 升级路径,与写出镜像无关。
|
|
952
|
-
const runtime = parseLoud("runtime", getRaw("runtime"));
|
|
953
|
-
// hosts: the PRECISE grandfather for the 0.8.0 tightening runs BEFORE parse (0.7-era legal data over the
|
|
954
|
-
// new bounds reads on, clamped + warned); any residual corruption falls to the generic guardrail above.
|
|
955
|
-
const hostsGf = grandfatherHostsRead(getRaw("hosts"));
|
|
956
|
-
if (hostsGf.droppedTotal > 0 || hostsGf.truncatedFrom !== undefined) {
|
|
957
|
-
const clipped = hostsGf.droppedTotal > hostsGf.droppedNames.length ? { droppedTotal: hostsGf.droppedTotal } : {};
|
|
958
|
-
onWarning?.({ domain: "hosts", kind: "hosts-grandfathered", droppedNames: hostsGf.droppedNames, ...clipped, truncatedFrom: hostsGf.truncatedFrom });
|
|
959
|
-
}
|
|
960
903
|
return {
|
|
961
904
|
version,
|
|
962
905
|
updatedAt,
|
|
@@ -969,11 +912,10 @@ export function buildEffective(version, getRaw, updatedAt, onWarning) {
|
|
|
969
912
|
systems: parseOr("systems", getRaw("systems")),
|
|
970
913
|
collab: parseOr("collab", getRaw("collab")),
|
|
971
914
|
workers: parseLoud("workers", getRaw("workers")),
|
|
972
|
-
|
|
973
|
-
//
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
governance,
|
|
915
|
+
hosts: parseOr("hosts", getRaw("hosts")),
|
|
916
|
+
// governance TRUTH = the domain itself (11.0.0: the read-side lift from a 0.5-era runtime document is gone — the
|
|
917
|
+
// runtime tombstone above refuses such a document, and the one-time migration SS-M1 moves its trio here).
|
|
918
|
+
governance: parseLoud("governance", getRaw("governance")),
|
|
977
919
|
entitlement: parseLoud("entitlement", getRaw("entitlement")),
|
|
978
920
|
// execution (0.8.0, S3): plain scope-level parse — MIRRORS governance (no overlay/merge invented here).
|
|
979
921
|
execution: parseLoud("execution", getRaw("execution")),
|
|
@@ -989,12 +931,15 @@ export function buildEffective(version, getRaw, updatedAt, onWarning) {
|
|
|
989
931
|
};
|
|
990
932
|
}
|
|
991
933
|
/** READ-side tolerant judgment for a GET /effective **wire payload**(the {@link EffectiveWire} contract)at
|
|
992
|
-
* its CONSUMPTION boundary — a service pulling from a remote
|
|
934
|
+
* its CONSUMPTION boundary — a service pulling from a remote web-admin passes the parsed JSON here instead of
|
|
993
935
|
* type-asserting it(0.12.0,[2281] 裁B:「两个契约共用一个拼写不是一个被检查的契约」——拼写副本要在
|
|
994
936
|
* 消费端被真校验)。ONE judgment, two legs: every known domain goes through the SAME `parseDomain` +
|
|
995
937
|
* {@link DOMAIN_READ_FALLBACK} policy as {@link buildEffective}(catalog 域坏形 → schema default + warning;
|
|
996
|
-
* gate 域坏形 → throw,caller 的整包 catch 决定回落),including the
|
|
997
|
-
*
|
|
938
|
+
* gate 域坏形 → throw,caller 的整包 catch 决定回落),including the retired-domain tombstone — DELEGATED to
|
|
939
|
+
* buildEffective, not re-implemented(单判据双腿消费,§M4)。A retired domain on the wire (an older web-admin still
|
|
940
|
+
* ships `runtime`) is judged by the same tombstone: carrying facts ⇒ {@link RetiredDomainError}; `{}` (what an older
|
|
941
|
+
* web-admin materialises for an unset domain) carries nothing and is dropped from the result — it never rides through as
|
|
942
|
+
* an open-world key.
|
|
998
943
|
*
|
|
999
944
|
* `skills` is the ONE wire divergence: on the wire it is a MANIFEST(content → contentHash,
|
|
1000
945
|
* {@link SkillsManifest}),so the full skills-domain schema would reject every LEGAL manifest and silently
|
|
@@ -1002,9 +947,8 @@ export function buildEffective(version, getRaw, updatedAt, onWarning) {
|
|
|
1002
947
|
* same "default" fallback tier as the skills domain itself.
|
|
1003
948
|
*
|
|
1004
949
|
* OPEN-WORLD on unknown top-level keys: they ride through VERBATIM(never validated, never dropped)— a
|
|
1005
|
-
* newer
|
|
1006
|
-
* and the
|
|
1007
|
-
* flowing. Throws ONLY on garbage no caller can partially trust: a non-object payload, or a missing/
|
|
950
|
+
* newer web-admin's new domain reaches an older engine untouched(§M2 能力面语义:读不到即走老路,不该炸),
|
|
951
|
+
* and so does any key no domain names(the 0.6-era `teams` key included — not a domain since 0.7, and no engine reads it). Throws ONLY on garbage no caller can partially trust: a non-object payload, or a missing/
|
|
1008
952
|
* non-finite `version` — that is not "a bad domain", it is not an effective config at all. */
|
|
1009
953
|
export function readEffectiveWire(raw, onWarning) {
|
|
1010
954
|
if (raw === null || typeof raw !== "object" || Array.isArray(raw))
|
|
@@ -1015,11 +959,12 @@ export function readEffectiveWire(raw, onWarning) {
|
|
|
1015
959
|
const updatedAt = typeof r.updatedAt === "string" ? r.updatedAt : "";
|
|
1016
960
|
// skills redirected to undefined: buildEffective must never see the manifest(it would fail the full-domain
|
|
1017
961
|
// schema and "default" it with a FALSE domain-defaulted warning);the manifest is judged separately below.
|
|
1018
|
-
// prompts likewise redirected(#100):on the wire it is
|
|
962
|
+
// prompts likewise redirected(#100):on the wire it is web-admin-assembled RESOLVED form(worker lane
|
|
1019
963
|
// strips the bindings storage table — types.ts PromptsConfig 注 [1057]⑤),consumer-validated additive JSON;
|
|
1020
964
|
// the storage-domain schema would swallow it to {bindings:[]} with ZERO warning(zod non-strict 剥键后
|
|
1021
965
|
// "成功")— 3.24.0 起 server 侧 promptsGate 恒败的根因。verbatim,by presence(见 return)。
|
|
1022
966
|
const built = buildEffective(r.version, (d) => (d === "skills" || d === "prompts" ? undefined : r[d]), updatedAt, onWarning);
|
|
967
|
+
// retired 域名不是 open-world 的未知键:墓碑已在 buildEffective 里判过(带事实 ⇒ 抛),剩下的 `{}` 没有可承运的东西。
|
|
1023
968
|
let skills;
|
|
1024
969
|
const manifest = SkillsManifestSchema.safeParse(r.skills ?? {});
|
|
1025
970
|
if (manifest.success) {
|
|
@@ -1047,15 +992,15 @@ export function readEffectiveWire(raw, onWarning) {
|
|
|
1047
992
|
// 与本文件另两处自有键判据(:548 `rebasePath` 查位移表、:612 `collectDropped` 比键集)同一条律,零特判。
|
|
1048
993
|
// `Object.fromEntries`,**不是**逐键 `passthrough[k] = v`:后者对 `"__proto__"` 走的是原型 setter
|
|
1049
994
|
// (改掉对象的原型,而不是加一个键),fromEntries 走 CreateDataProperty —— 同一条「键就是键」的律。
|
|
1050
|
-
const passthrough = Object.fromEntries(Object.entries(r).filter(([k]) => !Object.prototype.hasOwnProperty.call(built, k) && k !== "skills"));
|
|
995
|
+
const passthrough = Object.fromEntries(Object.entries(r).filter(([k]) => !Object.prototype.hasOwnProperty.call(built, k) && k !== "skills" && !isRetiredDomainName(k)));
|
|
1051
996
|
// prompts:剥掉 buildEffective 恒物化的 {bindings:[]} 底座,按 wire 在场性 verbatim 挂回(#100:
|
|
1052
|
-
// 缺席进缺席出——
|
|
997
|
+
// 缺席进缺席出——web-admin worker-lane 闸关是常态形,物化会让消费端「缺席」分支永不可达)。
|
|
1053
998
|
const { prompts: _promptsMaterialized, ...builtSansPrompts } = built;
|
|
1054
999
|
return { ...passthrough, ...builtSansPrompts, skills, ...("prompts" in r ? { prompts: r.prompts } : {}) };
|
|
1055
1000
|
}
|
|
1056
1001
|
/** 档位组→引擎单表的唯一解析点(0.9.0)。返回 ACTIVE 档位组的绑定表(剔除未绑档),即 core
|
|
1057
|
-
* `RunnerDeps.tiers` 要吃的那张单表——组切换=
|
|
1058
|
-
* 组内一档未绑 → `undefined`(引擎档位层恒惰性,与「未配 tiers」逐字同义)。消费方(
|
|
1002
|
+
* `RunnerDeps.tiers` 要吃的那张单表——组切换=web-admin 换这张表下发,零新引擎机制。无 active 组/未命中/
|
|
1003
|
+
* 组内一档未绑 → `undefined`(引擎档位层恒惰性,与「未配 tiers」逐字同义)。消费方(web-admin /effective
|
|
1059
1004
|
* 投影、service applyEffective 接线)都走这一个函数,不要各自解析。 */
|
|
1060
1005
|
export function resolveActiveTiers(models) {
|
|
1061
1006
|
if (!models.activeTierGroup)
|