@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.
- package/CHANGELOG.md +531 -0
- package/README.md +46 -3
- package/conformance/VERSION +1 -1
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/expected.json +153 -0
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/input.json +23 -0
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/meta.json +5 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/expected.json +153 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/input.json +23 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/meta.json +5 -0
- package/dist/api/auth-bridge.d.ts +16 -2
- package/dist/api/auth-bridge.js +59 -24
- package/dist/api/credentials.d.ts +281 -0
- package/dist/api/credentials.js +149 -0
- package/dist/api/scopes.js +5 -1
- package/dist/atomic-write.js +10 -7
- package/dist/bundle.d.ts +1 -1
- package/dist/bundle.js +1 -1
- package/dist/config-d-file.js +33 -15
- package/dist/config-dir-migration.js +2 -1
- package/dist/config-fns.js +142 -78
- package/dist/conformance/canonical.js +7 -10
- package/dist/conformance/contract-adapter.js +26 -1
- package/dist/conformance/corpus.js +5 -5
- package/dist/conformance/derive.js +6 -5
- package/dist/conformance/run.js +44 -6
- package/dist/cross-session.d.ts +4 -4
- package/dist/declared-capture.d.ts +41 -0
- package/dist/declared-capture.js +344 -0
- package/dist/file-edit.js +11 -15
- package/dist/file-store.d.ts +1 -31
- package/dist/file-store.js +3 -30
- package/dist/fleet.d.ts +11 -0
- package/dist/fleet.js +60 -26
- package/dist/fs-failure.d.ts +12 -7
- package/dist/fs-failure.js +53 -29
- package/dist/hash.d.ts +0 -9
- package/dist/hash.js +64 -7
- package/dist/hooks-config.d.ts +2 -2
- package/dist/hooks-config.js +129 -33
- package/dist/http-hook-allowlists.d.ts +27 -10
- package/dist/http-hook-allowlists.js +189 -25
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -1
- package/dist/mcp-request-entry.d.ts +13 -8
- package/dist/mcp-request-entry.js +246 -107
- package/dist/one-time-migrations.js +207 -95
- package/dist/own-key-read.d.ts +247 -0
- package/dist/own-key-read.js +449 -0
- package/dist/plain-json-object.d.ts +38 -3
- package/dist/plain-json-object.js +84 -7
- package/dist/portable-domains.d.ts +31 -0
- package/dist/portable-domains.js +30 -0
- package/dist/remote-exec.js +9 -1
- package/dist/safety-merge-spec.d.ts +88 -6
- package/dist/safety-merge-spec.js +246 -17
- package/dist/sandbox-git.d.ts +64 -0
- package/dist/sandbox-git.js +89 -0
- package/dist/scheduler-store.js +21 -12
- package/dist/settings-units.d.ts +85 -0
- package/dist/settings-units.js +177 -0
- package/dist/types.d.ts +6 -2
- package/dist/types.js +65 -56
- package/dist/url-shape.d.ts +27 -5
- package/dist/url-shape.js +29 -5
- package/dist/web-admin-credential.js +5 -2
- package/dist/web-admin-session-url.js +2 -2
- package/dist/worker-row-verdict.d.ts +11 -0
- package/dist/worker-row-verdict.js +51 -1
- package/dist/workers-rows.d.ts +2 -1
- package/dist/workers-rows.js +41 -12
- package/dist/zod-unwrap.d.ts +6 -0
- package/dist/zod-unwrap.js +14 -7
- package/docs/API-SURFACE.md +50 -5
- package/docs/CONSUMER-INTEGRATION.md +178 -0
- package/llms-full.txt +34 -5
- package/llms.txt +6 -3
- 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`)
|
|
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)
|
package/conformance/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
14.
|
|
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
|
+
}
|
|
@@ -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. */
|
package/dist/api/auth-bridge.js
CHANGED
|
@@ -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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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. */
|