@sema-agent/settings-schema 8.1.0 → 8.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +160 -0
- package/README.md +49 -3
- package/conformance/VERSION +1 -1
- package/conformance/migrate-workers/m05-row-verdict-reserved-env/expected.json +82 -0
- package/conformance/migrate-workers/m05-row-verdict-reserved-env/input.json +68 -0
- package/conformance/migrate-workers/m05-row-verdict-reserved-env/meta.json +4 -0
- package/dist/cli.js +4 -3
- package/dist/fleet-paths.json +14 -0
- package/dist/fleet.d.ts +157 -18
- package/dist/fleet.js +151 -40
- package/dist/issue-text.d.ts +10 -0
- package/dist/issue-text.js +8 -0
- package/dist/migrate-workers.d.ts +12 -4
- package/dist/migrate-workers.js +39 -15
- package/dist/remote-exec.d.ts +2 -2
- package/dist/types.d.ts +26 -26
- package/dist/types.js +3 -2
- package/package.json +3 -2
package/dist/fleet.d.ts
CHANGED
|
@@ -18,6 +18,42 @@
|
|
|
18
18
|
* the skeleton does not move).
|
|
19
19
|
*/
|
|
20
20
|
import { z } from "zod";
|
|
21
|
+
/**
|
|
22
|
+
* The center's `/api/fleet/*` endpoints that are addressed from outside the center, relative to its origin. ONE spelling: whoever
|
|
23
|
+
* calls one (the engine, the executor) and whoever serves or routes it (the center, the deployment's Service / Ingress) imports it
|
|
24
|
+
* here — nobody writes the string. Membership rule: an endpoint is here iff this package types its body, or it belongs to the
|
|
25
|
+
* liveness group ({@link FLEET_LIVE_PATHS}); a center-internal route is not.
|
|
26
|
+
* - `instances` — `POST` = the heartbeat ({@link InstanceAnnouncement}; its 2xx reply carries the center's
|
|
27
|
+
* {@link CenterCapabilities} under {@link HEARTBEAT_REPLY_CAPABILITIES_KEY}), `DELETE` = the goodbye ({@link InstanceDeregistration}).
|
|
28
|
+
* - `usage` — `POST` a {@link UsageReport}.
|
|
29
|
+
* - `quotaLeases` — the quota lease ({@link QuotaLease}).
|
|
30
|
+
* - `servingLease` — `POST` a {@link ServingLeaseRequest}, answered with a {@link ServingLeaseResponse}.
|
|
31
|
+
* - `hostPlanner` — `GET` one host's planner record, for the executor (its body is the center's and the executor's contract, not
|
|
32
|
+
* this package's; it is here because the deployment routes it as a member of the liveness group).
|
|
33
|
+
* Notation: a value is a path TEMPLATE — a parameter is a whole segment written `{name}` (`hostPlanner` ends in `/{host}`, filled
|
|
34
|
+
* with the URL-encoded host id); the other values have none and are the path itself. A prefix router (an Ingress rule) takes the
|
|
35
|
+
* template up to its first `{`.
|
|
36
|
+
*/
|
|
37
|
+
export declare const FLEET_PATHS: {
|
|
38
|
+
readonly instances: "/api/fleet/instances";
|
|
39
|
+
readonly usage: "/api/fleet/usage";
|
|
40
|
+
readonly quotaLeases: "/api/fleet/leases";
|
|
41
|
+
readonly servingLease: "/api/fleet/live/serving-lease";
|
|
42
|
+
readonly hostPlanner: "/api/fleet/live/host-planner/{host}";
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* The LIVENESS group — the fleet endpoints a center answers without its readiness gate and without its config-governance gates
|
|
46
|
+
* (they read no config domain and take service credentials only), so that a center whose management face is refused never turns
|
|
47
|
+
* into a fleet that gets no answers. A deployment routes exactly these past the readiness gate (on Kubernetes: a second Service
|
|
48
|
+
* with `publishNotReadyAddresses`, and the Ingress rules for them point at it). Membership is THIS list, not a path prefix — the
|
|
49
|
+
* heartbeat predates the `/api/fleet/live/` prefix; every {@link FLEET_PATHS} entry under that prefix is a member. The
|
|
50
|
+
* quota-lease and usage endpoints are not.
|
|
51
|
+
*
|
|
52
|
+
* The same two constants ship as JSON for tooling that cannot import TypeScript (a Helm chart, a smoke script): the subpath
|
|
53
|
+
* `@sema-agent/settings-schema/fleet-paths.json` is `{ "FLEET_PATHS": …, "FLEET_LIVE_PATHS": … }`, written from these values at
|
|
54
|
+
* build time — never edited by hand.
|
|
55
|
+
*/
|
|
56
|
+
export declare const FLEET_LIVE_PATHS: readonly ["/api/fleet/instances", "/api/fleet/live/serving-lease", "/api/fleet/live/host-planner/{host}"];
|
|
21
57
|
/** Lifecycle of a draining instance. `running`→`draining` on the stop signal; `drained` once in-flight hits
|
|
22
58
|
* zero (clean) or the grace deadline forces the issue. Exit always follows `drained`. */
|
|
23
59
|
export declare const DRAIN_PHASES: readonly ["running", "draining", "drained"];
|
|
@@ -45,6 +81,29 @@ export type DrainVerdict = "wait" | "exit_clean" | "exit_forced";
|
|
|
45
81
|
/** The drain state machine's single decision point: keep waiting, exit clean (drained), or force the exit
|
|
46
82
|
* (grace exhausted with legs still in flight — durable legs resume cross-instance via resumeFromRunId). */
|
|
47
83
|
export declare function drainVerdict(s: DrainSnapshot): DrainVerdict;
|
|
84
|
+
export declare function isCanonicalGeneration(value: unknown): value is string;
|
|
85
|
+
/** Numeric order of two canonical generations: `-1` when `a` is older than `b`, `0` when equal, `1` when newer — `"9"` is
|
|
86
|
+
* older than `"10"`; any number of digits. Throws a `TypeError` on an input that is not canonical (a comparison that quietly
|
|
87
|
+
* returned some number would be a fence decision made on garbage); judge untrusted input with {@link isCanonicalGeneration}
|
|
88
|
+
* first — the wire shapes of this file already do. */
|
|
89
|
+
export declare function compareGeneration(a: string, b: string): -1 | 0 | 1;
|
|
90
|
+
/**
|
|
91
|
+
* The serving lease as the ENGINE holds it — a CLOSED set of four words, owned here (the engine's vocabulary, word for word;
|
|
92
|
+
* a reader that switches on it exhaustively is told by the compiler when a word is added):
|
|
93
|
+
* - `none` — the lease is not in effect: the center has not announced {@link CenterCapabilities}`.servingLease` (an older or
|
|
94
|
+
* degraded center). The engine behaves exactly as an engine without a lease — no renewals, no fence.
|
|
95
|
+
* - `granted` — the engine holds a lease; `leaseUntil` says until when (epoch ms, the engine's own computation from the
|
|
96
|
+
* granted `ttlMs`, see {@link ServingLeaseResponse}).
|
|
97
|
+
* - `expired` — the lease is in effect but no grant is valid right now (the center is unreachable, answered `unknown`, or has
|
|
98
|
+
* not granted one yet): the engine keeps serving until its own outage allowance runs out. Not a denial.
|
|
99
|
+
* - `denied` — the center denied this instance on positive evidence (the `denied` verdict): sticky; the engine is draining and
|
|
100
|
+
* stops at its deadline.
|
|
101
|
+
* Only a fleet member says it: an engine without a generation has no lease and carries no `leaseState` at all (there is no
|
|
102
|
+
* fifth word for "not a member" — absence is that).
|
|
103
|
+
*/
|
|
104
|
+
export declare const LEASE_STATES: readonly ["none", "granted", "expired", "denied"];
|
|
105
|
+
export declare const LeaseState: z.ZodEnum<["none", "granted", "expired", "denied"]>;
|
|
106
|
+
export type LeaseState = z.infer<typeof LeaseState>;
|
|
48
107
|
/**
|
|
49
108
|
* "This instance must restart to apply config" — the engine's OWN observation (S-741), defined ONCE for both exits that
|
|
50
109
|
* carry it: the fleet heartbeat ({@link InstanceAnnouncement}`.restart`, typed) and GET /health (`restart`, read through
|
|
@@ -234,7 +293,10 @@ export declare const STALE_HEARTBEAT_MULTIPLIER = 2;
|
|
|
234
293
|
/** Register/heartbeat upsert (worker → center, D2: worker-active). ONE shape for both — registration is the
|
|
235
294
|
* first heartbeat; every heartbeat re-announces the full row (idempotent upsert, no drift between a
|
|
236
295
|
* "register" and a "renew" schema). `draining:true` is the instance SELF-removing from the routable set the
|
|
237
|
-
* moment the stop signal lands — faster than any observer.
|
|
296
|
+
* moment the stop signal lands — faster than any observer.
|
|
297
|
+
* Scope: the body carries NO `scope` — this shape has no such key, and one sent anyway is stripped. The row lands in the scope
|
|
298
|
+
* the CREDENTIAL names: under a per-worker token, the scope the token binds (a `?scope=` query parameter, when given, must equal
|
|
299
|
+
* it); under the shared token, the `?scope=` query parameter, absent ⇒ `"global"`. */
|
|
238
300
|
export declare const InstanceAnnouncement: z.ZodObject<{
|
|
239
301
|
instanceId: z.ZodString;
|
|
240
302
|
/** worker (deployment) this instance belongs to — the dispatch key */
|
|
@@ -292,6 +354,20 @@ export declare const InstanceAnnouncement: z.ZodObject<{
|
|
|
292
354
|
entries: Record<string, string>;
|
|
293
355
|
plane?: string | undefined;
|
|
294
356
|
}>>>;
|
|
357
|
+
/** Fleet membership and the lease reading, flat on the heartbeat body under the names and values {@link EngineSignal} gives
|
|
358
|
+
* them (one mint point on the engine; this shape reuses that shape's schemas, not copies):
|
|
359
|
+
* - `generation` — the canonical decimal generation ({@link isCanonicalGeneration}); present ⇔ the instance is a fleet member.
|
|
360
|
+
* - `leaseState` — {@link LeaseState}; present ⇔ fleet member.
|
|
361
|
+
* - `leaseUntil` — epoch ms until which the held lease runs; present ⇔ `leaseState` is `granted`.
|
|
362
|
+
* The presence law between the three is the engine's; this shape does not judge it across keys. Every heartbeat re-announces
|
|
363
|
+
* the full row, so a later heartbeat without a key means the engine no longer says it (the stored row is replaced, never merged).
|
|
364
|
+
* Failure direction = meaning direction: these are evidence a center keeps, never routing input — a value that breaks its schema
|
|
365
|
+
* drops that ONE key to ABSENT (`.catch`) and the heartbeat is still accepted, so a lease word a newer engine adds can never make
|
|
366
|
+
* an older center refuse every heartbeat of that engine. A dropped key reads exactly like an unsaid one; the key may then be
|
|
367
|
+
* present with the value `undefined`. */
|
|
368
|
+
generation: z.ZodCatch<z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>>;
|
|
369
|
+
leaseState: z.ZodCatch<z.ZodOptional<z.ZodEnum<["none", "granted", "expired", "denied"]>>>;
|
|
370
|
+
leaseUntil: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
|
|
295
371
|
}, "strip", z.ZodTypeAny, {
|
|
296
372
|
draining: boolean;
|
|
297
373
|
version: string;
|
|
@@ -310,6 +386,9 @@ export declare const InstanceAnnouncement: z.ZodObject<{
|
|
|
310
386
|
entries: Record<string, string>;
|
|
311
387
|
plane?: string | undefined;
|
|
312
388
|
} | undefined;
|
|
389
|
+
generation?: string | undefined;
|
|
390
|
+
leaseState?: "none" | "granted" | "expired" | "denied" | undefined;
|
|
391
|
+
leaseUntil?: number | undefined;
|
|
313
392
|
}, {
|
|
314
393
|
version: string;
|
|
315
394
|
instanceId: string;
|
|
@@ -325,6 +404,9 @@ export declare const InstanceAnnouncement: z.ZodObject<{
|
|
|
325
404
|
} | undefined;
|
|
326
405
|
modelRefusals?: unknown;
|
|
327
406
|
heartbeatMs?: number | undefined;
|
|
407
|
+
generation?: unknown;
|
|
408
|
+
leaseState?: unknown;
|
|
409
|
+
leaseUntil?: unknown;
|
|
328
410
|
}>;
|
|
329
411
|
export type InstanceAnnouncement = z.infer<typeof InstanceAnnouncement>;
|
|
330
412
|
/** Explicit goodbye (stop signal handler) — removes the row immediately instead of waiting out staleness. */
|
|
@@ -401,6 +483,20 @@ export declare const InstanceRegistration: z.ZodObject<{
|
|
|
401
483
|
entries: Record<string, string>;
|
|
402
484
|
plane?: string | undefined;
|
|
403
485
|
}>>>;
|
|
486
|
+
/** Fleet membership and the lease reading, flat on the heartbeat body under the names and values {@link EngineSignal} gives
|
|
487
|
+
* them (one mint point on the engine; this shape reuses that shape's schemas, not copies):
|
|
488
|
+
* - `generation` — the canonical decimal generation ({@link isCanonicalGeneration}); present ⇔ the instance is a fleet member.
|
|
489
|
+
* - `leaseState` — {@link LeaseState}; present ⇔ fleet member.
|
|
490
|
+
* - `leaseUntil` — epoch ms until which the held lease runs; present ⇔ `leaseState` is `granted`.
|
|
491
|
+
* The presence law between the three is the engine's; this shape does not judge it across keys. Every heartbeat re-announces
|
|
492
|
+
* the full row, so a later heartbeat without a key means the engine no longer says it (the stored row is replaced, never merged).
|
|
493
|
+
* Failure direction = meaning direction: these are evidence a center keeps, never routing input — a value that breaks its schema
|
|
494
|
+
* drops that ONE key to ABSENT (`.catch`) and the heartbeat is still accepted, so a lease word a newer engine adds can never make
|
|
495
|
+
* an older center refuse every heartbeat of that engine. A dropped key reads exactly like an unsaid one; the key may then be
|
|
496
|
+
* present with the value `undefined`. */
|
|
497
|
+
generation: z.ZodCatch<z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>>;
|
|
498
|
+
leaseState: z.ZodCatch<z.ZodOptional<z.ZodEnum<["none", "granted", "expired", "denied"]>>>;
|
|
499
|
+
leaseUntil: z.ZodCatch<z.ZodOptional<z.ZodNumber>>;
|
|
404
500
|
} & {
|
|
405
501
|
registeredMs: z.ZodNumber;
|
|
406
502
|
lastSeenMs: z.ZodNumber;
|
|
@@ -425,6 +521,9 @@ export declare const InstanceRegistration: z.ZodObject<{
|
|
|
425
521
|
entries: Record<string, string>;
|
|
426
522
|
plane?: string | undefined;
|
|
427
523
|
} | undefined;
|
|
524
|
+
generation?: string | undefined;
|
|
525
|
+
leaseState?: "none" | "granted" | "expired" | "denied" | undefined;
|
|
526
|
+
leaseUntil?: number | undefined;
|
|
428
527
|
observedReady?: boolean | undefined;
|
|
429
528
|
}, {
|
|
430
529
|
version: string;
|
|
@@ -443,6 +542,9 @@ export declare const InstanceRegistration: z.ZodObject<{
|
|
|
443
542
|
} | undefined;
|
|
444
543
|
modelRefusals?: unknown;
|
|
445
544
|
heartbeatMs?: number | undefined;
|
|
545
|
+
generation?: unknown;
|
|
546
|
+
leaseState?: unknown;
|
|
547
|
+
leaseUntil?: unknown;
|
|
446
548
|
observedReady?: boolean | undefined;
|
|
447
549
|
}>;
|
|
448
550
|
export type InstanceRegistration = z.infer<typeof InstanceRegistration>;
|
|
@@ -475,18 +577,24 @@ export declare const EngineDraining: z.ZodObject<{
|
|
|
475
577
|
deadlineAt: number;
|
|
476
578
|
}>;
|
|
477
579
|
export type EngineDraining = z.infer<typeof EngineDraining>;
|
|
478
|
-
export declare function isCanonicalGeneration(value: unknown): value is string;
|
|
479
|
-
/** Numeric order of two canonical generations: `-1` when `a` is older than `b`, `0` when equal, `1` when newer — `"9"` is
|
|
480
|
-
* older than `"10"`; any number of digits. Throws a `TypeError` on an input that is not canonical (a comparison that quietly
|
|
481
|
-
* returned some number would be a fence decision made on garbage); judge untrusted input with {@link isCanonicalGeneration}
|
|
482
|
-
* first — the wire shapes of this file already do. */
|
|
483
|
-
export declare function compareGeneration(a: string, b: string): -1 | 0 | 1;
|
|
484
580
|
/**
|
|
485
581
|
* Everything the ENGINE itself mints about its own state, as one object — the closed form of the engine-owned subset of the
|
|
486
582
|
* /health self-description ({@link InstanceHealth} is that self-description: an open record, `.passthrough()`, which also
|
|
487
|
-
* carries what the host adds — build, pid, port, store posture). One mint point
|
|
488
|
-
*
|
|
489
|
-
*
|
|
583
|
+
* carries what the host adds — build, pid, port, store posture). One mint point on the engine; {@link readEngineSignal} reads
|
|
584
|
+
* this shape off a body whose keys sit flat on it (no wrapper key).
|
|
585
|
+
*
|
|
586
|
+
* Where it rides today: on NEITHER exit whole. The heartbeat ({@link InstanceAnnouncement}) and `/health` ({@link InstanceHealth})
|
|
587
|
+
* still speak the 8.0 dialect for the keys they share with this shape, in three ways:
|
|
588
|
+
* 1. `draining` is a BOOLEAN there, not the {@link EngineDraining} object. The heartbeat always carries it (`false` included);
|
|
589
|
+
* `/health` carries `draining: true` only while draining.
|
|
590
|
+
* 2. `/health` carries `ready` only while NOT ready (`ready: false`); a ready engine's `/health` body has no `ready` key.
|
|
591
|
+
* 3. The engine writes `configTargetVersion` / `configAppliedVersion` as numbers; this shape types them as strings.
|
|
592
|
+
* So `readEngineSignal` over a heartbeat body is always `undefined` (point 1), and over a `/health` body it is `undefined` whenever
|
|
593
|
+
* any of the three applies. What does ride both exits flat, under these names and with these values: `restart`, `instanceId`
|
|
594
|
+
* and the fleet-membership keys `generation` / `leaseState` / `leaseUntil` — on the heartbeat, read them off the parsed
|
|
595
|
+
* {@link InstanceAnnouncement} (which types all five). The next major retires the boolean pair (`draining` / `drainingSince`),
|
|
596
|
+
* has both exits carry this shape whole and moves {@link isReadyForTraffic} / {@link isRoutable} onto it; until then this is
|
|
597
|
+
* the target form, and `readEngineSignal` is the reader for a body that already speaks it.
|
|
490
598
|
*
|
|
491
599
|
* - `ready` — always said (never "absent while ready"): the engine can take new work right now.
|
|
492
600
|
* - `draining` — present ⇔ draining ({@link EngineDraining}). NOTE the dialect: {@link InstanceHealth}`.draining` and
|
|
@@ -499,8 +607,10 @@ export declare function compareGeneration(a: string, b: string): -1 | 0 | 1;
|
|
|
499
607
|
* - `instanceId` — the engine's OWN process identity, minted by the engine every time the process starts (never read from
|
|
500
608
|
* env: replicas of one Deployment share one env, and a restarted container reuses its env, so env cannot tell two
|
|
501
609
|
* processes apart). It is present on a standalone engine too — only `generation` marks fleet membership.
|
|
610
|
+
* - `leaseState` — the serving lease as the engine holds it ({@link LeaseState}: `none` / `granted` / `expired` / `denied`);
|
|
611
|
+
* present ⇔ fleet member (the engine says it on every beat once it has a generation).
|
|
502
612
|
* - `leaseUntil` — epoch ms until which the engine holds its serving lease, as the engine computed it from the last
|
|
503
|
-
* `ttlMs` it was granted ({@link ServingLeaseResponse});
|
|
613
|
+
* `ttlMs` it was granted ({@link ServingLeaseResponse}); present ⇔ `leaseState` is `granted`.
|
|
504
614
|
* - `configTargetVersion` / `configAppliedVersion` — the config-center version the engine was told to converge on, and the
|
|
505
615
|
* one it has applied (opaque strings; equal ⇔ converged).
|
|
506
616
|
*
|
|
@@ -543,6 +653,7 @@ export declare const EngineSignal: z.ZodObject<{
|
|
|
543
653
|
}>>;
|
|
544
654
|
generation: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
545
655
|
instanceId: z.ZodOptional<z.ZodString>;
|
|
656
|
+
leaseState: z.ZodOptional<z.ZodEnum<["none", "granted", "expired", "denied"]>>;
|
|
546
657
|
leaseUntil: z.ZodOptional<z.ZodNumber>;
|
|
547
658
|
configTargetVersion: z.ZodOptional<z.ZodString>;
|
|
548
659
|
configAppliedVersion: z.ZodOptional<z.ZodString>;
|
|
@@ -561,6 +672,7 @@ export declare const EngineSignal: z.ZodObject<{
|
|
|
561
672
|
} | undefined;
|
|
562
673
|
instanceId?: string | undefined;
|
|
563
674
|
generation?: string | undefined;
|
|
675
|
+
leaseState?: "none" | "granted" | "expired" | "denied" | undefined;
|
|
564
676
|
leaseUntil?: number | undefined;
|
|
565
677
|
configTargetVersion?: string | undefined;
|
|
566
678
|
configAppliedVersion?: string | undefined;
|
|
@@ -579,16 +691,17 @@ export declare const EngineSignal: z.ZodObject<{
|
|
|
579
691
|
} | undefined;
|
|
580
692
|
instanceId?: string | undefined;
|
|
581
693
|
generation?: string | undefined;
|
|
694
|
+
leaseState?: "none" | "granted" | "expired" | "denied" | undefined;
|
|
582
695
|
leaseUntil?: number | undefined;
|
|
583
696
|
configTargetVersion?: string | undefined;
|
|
584
697
|
configAppliedVersion?: string | undefined;
|
|
585
698
|
}>;
|
|
586
699
|
export type EngineSignal = z.infer<typeof EngineSignal>;
|
|
587
700
|
/**
|
|
588
|
-
* The reader of {@link EngineSignal}
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
701
|
+
* The reader of {@link EngineSignal}: the shape's keys, flat on the given body, judged by the same guarded body reader as
|
|
702
|
+
* {@link readRestartSignal} and {@link readModelRefusals}, so the same odd input gets the same answer from all three. Shape
|
|
703
|
+
* mismatch, a missing `ready`, a non-object body, even a throwing accessor ⇒ `undefined` — never throws. See {@link EngineSignal}
|
|
704
|
+
* for what `undefined` means (not "ready") and for why today's heartbeat and `/health` bodies read as `undefined` here.
|
|
592
705
|
*/
|
|
593
706
|
export declare function readEngineSignal(health: unknown): EngineSignal | undefined;
|
|
594
707
|
/**
|
|
@@ -599,6 +712,18 @@ export declare function readEngineSignal(health: unknown): EngineSignal | undefi
|
|
|
599
712
|
* ({@link EngineSignal}`.instanceId`), and env cannot carry it (see there).
|
|
600
713
|
*/
|
|
601
714
|
export declare const FLEET_GENERATION_ENV = "FLEET_GENERATION";
|
|
715
|
+
/**
|
|
716
|
+
* The env name a fleet member's tenant scope travels under — the config renderer sets it to the scope this member's per-worker
|
|
717
|
+
* token binds (the same value, taken from the same place the token is issued for), and the engine reads it as the scope of its
|
|
718
|
+
* OWN identity: the scope it compares a {@link DrainRequest}'s `scope` against, sends as {@link ServingLeaseRequest}`.scope`, and
|
|
719
|
+
* names on its heartbeat ({@link InstanceAnnouncement}'s `?scope=`). Both sides import THIS constant; neither spells the name.
|
|
720
|
+
* One fact, one source: the scope is GIVEN by the renderer — the engine never derives it from the token's format (a token is
|
|
721
|
+
* opaque to it, and a format guess would be a second source that can disagree with the scope the center binds). Absent from
|
|
722
|
+
* the engine's env ⇒ the default scope, {@link GLOBAL_SCOPE} (`api/scopes`) — the same default the center reads where nothing
|
|
723
|
+
* names a scope; this note does not restate the value. Like {@link FLEET_GENERATION_ENV} it is a reserved worker env name
|
|
724
|
+
* (`WorkerSpec.env` / `secretEnv` cannot set it).
|
|
725
|
+
*/
|
|
726
|
+
export declare const FLEET_SCOPE_ENV = "FLEET_SCOPE";
|
|
602
727
|
/**
|
|
603
728
|
* The serving lease — the ONE fence between a superseded generation and the traffic it must stop taking. A fleet-member
|
|
604
729
|
* engine renews it with the center; the center answers per the lifecycle object it holds for that instance. Three constants
|
|
@@ -625,8 +750,11 @@ export declare function clampServingLeaseTtlMs(configuredMs: number | undefined)
|
|
|
625
750
|
* Engine → center: "may this instance keep serving?" The identity triple names the instance exactly — a lease is never
|
|
626
751
|
* asked for by worker name alone. `generation` is the env echo ({@link FLEET_GENERATION_ENV}); `instanceId` is the engine's
|
|
627
752
|
* own per-process identity ({@link EngineSignal}`.instanceId`), so a restarted process is a NEW instance to the center.
|
|
628
|
-
* `scope`
|
|
629
|
-
*
|
|
753
|
+
* `scope` — the tenant scope the lease is asked in. A renewal is taken on a PER-WORKER token only — the credential binds
|
|
754
|
+
* (scope, worker) — so the credential decides the scope and this field can only agree: absent = the scope the token binds; present
|
|
755
|
+
* ⇒ it must equal that scope (a mismatch is refused, never rewritten either way). The shared token may not renew a lease at all
|
|
756
|
+
* (the center answers 403). Only fleet members ask: an engine without a generation (the standalone form) has no lease and sends
|
|
757
|
+
* nothing.
|
|
630
758
|
*/
|
|
631
759
|
export declare const ServingLeaseRequest: z.ZodObject<{
|
|
632
760
|
worker: z.ZodString;
|
|
@@ -758,7 +886,10 @@ export declare function readServingLeaseResponse(body: unknown): ServingLeaseRea
|
|
|
758
886
|
* A reader asks `readCenterCapabilities(body)?.servingLease === true` and starts the lease client only on that.
|
|
759
887
|
* Unknown bits are stripped (a newer center's extra bits never break an older reader); a known bit whose value is not `true`
|
|
760
888
|
* makes the whole read `undefined`, which reads exactly as absent.
|
|
761
|
-
*
|
|
889
|
+
* It rides in ONE place: the heartbeat's 2xx reply ({@link FLEET_PATHS}`.instances`), under {@link HEARTBEAT_REPLY_CAPABILITIES_KEY}.
|
|
890
|
+
* The center answers the bits per CALLER CREDENTIAL: under a per-worker token, by what the center supports; under the shared token
|
|
891
|
+
* `servingLease` is always absent (the serving-lease endpoint takes per-worker tokens only, so announcing it there would only push
|
|
892
|
+
* that caller into the unreachable arm).
|
|
762
893
|
*/
|
|
763
894
|
export declare const CenterCapabilities: z.ZodObject<{
|
|
764
895
|
servingLease: z.ZodOptional<z.ZodLiteral<true>>;
|
|
@@ -768,6 +899,14 @@ export declare const CenterCapabilities: z.ZodObject<{
|
|
|
768
899
|
servingLease?: true | undefined;
|
|
769
900
|
}>;
|
|
770
901
|
export type CenterCapabilities = z.infer<typeof CenterCapabilities>;
|
|
902
|
+
/**
|
|
903
|
+
* The key of the heartbeat's 2xx reply body under which the center puts its {@link CenterCapabilities} — the object's only carrier.
|
|
904
|
+
* The center writes `{ …, [HEARTBEAT_REPLY_CAPABILITIES_KEY]: caps }`; the engine reads
|
|
905
|
+
* `readCenterCapabilities(reply[HEARTBEAT_REPLY_CAPABILITIES_KEY])`, and only on a 2xx (a failed heartbeat says nothing about the
|
|
906
|
+
* bits). Nested, not flat: the reply's own keys and the capability bits never share a namespace. A reply without the key is an
|
|
907
|
+
* older center — no capability said.
|
|
908
|
+
*/
|
|
909
|
+
export declare const HEARTBEAT_REPLY_CAPABILITIES_KEY = "capabilities";
|
|
771
910
|
/** Engine side: the center's capability object. Shape mismatch / non-object / throwing accessor ⇒ `undefined`, never throws
|
|
772
911
|
* (and `undefined` means "no capability said" — the same as every bit absent). */
|
|
773
912
|
export declare function readCenterCapabilities(body: unknown): CenterCapabilities | undefined;
|
package/dist/fleet.js
CHANGED
|
@@ -19,6 +19,43 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { fnv1a64 } from "./hash.js";
|
|
22
|
+
// ── The center's fleet endpoints (paths, one spelling) ─────────────────────────────────────────────────────
|
|
23
|
+
/**
|
|
24
|
+
* The center's `/api/fleet/*` endpoints that are addressed from outside the center, relative to its origin. ONE spelling: whoever
|
|
25
|
+
* calls one (the engine, the executor) and whoever serves or routes it (the center, the deployment's Service / Ingress) imports it
|
|
26
|
+
* here — nobody writes the string. Membership rule: an endpoint is here iff this package types its body, or it belongs to the
|
|
27
|
+
* liveness group ({@link FLEET_LIVE_PATHS}); a center-internal route is not.
|
|
28
|
+
* - `instances` — `POST` = the heartbeat ({@link InstanceAnnouncement}; its 2xx reply carries the center's
|
|
29
|
+
* {@link CenterCapabilities} under {@link HEARTBEAT_REPLY_CAPABILITIES_KEY}), `DELETE` = the goodbye ({@link InstanceDeregistration}).
|
|
30
|
+
* - `usage` — `POST` a {@link UsageReport}.
|
|
31
|
+
* - `quotaLeases` — the quota lease ({@link QuotaLease}).
|
|
32
|
+
* - `servingLease` — `POST` a {@link ServingLeaseRequest}, answered with a {@link ServingLeaseResponse}.
|
|
33
|
+
* - `hostPlanner` — `GET` one host's planner record, for the executor (its body is the center's and the executor's contract, not
|
|
34
|
+
* this package's; it is here because the deployment routes it as a member of the liveness group).
|
|
35
|
+
* Notation: a value is a path TEMPLATE — a parameter is a whole segment written `{name}` (`hostPlanner` ends in `/{host}`, filled
|
|
36
|
+
* with the URL-encoded host id); the other values have none and are the path itself. A prefix router (an Ingress rule) takes the
|
|
37
|
+
* template up to its first `{`.
|
|
38
|
+
*/
|
|
39
|
+
export const FLEET_PATHS = {
|
|
40
|
+
instances: "/api/fleet/instances",
|
|
41
|
+
usage: "/api/fleet/usage",
|
|
42
|
+
quotaLeases: "/api/fleet/leases",
|
|
43
|
+
servingLease: "/api/fleet/live/serving-lease",
|
|
44
|
+
hostPlanner: "/api/fleet/live/host-planner/{host}",
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* The LIVENESS group — the fleet endpoints a center answers without its readiness gate and without its config-governance gates
|
|
48
|
+
* (they read no config domain and take service credentials only), so that a center whose management face is refused never turns
|
|
49
|
+
* into a fleet that gets no answers. A deployment routes exactly these past the readiness gate (on Kubernetes: a second Service
|
|
50
|
+
* with `publishNotReadyAddresses`, and the Ingress rules for them point at it). Membership is THIS list, not a path prefix — the
|
|
51
|
+
* heartbeat predates the `/api/fleet/live/` prefix; every {@link FLEET_PATHS} entry under that prefix is a member. The
|
|
52
|
+
* quota-lease and usage endpoints are not.
|
|
53
|
+
*
|
|
54
|
+
* The same two constants ship as JSON for tooling that cannot import TypeScript (a Helm chart, a smoke script): the subpath
|
|
55
|
+
* `@sema-agent/settings-schema/fleet-paths.json` is `{ "FLEET_PATHS": …, "FLEET_LIVE_PATHS": … }`, written from these values at
|
|
56
|
+
* build time — never edited by hand.
|
|
57
|
+
*/
|
|
58
|
+
export const FLEET_LIVE_PATHS = [FLEET_PATHS.instances, FLEET_PATHS.servingLease, FLEET_PATHS.hostPlanner];
|
|
22
59
|
// ── Domain 1: drain protocol (graceful rolling update — the ground every form stands on) ──────────────────
|
|
23
60
|
/** Lifecycle of a draining instance. `running`→`draining` on the stop signal; `drained` once in-flight hits
|
|
24
61
|
* zero (clean) or the grace deadline forces the issue. Exit always follows `drained`. */
|
|
@@ -42,6 +79,50 @@ export function drainVerdict(s) {
|
|
|
42
79
|
}
|
|
43
80
|
/** A wall-clock instant as epoch milliseconds — one quantity, one rule, wherever the fleet contract carries a point in time. */
|
|
44
81
|
const EpochMs = z.number().int().nonnegative();
|
|
82
|
+
/**
|
|
83
|
+
* A generation is a non-negative integer written as its CANONICAL decimal string: `"0"`, `"7"`, `"10"` — no sign, no
|
|
84
|
+
* leading zero (`"07"` is not a generation), no fraction, no whitespace, ASCII digits only, and a string, never a number.
|
|
85
|
+
* One spelling per value, so equality is string equality and the order is numeric ({@link compareGeneration}) — the center's
|
|
86
|
+
* persisted generation floor and every "is this generation below the floor" judgement rest on that order. The renderer writes
|
|
87
|
+
* the value into {@link FLEET_GENERATION_ENV}; an env value that is present but not canonical is a misconfiguration to refuse
|
|
88
|
+
* loudly, not an absent generation (absence is what marks a non-member). Wherever a wire shape here carries a generation
|
|
89
|
+
* ({@link EngineSignal}`.generation`, {@link ServingLeaseRequest}, {@link DrainRequest}`.expectedGeneration`) it is judged by
|
|
90
|
+
* this one predicate.
|
|
91
|
+
*/
|
|
92
|
+
const CANONICAL_GENERATION = /^(?:0|[1-9][0-9]*)$/;
|
|
93
|
+
export function isCanonicalGeneration(value) {
|
|
94
|
+
return typeof value === "string" && CANONICAL_GENERATION.test(value);
|
|
95
|
+
}
|
|
96
|
+
/** Numeric order of two canonical generations: `-1` when `a` is older than `b`, `0` when equal, `1` when newer — `"9"` is
|
|
97
|
+
* older than `"10"`; any number of digits. Throws a `TypeError` on an input that is not canonical (a comparison that quietly
|
|
98
|
+
* returned some number would be a fence decision made on garbage); judge untrusted input with {@link isCanonicalGeneration}
|
|
99
|
+
* first — the wire shapes of this file already do. */
|
|
100
|
+
export function compareGeneration(a, b) {
|
|
101
|
+
if (!isCanonicalGeneration(a) || !isCanonicalGeneration(b)) {
|
|
102
|
+
throw new TypeError(`compareGeneration: a generation is a canonical non-negative decimal string (got ${JSON.stringify(isCanonicalGeneration(a) ? b : a)})`);
|
|
103
|
+
}
|
|
104
|
+
if (a.length !== b.length)
|
|
105
|
+
return a.length < b.length ? -1 : 1; // canonical ⇒ no leading zeros ⇒ more digits is larger
|
|
106
|
+
return a === b ? 0 : a < b ? -1 : 1;
|
|
107
|
+
}
|
|
108
|
+
/** The generation field of every wire shape that carries one — a string judged by {@link isCanonicalGeneration}. */
|
|
109
|
+
const Generation = z.string().refine(isCanonicalGeneration, { message: "a generation is a canonical non-negative decimal string (no sign, no leading zero)" });
|
|
110
|
+
/**
|
|
111
|
+
* The serving lease as the ENGINE holds it — a CLOSED set of four words, owned here (the engine's vocabulary, word for word;
|
|
112
|
+
* a reader that switches on it exhaustively is told by the compiler when a word is added):
|
|
113
|
+
* - `none` — the lease is not in effect: the center has not announced {@link CenterCapabilities}`.servingLease` (an older or
|
|
114
|
+
* degraded center). The engine behaves exactly as an engine without a lease — no renewals, no fence.
|
|
115
|
+
* - `granted` — the engine holds a lease; `leaseUntil` says until when (epoch ms, the engine's own computation from the
|
|
116
|
+
* granted `ttlMs`, see {@link ServingLeaseResponse}).
|
|
117
|
+
* - `expired` — the lease is in effect but no grant is valid right now (the center is unreachable, answered `unknown`, or has
|
|
118
|
+
* not granted one yet): the engine keeps serving until its own outage allowance runs out. Not a denial.
|
|
119
|
+
* - `denied` — the center denied this instance on positive evidence (the `denied` verdict): sticky; the engine is draining and
|
|
120
|
+
* stops at its deadline.
|
|
121
|
+
* Only a fleet member says it: an engine without a generation has no lease and carries no `leaseState` at all (there is no
|
|
122
|
+
* fifth word for "not a member" — absence is that).
|
|
123
|
+
*/
|
|
124
|
+
export const LEASE_STATES = ["none", "granted", "expired", "denied"];
|
|
125
|
+
export const LeaseState = z.enum(LEASE_STATES);
|
|
45
126
|
/**
|
|
46
127
|
* "This instance must restart to apply config" — the engine's OWN observation (S-741), defined ONCE for both exits that
|
|
47
128
|
* carry it: the fleet heartbeat ({@link InstanceAnnouncement}`.restart`, typed) and GET /health (`restart`, read through
|
|
@@ -236,7 +317,10 @@ export const STALE_HEARTBEAT_MULTIPLIER = 2;
|
|
|
236
317
|
/** Register/heartbeat upsert (worker → center, D2: worker-active). ONE shape for both — registration is the
|
|
237
318
|
* first heartbeat; every heartbeat re-announces the full row (idempotent upsert, no drift between a
|
|
238
319
|
* "register" and a "renew" schema). `draining:true` is the instance SELF-removing from the routable set the
|
|
239
|
-
* moment the stop signal lands — faster than any observer.
|
|
320
|
+
* moment the stop signal lands — faster than any observer.
|
|
321
|
+
* Scope: the body carries NO `scope` — this shape has no such key, and one sent anyway is stripped. The row lands in the scope
|
|
322
|
+
* the CREDENTIAL names: under a per-worker token, the scope the token binds (a `?scope=` query parameter, when given, must equal
|
|
323
|
+
* it); under the shared token, the `?scope=` query parameter, absent ⇒ `"global"`. */
|
|
240
324
|
export const InstanceAnnouncement = z.object({
|
|
241
325
|
instanceId: z.string().min(1),
|
|
242
326
|
/** worker (deployment) this instance belongs to — the dispatch key */
|
|
@@ -264,6 +348,20 @@ export const InstanceAnnouncement = z.object({
|
|
|
264
348
|
* ABSENT (`.catch`) — it never fails the announcement, so it can never make a healthy instance unroutable (unlike
|
|
265
349
|
* `restart`, whose shape still gates the row). The key may then be present with the value `undefined`. */
|
|
266
350
|
modelRefusals: ModelRefusalsWire.optional().catch(undefined),
|
|
351
|
+
/** Fleet membership and the lease reading, flat on the heartbeat body under the names and values {@link EngineSignal} gives
|
|
352
|
+
* them (one mint point on the engine; this shape reuses that shape's schemas, not copies):
|
|
353
|
+
* - `generation` — the canonical decimal generation ({@link isCanonicalGeneration}); present ⇔ the instance is a fleet member.
|
|
354
|
+
* - `leaseState` — {@link LeaseState}; present ⇔ fleet member.
|
|
355
|
+
* - `leaseUntil` — epoch ms until which the held lease runs; present ⇔ `leaseState` is `granted`.
|
|
356
|
+
* The presence law between the three is the engine's; this shape does not judge it across keys. Every heartbeat re-announces
|
|
357
|
+
* the full row, so a later heartbeat without a key means the engine no longer says it (the stored row is replaced, never merged).
|
|
358
|
+
* Failure direction = meaning direction: these are evidence a center keeps, never routing input — a value that breaks its schema
|
|
359
|
+
* drops that ONE key to ABSENT (`.catch`) and the heartbeat is still accepted, so a lease word a newer engine adds can never make
|
|
360
|
+
* an older center refuse every heartbeat of that engine. A dropped key reads exactly like an unsaid one; the key may then be
|
|
361
|
+
* present with the value `undefined`. */
|
|
362
|
+
generation: Generation.optional().catch(undefined),
|
|
363
|
+
leaseState: LeaseState.optional().catch(undefined),
|
|
364
|
+
leaseUntil: EpochMs.optional().catch(undefined),
|
|
267
365
|
});
|
|
268
366
|
/** Explicit goodbye (stop signal handler) — removes the row immediately instead of waiting out staleness. */
|
|
269
367
|
export const InstanceDeregistration = z.object({
|
|
@@ -305,40 +403,24 @@ export const EngineDraining = z.object({
|
|
|
305
403
|
since: EpochMs,
|
|
306
404
|
deadlineAt: EpochMs,
|
|
307
405
|
});
|
|
308
|
-
/**
|
|
309
|
-
* A generation is a non-negative integer written as its CANONICAL decimal string: `"0"`, `"7"`, `"10"` — no sign, no
|
|
310
|
-
* leading zero (`"07"` is not a generation), no fraction, no whitespace, ASCII digits only, and a string, never a number.
|
|
311
|
-
* One spelling per value, so equality is string equality and the order is numeric ({@link compareGeneration}) — the center's
|
|
312
|
-
* persisted generation floor and every "is this generation below the floor" judgement rest on that order. The renderer writes
|
|
313
|
-
* the value into {@link FLEET_GENERATION_ENV}; an env value that is present but not canonical is a misconfiguration to refuse
|
|
314
|
-
* loudly, not an absent generation (absence is what marks a non-member). Wherever a wire shape here carries a generation
|
|
315
|
-
* ({@link EngineSignal}`.generation`, {@link ServingLeaseRequest}, {@link DrainRequest}`.expectedGeneration`) it is judged by
|
|
316
|
-
* this one predicate.
|
|
317
|
-
*/
|
|
318
|
-
const CANONICAL_GENERATION = /^(?:0|[1-9][0-9]*)$/;
|
|
319
|
-
export function isCanonicalGeneration(value) {
|
|
320
|
-
return typeof value === "string" && CANONICAL_GENERATION.test(value);
|
|
321
|
-
}
|
|
322
|
-
/** Numeric order of two canonical generations: `-1` when `a` is older than `b`, `0` when equal, `1` when newer — `"9"` is
|
|
323
|
-
* older than `"10"`; any number of digits. Throws a `TypeError` on an input that is not canonical (a comparison that quietly
|
|
324
|
-
* returned some number would be a fence decision made on garbage); judge untrusted input with {@link isCanonicalGeneration}
|
|
325
|
-
* first — the wire shapes of this file already do. */
|
|
326
|
-
export function compareGeneration(a, b) {
|
|
327
|
-
if (!isCanonicalGeneration(a) || !isCanonicalGeneration(b)) {
|
|
328
|
-
throw new TypeError(`compareGeneration: a generation is a canonical non-negative decimal string (got ${JSON.stringify(isCanonicalGeneration(a) ? b : a)})`);
|
|
329
|
-
}
|
|
330
|
-
if (a.length !== b.length)
|
|
331
|
-
return a.length < b.length ? -1 : 1; // canonical ⇒ no leading zeros ⇒ more digits is larger
|
|
332
|
-
return a === b ? 0 : a < b ? -1 : 1;
|
|
333
|
-
}
|
|
334
|
-
/** The generation field of every wire shape that carries one — a string judged by {@link isCanonicalGeneration}. */
|
|
335
|
-
const Generation = z.string().refine(isCanonicalGeneration, { message: "a generation is a canonical non-negative decimal string (no sign, no leading zero)" });
|
|
336
406
|
/**
|
|
337
407
|
* Everything the ENGINE itself mints about its own state, as one object — the closed form of the engine-owned subset of the
|
|
338
408
|
* /health self-description ({@link InstanceHealth} is that self-description: an open record, `.passthrough()`, which also
|
|
339
|
-
* carries what the host adds — build, pid, port, store posture). One mint point
|
|
340
|
-
*
|
|
341
|
-
*
|
|
409
|
+
* carries what the host adds — build, pid, port, store posture). One mint point on the engine; {@link readEngineSignal} reads
|
|
410
|
+
* this shape off a body whose keys sit flat on it (no wrapper key).
|
|
411
|
+
*
|
|
412
|
+
* Where it rides today: on NEITHER exit whole. The heartbeat ({@link InstanceAnnouncement}) and `/health` ({@link InstanceHealth})
|
|
413
|
+
* still speak the 8.0 dialect for the keys they share with this shape, in three ways:
|
|
414
|
+
* 1. `draining` is a BOOLEAN there, not the {@link EngineDraining} object. The heartbeat always carries it (`false` included);
|
|
415
|
+
* `/health` carries `draining: true` only while draining.
|
|
416
|
+
* 2. `/health` carries `ready` only while NOT ready (`ready: false`); a ready engine's `/health` body has no `ready` key.
|
|
417
|
+
* 3. The engine writes `configTargetVersion` / `configAppliedVersion` as numbers; this shape types them as strings.
|
|
418
|
+
* So `readEngineSignal` over a heartbeat body is always `undefined` (point 1), and over a `/health` body it is `undefined` whenever
|
|
419
|
+
* any of the three applies. What does ride both exits flat, under these names and with these values: `restart`, `instanceId`
|
|
420
|
+
* and the fleet-membership keys `generation` / `leaseState` / `leaseUntil` — on the heartbeat, read them off the parsed
|
|
421
|
+
* {@link InstanceAnnouncement} (which types all five). The next major retires the boolean pair (`draining` / `drainingSince`),
|
|
422
|
+
* has both exits carry this shape whole and moves {@link isReadyForTraffic} / {@link isRoutable} onto it; until then this is
|
|
423
|
+
* the target form, and `readEngineSignal` is the reader for a body that already speaks it.
|
|
342
424
|
*
|
|
343
425
|
* - `ready` — always said (never "absent while ready"): the engine can take new work right now.
|
|
344
426
|
* - `draining` — present ⇔ draining ({@link EngineDraining}). NOTE the dialect: {@link InstanceHealth}`.draining` and
|
|
@@ -351,8 +433,10 @@ const Generation = z.string().refine(isCanonicalGeneration, { message: "a genera
|
|
|
351
433
|
* - `instanceId` — the engine's OWN process identity, minted by the engine every time the process starts (never read from
|
|
352
434
|
* env: replicas of one Deployment share one env, and a restarted container reuses its env, so env cannot tell two
|
|
353
435
|
* processes apart). It is present on a standalone engine too — only `generation` marks fleet membership.
|
|
436
|
+
* - `leaseState` — the serving lease as the engine holds it ({@link LeaseState}: `none` / `granted` / `expired` / `denied`);
|
|
437
|
+
* present ⇔ fleet member (the engine says it on every beat once it has a generation).
|
|
354
438
|
* - `leaseUntil` — epoch ms until which the engine holds its serving lease, as the engine computed it from the last
|
|
355
|
-
* `ttlMs` it was granted ({@link ServingLeaseResponse});
|
|
439
|
+
* `ttlMs` it was granted ({@link ServingLeaseResponse}); present ⇔ `leaseState` is `granted`.
|
|
356
440
|
* - `configTargetVersion` / `configAppliedVersion` — the config-center version the engine was told to converge on, and the
|
|
357
441
|
* one it has applied (opaque strings; equal ⇔ converged).
|
|
358
442
|
*
|
|
@@ -368,15 +452,16 @@ export const EngineSignal = z.object({
|
|
|
368
452
|
restart: RestartSignal.optional(),
|
|
369
453
|
generation: Generation.optional(),
|
|
370
454
|
instanceId: z.string().min(1).optional(),
|
|
455
|
+
leaseState: LeaseState.optional(),
|
|
371
456
|
leaseUntil: EpochMs.optional(),
|
|
372
457
|
configTargetVersion: z.string().min(1).optional(),
|
|
373
458
|
configAppliedVersion: z.string().min(1).optional(),
|
|
374
459
|
});
|
|
375
460
|
/**
|
|
376
|
-
* The reader of {@link EngineSignal}
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
*
|
|
461
|
+
* The reader of {@link EngineSignal}: the shape's keys, flat on the given body, judged by the same guarded body reader as
|
|
462
|
+
* {@link readRestartSignal} and {@link readModelRefusals}, so the same odd input gets the same answer from all three. Shape
|
|
463
|
+
* mismatch, a missing `ready`, a non-object body, even a throwing accessor ⇒ `undefined` — never throws. See {@link EngineSignal}
|
|
464
|
+
* for what `undefined` means (not "ready") and for why today's heartbeat and `/health` bodies read as `undefined` here.
|
|
380
465
|
*/
|
|
381
466
|
export function readEngineSignal(health) {
|
|
382
467
|
return readSignalBody(health, wholeBody, EngineSignal);
|
|
@@ -389,6 +474,18 @@ export function readEngineSignal(health) {
|
|
|
389
474
|
* ({@link EngineSignal}`.instanceId`), and env cannot carry it (see there).
|
|
390
475
|
*/
|
|
391
476
|
export const FLEET_GENERATION_ENV = "FLEET_GENERATION";
|
|
477
|
+
/**
|
|
478
|
+
* The env name a fleet member's tenant scope travels under — the config renderer sets it to the scope this member's per-worker
|
|
479
|
+
* token binds (the same value, taken from the same place the token is issued for), and the engine reads it as the scope of its
|
|
480
|
+
* OWN identity: the scope it compares a {@link DrainRequest}'s `scope` against, sends as {@link ServingLeaseRequest}`.scope`, and
|
|
481
|
+
* names on its heartbeat ({@link InstanceAnnouncement}'s `?scope=`). Both sides import THIS constant; neither spells the name.
|
|
482
|
+
* One fact, one source: the scope is GIVEN by the renderer — the engine never derives it from the token's format (a token is
|
|
483
|
+
* opaque to it, and a format guess would be a second source that can disagree with the scope the center binds). Absent from
|
|
484
|
+
* the engine's env ⇒ the default scope, {@link GLOBAL_SCOPE} (`api/scopes`) — the same default the center reads where nothing
|
|
485
|
+
* names a scope; this note does not restate the value. Like {@link FLEET_GENERATION_ENV} it is a reserved worker env name
|
|
486
|
+
* (`WorkerSpec.env` / `secretEnv` cannot set it).
|
|
487
|
+
*/
|
|
488
|
+
export const FLEET_SCOPE_ENV = "FLEET_SCOPE";
|
|
392
489
|
/**
|
|
393
490
|
* The serving lease — the ONE fence between a superseded generation and the traffic it must stop taking. A fleet-member
|
|
394
491
|
* engine renews it with the center; the center answers per the lifecycle object it holds for that instance. Three constants
|
|
@@ -419,8 +516,11 @@ export function clampServingLeaseTtlMs(configuredMs) {
|
|
|
419
516
|
* Engine → center: "may this instance keep serving?" The identity triple names the instance exactly — a lease is never
|
|
420
517
|
* asked for by worker name alone. `generation` is the env echo ({@link FLEET_GENERATION_ENV}); `instanceId` is the engine's
|
|
421
518
|
* own per-process identity ({@link EngineSignal}`.instanceId`), so a restarted process is a NEW instance to the center.
|
|
422
|
-
* `scope`
|
|
423
|
-
*
|
|
519
|
+
* `scope` — the tenant scope the lease is asked in. A renewal is taken on a PER-WORKER token only — the credential binds
|
|
520
|
+
* (scope, worker) — so the credential decides the scope and this field can only agree: absent = the scope the token binds; present
|
|
521
|
+
* ⇒ it must equal that scope (a mismatch is refused, never rewritten either way). The shared token may not renew a lease at all
|
|
522
|
+
* (the center answers 403). Only fleet members ask: an engine without a generation (the standalone form) has no lease and sends
|
|
523
|
+
* nothing.
|
|
424
524
|
*/
|
|
425
525
|
export const ServingLeaseRequest = z.object({
|
|
426
526
|
worker: z.string().min(1),
|
|
@@ -497,11 +597,22 @@ export function readServingLeaseResponse(body) {
|
|
|
497
597
|
* A reader asks `readCenterCapabilities(body)?.servingLease === true` and starts the lease client only on that.
|
|
498
598
|
* Unknown bits are stripped (a newer center's extra bits never break an older reader); a known bit whose value is not `true`
|
|
499
599
|
* makes the whole read `undefined`, which reads exactly as absent.
|
|
500
|
-
*
|
|
600
|
+
* It rides in ONE place: the heartbeat's 2xx reply ({@link FLEET_PATHS}`.instances`), under {@link HEARTBEAT_REPLY_CAPABILITIES_KEY}.
|
|
601
|
+
* The center answers the bits per CALLER CREDENTIAL: under a per-worker token, by what the center supports; under the shared token
|
|
602
|
+
* `servingLease` is always absent (the serving-lease endpoint takes per-worker tokens only, so announcing it there would only push
|
|
603
|
+
* that caller into the unreachable arm).
|
|
501
604
|
*/
|
|
502
605
|
export const CenterCapabilities = z.object({
|
|
503
606
|
servingLease: z.literal(true).optional(),
|
|
504
607
|
});
|
|
608
|
+
/**
|
|
609
|
+
* The key of the heartbeat's 2xx reply body under which the center puts its {@link CenterCapabilities} — the object's only carrier.
|
|
610
|
+
* The center writes `{ …, [HEARTBEAT_REPLY_CAPABILITIES_KEY]: caps }`; the engine reads
|
|
611
|
+
* `readCenterCapabilities(reply[HEARTBEAT_REPLY_CAPABILITIES_KEY])`, and only on a 2xx (a failed heartbeat says nothing about the
|
|
612
|
+
* bits). Nested, not flat: the reply's own keys and the capability bits never share a namespace. A reply without the key is an
|
|
613
|
+
* older center — no capability said.
|
|
614
|
+
*/
|
|
615
|
+
export const HEARTBEAT_REPLY_CAPABILITIES_KEY = "capabilities";
|
|
505
616
|
/** Engine side: the center's capability object. Shape mismatch / non-object / throwing accessor ⇒ `undefined`, never throws
|
|
506
617
|
* (and `undefined` means "no capability said" — the same as every bit absent). */
|
|
507
618
|
export function readCenterCapabilities(body) {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE rendering of a zod issue into a sentence that may leave in a report which promises to name fields, never values (the
|
|
3
|
+
* workers migration's `refused[].reason`, the `migrate-workers` CLI report). Internal — not on any package subpath.
|
|
4
|
+
*
|
|
5
|
+
* zod's own messages name the expected shape, not the input, with one exception: the default `invalid_enum_value` message ends in
|
|
6
|
+
* `, received '<the value>'`. That issue is re-worded from its options alone (the closed set it expected); every other issue keeps
|
|
7
|
+
* its message as is. The result still goes through the usual secret redaction where the caller applies it.
|
|
8
|
+
*/
|
|
9
|
+
import type { z } from "zod";
|
|
10
|
+
export declare function issueText(issue: z.ZodIssue): string;
|