@kici-dev/orchestrator 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/agent/dispatcher.d.ts +17 -0
  2. package/dist/cancel/peer-job-cancel.d.ts +12 -0
  3. package/dist/cli/api-client.d.ts +73 -0
  4. package/dist/cli/commands/cluster-settings.d.ts +31 -1
  5. package/dist/cli/commands/peer.d.ts +27 -1
  6. package/dist/cli/commands/scaler.d.ts +40 -4
  7. package/dist/cli.js +2210 -933
  8. package/dist/cluster/cluster-settings-reader.d.ts +8 -2
  9. package/dist/cluster/coordinator.d.ts +34 -8
  10. package/dist/cluster/peer-client.d.ts +37 -4
  11. package/dist/cluster/peer-forget.d.ts +138 -0
  12. package/dist/cluster/peer-handler.d.ts +18 -1
  13. package/dist/cluster/scaler-orphans-peer.d.ts +40 -0
  14. package/dist/cluster/settings-propagation.d.ts +85 -0
  15. package/dist/cluster/worker-cluster-settings.d.ts +29 -0
  16. package/dist/config.d.ts +9 -0
  17. package/dist/db/migrations/154_cluster_settings_firecracker_api_socket_wait.d.ts +4 -0
  18. package/dist/db/migrations/155_org_settings_reroute_spawn_retry.d.ts +17 -0
  19. package/dist/db/migrations/156_kici_events_match_outcome.d.ts +17 -0
  20. package/dist/db/types.d.ts +16 -0
  21. package/dist/events/event-router.d.ts +7 -1
  22. package/dist/events/event-store.d.ts +20 -3
  23. package/dist/events/types.d.ts +45 -0
  24. package/dist/helpers/windows-batch.d.ts +59 -0
  25. package/dist/index.js +60 -0
  26. package/dist/metrics/prometheus.d.ts +2 -0
  27. package/dist/orchestrator-core.d.ts +7 -0
  28. package/dist/registration/registration-store.d.ts +10 -0
  29. package/dist/routes/admin-cluster-settings.d.ts +13 -1
  30. package/dist/routes/admin-event-inspect.d.ts +36 -0
  31. package/dist/routes/admin-peer-forget.d.ts +43 -0
  32. package/dist/routes/admin-scaler-orphans.d.ts +64 -0
  33. package/dist/routes/admin.d.ts +23 -0
  34. package/dist/scaler/backend-factory.d.ts +6 -1
  35. package/dist/scaler/bare-metal-backend.d.ts +17 -15
  36. package/dist/scaler/bare-metal-launch.d.ts +90 -0
  37. package/dist/scaler/firecracker-backend.d.ts +181 -7
  38. package/dist/scaler/live-vms.d.ts +57 -0
  39. package/dist/scaler/manager.d.ts +112 -3
  40. package/dist/scaler/orphan-requests.d.ts +23 -0
  41. package/dist/scaler/overlay-template.d.ts +38 -0
  42. package/dist/scaler/scaler-state-store.d.ts +7 -0
  43. package/dist/scaler/spawn-errors.d.ts +17 -0
  44. package/dist/scaler/startup-orphan-sweep.d.ts +15 -0
  45. package/dist/scaler/types.d.ts +21 -2
  46. package/dist/scaler/vm-job-bindings.d.ts +17 -0
  47. package/dist/secrets/rbac.d.ts +7 -1
  48. package/dist/server.js +5498 -2193
  49. package/dist/standalone.js +10754 -7451
  50. package/dist/worker/in-memory-execution-tracker.d.ts +5 -0
  51. package/dist/worker/in-memory-job-queue.d.ts +21 -1
  52. package/dist/worker/queued-reroute-release.d.ts +39 -0
  53. package/dist/worker/reroute-spawn-control.d.ts +58 -0
  54. package/dist/worker/spawn-retry-budget.d.ts +87 -0
  55. package/dist/worker-core.d.ts +11 -2
  56. package/installer-image-digests.json +3 -3
  57. package/package.json +4 -4
  58. package/sbom.spdx.json +53 -53
@@ -1506,6 +1506,10 @@ export interface KiciEventTable {
1506
1506
  dlq_at: Date | null;
1507
1507
  /** Short DLQ reason: 'exhausted_retries' | 'non_retryable' */
1508
1508
  dlq_reason: string | null;
1509
+ /** How the router resolved the event (an `EventMatchOutcome`); NULL until processed */
1510
+ match_outcome: string | null;
1511
+ /** Registrations the event was dispatched to; NULL until processed */
1512
+ matched_count: number | null;
1509
1513
  }
1510
1514
  export type KiciEvent = Selectable<KiciEventTable>;
1511
1515
  export type NewKiciEvent = Insertable<KiciEventTable>;
@@ -1989,6 +1993,16 @@ export interface OrgSettingsTable {
1989
1993
  * (config.rerouteMaxHops). Plain INTEGER — pg returns a number on select.
1990
1994
  */
1991
1995
  reroute_max_hops: ColumnType<number | null, number | null | undefined, number | null>;
1996
+ /**
1997
+ * Per-org agent spawns a worker attempts for one rerouted job. Null = cluster
1998
+ * default (config.rerouteSpawnMaxAttempts). Plain INTEGER.
1999
+ */
2000
+ reroute_spawn_max_attempts: ColumnType<number | null, number | null | undefined, number | null>;
2001
+ /**
2002
+ * Per-org wait (ms) after a failed spawn of a rerouted job. Null = cluster
2003
+ * default (config.rerouteSpawnRetryBackoffMs). BIGINT.
2004
+ */
2005
+ reroute_spawn_retry_backoff_ms: ColumnType<string | null, number | null | undefined, number | null>;
1992
2006
  /**
1993
2007
  * Per-org staleness threshold (hours) for the DB-backup freshness diagnostic.
1994
2008
  * Null = cluster default (config.backupStalenessWarnHours). Plain INTEGER.
@@ -2197,6 +2211,8 @@ export interface ClusterSettingsTable {
2197
2211
  * per round.
2198
2212
  */
2199
2213
  global_eval_wait_timeout_ms: ColumnType<string | null, number | null | undefined, number | null>;
2214
+ /** How long a Firecracker spawn waits for the VM's API socket. NULL: config default. */
2215
+ firecracker_api_socket_wait_ms: ColumnType<string | null, number | null | undefined, number | null>;
2200
2216
  /**
2201
2217
  * Retention window in days for `check_run_tracking` rows. The hourly cleanup
2202
2218
  * sweep deletes rows untouched for longer than this; 0 disables the sweep.
@@ -6,7 +6,7 @@ import type { EventStore } from './event-store.js';
6
6
  import type { EventCircuitBreaker } from './circuit-breaker.js';
7
7
  import type { TrustStore } from './trust-store.js';
8
8
  import type { RegisteredWorkflow, RegistrationIndex } from '../registration/registration-index.js';
9
- import type { EventRouterConfig, StoredEvent } from './types.js';
9
+ import { type EventRouterConfig, type StoredEvent } from './types.js';
10
10
  import type { ClusterSettingsReader } from '../cluster/cluster-settings-reader.js';
11
11
  /**
12
12
  * System events are exempt from the per-source event-storm rate limiter: they
@@ -213,6 +213,10 @@ export declare class EventRouter {
213
213
  * schedule a retry or move the event to the DLQ. Swallowing the error
214
214
  * here would silently lose the event — exactly the failure mode the
215
215
  * lease pattern fixes.
216
+ *
217
+ * Returns how the event resolved, which `dispatchAndRecord` persists on the
218
+ * processed row: `matched` when it dispatched at least one workflow, else
219
+ * the reason nothing ran.
216
220
  */
217
221
  private processSubscriptions;
218
222
  /**
@@ -225,6 +229,8 @@ export declare class EventRouter {
225
229
  * itself dispatched by a failure-lifecycle trigger is neither buffered nor
226
230
  * dispatched — a broken notifier must not re-trigger the batch on its own
227
231
  * failure. The chain-depth breaker remains the backstop for anything missed.
232
+ *
233
+ * `bufferedCount` counts the decisions appended to a batch window.
228
234
  */
229
235
  private bufferBatchDecisions;
230
236
  /** Open (or reuse) the registration's batch window and append the failed run. */
@@ -1,6 +1,6 @@
1
1
  import { type Kysely, type Transaction } from 'kysely';
2
2
  import type { Database } from '../db/types.js';
3
- import type { DlqReason, EventRouterConfig, StoredEvent } from './types.js';
3
+ import { EventMatchOutcome, type DlqReason, type EventMatchResult, type EventRouterConfig, type StoredEvent } from './types.js';
4
4
  /** Either a Kysely DB handle or an active transaction. */
5
5
  export type DbExecutor = Kysely<Database> | Transaction<Database>;
6
6
  /**
@@ -81,11 +81,28 @@ export declare class EventStore {
81
81
  * re-queries until a page returns fewer than `limit` rows.
82
82
  */
83
83
  getUnprocessedSince(sinceId: string | null, limit?: number): Promise<StoredEvent[]>;
84
+ /**
85
+ * List events newest first, for `kici-admin event list`. Every filter is
86
+ * optional except the limit. `before` keeps events created before a
87
+ * timestamp. `afterEventId` is the next-page cursor: the events that sort
88
+ * after that event in this order, compared on the stored `(created_at, id)`
89
+ * so events created in the same millisecond are neither skipped nor repeated.
90
+ */
91
+ list(opts: {
92
+ name?: string;
93
+ outcome?: EventMatchOutcome;
94
+ since?: Date;
95
+ before?: Date;
96
+ afterEventId?: string;
97
+ limit: number;
98
+ sourceRoutingKey?: string;
99
+ }): Promise<StoredEvent[]>;
84
100
  /**
85
101
  * Mark an event as fully processed. Called after a successful dispatch.
86
- * Clears any prior lease so the row is unambiguously terminal.
102
+ * Clears any prior lease so the row is unambiguously terminal, and records
103
+ * how the router resolved it when `result` is given.
87
104
  */
88
- markProcessed(id: string): Promise<void>;
105
+ markProcessed(id: string, result?: EventMatchResult): Promise<void>;
89
106
  /**
90
107
  * Atomically take a dispatch lease on an event. Replaces the previous
91
108
  * "flip processed=true upfront" pattern that silently lost events when
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Event routing configuration and types.
3
3
  */
4
+ import { z } from 'zod';
4
5
  /**
5
6
  * Configuration for the event routing system.
6
7
  */
@@ -45,6 +46,22 @@ export interface EventRouterConfig {
45
46
  * Reason an event landed in the DLQ.
46
47
  */
47
48
  export type DlqReason = 'exhausted_retries' | 'non_retryable';
49
+ /** How the router resolved one event against the registered workflows. */
50
+ export declare const EventMatchOutcome: z.ZodEnum<{
51
+ buffered: "buffered";
52
+ matched: "matched";
53
+ "no-registration": "no-registration";
54
+ "no-target-repo": "no-target-repo";
55
+ "no-trigger-match": "no-trigger-match";
56
+ "trust-blocked": "trust-blocked";
57
+ }>;
58
+ export type EventMatchOutcome = z.infer<typeof EventMatchOutcome>;
59
+ /** What the router resolved for one event, persisted on its processed row. */
60
+ export interface EventMatchResult {
61
+ outcome: EventMatchOutcome;
62
+ /** Registrations the event was dispatched to. */
63
+ matchedCount: number;
64
+ }
48
65
  /**
49
66
  * A stored internal event (matches DB row shape but with JS types).
50
67
  */
@@ -70,7 +87,35 @@ export interface StoredEvent {
70
87
  nextRetryAt: Date | null;
71
88
  dlqAt: Date | null;
72
89
  dlqReason: DlqReason | null;
90
+ /**
91
+ * How the router resolved the event. Set when the event is processed; null
92
+ * before, and for rows processed before the column existed.
93
+ */
94
+ matchOutcome: EventMatchOutcome | null;
95
+ /** Registrations the event was dispatched to; null when `matchOutcome` is. */
96
+ matchedCount: number | null;
73
97
  }
98
+ /**
99
+ * An event payload safe to show an operator. An event-scaler scale-up carries
100
+ * a single-use claim code an agent redeems for credentials, so every read
101
+ * surface (event show, the dead-letter queue) replaces it with `[redacted]`.
102
+ */
103
+ export declare function redactEventPayload(payload: Record<string, unknown>): Record<string, unknown>;
104
+ /** Where an internal event is in its dispatch lifecycle. */
105
+ export declare const EventProcessingState: z.ZodEnum<{
106
+ dlq: "dlq";
107
+ leased: "leased";
108
+ pending: "pending";
109
+ processed: "processed";
110
+ retrying: "retrying";
111
+ }>;
112
+ export type EventProcessingState = z.infer<typeof EventProcessingState>;
113
+ /**
114
+ * Derive an event's dispatch state from its row: DLQ wins, then processed;
115
+ * an unprocessed row is leased while a node holds it, retrying once a failed
116
+ * dispatch scheduled another attempt, and pending before its first lease.
117
+ */
118
+ export declare function eventProcessingState(e: StoredEvent): EventProcessingState;
74
119
  /**
75
120
  * Default event router configuration.
76
121
  */
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Running a Windows batch file (`.cmd` / `.bat`) through cmd.exe.
3
+ *
4
+ * Node.js refuses to start a batch file without a shell (it throws `EINVAL`),
5
+ * and every launcher a KiCI package ships on Windows is a batch file. Two
6
+ * callers run one: the service installer, which registers the command with
7
+ * shawl, and the bare-metal scaler, which spawns it.
8
+ *
9
+ * Both quote a token on cmd.exe's command line only when it contains a space
10
+ * or a tab — shawl does, and so does libuv when Node.js builds a Windows
11
+ * command line — so one safety rule fits both.
12
+ */
13
+ /**
14
+ * The cmd.exe arguments that run a batch-file launcher, ahead of its path.
15
+ *
16
+ * shawl stops the service with a ctrl-C to its console, and cmd.exe gets it
17
+ * too: when the batch file's node process exits, cmd.exe asks "Terminate batch
18
+ * job (Y/N)?" and waits for an answer. shawl sees the service exit only when its
19
+ * stop timeout kills cmd.exe. With stdin from NUL ({@link BATCH_STDIN}), cmd.exe
20
+ * reads end-of-input at that prompt and exits with node.
21
+ *
22
+ * `call` keeps the launcher path out of the first position after `/c`, where
23
+ * cmd.exe strips the quotes from a path that contains `(`, `)` or `&`. `/e:on`
24
+ * and `/v:off` pin command extensions (the launchers use `%~dp0`) and delayed
25
+ * expansion whatever the host's registry defaults are.
26
+ */
27
+ export declare const BATCH_WRAPPER_ARGS: readonly ['/d', '/e:on', '/v:off', '/c', 'call'];
28
+ /** The redirect that closes {@link BATCH_WRAPPER_ARGS}: the launcher's stdin is NUL. */
29
+ export declare const BATCH_STDIN = "<NUL";
30
+ /** A `.cmd` or `.bat` file, which Windows runs through cmd.exe. */
31
+ export declare function isBatchFile(executablePath: string): boolean;
32
+ /**
33
+ * The cmd.exe that COMSPEC names in `env`, as Node itself uses to run a shell on
34
+ * Windows. A COMSPEC that names another shell is not used: the wrapper
35
+ * arguments are cmd.exe's.
36
+ */
37
+ export declare function cmdExePath(env?: NodeJS.ProcessEnv): string;
38
+ /**
39
+ * Why cmd.exe would read part of `token` as syntax, or null when it would not.
40
+ *
41
+ * A token is quoted on cmd.exe's command line only when it contains a space or
42
+ * a tab. Inside quotes cmd.exe reads `&`, `|`, `<`, `>`, `(`, `)` and the `,`
43
+ * `;` `=` delimiters as text; outside quotes it reads them as syntax. `%` and
44
+ * `^` change even inside quotes (`call` expands `%` again and doubles `^`), and
45
+ * a `"` ends the quoting.
46
+ *
47
+ * @param use - what the token runs as, completing "cannot run <token> …".
48
+ * @param remedy - the sentence that tells the operator what to change.
49
+ */
50
+ export declare function cmdSafetyRefusal(token: string, use: string, remedy: string): string | null;
51
+ /** Throw the {@link cmdSafetyRefusal} message for `token`, if it has one. */
52
+ export declare function assertCmdSafe(token: string, use: string, remedy: string): void;
53
+ /**
54
+ * The cmd.exe command that runs `command` — a batch file and its arguments —
55
+ * with stdin from NUL. The caller checks each token with
56
+ * {@link assertCmdSafe} first.
57
+ */
58
+ export declare function batchFileCommand(command: readonly string[], env?: NodeJS.ProcessEnv): string[];
59
+ //# sourceMappingURL=windows-batch.d.ts.map
package/dist/index.js CHANGED
@@ -2860,6 +2860,51 @@ var AdminApiClient = class {
2860
2860
  async drainStatus() {
2861
2861
  return this.request("GET", "/api/v1/admin/orchestrator/drain");
2862
2862
  }
2863
+ /** Forget a departed peer on this coordinator and every connected sibling. */
2864
+ /**
2865
+ * Forget a departed peer on this coordinator and every connected sibling. A
2866
+ * refusal that only needs the backstop acknowledgement comes back as a body
2867
+ * with `acknowledgementRequired`, so the caller can ask and retry; every
2868
+ * other failure throws.
2869
+ */
2870
+ async forgetPeer(body) {
2871
+ const res = await fetchAdminApi(`${this.baseUrl}/api/v1/admin/peers/forget`, {
2872
+ method: "POST",
2873
+ headers: {
2874
+ Authorization: `Bearer ${this.token}`,
2875
+ "Content-Type": "application/json"
2876
+ },
2877
+ body: JSON.stringify(body)
2878
+ }, this.baseUrl);
2879
+ const text = await res.text();
2880
+ let parsed;
2881
+ try {
2882
+ parsed = JSON.parse(text);
2883
+ } catch {
2884
+ parsed = void 0;
2885
+ }
2886
+ const json = parsed ?? {};
2887
+ if (res.ok) return json;
2888
+ if (res.status === 409 && json.acknowledgementRequired === true) return {
2889
+ instanceId: body.instanceId,
2890
+ results: json.results ?? [],
2891
+ acknowledgementRequired: true,
2892
+ ...json.error ? { error: json.error } : {}
2893
+ };
2894
+ throw new Error(`HTTP ${res.status}: ${json.error ?? text}`);
2895
+ }
2896
+ /** List the live Firecracker VMs on this coordinator, or on the `target` peer. */
2897
+ async listScalerOrphans(opts = {}) {
2898
+ const params = new URLSearchParams();
2899
+ if (opts.target !== void 0) params.set("target", opts.target);
2900
+ if (opts.timeoutMs !== void 0) params.set("timeoutMs", String(opts.timeoutMs));
2901
+ const query = params.toString();
2902
+ return this.request("GET", `/api/v1/admin/scaler/orphans${query ? `?${query}` : ""}`);
2903
+ }
2904
+ /** Stop the named VMs on this coordinator, or on the `target` peer. */
2905
+ async stopScalerOrphans(body) {
2906
+ return this.request("POST", "/api/v1/admin/scaler/orphans/stop", body);
2907
+ }
2863
2908
  async createApiKey(opts) {
2864
2909
  return this.request("POST", "/api/v1/api-keys", opts);
2865
2910
  }
@@ -2986,6 +3031,21 @@ var AdminApiClient = class {
2986
3031
  if (opts.routingKey) params.set("routingKey", opts.routingKey);
2987
3032
  return this.request("GET", `/api/v1/admin/event-log/${encodeURIComponent(deliveryId)}?${params}`);
2988
3033
  }
3034
+ /** List internal events, newest first (`GET /api/v1/admin/events`). */
3035
+ async listEvents(opts) {
3036
+ const params = new URLSearchParams();
3037
+ if (opts?.name) params.set("name", opts.name);
3038
+ if (opts?.outcome) params.set("outcome", opts.outcome);
3039
+ if (opts?.since) params.set("since", opts.since);
3040
+ if (opts?.before) params.set("before", opts.before);
3041
+ if (opts?.limit !== void 0) params.set("limit", String(opts.limit));
3042
+ const qs = params.toString();
3043
+ return this.request("GET", `/api/v1/admin/events${qs ? `?${qs}` : ""}`);
3044
+ }
3045
+ /** One internal event with its redacted payload and dispatched runs. */
3046
+ async getEvent(id) {
3047
+ return this.request("GET", `/api/v1/admin/events/${encodeURIComponent(id)}`);
3048
+ }
2989
3049
  async listAccessLog(opts) {
2990
3050
  const params = new URLSearchParams();
2991
3051
  if (opts?.orgId) params.set("orgId", opts.orgId);
@@ -539,6 +539,8 @@ export declare const eventRetryTotal: import("@opentelemetry/api").Counter<impor
539
539
  * - reason: why the event was DLQ'd (exhausted_retries)
540
540
  */
541
541
  export declare const eventDlqTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
542
+ /** Record a reserved internal event that matched no subscriber. */
543
+ export declare function incEventUnmatched(eventName: string, reason: string): void;
542
544
  /**
543
545
  * Startup catch-up scans that threw. Emits no labels.
544
546
  *
@@ -41,6 +41,7 @@ import { ArtifactStore } from './artifacts/artifact-store.js';
41
41
  import { CheckRunReporter } from './reporting/check-run-reporter.js';
42
42
  import type { ProcessingDeps } from './pipeline/processor.js';
43
43
  import type { InvokeGateDeps, SummonArgs, SummonedRun } from './pipeline/invoke-gate.js';
44
+ import { type PeerForgetRequestHandler } from './cluster/peer-forget.js';
44
45
  import { StepLogBuffer } from './reporting/step-log-buffer.js';
45
46
  import { type LogStorage } from './reporting/log-storage.js';
46
47
  import { ExecutionTracker, type ExecutionTrackerDeps } from './reporting/execution-tracker.js';
@@ -167,6 +168,12 @@ export interface OrchestratorSubsystems {
167
168
  orphanRecovery: OrphanRecovery;
168
169
  peerHandler: ReturnType<typeof createPeerHandler>;
169
170
  peerClients: Map<string, PeerClient>;
171
+ /**
172
+ * Answers a `kici-admin peer forget` a sibling coordinator fanned out. Wired
173
+ * into every outgoing PeerClient (the incoming peer-handler is wired directly
174
+ * in setupCluster).
175
+ */
176
+ answerPeerForgetRequest: PeerForgetRequestHandler;
170
177
  getLocalInventory: () => Omit<PeerHeartbeat, 'type'>;
171
178
  broadcastHeartbeatToAllPeers: () => void;
172
179
  broadcastAgentTokenRevoke: (tokenId: string) => void;
@@ -74,6 +74,16 @@ interface ReplaceAllOptions {
74
74
  depCacheKey?: DepCacheKey;
75
75
  /** Set of workflow names that should be marked as global */
76
76
  globalWorkflowNames?: Set<string>;
77
+ /**
78
+ * Delete this routing key's rows for the repo whose workflow the incoming set
79
+ * does not name. `true` (the default) is the default-branch push sync: the
80
+ * lock file is the whole truth for the repo. The manual registration route
81
+ * passes `false`: it upserts the workflows it names and leaves every other
82
+ * registration of the repo alone, which is what
83
+ * `kici-admin workflow register-manual` documents and what its direct-DB
84
+ * mode does.
85
+ */
86
+ prune?: boolean;
77
87
  }
78
88
  /**
79
89
  * DB CRUD for workflow_registrations table with atomic upsert-based replace-all.
@@ -4,7 +4,9 @@
4
4
  * Exposes GET / PATCH `/api/v1/admin/cluster-settings` so `kici-admin
5
5
  * cluster-settings` can read and write the single `cluster_settings` row
6
6
  * (id='default') without going through the Platform dashboard proxy — the CLI
7
- * stays operable even when Platform is unavailable.
7
+ * stays operable even when Platform is unavailable. `GET
8
+ * /api/v1/admin/cluster-settings/propagation` reports which settings version
9
+ * each orchestrator this coordinator knows has applied.
8
10
  *
9
11
  * These knobs are fleet-wide, not per-tenant, so the route is deliberately NOT
10
12
  * under `/orgs/:customerId/*`. Mutations require the `secret.write` RBAC
@@ -15,9 +17,19 @@ import { Hono } from 'hono';
15
17
  import { type Kysely } from 'kysely';
16
18
  import type { Database } from '../db/types.js';
17
19
  import type { RbacEnforcer, Role } from '../secrets/rbac.js';
20
+ import type { PeerRegistry } from '../cluster/peer-registry.js';
21
+ /** The live view behind `GET /cluster-settings/propagation`. */
22
+ export interface ClusterSettingsPropagationSource {
23
+ instanceId: string;
24
+ peerRegistry: Pick<PeerRegistry, 'getAllPeers'>;
25
+ /** The cluster-settings version this coordinator's reads currently serve. */
26
+ localVersion: () => number;
27
+ }
18
28
  interface ClusterSettingsRouteDeps {
19
29
  db: Kysely<Database>;
20
30
  rbac: RbacEnforcer;
31
+ /** Mounts `GET /cluster-settings/propagation` when set. */
32
+ propagation?: ClusterSettingsPropagationSource;
21
33
  }
22
34
  type AdminEnv = {
23
35
  Variables: {
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Admin API routes for inspecting internal events (`kici_events`).
3
+ *
4
+ * GET /api/v1/admin/events — list events, newest first (name, outcome, time filters)
5
+ * GET /api/v1/admin/events/:id — one event: its payload and the runs it dispatched
6
+ *
7
+ * Each row carries its dispatch state (`pending`, `leased`, `retrying`,
8
+ * `processed`, `dlq`) and, once processed, how the router resolved it
9
+ * (`matched`, `no-target-repo`, …) with the number of workflows it reached.
10
+ * Both routes need `event_dlq.read`, the permission that already gates reading
11
+ * `kici_events` rows; the payload on `show` also needs
12
+ * `event_log.read_payload`. A routing-key-scoped token sees only events
13
+ * emitted under its own key.
14
+ */
15
+ import { Hono } from 'hono';
16
+ import type { Kysely } from 'kysely';
17
+ import type { Database } from '../db/types.js';
18
+ import type { EventStore } from '../events/event-store.js';
19
+ import type { TokenManager } from '../secrets/token-manager.js';
20
+ import type { RbacEnforcer, Role } from '../secrets/rbac.js';
21
+ export interface AdminEventInspectRoutesDeps {
22
+ db: Kysely<Database>;
23
+ eventStore: EventStore;
24
+ tokenManager: TokenManager;
25
+ rbac: RbacEnforcer;
26
+ }
27
+ type AdminEnv = {
28
+ Variables: {
29
+ role: Role;
30
+ userId: string;
31
+ routingKey: string | null;
32
+ };
33
+ };
34
+ export declare function createAdminEventInspectRoutes(deps: AdminEventInspectRoutesDeps): Hono<AdminEnv>;
35
+ export {};
36
+ //# sourceMappingURL=admin-event-inspect.d.ts.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Forget a departed peer, behind `kici-admin peer forget <instance-id>`.
3
+ *
4
+ * Mounted inside `createAdminRoutes` at `/api/v1/admin`, so the Bearer-token
5
+ * auth middleware has already resolved the caller's role. Needs an unscoped
6
+ * token and the `peer.manage` permission (owner, admin).
7
+ *
8
+ * - `POST /api/v1/admin/peers/forget { instanceId, timeoutMs? }` — drop the
9
+ * peer from this coordinator's live peer registry, then from every connected
10
+ * sibling coordinator's, and answer with one result per coordinator.
11
+ *
12
+ * Forgetting never touches the peer's credential (`kici-admin peer revoke`
13
+ * refuses its next connection). A peer that connects again later is
14
+ * registered again as usual.
15
+ */
16
+ import { Hono } from 'hono';
17
+ import type { AccessLogWriter } from '../audit/access-log.js';
18
+ import { type PeerForgetResult } from '../cluster/peer-forget.js';
19
+ import type { RbacEnforcer, Role } from '../secrets/rbac.js';
20
+ type AdminEnv = {
21
+ Variables: {
22
+ role: Role;
23
+ userId: string;
24
+ routingKey: string | null;
25
+ };
26
+ };
27
+ /** What the route needs from the coordinator it runs on. */
28
+ export interface PeerForgetRouteDeps {
29
+ /**
30
+ * Forget `instanceId` here, then on every connected sibling coordinator
31
+ * (skipped when this coordinator keeps the peer). The first result is this
32
+ * coordinator's. `acknowledgeBackstop` lets a forget that switches an
33
+ * event-provision backstop back on go ahead.
34
+ */
35
+ forget: (instanceId: string, timeoutMs: number, acknowledgeBackstop: boolean) => Promise<PeerForgetResult[]>;
36
+ }
37
+ export declare function createPeerForgetRoutes(deps: {
38
+ peerForget: PeerForgetRouteDeps;
39
+ rbac: RbacEnforcer;
40
+ accessLog?: AccessLogWriter;
41
+ }): Hono<AdminEnv>;
42
+ export {};
43
+ //# sourceMappingURL=admin-peer-forget.d.ts.map
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Live orphaned Firecracker VM admin routes, behind `kici-admin scaler orphans`.
3
+ *
4
+ * Mounted inside `createAdminRoutes` at `/api/v1/admin`, so the Bearer-token
5
+ * auth middleware has already resolved the caller's role. Both routes need an
6
+ * unscoped token: a VM belongs to a host, not to one routing key.
7
+ *
8
+ * - `GET /api/v1/admin/scaler/orphans?target=<instance-id>&timeoutMs=<n>` —
9
+ * every live Firecracker VM on the node, with how it is tracked
10
+ * (`scaler.read`).
11
+ * - `POST /api/v1/admin/scaler/orphans/stop { target?, vmIds, timeoutMs? }` —
12
+ * stop the approved VMs nothing tracks (`scaler.manage`).
13
+ *
14
+ * The node is this coordinator, or the peer `target` names, reached over the
15
+ * authenticated peer channel. The coordinator adds the one tracker only it
16
+ * can answer — a non-terminal job in its dispatch queue bound to the VM's
17
+ * agent — and never forwards a bound VM to the node's stop.
18
+ */
19
+ import { Hono } from 'hono';
20
+ import { type PeerScalerOrphansResponse } from '@kici-dev/engine';
21
+ import type { AccessLogWriter } from '../audit/access-log.js';
22
+ import type { RbacEnforcer, Role } from '../secrets/rbac.js';
23
+ import { type ScalerOrphansAnswer, type ScalerOrphansRequest } from '../scaler/orphan-requests.js';
24
+ import { ScalerOrphansForwardFailure } from '../cluster/scaler-orphans-peer.js';
25
+ type AdminEnv = {
26
+ Variables: {
27
+ role: Role;
28
+ userId: string;
29
+ routingKey: string | null;
30
+ };
31
+ };
32
+ /** What the routes need from the coordinator they run on. */
33
+ export interface ScalerOrphansRouteDeps {
34
+ /** This coordinator's instance id. */
35
+ instanceId: string;
36
+ role: 'coordinator' | 'worker';
37
+ /** The cluster role a connected peer registered with, if known. */
38
+ peerRole: (instanceId: string) => 'coordinator' | 'worker' | undefined;
39
+ /** Answer from this coordinator's own host and tracking. */
40
+ answerLocal: (req: ScalerOrphansRequest) => Promise<ScalerOrphansAnswer>;
41
+ /** Forward to a peer over the peer channel. */
42
+ forward: (target: string, req: ScalerOrphansRequest, timeoutMs: number) => Promise<PeerScalerOrphansResponse | ScalerOrphansForwardFailure>;
43
+ /** Non-terminal dispatch-queue jobs bound to these agent ids, keyed by agent id. */
44
+ findBindings: (agentIds: string[]) => Promise<Map<string, string>>;
45
+ /**
46
+ * The cluster instance each of these agent ids is registered with — this
47
+ * coordinator or a peer whose heartbeat lists it — keyed by agent id. An
48
+ * agent can register with an HA sibling of the node that runs its VM.
49
+ */
50
+ findRegistrations: (agentIds: string[]) => Map<string, string>;
51
+ /**
52
+ * The coordinator peers this cluster expects but has no live link to. Their
53
+ * registered agents are unknown, so while any is missing no VM can be shown
54
+ * to be untracked.
55
+ */
56
+ disconnectedCoordinators: () => string[];
57
+ }
58
+ export declare function createScalerOrphansRoutes(deps: {
59
+ orphans: ScalerOrphansRouteDeps;
60
+ rbac: RbacEnforcer;
61
+ accessLog?: AccessLogWriter;
62
+ }): Hono<AdminEnv>;
63
+ export {};
64
+ //# sourceMappingURL=admin-scaler-orphans.d.ts.map
@@ -18,6 +18,9 @@ import type { AgentRegistry } from '../agent/registry.js';
18
18
  import type { SharedConfigStore } from '../config/shared-store.js';
19
19
  import type { ResolvedMasterKeys } from '../secrets/config.js';
20
20
  import { type HeldRunReleaseWiring } from './admin-held-runs.js';
21
+ import { type ClusterSettingsPropagationSource } from './admin-cluster-settings.js';
22
+ import { type ScalerOrphansRouteDeps } from './admin-scaler-orphans.js';
23
+ import { type PeerForgetRouteDeps } from './admin-peer-forget.js';
21
24
  import type { DrainController } from '../drain/drain-controller.js';
22
25
  import type { SourceStore } from '../sources/source-store.js';
23
26
  import type { JoinTokenManager } from '../cluster/join-token.js';
@@ -159,6 +162,26 @@ export interface AdminRouteDeps {
159
162
  * write is best-effort, never gating.
160
163
  */
161
164
  accessLog?: AccessLogWriter;
165
+ /**
166
+ * Optional -- the coordinator's live-VM listing and stop, for this node or a
167
+ * `--target` peer. Backs `GET /api/v1/admin/scaler/orphans` and
168
+ * `POST /api/v1/admin/scaler/orphans/stop` (`kici-admin scaler orphans`).
169
+ * Bound by orchestrator-core once the cluster is up.
170
+ */
171
+ scalerOrphans?: ScalerOrphansRouteDeps;
172
+ /**
173
+ * Optional -- forget a departed peer here and on every connected sibling
174
+ * coordinator. Backs `POST /api/v1/admin/peers/forget` (`kici-admin peer
175
+ * forget`). Bound by orchestrator-core once the cluster is up.
176
+ */
177
+ peerForget?: PeerForgetRouteDeps;
178
+ /**
179
+ * Optional -- the peer view behind
180
+ * `GET /api/v1/admin/cluster-settings/propagation` (the propagation section of
181
+ * `kici-admin cluster-settings show`). Bound by orchestrator-core once the
182
+ * cluster is up; the route is not mounted without it.
183
+ */
184
+ clusterSettingsPropagation?: ClusterSettingsPropagationSource;
162
185
  /**
163
186
  * Optional -- fulfil deferred attestations on demand (mints in the running
164
187
  * orchestrator process, which owns the Platform WS). Backs
@@ -38,7 +38,12 @@ export interface BackendFactoryContext {
38
38
  tokenTtlMs?: number;
39
39
  /** Live per-spawn resolver for the fleet-wide agent-token TTL. */
40
40
  tokenTtlProvider: () => Promise<number>;
41
- /** DB-backed on the leader, in-memory on the worker. */
41
+ /**
42
+ * Live per-spawn resolver for the fleet-wide Firecracker API-socket wait.
43
+ * A coordinator reads `cluster_settings`; a worker reads its pulled snapshot.
44
+ */
45
+ firecrackerApiSocketWaitMsProvider: () => Promise<number>;
46
+ /** DB-backed on a coordinator, in-memory on a worker. */
42
47
  ipAllocator: (params: IpAllocatorParams) => IpAllocator;
43
48
  /**
44
49
  * The already-constructed scaler state store. The event backend's claim
@@ -2,7 +2,8 @@
2
2
  * Bare-metal scaler backend implementation.
3
3
  *
4
4
  * Manages ephemeral agent processes using child_process.spawn.
5
- * Uses detached process groups for clean killing of entire process trees.
5
+ * On Linux and macOS each agent leads its own process group, which teardown
6
+ * signals; on Windows teardown ends the agent's process tree with taskkill.
6
7
  * All agents are single-use: destroyed after job completion.
7
8
  */
8
9
  import { type ToolRequirement } from '@kici-dev/shared';
@@ -98,6 +99,11 @@ export declare class BareMetalScalerBackend implements ScalerBackend {
98
99
  get labelSets(): LabelSetConfig[];
99
100
  getActiveCount(): number;
100
101
  spawn(labelSet: string[], agentId: string, orchestratorUrl: string, onEvent?: ScalerEventCallback, effectiveLimits?: EffectiveLimits, spawnContext?: SpawnContext, signal?: AbortSignal): Promise<ManagedAgent>;
102
+ /**
103
+ * Record that the agent registered. From then on its exit is a disconnect
104
+ * the manager handles, not a failed spawn.
105
+ */
106
+ markRegistered(managedId: string): void;
101
107
  /**
102
108
  * Launch the agent inside the job's own container image.
103
109
  *
@@ -145,23 +151,19 @@ export declare class BareMetalScalerBackend implements ScalerBackend {
145
151
  entry?: ScalerEntry;
146
152
  }): ValidationResult;
147
153
  /**
148
- * Build the (command, args) tuple used to spawn an agent process.
154
+ * End a Windows agent's whole process tree.
149
155
  *
150
- * Two modes:
151
- * - Direct: returns (binaryPath, []) — the historical bare-metal flow.
152
- * Used when enforceCgroups is false, the host is non-Linux, or the
153
- * resolved limits carry no positive cpus/memBytes.
154
- * - systemd-run scope: returns ('systemd-run', [...]) wrapping the
155
- * binary in a transient `--user --scope --slice=kici-scaler` cgroup
156
- * with `CPUQuota=` / `MemoryMax=` properties translated from the
157
- * effective limits. The scope name embeds `agentId` so each agent is
158
- * a separately-named transient unit (and `systemctl --user status
159
- * kici-agent-<agentId>.scope` works).
156
+ * Windows has no process groups to signal and no SIGTERM, and Node.js cannot
157
+ * send a ctrl-C there (`process.kill` terminates the target unconditionally),
158
+ * so the tree is ended at once with `taskkill /T /F`. The pid is cmd.exe's, which runs the agent's `node` and
159
+ * everything it started.
160
160
  *
161
- * `CPUQuota` is expressed as a percent (1.0 cpus = 100%). `MemoryMax`
162
- * accepts a raw byte count.
161
+ * Nothing runs once Node.js has seen the child exit: libuv then releases its
162
+ * handle on the process, Windows may give the pid to an unrelated process,
163
+ * and `/T` would end that process's tree. While the child runs, the open
164
+ * handle keeps the pid ours.
163
165
  */
164
- private buildSpawnInvocation;
166
+ private endWindowsTree;
165
167
  /**
166
168
  * Wait for a process to exit within a timeout.
167
169
  * Returns true if the process exited, false if timed out.