@sema-agent/settings-schema 6.1.0 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/README.md +40 -9
  3. package/conformance/VERSION +1 -1
  4. package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +166 -0
  5. package/conformance/cases/c41-s792-encoded-pem-identity/input.json +62 -0
  6. package/conformance/cases/c41-s792-encoded-pem-identity/meta.json +12 -0
  7. package/conformance/cases/c42-s792-unknown-key-secret-redacted/expected.json +120 -0
  8. package/conformance/cases/c42-s792-unknown-key-secret-redacted/input.json +25 -0
  9. package/conformance/cases/c42-s792-unknown-key-secret-redacted/meta.json +11 -0
  10. package/conformance/cases/c43-s792-form-and-deep-encoded-pem/expected.json +137 -0
  11. package/conformance/cases/c43-s792-form-and-deep-encoded-pem/input.json +44 -0
  12. package/conformance/cases/c43-s792-form-and-deep-encoded-pem/meta.json +12 -0
  13. package/conformance/cases/c44-s792-utf8-escape-glued-token/expected.json +115 -0
  14. package/conformance/cases/c44-s792-utf8-escape-glued-token/input.json +30 -0
  15. package/conformance/cases/c44-s792-utf8-escape-glued-token/meta.json +10 -0
  16. package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +185 -0
  17. package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/input.json +78 -0
  18. package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/meta.json +11 -0
  19. package/conformance/cases/c46-s811-host-name-url-userinfo-characterization/expected.json +88 -0
  20. package/conformance/cases/c46-s811-host-name-url-userinfo-characterization/input.json +18 -0
  21. package/conformance/cases/c46-s811-host-name-url-userinfo-characterization/meta.json +6 -0
  22. package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/expected.json +98 -0
  23. package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/input.json +26 -0
  24. package/conformance/cases/c47-s811-roles-key-url-userinfo-characterization/meta.json +6 -0
  25. package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +0 -8
  26. package/conformance/cases/cli01-model-name-nfc-vs-nfd/meta.json +2 -1
  27. package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +0 -8
  28. package/conformance/cases/cli02-control-and-zero-width-chars/meta.json +2 -1
  29. package/dist/config-fns.d.ts +62 -14
  30. package/dist/config-fns.js +105 -30
  31. package/dist/cross-session.d.ts +8 -5
  32. package/dist/cross-session.js +8 -5
  33. package/dist/fleet.d.ts +163 -0
  34. package/dist/fleet.js +126 -5
  35. package/dist/secret-scan.d.ts +39 -0
  36. package/dist/secret-scan.js +110 -0
  37. package/dist/types.d.ts +79 -72
  38. package/dist/types.js +17 -31
  39. package/package.json +2 -2
package/dist/types.js CHANGED
@@ -18,6 +18,7 @@
18
18
  import { z } from "zod";
19
19
  import { MAX_EXTRA_BODY_DEPTH } from "./hash.js"; // reconcile the extraBody depth cap with the hash cap (R18)
20
20
  import { DispatchPolicy } from "./fleet.js"; // WorkerSpec.fleet.dispatch (scheduling framework D3)
21
+ import { carriesUserinfo, percentLayers } from "./secret-scan.js"; // package-private normalization + the userinfo judgement (not in the barrel)
21
22
  // COARSE, best-effort literal-secret SHAPE detector (NOT exhaustive — the real boundary is the env-NAME schema
22
23
  // on structured fields). Rejected in free-form value fields (worker `env`, model `extraBody`), URL components,
23
24
  // 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-
@@ -28,9 +29,10 @@ import { DispatchPolicy } from "./fleet.js"; // WorkerSpec.fleet.dispatch (sched
28
29
  // which review flagged as a bypass (a private key misnamed `*_PUBKEY` skipped scanning entirely).
29
30
  /** A private-key PEM header (`-----BEGIN [RSA |EC |OPENSSH |ENCRYPTED ]PRIVATE KEY`), unanchored — the ONE definition
30
31
  * of that shape: {@link SECRET_LITERAL_RE} is composed from it (anchored, as one of its branches), and
31
- * `redactSecrets` (config-fns) tests it ANYWHERE in a string. It carries spaces, so the token-splitting detector
32
- * {@link containsSecretToken} only sees it at the very start of a string; a PEM behind a newline or a `key: ` prefix
33
- * slipped past (S-712 codex review [high]). Public keys / certificates never match (see above). */
32
+ * `redactSecrets` (config-fns) tests it ANYWHERE in a string, on every display-side normalized form. It carries spaces,
33
+ * so the token-splitting detector {@link containsSecretToken} only sees it at the very start of a string; a PEM behind
34
+ * a newline or a `key: ` prefix slipped past (S-712 codex review [high]), and a `key=`-prefixed percent-encoded one
35
+ * hid it behind `%20` on the raw form, a form-urlencoded one behind `+` (6.3.0). Public keys / certificates never match. */
34
36
  export const PRIVATE_KEY_PEM_HEADER = /-----BEGIN[A-Z0-9 ]*PRIVATE KEY/i;
35
37
  export const SECRET_LITERAL_RE = new RegExp(`^(sk-|gh[opus]_|github_pat_|xox[baprse]-|xapp-|bearer\\s|basic\\s|eyJ[A-Za-z0-9_-]{10,}\\.|AKIA[0-9A-Z]{12,}|AIza[0-9A-Za-z_-]{20,}|${PRIVATE_KEY_PEM_HEADER.source})`, "i");
36
38
  // 所有 *Env / *tokenEnv 字段的值是宿主机 env 变量的 NAME(不是值)。合法 env 变量名 + 拒绝 secret 形状的值(AKIA…
@@ -71,33 +73,17 @@ function scanForSecret(s) {
71
73
  * fs path — so the worker dies at exec/boot, not at config edit. A NUL is never legitimate in a command / path
72
74
  * / host / image, so reject it on every exec-bound string field (mcp stdio + the whole remoteExec contract). R20. */
73
75
  export const hasNul = (s) => s.includes("\0");
74
- /** Does `s` contain a secret-shaped token? Scans the raw string AND a percent-decoded form (a secret can hide
75
- * as `%73%6B-…`) — so mcp args / dockerHost / URL components all get uniform percent-decode coverage. Tokenized
76
- * + JWT-aware (see scanForSecret). Shared by NON_SECRET_URL + dockerHost + mcp stdio. Round-4/5/11. */
76
+ /** Does `s` contain a secret-shaped token? Scans every percent layer of it — the raw string AND up to three tolerant
77
+ * percent-decoded layers (a secret can hide as `%73%6B-…`; the ONE definition of those layers is `percentLayers` in the
78
+ * package-private `./secret-scan`) — so mcp args / dockerHost / URL components all get uniform percent-decode coverage.
79
+ * Tokenized + JWT-aware (see scanForSecret). Shared by NON_SECRET_URL + dockerHost + mcp stdio. Round-4/5/11. (6.3.0 moved
80
+ * the decode loop out, unchanged in result — same layers, same bound, same verdict — and made it exception-free; the
81
+ * display-side readings the redactor also judges are not this detector's.) We do NOT fail-closed on a bare `%` — that
82
+ * would false-reject legit values like `100%` / `%s`. A secret glued to a malformed escape (`%ZZsk-`) is an accepted
83
+ * coarse-heuristic gap (no consumer reconstructs it; the real guarantee is the env-NAME boundary on structured fields).
84
+ * R11/R13/R14. */
77
85
  export function containsSecretToken(s) {
78
- // scan the raw string AND up to 3 percent-decoded layers (handles a multi-encoded `%2573%256B…` → `%73%6B…`
79
- // → `sk-…`). TOLERANT decode: each valid %HH is decoded, a malformed escape (`%ZZ`, `%` at end) is left as-is
80
- // so one bad escape elsewhere can't abort the whole decode (R13's all-or-nothing decodeURIComponent did). We
81
- // do NOT fail-closed on a bare `%` — that would false-reject legit values like `100%` / `%s`. A secret glued
82
- // to a malformed escape (`%ZZsk-`) is an accepted coarse-heuristic gap (no consumer reconstructs it; the real
83
- // guarantee is the env-NAME boundary on structured fields). Bounded → deterministic. R11/R13/R14. */
84
- let cur = s;
85
- for (let i = 0; i < 4; i++) {
86
- if (scanForSecret(cur))
87
- return true;
88
- const next = cur.replace(/%[0-9A-Fa-f]{2}/g, (m) => {
89
- try {
90
- return decodeURIComponent(m);
91
- }
92
- catch {
93
- return m;
94
- }
95
- });
96
- if (next === cur)
97
- break;
98
- cur = next;
99
- }
100
- return false;
86
+ return percentLayers(s).some((form) => scanForSecret(form));
101
87
  }
102
88
  /** Scan a URL's PATH + query + fragment (KEYS and values, decoded, tokenized) for a secret-shaped literal.
103
89
  * Userinfo is handled separately (NON_SECRET_URL bans it; dockerHost scans the ssh user). Round-4/5. */
@@ -141,7 +127,7 @@ export const NON_SECRET_URL = z
141
127
  .refine((s) => {
142
128
  try {
143
129
  const u = new URL(s);
144
- if (!(u.protocol === "http:" || u.protocol === "https:") || !u.host || u.username || u.password)
130
+ if (!(u.protocol === "http:" || u.protocol === "https:") || !u.host || carriesUserinfo(u))
145
131
  return false;
146
132
  return !urlQueryFragmentHasSecret(u);
147
133
  }
@@ -2053,7 +2039,7 @@ export const PLUGIN_ORIGIN = z
2053
2039
  return true; // 已被上一条拒;避免对非 URL 再报一条噪声
2054
2040
  try {
2055
2041
  const u = new URL(s);
2056
- return !u.username && !u.password && !urlQueryFragmentHasSecret(u);
2042
+ return !carriesUserinfo(u) && !urlQueryFragmentHasSecret(u);
2057
2043
  }
2058
2044
  catch {
2059
2045
  return false; // claims to be https but does not parse → reject
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/settings-schema",
3
- "version": "6.1.0",
3
+ "version": "6.3.0",
4
4
  "description": "Sema settings schema — pure config contract (zod domains + effective-config + roster resolution + ref-integrity + remoteExec) shared by sema-registry, sema-server, and the TOC desktop/CLI. One source of truth. (Renamed at 1.0.0 from @sema-agent/registry-core — name and docs only, no behavior change; note the range moves 0.19.0 -> 1.0.0, so a dependency must become ^1.0.0. The old names @sema-agent/registry-core <=0.19.0 and @sema-ai/registry-core remain published, frozen, for migration.)",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -128,7 +128,7 @@
128
128
  "publish:npm": "npm publish --access public --@sema-agent:registry=https://registry.npmjs.org/ --\"//registry.npmjs.org/:_authToken=${NPM_TOKEN}\""
129
129
  },
130
130
  "devDependencies": {
131
- "@sema-agent/core": "^2.1.0",
131
+ "@sema-agent/core": "7.33.1",
132
132
  "@types/node": "^22.10.0",
133
133
  "esbuild": "^0.21.5",
134
134
  "typescript": "^5.7.0",