@sema-agent/settings-schema 14.5.0 → 14.7.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 (77) hide show
  1. package/CHANGELOG.md +531 -0
  2. package/README.md +46 -3
  3. package/conformance/VERSION +1 -1
  4. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/expected.json +153 -0
  5. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/input.json +23 -0
  6. package/conformance/cases/c84-sandbox-git-block-carried-verbatim/meta.json +5 -0
  7. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/expected.json +153 -0
  8. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/input.json +23 -0
  9. package/conformance/cases/c85-sandbox-git-refused-block-still-carried/meta.json +5 -0
  10. package/dist/api/auth-bridge.d.ts +16 -2
  11. package/dist/api/auth-bridge.js +59 -24
  12. package/dist/api/credentials.d.ts +281 -0
  13. package/dist/api/credentials.js +149 -0
  14. package/dist/api/scopes.js +5 -1
  15. package/dist/atomic-write.js +10 -7
  16. package/dist/bundle.d.ts +1 -1
  17. package/dist/bundle.js +1 -1
  18. package/dist/config-d-file.js +33 -15
  19. package/dist/config-dir-migration.js +2 -1
  20. package/dist/config-fns.js +142 -78
  21. package/dist/conformance/canonical.js +7 -10
  22. package/dist/conformance/contract-adapter.js +26 -1
  23. package/dist/conformance/corpus.js +5 -5
  24. package/dist/conformance/derive.js +6 -5
  25. package/dist/conformance/run.js +44 -6
  26. package/dist/cross-session.d.ts +4 -4
  27. package/dist/declared-capture.d.ts +41 -0
  28. package/dist/declared-capture.js +344 -0
  29. package/dist/file-edit.js +11 -15
  30. package/dist/file-store.d.ts +1 -31
  31. package/dist/file-store.js +3 -30
  32. package/dist/fleet.d.ts +11 -0
  33. package/dist/fleet.js +60 -26
  34. package/dist/fs-failure.d.ts +12 -7
  35. package/dist/fs-failure.js +53 -29
  36. package/dist/hash.d.ts +0 -9
  37. package/dist/hash.js +64 -7
  38. package/dist/hooks-config.d.ts +2 -2
  39. package/dist/hooks-config.js +129 -33
  40. package/dist/http-hook-allowlists.d.ts +27 -10
  41. package/dist/http-hook-allowlists.js +189 -25
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.js +4 -1
  44. package/dist/mcp-request-entry.d.ts +13 -8
  45. package/dist/mcp-request-entry.js +246 -107
  46. package/dist/one-time-migrations.js +207 -95
  47. package/dist/own-key-read.d.ts +247 -0
  48. package/dist/own-key-read.js +449 -0
  49. package/dist/plain-json-object.d.ts +38 -3
  50. package/dist/plain-json-object.js +84 -7
  51. package/dist/portable-domains.d.ts +31 -0
  52. package/dist/portable-domains.js +30 -0
  53. package/dist/remote-exec.js +9 -1
  54. package/dist/safety-merge-spec.d.ts +88 -6
  55. package/dist/safety-merge-spec.js +246 -17
  56. package/dist/sandbox-git.d.ts +64 -0
  57. package/dist/sandbox-git.js +89 -0
  58. package/dist/scheduler-store.js +21 -12
  59. package/dist/settings-units.d.ts +85 -0
  60. package/dist/settings-units.js +177 -0
  61. package/dist/types.d.ts +6 -2
  62. package/dist/types.js +65 -56
  63. package/dist/url-shape.d.ts +27 -5
  64. package/dist/url-shape.js +29 -5
  65. package/dist/web-admin-credential.js +5 -2
  66. package/dist/web-admin-session-url.js +2 -2
  67. package/dist/worker-row-verdict.d.ts +11 -0
  68. package/dist/worker-row-verdict.js +51 -1
  69. package/dist/workers-rows.d.ts +2 -1
  70. package/dist/workers-rows.js +41 -12
  71. package/dist/zod-unwrap.d.ts +6 -0
  72. package/dist/zod-unwrap.js +14 -7
  73. package/docs/API-SURFACE.md +50 -5
  74. package/docs/CONSUMER-INTEGRATION.md +178 -0
  75. package/llms-full.txt +34 -5
  76. package/llms.txt +6 -3
  77. package/package.json +1 -1
package/README.md CHANGED
@@ -312,6 +312,21 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
312
312
  use `task`; the widened vocabulary is visible to consumers that hold a `Record<MinTrust, …>` / exhaustive
313
313
  switch (compile error on upgrade — migrate to `trustRank`) or that validate external spec values with these
314
314
  schemas (they now accept `task` and must rank it — again `trustRank`, never a five-word table of their own).
315
+ **14.6.0 — comparison and the settings units.** "Which of two values is stricter" has ONE implementation,
316
+ `tightenVerdict(rule, existing, candidate)` → `equal` / `stricter` / `looser` / `unordered` / `unreadable` (`TIGHTEN_VERDICTS`).
317
+ It reads order only: each `TightenRule` has a normalized domain (`enumRank` a word of `order`; `boolStricter` a boolean, never
318
+ truthiness; `setUnion` / `setNarrower` a string array compared as a set; `numericLower` a finite number or `+Infinity` = no limit;
319
+ `none` a strict JSON value), a side outside it — or absent — is `unreadable`, and `policyDenyWins` is a RangeError (a fold law
320
+ between members, not an order). `TightenRule` appends `setUnion` (a superset is stricter) and `numericLower` (smaller is stricter),
321
+ both parameter-free: spellings such as an empty-set sentinel or a "≤ 0 means off" cap are normalized by the value's owner first.
322
+ A category's `tighten` answers only a `whole` unit's value; a declared member's comparator comes from its row's value class —
323
+ `MemberAdmission.list: true` marks a string-list member, `boolPole` a boolean one, exactly one per row — via
324
+ `memberTighten(category, member)` (`list` ⇒ setUnion for tighten / setNarrower for loosen; neither or both ⇒ RangeError).
325
+ The settings-document unit table is published here too: `SETTINGS_UNITS` (the ten units `permissions` · `permissionMode` ·
326
+ `additionalDirectories` · `sandbox` · `disableBypassPermissionsMode` · `disableAutoMode` · `hooks` · `env` · `model` · `outputStyle`,
327
+ each with its document `path`, category, addressing and reading), `unitTighten(unit, member?)` (a unit's comparator) and
328
+ `readSettingsUnit(unit, raw, member?)` (its raw-value reading: `"manual"` ⇒ `"default"`, `"disable"` ⇒ `true`, `false` stays
329
+ `false`, `"bypassPermissions"` ⇒ `launch_only`, everything outside the unit's reading — and `undefined` / `null` — ⇒ `unreadable`).
315
330
  - **Settings sources + layer admission** (`settings-sources.ts`, main barrel): `SETTINGS_SOURCES` (declaration order =
316
331
  merge order: user < project < local < flag < policy), `SETTINGS_SOURCE_CONTRACT` (each source's trust tier and whether a
317
332
  repository controls it — `projectSettings` / `localSettings` are repository-controllable and share the `project` tier, as in
@@ -341,8 +356,12 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
341
356
  `VerifierConfig` / `VerifiedIdentity` / `AuthBridgeErrorCode` + pure decision fns
342
357
  (`shouldForceJwksRefresh`, `jwksWithinOfflineGrace`, `classifyTimeClaims`, `toVerifiedIdentity`).
343
358
  The normative rules R1–R10 in its header are the service verifier's implementation spec.
344
- `VerifiedIdentity` carries an optional `scope` (the token's active tenant scope — see `api/scopes`) and an optional `iat`
345
- (issued-at, integer seconds, verbatim — absent claim ⇒ no key; not a non-negative integer ⇒ the token is refused).
359
+ `VerifiedIdentity` carries an optional `scope` (the token's active tenant scope — see `api/scopes`), an optional `iat`
360
+ (issued-at, integer seconds, verbatim — absent claim ⇒ no key; not a non-negative integer ⇒ the token is refused) and an optional
361
+ `principalInc` (14.7.0: the principal's incarnation id, read from the token claim `PRINCIPAL_INC_CLAIM` = `sub_inc` — a non-empty string
362
+ verbatim; absent claim ⇒ no key, never a placeholder; present but not a non-empty string ⇒ the token is refused. A deleted-and-re-created
363
+ user keeps the name and gets a new incarnation, so `sub` alone names a slot, not a person; `PrincipalIncarnation` is the one schema every
364
+ incarnation field reads).
346
365
  - **web-admin credential file + connection env names** (`web-admin-credential.ts`, main barrel; S-698): the file behind
347
366
  `SEMA_WEB_ADMIN_TOKEN_FILE` is one of two KINDS — a bare line (one static token — the same kind of file as today:
348
367
  its first line, printable ASCII with no control / non-ASCII characters; user JWT vs worker pull token stays the reader's `looksLikeJwt` call) or a
@@ -363,6 +382,30 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
363
382
  no default) → `env` / `file` / `absent`, the one pick-exactly-one rule for every credential: an empty string is unset, BOTH set (non-empty) throws (the refusal
364
383
  names the two keys, never a value), the file path comes back verbatim (resolving a relative one against a cwd
365
384
  is the reader's job).
385
+ - **Sandbox git credential plane** (14.7.0; `sandbox-git.ts` + `api/credentials.ts`, main barrel only — no subpath): a sandbox's git uses the
386
+ MEMBER's own credential, fetched by the data plane from web-admin's credential port and used only through a proxy in that member's own pod. This
387
+ package carries the shapes and word tables, no IO. (1) `isSandboxGitOrigin` — a string equal to its own WHATWG origin with scheme http / https (the
388
+ canonical form is that one equation's consequence: lowercase host, no default port, no trailing slash, no userinfo / path / query / fragment;
389
+ an origin cannot express a sub-path deployment) — and the `sandboxGit` effective-config block `SandboxGitConfig`
390
+ (`{ origins: [{ origin, connectUrl }] }`, ≤ `SANDBOX_GIT_MAX_ORIGINS` = 16, distinct, empty list legal; an http origin is READABLE in the block — its
391
+ refusal is a per-origin answer word, not a block refusal). `sandboxGit` is a non-domain top-level key of the `/effective` payload: optional, no
392
+ default, carried verbatim by `readEffectiveWire` (`EffectiveWire.sandboxGit` is `unknown`) and judged by the consumer with `SandboxGitConfig`.
393
+ (2) The credential port's wire for `kind: "sandbox_git"`: `SandboxGitCredentialRequest` (claim a handle), `CredentialHandleAnswer` /
394
+ `CredentialResolveRequest` (kind-independent), `SandboxGitCredentialAnswer` with one `SandboxGitOriginAnswer` per origin (discriminated by
395
+ `state`; `secret` only on the `active` arm — other arms strip it), `SandboxGitCredentialRefusal` (`{ error, permanence, message }`). Three word tables,
396
+ each spelled once: `SANDBOX_GIT_ORIGIN_VERDICTS` (the 11-row per-origin judgment order; `SANDBOX_GIT_ORIGIN_STATES` / `SANDBOX_GIT_ORIGIN_REASONS`
397
+ and the answer arms' reason domains derive from it), `SANDBOX_GIT_REFUSALS` (10 refusal words → HTTP statuses + `permanence`;
398
+ `SANDBOX_GIT_REFUSAL_ERRORS` derives from it), `SANDBOX_GIT_PERMANENCE` (`terminal` / `transient`). Both sides' reading is in
399
+ `docs/CONSUMER-INTEGRATION.md` §12.
400
+ - **Path-base judge and the fleet advertise address** (14.7.0; `url-shape.ts` main barrel, and `fleet.ts` main barrel + `./fleet`): a seat whose value is a
401
+ "base that other code appends request paths to" — the center base URLs, the address a fleet member advertises to the control plane, the web link base of
402
+ `webAdminSessionUrl` — has one rule: no credential, no query, no fragment; a path prefix is fine. `PATH_BASE_URL_FORBIDDEN_PARTS` is the ONE closed, frozen,
403
+ ordered set of the URL parts such a seat forbids (`userinfo` · `query` · `fragment`; a POLICY set, independent of the judge's vocabulary `HTTP_URL_PARTS` — which says what parts a URL has — so a new vocabulary word
404
+ has to be ruled on here explicitly); `pathBaseUrlFlaws(url)` is `httpUrlFlaws(url, that set)` — `[]` = acceptable, otherwise the judge's rule words, never a piece of the
405
+ value. `webAdminSessionUrl` reads it too. `FLEET_ADVERTISE_ADDRESS_ENV` (`"FLEET_ADVERTISE_ADDRESS"`, beside `FLEET_GENERATION_ENV` / `FLEET_SCOPE_ENV`) is the env name both the
406
+ writer (the orchestrator that renders the value) and the reader (the engine at boot) import. Judge the value AFTER placeholders are expanded: `http://$(POD_IP):8090` passes,
407
+ `http://[$(POD_IP)]:8090` does not (a bracketed host must parse as IPv6). Placeholder expansion, reachability and the absent / empty-string reading are not this package's;
408
+ the env name is not a reserved worker env name. Both sides' reading is in `docs/CONSUMER-INTEGRATION.md` §13.
366
409
  - **Session web link** (`web-admin-session-url.ts`, main barrel): `webAdminSessionUrl(webBaseUrl, { worker, sessionId })` — the ONE
367
410
  template for a web-admin session link, `<webBaseUrl>/chat?agent=<worker>&session=<sessionId>`: both parts required (a link with only
368
411
  the session id lands on whichever worker the page picks and may not find it), each part `encodeURIComponent`-escaped, the base a
@@ -481,7 +524,7 @@ therefore shows only `zod`. Node built-ins only (`node:crypto`, `node:fs`).
481
524
  `urlAllowPatternMatches` (⊆ CC `g9`, four upstream holes closed) / `urlDenyPatternMatches` (⊇ `g9` ∪ whole-string glob);
482
525
  divergences in `docs/CC-DIVERGENCE.md`.
483
526
  - **HTTP hook allowlists** (`http-hook-allowlists.ts`, 13.0.0): category `SAFETY_MERGE_SPEC.httpHookAllowlists`; `readHttpHookAllowlistLayer(source, settings)`,
484
- `httpHookUrlVerdict(url, layers, workspace)` (every admitted layer's list must pass — layers intersect) and `httpHookEnvVarsAllowed(hookVars, layers, workspace)`;
527
+ `httpHookUrlVerdict(url, layers, workspace)` (every admitted layer's list must pass — layers intersect) and `httpHookEnvVarsAllowed(hookVars, layers, workspace)`, where every layer in `layers` is read by the reader's own reading (a reader-produced `read.layer`, a hand-built plain layer and a `JSON.parse`d layer from this realm with the same content get the same answer; a layer whose `source` cannot be read — missing, inherited, inconsistent or not a string — is a malformed layer that blocks everything (`rule: "malformed"`, not named in `sources`; a readable source with an unreadable list is `malformed` too and is named; `empty` means a list that really is `[]`), and a readable source word outside the closed set throws `RangeError`; lists, `hookVars` and `layers` must be plain arrays — the prototype is this realm's `Array.prototype` (an array from another realm — a `vm` context, another iframe — is refused; round-trip it through JSON first), `constructor` / `filter` / `flatMap` / `Symbol.iterator` read as this realm's built-ins, and the own keys are only indices and `length`; anything else, subclasses included, is read as malformed);
485
528
  a layer is admitted ⇔ `layerAdmission(source, workspace, "httpHookAllowlists") === "admitted"` (user / flag / managed; project / local are below the floor).
486
529
  - **MCP server list verdict** (`mcp-server-policy.ts`, 13.0.0): `mcpServerPolicyVerdict` → `McpPolicyBlock{name, list, rule, target,
487
530
  written, sources}` — judged on the expanded target, echoed only as written; the managed layer (whose `allowManagedMcpServersOnly` counts)
@@ -1 +1 @@
1
- 14.5.0
1
+ 14.7.0
@@ -0,0 +1,153 @@
1
+ {
2
+ "domains": {
3
+ "outcome": "effective",
4
+ "effective": {
5
+ "collab": {
6
+ "templates": []
7
+ },
8
+ "entitlement": {
9
+ "budgetGroups": [],
10
+ "groupBindings": [],
11
+ "subjects": [],
12
+ "tiers": []
13
+ },
14
+ "execution": {
15
+ "allowedLanes": [
16
+ "host"
17
+ ],
18
+ "required": false
19
+ },
20
+ "governance": {},
21
+ "hosts": {
22
+ "hosts": []
23
+ },
24
+ "limits": {},
25
+ "mcp": {
26
+ "servers": []
27
+ },
28
+ "models": {
29
+ "atMention": {
30
+ "mode": "all"
31
+ },
32
+ "models": [],
33
+ "roles": {},
34
+ "tierGroups": []
35
+ },
36
+ "plugins": {
37
+ "plugins": []
38
+ },
39
+ "projects": {
40
+ "projects": {}
41
+ },
42
+ "prompts": {},
43
+ "rosters": {
44
+ "rosters": []
45
+ },
46
+ "scenarios": {
47
+ "scenarios": []
48
+ },
49
+ "skills": {
50
+ "skills": []
51
+ },
52
+ "systems": {
53
+ "systems": []
54
+ },
55
+ "workers": {
56
+ "workers": []
57
+ }
58
+ },
59
+ "warnings": [],
60
+ "metadata": {
61
+ "version": 7,
62
+ "updatedAt": "2026-10-23T00:00:00.000Z"
63
+ }
64
+ },
65
+ "strictWrite": {
66
+ "outcome": "effective",
67
+ "effective": {},
68
+ "warnings": [],
69
+ "metadata": {
70
+ "version": 7,
71
+ "updatedAt": "2026-10-23T00:00:00.000Z"
72
+ },
73
+ "why": "受理空集:本条打的是 wire 腿(提交的域文档集为空)。"
74
+ },
75
+ "wire": {
76
+ "outcome": "effective",
77
+ "effective": {
78
+ "collab": {
79
+ "templates": []
80
+ },
81
+ "entitlement": {
82
+ "budgetGroups": [],
83
+ "groupBindings": [],
84
+ "subjects": [],
85
+ "tiers": []
86
+ },
87
+ "execution": {
88
+ "allowedLanes": [
89
+ "host"
90
+ ],
91
+ "required": false
92
+ },
93
+ "governance": {},
94
+ "hosts": {
95
+ "hosts": []
96
+ },
97
+ "limits": {},
98
+ "mcp": {
99
+ "servers": []
100
+ },
101
+ "models": {
102
+ "atMention": {
103
+ "mode": "all"
104
+ },
105
+ "models": [],
106
+ "roles": {},
107
+ "tierGroups": []
108
+ },
109
+ "plugins": {
110
+ "plugins": []
111
+ },
112
+ "projects": {
113
+ "projects": {}
114
+ },
115
+ "rosters": {
116
+ "rosters": []
117
+ },
118
+ "sandboxGit": {
119
+ "origins": [
120
+ {
121
+ "connectUrl": "https://console.example.com/connect/git",
122
+ "origin": "https://git.example.com:9999"
123
+ },
124
+ {
125
+ "connectUrl": "https://console.example.com/connect/git",
126
+ "origin": "http://git.legacy.example:8080"
127
+ }
128
+ ]
129
+ },
130
+ "scenarios": {
131
+ "scenarios": []
132
+ },
133
+ "skills": {
134
+ "skills": []
135
+ },
136
+ "systems": {
137
+ "systems": []
138
+ },
139
+ "workers": {
140
+ "workers": []
141
+ }
142
+ },
143
+ "warnings": [],
144
+ "metadata": {
145
+ "version": 7,
146
+ "updatedAt": "2026-10-23T00:00:00.000Z"
147
+ }
148
+ },
149
+ "localRoot": {
150
+ "derive": true,
151
+ "localVersion": 2617555914511015
152
+ }
153
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "meta": {
3
+ "version": 7,
4
+ "updatedAt": "2026-10-23T00:00:00.000Z"
5
+ },
6
+ "domains": {},
7
+ "wirePayload": {
8
+ "version": 7,
9
+ "updatedAt": "2026-10-23T00:00:00.000Z",
10
+ "sandboxGit": {
11
+ "origins": [
12
+ {
13
+ "origin": "https://git.example.com:9999",
14
+ "connectUrl": "https://console.example.com/connect/git"
15
+ },
16
+ {
17
+ "origin": "http://git.legacy.example:8080",
18
+ "connectUrl": "https://console.example.com/connect/git"
19
+ }
20
+ ]
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "contributor": "contract",
3
+ "claim": "`sandboxGit` 是 /effective 载荷上的**非域**顶层键(web-admin 按部署登记册投影,不是存储域):wire 腿按 open-world 把它**逐字承运**(含块里的 http origin —— 它的拒因是逐 origin 应答里的一个词,不是块的校验拒),不出告警;其余三腿的入参里没有这个块,读出的 effective 里也不铸它(可选、无默认)。把它当域去校验、或当未知键丢掉的读腿,会让取值平面静默失去 origin 表。",
4
+ "firstRedOn": []
5
+ }
@@ -0,0 +1,153 @@
1
+ {
2
+ "domains": {
3
+ "outcome": "effective",
4
+ "effective": {
5
+ "collab": {
6
+ "templates": []
7
+ },
8
+ "entitlement": {
9
+ "budgetGroups": [],
10
+ "groupBindings": [],
11
+ "subjects": [],
12
+ "tiers": []
13
+ },
14
+ "execution": {
15
+ "allowedLanes": [
16
+ "host"
17
+ ],
18
+ "required": false
19
+ },
20
+ "governance": {},
21
+ "hosts": {
22
+ "hosts": []
23
+ },
24
+ "limits": {},
25
+ "mcp": {
26
+ "servers": []
27
+ },
28
+ "models": {
29
+ "atMention": {
30
+ "mode": "all"
31
+ },
32
+ "models": [],
33
+ "roles": {},
34
+ "tierGroups": []
35
+ },
36
+ "plugins": {
37
+ "plugins": []
38
+ },
39
+ "projects": {
40
+ "projects": {}
41
+ },
42
+ "prompts": {},
43
+ "rosters": {
44
+ "rosters": []
45
+ },
46
+ "scenarios": {
47
+ "scenarios": []
48
+ },
49
+ "skills": {
50
+ "skills": []
51
+ },
52
+ "systems": {
53
+ "systems": []
54
+ },
55
+ "workers": {
56
+ "workers": []
57
+ }
58
+ },
59
+ "warnings": [],
60
+ "metadata": {
61
+ "version": 7,
62
+ "updatedAt": "2026-10-23T00:00:00.000Z"
63
+ }
64
+ },
65
+ "strictWrite": {
66
+ "outcome": "effective",
67
+ "effective": {},
68
+ "warnings": [],
69
+ "metadata": {
70
+ "version": 7,
71
+ "updatedAt": "2026-10-23T00:00:00.000Z"
72
+ },
73
+ "why": "受理空集:本条打的是 wire 腿(提交的域文档集为空)。"
74
+ },
75
+ "wire": {
76
+ "outcome": "effective",
77
+ "effective": {
78
+ "collab": {
79
+ "templates": []
80
+ },
81
+ "entitlement": {
82
+ "budgetGroups": [],
83
+ "groupBindings": [],
84
+ "subjects": [],
85
+ "tiers": []
86
+ },
87
+ "execution": {
88
+ "allowedLanes": [
89
+ "host"
90
+ ],
91
+ "required": false
92
+ },
93
+ "governance": {},
94
+ "hosts": {
95
+ "hosts": []
96
+ },
97
+ "limits": {},
98
+ "mcp": {
99
+ "servers": []
100
+ },
101
+ "models": {
102
+ "atMention": {
103
+ "mode": "all"
104
+ },
105
+ "models": [],
106
+ "roles": {},
107
+ "tierGroups": []
108
+ },
109
+ "plugins": {
110
+ "plugins": []
111
+ },
112
+ "projects": {
113
+ "projects": {}
114
+ },
115
+ "rosters": {
116
+ "rosters": []
117
+ },
118
+ "sandboxGit": {
119
+ "origins": [
120
+ {
121
+ "connectUrl": "http://console.example.com/connect/git",
122
+ "origin": "https://Git.Example.com:443/"
123
+ },
124
+ {
125
+ "connectUrl": "https://console.example.com/connect/git",
126
+ "origin": "https://Git.Example.com:443/"
127
+ }
128
+ ]
129
+ },
130
+ "scenarios": {
131
+ "scenarios": []
132
+ },
133
+ "skills": {
134
+ "skills": []
135
+ },
136
+ "systems": {
137
+ "systems": []
138
+ },
139
+ "workers": {
140
+ "workers": []
141
+ }
142
+ },
143
+ "warnings": [],
144
+ "metadata": {
145
+ "version": 7,
146
+ "updatedAt": "2026-10-23T00:00:00.000Z"
147
+ }
148
+ },
149
+ "localRoot": {
150
+ "derive": true,
151
+ "localVersion": 2617555914511015
152
+ }
153
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "meta": {
3
+ "version": 7,
4
+ "updatedAt": "2026-10-23T00:00:00.000Z"
5
+ },
6
+ "domains": {},
7
+ "wirePayload": {
8
+ "version": 7,
9
+ "updatedAt": "2026-10-23T00:00:00.000Z",
10
+ "sandboxGit": {
11
+ "origins": [
12
+ {
13
+ "origin": "https://Git.Example.com:443/",
14
+ "connectUrl": "http://console.example.com/connect/git"
15
+ },
16
+ {
17
+ "origin": "https://Git.Example.com:443/",
18
+ "connectUrl": "https://console.example.com/connect/git"
19
+ }
20
+ ]
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "contributor": "contract",
3
+ "claim": "块不合格(非规范形 origin、http 的 connectUrl、重复 origin)时 wire 腿**照样逐字承运**、不出告警:读腿不替消费方判 `sandboxGit`,判它的唯一座位是 `SandboxGitConfig`(消费方在用之前跑)。读腿若『好心』校验并丢弃坏块,消费方分不清『没有投影』与『投影坏了』。",
4
+ "firstRedOn": []
5
+ }
@@ -54,10 +54,14 @@
54
54
  * signature checks). (Decision function: `jwksWithinOfflineGrace`.)
55
55
  *
56
56
  * R9 OUTPUT. Success → `VerifiedIdentity { principal: sub, role: token.role, grantId: token.jti,
57
- * scope: token.scope, iat: token.iat }` (`scope` = the multi-tenant active scope claim, `api/scopes`; read it with
57
+ * scope: token.scope, iat: token.iat, principalInc: token.sub_inc }` (`scope` = the multi-tenant active scope claim, `api/scopes`; read it with
58
58
  * `readTokenScope` — an absent claim is refused, not read as "global"; `iat` = issued-at, integer seconds, carried
59
59
  * verbatim — a reader that learns a principal was deleted at T cuts off its tokens issued before T; absent claim ⇒ no
60
- * `iat` key; present but not a non-negative integer ⇒ the token is `malformed`).
60
+ * `iat` key; present but not a non-negative integer ⇒ the token is `malformed`; `principalInc` = the incarnation claim
61
+ * {@link PRINCIPAL_INC_CLAIM} carried verbatim — the same name deleted and re-created is ANOTHER person, and this is what
62
+ * tells the two apart; absent claim ⇒ no `principalInc` key — never a placeholder: what a reader does with a token that names
63
+ * no incarnation is that reader's decision, not a refusal here; present but not a non-empty string ⇒ the token is `malformed`,
64
+ * the same law as `sub`).
61
65
  * `role` is web-admin RBAC role claim (string, informational for the worker's own authz mapping);
62
66
  * `grantId` is the jti when web-admin stamps one (absent today — optional forever). Failure → one
63
67
  * of AUTH_BRIDGE_ERROR_CODES; the HTTP surface answers 401 with a GENERIC message (no error detail
@@ -87,6 +91,14 @@ export declare const DEFAULT_OFFLINE_GRACE_SEC: number;
87
91
  * split one identity into two. Anyone validating or minting a principal (a token `sub`, a membership row, an outbound
88
92
  * principal header) checks against THIS value. */
89
93
  export declare const PRINCIPAL_MAX_LENGTH = 190;
94
+ /** R9 — the token claim that carries the principal's INCARNATION: a deleted-and-re-created user keeps the name (`sub`) and gets a new
95
+ * incarnation, so `sub` alone names a slot, not a person. One spelling for whoever mints the claim and whoever reads it
96
+ * ({@link toVerifiedIdentity} reads it by this constant), the way `SCOPE_CLAIM` (`api/scopes`) is spelled once for the scope claim. */
97
+ export declare const PRINCIPAL_INC_CLAIM = "sub_inc";
98
+ /** One incarnation id: a non-empty string, opaque (never trimmed, folded or parsed). The token claim, {@link VerifiedIdentity.principalInc},
99
+ * and every wire field that names an incarnation (the credential port's request and answer) are THIS schema — one rule, read by object. */
100
+ export declare const PrincipalIncarnation: z.ZodString;
101
+ export type PrincipalIncarnation = z.infer<typeof PrincipalIncarnation>;
90
102
  /** One RSA public JWK as web-admin publishes it (public members ONLY — a `d`/`p`/`q` here is a web-admin
91
103
  * bug, but the verifier MUST still never use/echo private members). */
92
104
  export declare const WebAdminJwk: z.ZodObject<{
@@ -140,6 +152,7 @@ export declare const VerifiedIdentity: z.ZodObject<{
140
152
  grantId: z.ZodOptional<z.ZodString>;
141
153
  scope: z.ZodOptional<z.ZodString>;
142
154
  iat: z.ZodOptional<z.ZodNumber>;
155
+ principalInc: z.ZodOptional<z.ZodString>;
143
156
  }, z.core.$strip>;
144
157
  export type VerifiedIdentity = z.infer<typeof VerifiedIdentity>;
145
158
  /** The verifier's total result — exactly one of ok/error. */
@@ -195,6 +208,7 @@ export declare function toVerifiedIdentity(payload: {
195
208
  aud?: unknown;
196
209
  scope?: unknown;
197
210
  iat?: unknown;
211
+ sub_inc?: unknown;
198
212
  }, cfg?: Pick<VerifierConfig, "audience">): AuthBridgeResult;
199
213
  /** Cheap pre-filter: does this bearer LOOK like a JWT (three non-empty dot-separated segments)? Use it to
200
214
  * route between the existing static-token branch and this verifier — NOT as any kind of validation. */
@@ -54,10 +54,14 @@
54
54
  * signature checks). (Decision function: `jwksWithinOfflineGrace`.)
55
55
  *
56
56
  * R9 OUTPUT. Success → `VerifiedIdentity { principal: sub, role: token.role, grantId: token.jti,
57
- * scope: token.scope, iat: token.iat }` (`scope` = the multi-tenant active scope claim, `api/scopes`; read it with
57
+ * scope: token.scope, iat: token.iat, principalInc: token.sub_inc }` (`scope` = the multi-tenant active scope claim, `api/scopes`; read it with
58
58
  * `readTokenScope` — an absent claim is refused, not read as "global"; `iat` = issued-at, integer seconds, carried
59
59
  * verbatim — a reader that learns a principal was deleted at T cuts off its tokens issued before T; absent claim ⇒ no
60
- * `iat` key; present but not a non-negative integer ⇒ the token is `malformed`).
60
+ * `iat` key; present but not a non-negative integer ⇒ the token is `malformed`; `principalInc` = the incarnation claim
61
+ * {@link PRINCIPAL_INC_CLAIM} carried verbatim — the same name deleted and re-created is ANOTHER person, and this is what
62
+ * tells the two apart; absent claim ⇒ no `principalInc` key — never a placeholder: what a reader does with a token that names
63
+ * no incarnation is that reader's decision, not a refusal here; present but not a non-empty string ⇒ the token is `malformed`,
64
+ * the same law as `sub`).
61
65
  * `role` is web-admin RBAC role claim (string, informational for the worker's own authz mapping);
62
66
  * `grantId` is the jti when web-admin stamps one (absent today — optional forever). Failure → one
63
67
  * of AUTH_BRIDGE_ERROR_CODES; the HTTP surface answers 401 with a GENERIC message (no error detail
@@ -72,6 +76,8 @@
72
76
  */
73
77
  import { z } from "zod";
74
78
  import { INTEGER } from "../integer.js";
79
+ import { inspect, orWhenUninspectable, ownJsonOrThrow, ownOrThrow } from "../own-key-read.js";
80
+ import { isJsonArray } from "../plain-json-object.js";
75
81
  // ── constants ────────────────────────────────────────────────────────────────────────────────────────────
76
82
  /** The web-admin JWKS endpoint, relative to the web-admin origin (shipped; public by design, RFC 7517 §5). */
77
83
  export const WEB_ADMIN_JWKS_PATH = "/api/auth/sso/jwks";
@@ -88,6 +94,13 @@ export const DEFAULT_OFFLINE_GRACE_SEC = 24 * 60 * 60;
88
94
  * split one identity into two. Anyone validating or minting a principal (a token `sub`, a membership row, an outbound
89
95
  * principal header) checks against THIS value. */
90
96
  export const PRINCIPAL_MAX_LENGTH = 190;
97
+ /** R9 — the token claim that carries the principal's INCARNATION: a deleted-and-re-created user keeps the name (`sub`) and gets a new
98
+ * incarnation, so `sub` alone names a slot, not a person. One spelling for whoever mints the claim and whoever reads it
99
+ * ({@link toVerifiedIdentity} reads it by this constant), the way `SCOPE_CLAIM` (`api/scopes`) is spelled once for the scope claim. */
100
+ export const PRINCIPAL_INC_CLAIM = "sub_inc";
101
+ /** One incarnation id: a non-empty string, opaque (never trimmed, folded or parsed). The token claim, {@link VerifiedIdentity.principalInc},
102
+ * and every wire field that names an incarnation (the credential port's request and answer) are THIS schema — one rule, read by object. */
103
+ export const PrincipalIncarnation = z.string().min(1);
91
104
  // ── JWKS wire shape (what GET /api/auth/sso/jwks returns) ────────────────────────────────────────────────
92
105
  /** One RSA public JWK as web-admin publishes it (public members ONLY — a `d`/`p`/`q` here is a web-admin
93
106
  * bug, but the verifier MUST still never use/echo private members). */
@@ -159,6 +172,11 @@ export const VerifiedIdentity = z.object({
159
172
  * a claim that is not a non-negative integer makes the token `malformed` (R9), and one issued later than now + the clock
160
173
  * skew is `expired` (R5, the same tolerance as `exp` / `nbf`). */
161
174
  iat: INTEGER.nonnegative().optional(),
175
+ /** = token {@link PRINCIPAL_INC_CLAIM} (`sub_inc`) when present, verbatim — the principal's incarnation id ({@link PrincipalIncarnation}).
176
+ * Absent claim ⇒ the key is absent (never minted; never `0`, `""` or any placeholder): a reader that needs an incarnation and finds none
177
+ * answers "incarnation unrecorded" itself — that is not a refusal of the token. A claim that is not a non-empty string makes the token
178
+ * `malformed` (R9): a broken issuer is not read as an old one. */
179
+ principalInc: PrincipalIncarnation.optional(),
162
180
  });
163
181
  /**
164
182
  * R3 — should a token bearing `kid` trigger a forced JWKS refetch?
@@ -203,29 +221,46 @@ export function classifyTimeClaims(claims, nowSec, clockSkewSec) {
203
221
  * library has verified signature (R1/R2/R3), issuer (R4) and time (R5).
204
222
  */
205
223
  export function toVerifiedIdentity(payload, cfg) {
206
- if (typeof payload.sub !== "string" || payload.sub.length === 0)
207
- return { ok: false, error: "malformed" };
208
- // The iat rule is the output field's own schema (one rule, not a second hand-written check).
209
- const iatRead = VerifiedIdentity.shape.iat.safeParse(payload.iat);
210
- if (!iatRead.success)
211
- return { ok: false, error: "malformed" };
212
- const iat = iatRead.data;
213
- if (cfg?.audience) {
214
- const aud = payload.aud;
215
- const match = typeof aud === "string" ? aud === cfg.audience : Array.isArray(aud) && aud.includes(cfg.audience);
216
- if (!match)
224
+ // Every claim is read from the payload's OWN keys (identity reads only what the token carries: a polluted `Object.prototype.role` is not a role the token was minted with,
225
+ // and a token with no `aud` is not given one); a claim that cannot be read ⇒ the token is malformed (fail-closed), never an engine error.
226
+ return orWhenUninspectable(() => {
227
+ const sub = ownOrThrow(payload, "sub"); // 只问「是不是非空串」:读一次值,不深拷
228
+ if (typeof sub !== "string" || sub.length === 0)
217
229
  return { ok: false, error: "malformed" };
218
- }
219
- return {
220
- ok: true,
221
- identity: {
222
- principal: payload.sub,
223
- role: typeof payload.role === "string" && payload.role.length > 0 ? payload.role : undefined,
224
- grantId: typeof payload.jti === "string" && payload.jti.length > 0 ? payload.jti : undefined,
225
- scope: typeof payload.scope === "string" && payload.scope.length > 0 ? payload.scope : undefined,
226
- ...(iat !== undefined ? { iat } : {}),
227
- },
228
- };
230
+ // The iat and incarnation rules are the output fields' own schemas (one rule each, not a second hand-written check).
231
+ // Each schema call runs under `inspect`: the validator reads a snapshot THIS package took, and a throw there (a length check coercing an object value to a primitive) is not the caller's
232
+ // object being unreadable — but it is still no identity: the claim is malformed, and no engine error escapes the judge.
233
+ const iatRead = inspect(() => VerifiedIdentity.shape.iat.safeParse(ownJsonOrThrow(payload, "iat")));
234
+ if (!iatRead.ok || !iatRead.value.success)
235
+ return { ok: false, error: "malformed" };
236
+ const iat = iatRead.value.data;
237
+ const incRead = inspect(() => VerifiedIdentity.shape.principalInc.safeParse(ownJsonOrThrow(payload, PRINCIPAL_INC_CLAIM)));
238
+ if (!incRead.ok || !incRead.value.success)
239
+ return { ok: false, error: "malformed" };
240
+ const principalInc = incRead.value.data;
241
+ if (cfg?.audience) {
242
+ const aud = ownJsonOrThrow(payload, "aud");
243
+ const match = typeof aud === "string" ? aud === cfg.audience : isJsonArray(aud) && aud.includes(cfg.audience);
244
+ if (!match)
245
+ return { ok: false, error: "malformed" };
246
+ }
247
+ // role / jti / scope 只问「是不是非空串」(别的值读成缺省):读一次值,不深拷 —— 深拷会让「值是个带 Date 的对象」把整枚令牌拖成 malformed(v14.6.0 只做 typeof 判)。
248
+ // 要深拷的只有交给会反射它的代码的值:`aud`(数组上 `includes`)与 `iat` / `sub_inc`(交给校验器)
249
+ const role = ownOrThrow(payload, "role");
250
+ const jti = ownOrThrow(payload, "jti");
251
+ const scope = ownOrThrow(payload, "scope");
252
+ return {
253
+ ok: true,
254
+ identity: {
255
+ principal: sub,
256
+ role: typeof role === "string" && role.length > 0 ? role : undefined,
257
+ grantId: typeof jti === "string" && jti.length > 0 ? jti : undefined,
258
+ scope: typeof scope === "string" && scope.length > 0 ? scope : undefined,
259
+ ...(iat !== undefined ? { iat } : {}),
260
+ ...(principalInc !== undefined ? { principalInc } : {}),
261
+ },
262
+ };
263
+ }, () => ({ ok: false, error: "malformed" }));
229
264
  }
230
265
  /** Cheap pre-filter: does this bearer LOOK like a JWT (three non-empty dot-separated segments)? Use it to
231
266
  * route between the existing static-token branch and this verifier — NOT as any kind of validation. */