@sema-agent/settings-schema 6.0.0 → 6.1.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/dist/fleet.d.ts CHANGED
@@ -45,6 +45,42 @@ export type DrainVerdict = "wait" | "exit_clean" | "exit_forced";
45
45
  /** The drain state machine's single decision point: keep waiting, exit clean (drained), or force the exit
46
46
  * (grace exhausted with legs still in flight — durable legs resume cross-instance via resumeFromRunId). */
47
47
  export declare function drainVerdict(s: DrainSnapshot): DrainVerdict;
48
+ /**
49
+ * "This instance must restart to apply config" — the engine's OWN observation (S-741), defined ONCE for both exits that
50
+ * carry it: the fleet heartbeat ({@link InstanceAnnouncement}`.restart`, typed) and GET /health (`restart`, read through
51
+ * {@link readRestartSignal} — {@link InstanceHealth} keeps carrying it untyped, see there). One mint point, two exits —
52
+ * never a second judgement.
53
+ *
54
+ * Shape = what the server already ships on /health (`restart: { reasons, version, since }`). PRESENCE is the signal:
55
+ * present ⇔ a restart is required, absent ⇔ not; there is deliberately no `required` boolean inside (a boolean next
56
+ * to presence is redundant, and two spellings of one fact drift).
57
+ * - `reasons` — which restart-to-apply slices differ from the value baked at boot (the vocabulary is the engine's;
58
+ * the contract only guarantees non-empty words, at least one).
59
+ * - `version` — the config-center version that currently differs from boot (the LATEST seen — it moves forward with
60
+ * later publishes, so a "one restart per version" dedupe bounds restarts per config version, not per reason set).
61
+ * Judged exactly as `readEffectiveWire` judges a config version (a finite number) — one quantity, one rule.
62
+ * - `since` — epoch ms when this restart first became required (stable across refreshes while `reasons` holds).
63
+ */
64
+ export declare const RestartSignal: z.ZodObject<{
65
+ reasons: z.ZodArray<z.ZodString, "many">;
66
+ version: z.ZodNumber;
67
+ since: z.ZodNumber;
68
+ }, "strip", z.ZodTypeAny, {
69
+ reasons: string[];
70
+ version: number;
71
+ since: number;
72
+ }, {
73
+ reasons: string[];
74
+ version: number;
75
+ since: number;
76
+ }>;
77
+ export type RestartSignal = z.infer<typeof RestartSignal>;
78
+ /**
79
+ * The /health exit of {@link RestartSignal}: `health.restart` judged against the one shape. Shape mismatch, a missing
80
+ * key, a non-object body, even a throwing accessor ⇒ `undefined` — never throws (a probe must not die on one odd body).
81
+ * Presence law: only `restart` counts; the top-level `restartRequired` boolean the server also ships is not read.
82
+ */
83
+ export declare function readRestartSignal(health: unknown): RestartSignal | undefined;
48
84
  /** The `/health` self-description every orchestrator reads (shipped shape, server 1.96.0). Liveness and
49
85
  * readiness are deliberately SPLIT: `status` stays ok while draining (the process is alive); routability is
50
86
  * `isReadyForTraffic`. Unknown fields pass through — health is a self-description, not a closed record. */
@@ -93,25 +129,52 @@ export declare const InstanceAnnouncement: z.ZodObject<{
93
129
  /** reachable base URL for the deployment form: k8s pod IP:port / docker net alias / 127.0.0.1:<engine.port> */
94
130
  address: z.ZodString;
95
131
  version: z.ZodString;
96
- /** self-declared: bootDone && !draining */
132
+ /** self-declared readiness: model roster landed AND not draining (server ≥7.102.0; older engines: !draining only) */
97
133
  ready: z.ZodBoolean;
98
134
  draining: z.ZodDefault<z.ZodBoolean>;
99
135
  heartbeatMs: z.ZodDefault<z.ZodNumber>;
136
+ /** S-741: present ⇔ this instance must restart to apply config — the SAME {@link RestartSignal} /health carries,
137
+ * minted by the same getter (self-reported: a worker-scoped credential can only report for its own worker).
138
+ * Every heartbeat re-announces the full row, so a later heartbeat WITHOUT it means "no longer required" — the
139
+ * stored row ({@link InstanceRegistration}) is replaced, never merged. */
140
+ restart: z.ZodOptional<z.ZodObject<{
141
+ reasons: z.ZodArray<z.ZodString, "many">;
142
+ version: z.ZodNumber;
143
+ since: z.ZodNumber;
144
+ }, "strip", z.ZodTypeAny, {
145
+ reasons: string[];
146
+ version: number;
147
+ since: number;
148
+ }, {
149
+ reasons: string[];
150
+ version: number;
151
+ since: number;
152
+ }>>;
100
153
  }, "strip", z.ZodTypeAny, {
101
154
  draining: boolean;
102
- instanceId: string;
103
155
  version: string;
156
+ instanceId: string;
104
157
  worker: string;
105
158
  address: string;
106
159
  ready: boolean;
107
160
  heartbeatMs: number;
161
+ restart?: {
162
+ reasons: string[];
163
+ version: number;
164
+ since: number;
165
+ } | undefined;
108
166
  }, {
109
- instanceId: string;
110
167
  version: string;
168
+ instanceId: string;
111
169
  worker: string;
112
170
  address: string;
113
171
  ready: boolean;
114
172
  draining?: boolean | undefined;
173
+ restart?: {
174
+ reasons: string[];
175
+ version: number;
176
+ since: number;
177
+ } | undefined;
115
178
  heartbeatMs?: number | undefined;
116
179
  }>;
117
180
  export type InstanceAnnouncement = z.infer<typeof InstanceAnnouncement>;
@@ -139,34 +202,61 @@ export declare const InstanceRegistration: z.ZodObject<{
139
202
  /** reachable base URL for the deployment form: k8s pod IP:port / docker net alias / 127.0.0.1:<engine.port> */
140
203
  address: z.ZodString;
141
204
  version: z.ZodString;
142
- /** self-declared: bootDone && !draining */
205
+ /** self-declared readiness: model roster landed AND not draining (server ≥7.102.0; older engines: !draining only) */
143
206
  ready: z.ZodBoolean;
144
207
  draining: z.ZodDefault<z.ZodBoolean>;
145
208
  heartbeatMs: z.ZodDefault<z.ZodNumber>;
209
+ /** S-741: present ⇔ this instance must restart to apply config — the SAME {@link RestartSignal} /health carries,
210
+ * minted by the same getter (self-reported: a worker-scoped credential can only report for its own worker).
211
+ * Every heartbeat re-announces the full row, so a later heartbeat WITHOUT it means "no longer required" — the
212
+ * stored row ({@link InstanceRegistration}) is replaced, never merged. */
213
+ restart: z.ZodOptional<z.ZodObject<{
214
+ reasons: z.ZodArray<z.ZodString, "many">;
215
+ version: z.ZodNumber;
216
+ since: z.ZodNumber;
217
+ }, "strip", z.ZodTypeAny, {
218
+ reasons: string[];
219
+ version: number;
220
+ since: number;
221
+ }, {
222
+ reasons: string[];
223
+ version: number;
224
+ since: number;
225
+ }>>;
146
226
  } & {
147
227
  registeredMs: z.ZodNumber;
148
228
  lastSeenMs: z.ZodNumber;
149
229
  observedReady: z.ZodOptional<z.ZodBoolean>;
150
230
  }, "strip", z.ZodTypeAny, {
151
231
  draining: boolean;
152
- instanceId: string;
153
232
  version: string;
233
+ instanceId: string;
154
234
  worker: string;
155
235
  address: string;
156
236
  ready: boolean;
157
237
  heartbeatMs: number;
158
238
  registeredMs: number;
159
239
  lastSeenMs: number;
240
+ restart?: {
241
+ reasons: string[];
242
+ version: number;
243
+ since: number;
244
+ } | undefined;
160
245
  observedReady?: boolean | undefined;
161
246
  }, {
162
- instanceId: string;
163
247
  version: string;
248
+ instanceId: string;
164
249
  worker: string;
165
250
  address: string;
166
251
  ready: boolean;
167
252
  registeredMs: number;
168
253
  lastSeenMs: number;
169
254
  draining?: boolean | undefined;
255
+ restart?: {
256
+ reasons: string[];
257
+ version: number;
258
+ since: number;
259
+ } | undefined;
170
260
  heartbeatMs?: number | undefined;
171
261
  observedReady?: boolean | undefined;
172
262
  }>;
@@ -410,7 +500,6 @@ export declare const UsageReport: z.ZodObject<{
410
500
  quotaWeightAtUse?: number | undefined;
411
501
  }>, "many">;
412
502
  }, "strip", z.ZodTypeAny, {
413
- instanceId: string;
414
503
  entries: {
415
504
  principal: string;
416
505
  model: string;
@@ -421,11 +510,11 @@ export declare const UsageReport: z.ZodObject<{
421
510
  runs?: number | undefined;
422
511
  quotaWeightAtUse?: number | undefined;
423
512
  }[];
513
+ instanceId: string;
424
514
  worker: string;
425
515
  windowStartMs: number;
426
516
  windowEndMs: number;
427
517
  }, {
428
- instanceId: string;
429
518
  entries: {
430
519
  principal: string;
431
520
  model: string;
@@ -436,6 +525,7 @@ export declare const UsageReport: z.ZodObject<{
436
525
  runs?: number | undefined;
437
526
  quotaWeightAtUse?: number | undefined;
438
527
  }[];
528
+ instanceId: string;
439
529
  worker: string;
440
530
  windowStartMs: number;
441
531
  windowEndMs: number;
package/dist/fleet.js CHANGED
@@ -40,6 +40,43 @@ export function drainVerdict(s) {
40
40
  const grace = Math.max(DRAIN_GRACE_FLOOR_MS, s.graceMs);
41
41
  return s.nowMs - s.startedMs >= grace ? "exit_forced" : "wait";
42
42
  }
43
+ /**
44
+ * "This instance must restart to apply config" — the engine's OWN observation (S-741), defined ONCE for both exits that
45
+ * carry it: the fleet heartbeat ({@link InstanceAnnouncement}`.restart`, typed) and GET /health (`restart`, read through
46
+ * {@link readRestartSignal} — {@link InstanceHealth} keeps carrying it untyped, see there). One mint point, two exits —
47
+ * never a second judgement.
48
+ *
49
+ * Shape = what the server already ships on /health (`restart: { reasons, version, since }`). PRESENCE is the signal:
50
+ * present ⇔ a restart is required, absent ⇔ not; there is deliberately no `required` boolean inside (a boolean next
51
+ * to presence is redundant, and two spellings of one fact drift).
52
+ * - `reasons` — which restart-to-apply slices differ from the value baked at boot (the vocabulary is the engine's;
53
+ * the contract only guarantees non-empty words, at least one).
54
+ * - `version` — the config-center version that currently differs from boot (the LATEST seen — it moves forward with
55
+ * later publishes, so a "one restart per version" dedupe bounds restarts per config version, not per reason set).
56
+ * Judged exactly as `readEffectiveWire` judges a config version (a finite number) — one quantity, one rule.
57
+ * - `since` — epoch ms when this restart first became required (stable across refreshes while `reasons` holds).
58
+ */
59
+ export const RestartSignal = z.object({
60
+ reasons: z.array(z.string().min(1)).min(1),
61
+ version: z.number().finite(),
62
+ since: z.number().int().nonnegative(),
63
+ });
64
+ /**
65
+ * The /health exit of {@link RestartSignal}: `health.restart` judged against the one shape. Shape mismatch, a missing
66
+ * key, a non-object body, even a throwing accessor ⇒ `undefined` — never throws (a probe must not die on one odd body).
67
+ * Presence law: only `restart` counts; the top-level `restartRequired` boolean the server also ships is not read.
68
+ */
69
+ export function readRestartSignal(health) {
70
+ try {
71
+ if (health === null || typeof health !== "object" || Array.isArray(health))
72
+ return undefined;
73
+ const parsed = RestartSignal.safeParse(health.restart);
74
+ return parsed.success ? parsed.data : undefined;
75
+ }
76
+ catch {
77
+ return undefined;
78
+ }
79
+ }
43
80
  /** The `/health` self-description every orchestrator reads (shipped shape, server 1.96.0). Liveness and
44
81
  * readiness are deliberately SPLIT: `status` stays ok while draining (the process is alive); routability is
45
82
  * `isReadyForTraffic`. Unknown fields pass through — health is a self-description, not a closed record. */
@@ -52,6 +89,10 @@ export const InstanceHealth = z
52
89
  draining: z.boolean().optional(),
53
90
  /** epoch ms, present iff draining */
54
91
  drainingSince: z.number().optional(),
92
+ // S-741: the server's `restart` (and its redundant top-level `restartRequired` boolean) ride the passthrough
93
+ // UNTYPED on purpose — typing `restart` here would turn today's lenient read into a strict one (a /health body with
94
+ // an odd `restart` would stop parsing), which is a MAJOR by this package's release rules. Read it with
95
+ // {@link readRestartSignal}. Typing it on this schema is an item for the next major.
55
96
  })
56
97
  .passthrough();
57
98
  /** Readiness = the routability predicate (k8s readiness probe, gateway health filter, shell seam switch). */
@@ -76,10 +117,15 @@ export const InstanceAnnouncement = z.object({
76
117
  /** reachable base URL for the deployment form: k8s pod IP:port / docker net alias / 127.0.0.1:<engine.port> */
77
118
  address: z.string().min(1),
78
119
  version: z.string().min(1),
79
- /** self-declared: bootDone && !draining */
120
+ /** self-declared readiness: model roster landed AND not draining (server ≥7.102.0; older engines: !draining only) */
80
121
  ready: z.boolean(),
81
122
  draining: z.boolean().default(false),
82
123
  heartbeatMs: z.number().int().positive().default(DEFAULT_HEARTBEAT_MS),
124
+ /** S-741: present ⇔ this instance must restart to apply config — the SAME {@link RestartSignal} /health carries,
125
+ * minted by the same getter (self-reported: a worker-scoped credential can only report for its own worker).
126
+ * Every heartbeat re-announces the full row, so a later heartbeat WITHOUT it means "no longer required" — the
127
+ * stored row ({@link InstanceRegistration}) is replaced, never merged. */
128
+ restart: RestartSignal.optional(),
83
129
  });
84
130
  /** Explicit goodbye (stop signal handler) — removes the row immediately instead of waiting out staleness. */
85
131
  export const InstanceDeregistration = z.object({
package/dist/index.d.ts CHANGED
@@ -21,3 +21,4 @@ export * from "./migrate.js";
21
21
  export * from "./api/auth.js";
22
22
  export * from "./api/auth-bridge.js";
23
23
  export * from "./api/scopes.js";
24
+ export * from "./center-credential.js";
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ export * from "./migrate.js"; // 0.5→0.6 migration tools (hosts grandfathering
21
21
  export * from "./api/auth.js"; // /api/v1/auth/* wire contract (RFC 8628 device flow + rotating refresh + logout + approve) — REGISTRY-REBUILD-DESIGN §2, M1 shipped shapes frozen, pure/browser-safe
22
22
  export * from "./api/auth-bridge.js"; // 鉴权桥 contract: worker verifies registry user JWT (RS256-only, JWKS kid-miss refresh + throttle, offline grace fail-closed) → VerifiedIdentity — design §5, the service verifier's spec
23
23
  export * from "./api/scopes.js"; // multi-tenant scope contract: /api/v1/scopes* + /api/v1/auth/scope wire shapes + THE minting/switch decision functions (resolveTokenScope / canSwitchScope / effectiveScopeRole / tokenScopeOf) — registry imports these, never re-implements
24
+ export * from "./center-credential.js"; // S-698 center-credential FILE contract: CenterCredentialEnvelope (user-session envelope, strict) + parseCenterCredentialFile (bare / envelope / unreadable — the one discrimination law) + S-742 CENTER_CREDENTIAL_ENV (the two declaration keys) + judgeCenterCredentialDeclaration (pick-exactly-one), pure/browser-safe
24
25
  // NOTE: the Node-fs-backed helpers (FileConfigStore + the file editor) are NOT in this barrel — they pull
25
26
  // `node:fs`/`node:path` and would break a browser/client bundle that only wants the contract (e.g.
26
27
  // sema-registry's client components import the zod types through here). Import them from the Node-only
package/dist/types.d.ts CHANGED
@@ -16,6 +16,12 @@
16
16
  * `ENV_NAME` schema (regex) on every `*Env` field — a pasted secret value is rejected, not just discouraged.
17
17
  */
18
18
  import { z } from "zod";
19
+ /** A private-key PEM header (`-----BEGIN [RSA |EC |OPENSSH |ENCRYPTED ]PRIVATE KEY`), unanchored — the ONE definition
20
+ * of that shape: {@link SECRET_LITERAL_RE} is composed from it (anchored, as one of its branches), and
21
+ * `redactSecrets` (config-fns) tests it ANYWHERE in a string. It carries spaces, so the token-splitting detector
22
+ * {@link containsSecretToken} only sees it at the very start of a string; a PEM behind a newline or a `key: ` prefix
23
+ * slipped past (S-712 codex review [high]). Public keys / certificates never match (see above). */
24
+ export declare const PRIVATE_KEY_PEM_HEADER: RegExp;
19
25
  export declare const SECRET_LITERAL_RE: RegExp;
20
26
  export declare const ENV_NAME: z.ZodEffects<z.ZodString, string, string>;
21
27
  /** A NUL byte in a string the consumer feeds to spawn()/fs is a FAIL-OPEN config: it validates+persists here
package/dist/types.js CHANGED
@@ -26,7 +26,13 @@ import { DispatchPolicy } from "./fleet.js"; // WorkerSpec.fleet.dispatch (sched
26
26
  // — a PUBLIC KEY / CERTIFICATE is NOT a secret and must NOT be flagged (PRINCIPAL_JWT_PUBKEYS lives in env).
27
27
  // 0.1.32: this is the root fix for the public-key false-positive — replaces the 0.1.31 name-based exemption,
28
28
  // which review flagged as a bypass (a private key misnamed `*_PUBKEY` skipped scanning entirely).
29
- export const SECRET_LITERAL_RE = /^(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,}|-----BEGIN[A-Z0-9 ]*PRIVATE KEY)/i;
29
+ /** A private-key PEM header (`-----BEGIN [RSA |EC |OPENSSH |ENCRYPTED ]PRIVATE KEY`), unanchored — the ONE definition
30
+ * 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). */
34
+ export const PRIVATE_KEY_PEM_HEADER = /-----BEGIN[A-Z0-9 ]*PRIVATE KEY/i;
35
+ 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");
30
36
  // 所有 *Env / *tokenEnv 字段的值是宿主机 env 变量的 NAME(不是值)。合法 env 变量名 + 拒绝 secret 形状的值(AKIA…
31
37
  // 这类是合法 env-NAME 字符集却是密钥)—— "只存 env-NAME、绝不存 secret 值" 由 SCHEMA 保证。Round-5。
32
38
  export const ENV_NAME = z
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/settings-schema",
3
- "version": "6.0.0",
3
+ "version": "6.1.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",