@kici-dev/orchestrator 0.1.3 → 0.1.5

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 (66) hide show
  1. package/dist/agent/dispatcher.d.ts +26 -0
  2. package/dist/agent/ownership-tracker.d.ts +45 -3
  3. package/dist/audit/access-log.d.ts +7 -0
  4. package/dist/cli/api-client.d.ts +14 -0
  5. package/dist/cli/commands/cluster-name.d.ts +18 -0
  6. package/dist/cli/commands/db.d.ts +9 -6
  7. package/dist/cli/commands/org-settings.d.ts +2 -1
  8. package/dist/cli/commands/shared/secret-input.d.ts +46 -0
  9. package/dist/cli/commands/variable.d.ts +18 -0
  10. package/dist/cli/service/index.d.ts +1 -0
  11. package/dist/cli/service/launchd.d.ts +19 -0
  12. package/dist/cli/service/privilege.d.ts +21 -0
  13. package/dist/cli/wizard/orchestrator-wizard.d.ts +4 -2
  14. package/dist/cli.js +1056 -236
  15. package/dist/cluster/join-token.d.ts +21 -13
  16. package/dist/cluster/peer-client.d.ts +9 -0
  17. package/dist/config/cluster-id.d.ts +23 -0
  18. package/dist/config/cluster-name.d.ts +39 -0
  19. package/dist/config.d.ts +6 -0
  20. package/dist/dashboard/handler.d.ts +54 -2
  21. package/dist/db/migrations/019_generic_sources_change_notify.d.ts +25 -0
  22. package/dist/db/migrations/020_org_settings_dashboard_write_policy.d.ts +17 -0
  23. package/dist/db/migrations/021_check_run_tracking.d.ts +26 -0
  24. package/dist/db/migrations/022_scaler_manager_state.d.ts +29 -0
  25. package/dist/db/migrations/023_dispatch_queue_recovery_deadline.d.ts +26 -0
  26. package/dist/db/types.d.ts +137 -0
  27. package/dist/events/event-store.d.ts +9 -2
  28. package/dist/events/trust-store.d.ts +7 -0
  29. package/dist/events/types.d.ts +33 -0
  30. package/dist/metrics/scheduled-jobs.d.ts +26 -5
  31. package/dist/orchestrator-core.d.ts +7 -0
  32. package/dist/policy/dashboard-write-policy.d.ts +115 -0
  33. package/dist/queue/job-queue.d.ts +54 -1
  34. package/dist/reporting/check-run-tracking-store.d.ts +133 -0
  35. package/dist/reporting/commit-status.d.ts +87 -9
  36. package/dist/routes/admin-access-log.d.ts +1 -0
  37. package/dist/routes/admin-backends.d.ts +9 -1
  38. package/dist/routes/admin-cluster-name.d.ts +50 -0
  39. package/dist/routes/admin-db.d.ts +9 -1
  40. package/dist/routes/admin-environments.d.ts +10 -6
  41. package/dist/routes/admin-event-dlq.d.ts +1 -0
  42. package/dist/routes/admin-event-log.d.ts +1 -0
  43. package/dist/routes/admin-events.d.ts +19 -0
  44. package/dist/routes/admin-maintenance.d.ts +9 -1
  45. package/dist/routes/admin-org-settings.d.ts +10 -1
  46. package/dist/routes/admin-queue-execution.d.ts +1 -0
  47. package/dist/routes/admin-registrations.d.ts +1 -0
  48. package/dist/routes/admin-runs.d.ts +1 -0
  49. package/dist/routes/admin-scheduled-jobs.d.ts +1 -0
  50. package/dist/routes/admin-sources.d.ts +9 -1
  51. package/dist/routes/admin.d.ts +8 -0
  52. package/dist/scaler/manager.d.ts +38 -0
  53. package/dist/scaler/scaler-state-store.d.ts +102 -0
  54. package/dist/secrets/routing-key-scope.d.ts +43 -0
  55. package/dist/server.js +9846 -6884
  56. package/dist/stale-detector/stale-run-detector.d.ts +14 -2
  57. package/dist/standalone.js +6120 -3838
  58. package/dist/webhook/generic-sources-listener.d.ts +74 -0
  59. package/dist/webhook/register-source-bundle.d.ts +57 -0
  60. package/dist/ws/dashboard-backends-handler.d.ts +14 -0
  61. package/dist/ws/dashboard-env-handler.d.ts +7 -0
  62. package/dist/ws/dashboard-global-workflows-handler.d.ts +6 -0
  63. package/dist/ws/dashboard-registrations-handler.d.ts +7 -0
  64. package/dist/ws/platform-client.d.ts +30 -1
  65. package/package.json +3 -3
  66. package/sbom.spdx.json +40 -35
@@ -46,25 +46,33 @@ export declare class JoinTokenManager {
46
46
  expiryMs?: number;
47
47
  }): Promise<string>;
48
48
  /**
49
- * Validate a token: check hash exists in DB, not expired, not consumed.
50
- * Returns routing info and derived keys on success.
49
+ * Atomically validate and consume a token in one DB round-trip.
50
+ *
51
+ * Single UPDATE with `WHERE token_hash = ? AND consumed_at IS NULL AND
52
+ * expires_at > NOW()` — only one caller can win the claim across a
53
+ * shared-DB multi-coordinator mesh. The winner gets `{ routing, keys }`;
54
+ * every other concurrent caller gets `TOKEN_ALREADY_USED_MESSAGE` and is
55
+ * expected to fall into the idempotent recovery branch in
56
+ * `peer-handler.ts` (which serialises credential issuance via the
57
+ * `peer_credentials_active_uniq` partial unique index).
58
+ *
59
+ * On a 0-row claim, a follow-up SELECT disambiguates not-found / expired
60
+ * / already-used so callers can branch on the specific reason — the
61
+ * recovery path in peer-handler.ts keys on the "already used" string
62
+ * specifically. The follow-up only fires on the unhappy path.
51
63
  */
52
- validateToken(token: string): Promise<{
64
+ validateAndConsumeToken(token: string, consumedBy: string): Promise<{
53
65
  routing: TokenRouting;
54
66
  keys: DerivedKeys;
55
67
  }>;
56
- /**
57
- * Mark token as consumed (one-time use).
58
- */
59
- consumeToken(validationHash: string, consumedBy: string): Promise<void>;
60
68
  }
61
69
  /**
62
- * Narrow detector for the "already been used" error thrown by validateToken().
63
- * Used by peer-handler.ts to branch into the idempotent recovery path on
64
- * legitimate mesh-join races (sibling peer-clients on the same peer identity
65
- * racing on a shared join token across a multi-coordinator shared-DB mesh).
66
- * Other validation failures (expired, not found, bad parse) MUST NOT be
67
- * treated as recoverable.
70
+ * Narrow detector for the "already been used" error thrown by
71
+ * validateAndConsumeToken(). Used by peer-handler.ts to branch into the
72
+ * idempotent recovery path on legitimate mesh-join races (sibling
73
+ * peer-clients on the same peer identity racing on a shared join token
74
+ * across a multi-coordinator shared-DB mesh). Other validation failures
75
+ * (expired, not found, bad parse) MUST NOT be treated as recoverable.
68
76
  */
69
77
  export declare function isTokenAlreadyUsedError(err: unknown): boolean;
70
78
  /**
@@ -70,6 +70,14 @@ export interface PeerClientOptions {
70
70
  restartRequired?: string[];
71
71
  fieldsChanged?: string[];
72
72
  }>;
73
+ /**
74
+ * Callback invoked once the remote peer accepts our auth handshake,
75
+ * carrying the remote peer's instanceId. Used by callers that initially
76
+ * register this client in `sub.peerClients` keyed by a placeholder (URL
77
+ * or stale id) to re-key the map by the canonical instanceId so later
78
+ * Platform-mediated discovery dedupes against the same client.
79
+ */
80
+ onAuthenticated?: (targetInstanceId: string) => void;
73
81
  }
74
82
  export declare class PeerClient {
75
83
  private ws;
@@ -103,6 +111,7 @@ export declare class PeerClient {
103
111
  private readonly onPeerLeaving?;
104
112
  private readonly onAgentTokenRevoke?;
105
113
  private readonly onPeerConfigReload?;
114
+ private readonly onAuthenticated?;
106
115
  constructor(options: PeerClientOptions);
107
116
  /** Current connection state. */
108
117
  get state(): PeerConnectionState;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Orchestrator cluster-id resolution.
3
+ *
4
+ * Every orchestrator DB carries a stable UUID identifier
5
+ * (`cluster_meta.cluster_id`) seeded once on first boot by the initial
6
+ * schema migration. HA-cluster coords share the same orchestrator DB,
7
+ * so they share the same `cluster_id`; two genuinely-different
8
+ * orchestrator clusters carry distinct values.
9
+ *
10
+ * The orchestrator publishes `cluster_id` on `source.register` so
11
+ * Platform can warn when two unrelated clusters in one org accidentally
12
+ * share a `cluster_name`. This module is read-only — the row is seeded
13
+ * by the schema migration and never written from application code.
14
+ */
15
+ import type { Kysely } from 'kysely';
16
+ import type { Database } from '../db/types.js';
17
+ /**
18
+ * Read the cluster id from `cluster_meta`. Throws when no row exists —
19
+ * the row is seeded by the initial schema migration, so a missing row
20
+ * indicates the DB was opened before migrations ran.
21
+ */
22
+ export declare function getClusterId(db: Kysely<Database>): Promise<string>;
23
+ //# sourceMappingURL=cluster-id.d.ts.map
@@ -0,0 +1,39 @@
1
+ import type { Kysely } from 'kysely';
2
+ import { type ClusterName } from '@kici-dev/engine/protocol/cluster-name';
3
+ import type { Database } from '../db/types.js';
4
+ /**
5
+ * Source attribution for the resolved cluster name. Useful for operator
6
+ * tooling (`kici-admin cluster-name get`) to explain where the current
7
+ * value came from.
8
+ */
9
+ export type ClusterNameSource = 'stored' | 'env-seeded' | 'auto-generated';
10
+ export interface ResolveResult {
11
+ clusterName: ClusterName;
12
+ source: ClusterNameSource;
13
+ }
14
+ /**
15
+ * Read the cluster name from `cluster_meta`. Returns null if no row
16
+ * exists yet (orch has never resolved its name).
17
+ */
18
+ export declare function readClusterName(db: Kysely<Database>): Promise<ClusterName | null>;
19
+ /**
20
+ * Look up the cluster name. Throws if no row exists — callers that hit
21
+ * this before the boot resolver has run should be considered bugs.
22
+ */
23
+ export declare function getClusterName(db: Kysely<Database>): Promise<ClusterName>;
24
+ /**
25
+ * Persist a validated cluster name. Upsert semantics: replaces the
26
+ * existing row if one exists. Called by both the boot resolver and the
27
+ * kici-admin CLI's `cluster-name set`.
28
+ */
29
+ export declare function setClusterName(db: Kysely<Database>, name: string): Promise<ClusterName>;
30
+ /**
31
+ * Boot-time resolution: returns the existing row, seeds from env, or
32
+ * auto-generates. Idempotent: a re-run after first boot just returns
33
+ * the stored value.
34
+ *
35
+ * `randomSource` is dependency-injected so tests can drive deterministic
36
+ * suffixes; production callers pass `node:crypto.randomBytes`.
37
+ */
38
+ export declare function resolveAndPersistClusterName(db: Kysely<Database>, env?: NodeJS.ProcessEnv, randomSource?: (size: number) => Uint8Array): Promise<ResolveResult>;
39
+ //# sourceMappingURL=cluster-name.d.ts.map
package/dist/config.d.ts CHANGED
@@ -85,6 +85,8 @@ declare const configSchema: z.ZodObject<{
85
85
  eventRouterRetryBaseBackoffMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
86
86
  eventRouterRetryMaxBackoffMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
87
87
  eventRouterRetryScanIntervalMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
88
+ testMode: z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<boolean, string>>;
89
+ testEventFailFirstN: z.ZodOptional<z.ZodString>;
88
90
  eventLogMaxPayloadBytes: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
89
91
  logLevel: z.ZodDefault<z.ZodEnum<{
90
92
  error: "error";
@@ -103,6 +105,7 @@ declare const configSchema: z.ZodObject<{
103
105
  agentMaxReconnectDelayMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
104
106
  skipS3SentinelValidation: z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<boolean, string>>;
105
107
  otelExporterOtlpEndpoint: z.ZodOptional<z.ZodString>;
108
+ clusterName: z.ZodOptional<z.ZodString>;
106
109
  cluster: z.ZodPrefault<z.ZodObject<{
107
110
  instanceId: z.ZodDefault<z.ZodOptional<z.ZodString>>;
108
111
  address: z.ZodOptional<z.ZodString>;
@@ -191,6 +194,7 @@ export declare const envDef: import("@kici-dev/shared/env").DefineEnvResult<{
191
194
  eventRouterRetryBaseBackoffMs: number;
192
195
  eventRouterRetryMaxBackoffMs: number;
193
196
  eventRouterRetryScanIntervalMs: number;
197
+ testMode: boolean;
194
198
  eventLogMaxPayloadBytes: number;
195
199
  logLevel: "error" | "debug" | "info" | "warn";
196
200
  nodeEnv: "development" | "production" | "test";
@@ -240,9 +244,11 @@ export declare const envDef: import("@kici-dev/shared/env").DefineEnvResult<{
240
244
  secretKeyOld?: string | undefined;
241
245
  secretKeyFileOld?: string | undefined;
242
246
  bootstrapAdminToken?: string | undefined;
247
+ testEventFailFirstN?: string | undefined;
243
248
  internalProviderRepoPath?: string | undefined;
244
249
  internalProviderCloneUrl?: string | undefined;
245
250
  otelExporterOtlpEndpoint?: string | undefined;
251
+ clusterName?: string | undefined;
246
252
  }>;
247
253
  export declare function loadConfig(): AppConfig;
248
254
  export {};
@@ -11,12 +11,13 @@
11
11
  * - run.rerun.request: re-runs a completed workflow run
12
12
  * - run.cancel.request: cancels a running workflow run
13
13
  */
14
- import type { Kysely } from 'kysely';
14
+ import { type Kysely } from 'kysely';
15
15
  import { type ColdStore } from '@kici-dev/shared';
16
- import type { DashboardRunDetailRequest, DashboardStepLogsRequest, DashboardPayloadRequest, DashboardOrchLogsRequest, DashboardEventLogListRequest, DashboardEventLogDetailRequest, DashboardEventLogPayloadStreamRequest, DashboardAccessLogListRequest, RunRerunRequest, RunCancelRequest, ManualScheduleRequest } from '@kici-dev/engine';
16
+ import type { DashboardRunDetailRequest, DashboardStepLogsRequest, DashboardPayloadRequest, DashboardOrchLogsRequest, DashboardEventLogListRequest, DashboardEventLogDetailRequest, DashboardEventLogPayloadStreamRequest, DashboardAccessLogListRequest, DashboardEventDlqListRequest, DashboardEventDlqCountRequest, DashboardEventDlqRetryRequest, DashboardEventDlqDiscardRequest, RunRerunRequest, RunCancelRequest, ManualScheduleRequest } from '@kici-dev/engine';
17
17
  import type { Database } from '../db/types.js';
18
18
  import type { LogStorage } from '../reporting/log-storage.js';
19
19
  import type { AccessLogWriter } from '../audit/access-log.js';
20
+ import type { EventStore } from '../events/event-store.js';
20
21
  interface DashboardHandlerDeps {
21
22
  db: Kysely<Database>;
22
23
  logStorage: LogStorage;
@@ -66,6 +67,13 @@ interface DashboardHandlerDeps {
66
67
  onManualSchedule: (registrationId: string, triggeredBy: string | null) => Promise<{
67
68
  newRunId: string;
68
69
  }>;
70
+ /**
71
+ * Event store for the per-org DLQ surface. Optional — when absent the
72
+ * `handleEventDlq*` methods reply with a structured `error` so the
73
+ * dashboard renders an empty-state without crashing. In real
74
+ * deployments the store is wired by `bootstrapOrchestrator`.
75
+ */
76
+ eventStore?: EventStore | null;
69
77
  }
70
78
  export declare class DashboardHandler {
71
79
  private readonly db;
@@ -79,6 +87,7 @@ export declare class DashboardHandler {
79
87
  private readonly onCancel;
80
88
  private readonly onManualSchedule;
81
89
  private readonly coldStore;
90
+ private readonly eventStore;
82
91
  constructor(deps: DashboardHandlerDeps);
83
92
  /**
84
93
  * Update the bound orgId + routingKey. Called from server.ts after resolving
@@ -125,6 +134,13 @@ export declare class DashboardHandler {
125
134
  * fallback rather than the source of truth.
126
135
  */
127
136
  private contextOrFallback;
137
+ /**
138
+ * Defense-in-depth dashboard-write policy gate for the DLQ handlers.
139
+ * Returns true when allowed; false (with a `denied` access_log row and
140
+ * an `operation_disabled` envelope on the wire) when the orch policy
141
+ * has the operation switched off.
142
+ */
143
+ private enforcePolicy;
128
144
  /**
129
145
  * Write an access_log row for a handler invocation. The caller resolves
130
146
  * the run-owning org via `resolveOrgForRun` (or the registration / event-
@@ -225,6 +241,42 @@ export declare class DashboardHandler {
225
241
  * ~80 yields per stream — negligible.
226
242
  */
227
243
  handleEventLogPayloadStream(msg: DashboardEventLogPayloadStreamRequest): Promise<void>;
244
+ /**
245
+ * Handle a dashboard.event-dlq.list request.
246
+ *
247
+ * Returns a paginated page of DLQ rows (events whose dispatch attempts
248
+ * exhausted the retry budget). The page is keyed off `dlq_at DESC` and
249
+ * paginates with a single `before` ISO cursor.
250
+ *
251
+ * Records one `event_dlq.list.read` access_log entry using the user
252
+ * actor on the wire — mirrors the HTTP admin route's audit shape but
253
+ * with the calling user's identity instead of the bearer-token role.
254
+ */
255
+ handleEventDlqList(msg: DashboardEventDlqListRequest): Promise<void>;
256
+ /**
257
+ * Handle a dashboard.event-dlq.count request.
258
+ *
259
+ * Returns the DLQ depth for the sidebar badge. Polled ~30s by the
260
+ * dashboard. No access_log row — the count surface is intentionally
261
+ * un-audited (it's a frequent badge poll, not a triage action).
262
+ */
263
+ handleEventDlqCount(msg: DashboardEventDlqCountRequest): Promise<void>;
264
+ /**
265
+ * Handle a dashboard.event-dlq.retry request.
266
+ *
267
+ * Clears the DLQ flag on the row and issues a `pg_notify` so a healthy
268
+ * node picks the event up immediately rather than waiting for the
269
+ * leader-only retry scanner's next tick. Notify failure is non-fatal —
270
+ * the scanner will catch up.
271
+ */
272
+ handleEventDlqRetry(msg: DashboardEventDlqRetryRequest): Promise<void>;
273
+ /**
274
+ * Handle a dashboard.event-dlq.discard request.
275
+ *
276
+ * Permanently deletes the DLQ row. No retry, no archive — used when the
277
+ * payload is corrupt or the routing target has been removed.
278
+ */
279
+ handleEventDlqDiscard(msg: DashboardEventDlqDiscardRequest): Promise<void>;
228
280
  /**
229
281
  * Handle a run.manual_schedule.request.
230
282
  * Delegates to the onManualSchedule callback which invokes handleManualSchedule.
@@ -0,0 +1,25 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add a Postgres trigger on `generic_webhook_sources` that emits
4
+ * `pg_notify('generic_sources_change', routing_key)` on every INSERT,
5
+ * UPDATE, and DELETE. Soft-deletes (UPDATE that sets `deleted_at`) ride
6
+ * the same UPDATE path — the listener treats a row with `deleted_at IS
7
+ * NOT NULL` (or a row that no longer exists for that routing_key) as a
8
+ * de-registration signal.
9
+ *
10
+ * The consumer is `GenericSourcesChangeListener`
11
+ * (`packages/orchestrator/src/webhook/generic-sources-listener.ts`),
12
+ * which runs in every orchestrator peer, LISTENs on the channel, and
13
+ * mutates the local `ProviderRegistry` in place
14
+ * (`registerProviderBundleForSource` for INSERT/UPDATE; `unregister` for
15
+ * DELETE / soft-delete). Without this round-trip, peers other than the
16
+ * one that handled `kici-admin source add generic` would 404 incoming
17
+ * webhooks until restart.
18
+ *
19
+ * Mirrors the existing GitHub-app `sources` trigger
20
+ * (`notify_sources_change()` + `sources_change_trigger`, defined in
21
+ * `001_initial.ts`).
22
+ */
23
+ export declare function up(db: Kysely<unknown>): Promise<void>;
24
+ export declare function down(db: Kysely<unknown>): Promise<void>;
25
+ //# sourceMappingURL=019_generic_sources_change_notify.d.ts.map
@@ -0,0 +1,17 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add `org_settings.dashboard_write_policy jsonb NOT NULL DEFAULT '{}'`.
4
+ *
5
+ * Stores the per-customer policy that decides which Platform-routed
6
+ * dashboard.* write operations the orchestrator accepts. An empty
7
+ * JSONB object means everything is enabled (permissive default). To
8
+ * disable an operation, set its key to `false`, e.g.
9
+ * `{"secrets.set": false, "variables.set": false}`. Unknown / missing
10
+ * keys are treated as `true` by the resolver in
11
+ * `@kici-dev/engine/protocol/dashboard-write-operations`.
12
+ *
13
+ * Idempotent: a re-run on a DB that already has the column is a no-op.
14
+ */
15
+ export declare function up(db: Kysely<unknown>): Promise<void>;
16
+ export declare function down(db: Kysely<unknown>): Promise<void>;
17
+ //# sourceMappingURL=020_org_settings_dashboard_write_policy.d.ts.map
@@ -0,0 +1,26 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add `check_run_tracking` table for HA-safe check-run state persistence.
4
+ *
5
+ * The `CommitStatusReporter` previously held all of its check-run bookkeeping
6
+ * in per-process `Map`s. Under HA (Raft leader switch / coord crash) those
7
+ * maps reset to empty on the replacement coord, leaving GitHub check runs
8
+ * stuck in `queued` forever.
9
+ *
10
+ * Single composite-keyed table per (provider, owner, repo, sha, check_name):
11
+ *
12
+ * - `check_run_id` replaces in-memory `checkRunIds`.
13
+ * - `build_creation_state` replaces `pendingBuildCreations` Promise map.
14
+ * - `step_progress_json` replaces `stepProgress` array map.
15
+ * - `in_progress_sent_at` replaces `inProgressSent` boolean map.
16
+ * - `run_id` indexed; replaces `runIdToKeys` reverse map.
17
+ *
18
+ * Progress-timer debounce state (the `progressTimers` Map) is NOT persisted —
19
+ * timers are recreated on demand when an update arrives and the row's
20
+ * `updated_at` is older than the debounce window.
21
+ *
22
+ * Idempotent: a re-run on a DB that already has the table is a no-op.
23
+ */
24
+ export declare function up(db: Kysely<unknown>): Promise<void>;
25
+ export declare function down(db: Kysely<unknown>): Promise<void>;
26
+ //# sourceMappingURL=021_check_run_tracking.d.ts.map
@@ -0,0 +1,29 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add three tables persisting `ScalerManager` per-coord state:
4
+ *
5
+ * - `scaler_spawning_agents`: agents spawned via Docker/Podman/Firecracker
6
+ * that have not yet registered via WS. Carries the `bound_job_id` so a
7
+ * replacement coord still issues the eager-dispatch hop on register.
8
+ *
9
+ * - `scaler_agent_jobs`: agentId → (runId, jobId) correlation for
10
+ * scaler-lifecycle event routing. Inserted on `correlateAgentToJob`,
11
+ * deleted on disconnect / job completion.
12
+ *
13
+ * - `scaler_reservations`: outstanding resource reservations keyed by
14
+ * agentId. Per-scaler / global usage counters are derived state —
15
+ * recomputed on coord boot as `SUM(...) GROUP BY scaler_name` so the
16
+ * cap-check critical section stays correct.
17
+ *
18
+ * Without these tables, a coord crash mid-spawn orphans the agent (eager
19
+ * dispatch lost), strands the reservation (resource leak until backend
20
+ * GC eventually disconnects the WS, minutes later), and loses every
21
+ * scaler-lifecycle event emitted before correlation (execution-tracker
22
+ * sees a hole in the run timeline).
23
+ *
24
+ * Idempotent: a re-run on a DB that already has any of these tables
25
+ * leaves the existing one alone.
26
+ */
27
+ export declare function up(db: Kysely<unknown>): Promise<void>;
28
+ export declare function down(db: Kysely<unknown>): Promise<void>;
29
+ //# sourceMappingURL=022_scaler_manager_state.d.ts.map
@@ -0,0 +1,26 @@
1
+ import { type Kysely } from 'kysely';
2
+ /**
3
+ * Add `dispatch_queue.recovery_deadline TIMESTAMPTZ` and
4
+ * `dispatch_queue.recovery_agent_id TEXT` columns persisting per-job
5
+ * recovery state for HA-safe agent-disconnect handling.
6
+ *
7
+ * `Dispatcher.recoveringJobs` previously held the timer handle, agent
8
+ * ID, and deadline entirely in process memory. A coord crash between
9
+ * starting the timer and it firing dropped the timer; the replacement
10
+ * coord saw the row as `status='recovering'` but had no record of when
11
+ * the deadline expired or which agent owned the job. Jobs lingered
12
+ * forever.
13
+ *
14
+ * The new columns make the deadline durable. A leader-gated sweep
15
+ * (`Dispatcher.sweepExpiredRecoveries`) scans
16
+ * `WHERE status='recovering' AND recovery_deadline < now()` and marks
17
+ * each row failed; on coord boot, `Dispatcher.recoverState()`
18
+ * hydrates the in-memory Map by re-creating timers from the persisted
19
+ * deadlines.
20
+ *
21
+ * Idempotent: re-running on a DB that already has either column is a
22
+ * no-op.
23
+ */
24
+ export declare function up(db: Kysely<unknown>): Promise<void>;
25
+ export declare function down(db: Kysely<unknown>): Promise<void>;
26
+ //# sourceMappingURL=023_dispatch_queue_recovery_deadline.d.ts.map
@@ -41,6 +41,10 @@ export interface Database {
41
41
  access_log: AccessLogTable;
42
42
  cold_store_chunk_counts: ColdStoreChunkCountsTable;
43
43
  cold_store_chunks: ColdStoreChunksTable;
44
+ check_run_tracking: CheckRunTrackingTable;
45
+ scaler_spawning_agents: ScalerSpawningAgentsTable;
46
+ scaler_agent_jobs: ScalerAgentJobsTable;
47
+ scaler_reservations: ScalerReservationsTable;
44
48
  }
45
49
  /**
46
50
  * Cluster metadata table
@@ -132,6 +136,20 @@ export interface DispatchQueueTable {
132
136
  /** Routing key (e.g. "github:12345") so dispatch can pick the right
133
137
  * per-app provider bundle in multi-app setups. Required (NOT NULL). */
134
138
  routing_key: string;
139
+ /**
140
+ * For jobs in `status='recovering'`, the moment the recovery grace
141
+ * period elapses. Populated when an agent disconnects with this job
142
+ * in-flight; cleared when the agent reconnects + claims the job, OR
143
+ * when the leader-gated sweep transitions the row to `failed`.
144
+ * NULL for all other statuses.
145
+ */
146
+ recovery_deadline: ColumnType<Date | null, Date | null | undefined, Date | null>;
147
+ /**
148
+ * Companion to `recovery_deadline`: the agent id that owned the job
149
+ * before disconnect. Used to validate that a reconnecting agent is
150
+ * the rightful claimant. NULL for non-recovering rows.
151
+ */
152
+ recovery_agent_id: ColumnType<string | null, string | null | undefined, string | null>;
135
153
  }
136
154
  /**
137
155
  * Deduplication cache table
@@ -1108,6 +1126,15 @@ export interface OrgSettingsTable {
1108
1126
  * URLs are accepted; arbitrary `http://` registries are rejected at dispatch.
1109
1127
  */
1110
1128
  allow_http_npm_registries: ColumnType<boolean, boolean | undefined, boolean>;
1129
+ /**
1130
+ * Per-operation policy controlling which dashboard.* writes the orch
1131
+ * accepts when routed through Platform. JSONB shape:
1132
+ * `{ [operation]: boolean }` where operation matches the engine enum
1133
+ * `DashboardWriteOperation`. Empty object = all enabled (permissive).
1134
+ * Resolver in `@kici-dev/engine/protocol/dashboard-write-operations`
1135
+ * treats missing keys as `true`.
1136
+ */
1137
+ dashboard_write_policy: ColumnType<Record<string, boolean>, Record<string, boolean> | string | undefined, Record<string, boolean> | string>;
1111
1138
  /** When this setting was created */
1112
1139
  created_at: Generated<Date>;
1113
1140
  /** When this setting was last updated */
@@ -1263,4 +1290,114 @@ export interface ColdStoreChunksTable {
1263
1290
  export type ColdStoreChunksRow = Selectable<ColdStoreChunksTable>;
1264
1291
  export type NewColdStoreChunksRow = Insertable<ColdStoreChunksTable>;
1265
1292
  export type ColdStoreChunksUpdate = Updateable<ColdStoreChunksTable>;
1293
+ /**
1294
+ * Check-run tracking table (check_run_tracking).
1295
+ *
1296
+ * HA-safe persistence for the per-coord state previously held in
1297
+ * `CommitStatusReporter`'s six in-memory `Map`s. Replacement coord on a
1298
+ * Raft leader switch reads this table to recover check-run IDs, build
1299
+ * creation state, step-progress entries, in-progress-sent timestamps, and
1300
+ * the run-id reverse index used for cleanup. A coord crash mid-check-run
1301
+ * no longer leaves a GitHub check stuck in `queued` forever.
1302
+ */
1303
+ export interface CheckRunTrackingTable {
1304
+ /** Provider type (e.g. 'github'). */
1305
+ provider: string;
1306
+ /** Repo owner / namespace. */
1307
+ owner: string;
1308
+ /** Repo name. */
1309
+ repo: string;
1310
+ /** Git commit SHA the check run is anchored to. */
1311
+ sha: string;
1312
+ /** Check-run name (e.g. 'kici/build', 'kici/build/job/test', 'kici/build/setup'). */
1313
+ check_name: string;
1314
+ /**
1315
+ * GitHub Checks API check-run ID. Populated by `checks.create()`; nullable
1316
+ * during the in-flight build-creation window (`build_creation_state =
1317
+ * 'pending'` before the create finishes).
1318
+ */
1319
+ check_run_id: ColumnType<number | null, number | null | undefined, number | null>;
1320
+ /**
1321
+ * Build check-run creation state: 'pending' while a `setBuildPending`
1322
+ * create is in flight, 'completed' once `setBuildComplete` has reconciled.
1323
+ * Replaces the in-memory `pendingBuildCreations` Promise map.
1324
+ */
1325
+ build_creation_state: ColumnType<string | null, string | null | undefined, string | null>;
1326
+ /**
1327
+ * Step-progress entries as JSONB array. Each entry shape is
1328
+ * `{ name: string, status: string, durationMs?: number }`. Replaces the
1329
+ * in-memory `stepProgress` map.
1330
+ */
1331
+ step_progress_json: ColumnType<unknown, string | unknown, string | unknown>;
1332
+ /**
1333
+ * Timestamp the first in-progress transition was sent to GitHub. NULL
1334
+ * before the first running step. Replaces the in-memory `inProgressSent`
1335
+ * boolean map (presence-as-truth).
1336
+ */
1337
+ in_progress_sent_at: ColumnType<Date | null, Date | null | undefined, Date | null>;
1338
+ /**
1339
+ * KiCI run identifier this check-run belongs to. Indexed (partial, NOT
1340
+ * NULL) to power `cleanupRun(runId)` without scanning the table.
1341
+ */
1342
+ run_id: ColumnType<string | null, string | null | undefined, string | null>;
1343
+ /** When this row was first inserted. */
1344
+ created_at: Generated<Date>;
1345
+ /** When this row was last updated. */
1346
+ updated_at: Generated<Date>;
1347
+ }
1348
+ export type CheckRunTrackingRow = Selectable<CheckRunTrackingTable>;
1349
+ export type NewCheckRunTrackingRow = Insertable<CheckRunTrackingTable>;
1350
+ export type CheckRunTrackingUpdate = Updateable<CheckRunTrackingTable>;
1351
+ /**
1352
+ * Scaler spawning-agents table (scaler_spawning_agents).
1353
+ *
1354
+ * One row per agent that has been spawned via a scaler backend
1355
+ * (container / bare-metal / firecracker) but has not yet registered via
1356
+ * WS. Persists `bound_job_id` so a replacement coord still issues the
1357
+ * eager-dispatch hop when the agent eventually registers. GC'd by a
1358
+ * leader-gated sweep that drops rows older than the spawn-timeout.
1359
+ */
1360
+ export interface ScalerSpawningAgentsTable {
1361
+ agent_id: string;
1362
+ scaler_name: string;
1363
+ label_set: ColumnType<string[], string | string[], string | string[]>;
1364
+ run_id: ColumnType<string | null, string | null | undefined, string | null>;
1365
+ job_id: ColumnType<string | null, string | null | undefined, string | null>;
1366
+ bound_job_id: ColumnType<string | null, string | null | undefined, string | null>;
1367
+ spawned_at: Generated<Date>;
1368
+ }
1369
+ export type ScalerSpawningAgentRow = Selectable<ScalerSpawningAgentsTable>;
1370
+ export type NewScalerSpawningAgentRow = Insertable<ScalerSpawningAgentsTable>;
1371
+ /**
1372
+ * Scaler agent-jobs table (scaler_agent_jobs).
1373
+ *
1374
+ * agentId → (runId, jobId) correlation used to route scaler-lifecycle
1375
+ * events (spawn / boot / ready / kill) to the execution tracker. Row
1376
+ * inserted in `correlateAgentToJob`, deleted on agent disconnect / job
1377
+ * completion.
1378
+ */
1379
+ export interface ScalerAgentJobsTable {
1380
+ agent_id: string;
1381
+ run_id: string;
1382
+ job_id: string;
1383
+ correlated_at: Generated<Date>;
1384
+ }
1385
+ export type ScalerAgentJobRow = Selectable<ScalerAgentJobsTable>;
1386
+ export type NewScalerAgentJobRow = Insertable<ScalerAgentJobsTable>;
1387
+ /**
1388
+ * Scaler reservations table (scaler_reservations).
1389
+ *
1390
+ * One row per outstanding resource reservation. `perScalerUsage` /
1391
+ * `globalUsage` are derived state — recomputed from `SUM(...)` on coord
1392
+ * boot so the cap-check critical section is correct under HA.
1393
+ */
1394
+ export interface ScalerReservationsTable {
1395
+ agent_id: string;
1396
+ scaler_name: string;
1397
+ cpu_units: number;
1398
+ mem_bytes: ColumnType<string, string | number, string | number>;
1399
+ reserved_at: Generated<Date>;
1400
+ }
1401
+ export type ScalerReservationRow = Selectable<ScalerReservationsTable>;
1402
+ export type NewScalerReservationRow = Insertable<ScalerReservationsTable>;
1266
1403
  //# sourceMappingURL=types.d.ts.map
@@ -120,13 +120,20 @@ export declare class EventStore {
120
120
  /**
121
121
  * List events currently in the DLQ. Most recent first. Used by the
122
122
  * dashboard admin DLQ page.
123
+ *
124
+ * @param sourceRoutingKey - When provided, restrict the result to
125
+ * events whose `source_routing_key` matches. Used by routing-key
126
+ * -scoped admin tokens so the operator only sees their slice.
123
127
  */
124
- listDlq(limit: number, beforeDlqAt?: Date): Promise<StoredEvent[]>;
128
+ listDlq(limit: number, beforeDlqAt?: Date, sourceRoutingKey?: string): Promise<StoredEvent[]>;
125
129
  /**
126
130
  * Count events currently in the DLQ. Used by the dashboard summary row
127
131
  * and the `kici_orch_event_dlq_depth` gauge.
132
+ *
133
+ * @param sourceRoutingKey - When provided, restrict the count to
134
+ * events whose `source_routing_key` matches.
128
135
  */
129
- countDlq(): Promise<number>;
136
+ countDlq(sourceRoutingKey?: string): Promise<number>;
130
137
  /**
131
138
  * Reset a DLQ event so it gets retried on the next scanner tick. Used by
132
139
  * the dashboard "Retry" action.
@@ -41,6 +41,13 @@ export declare class TrustStore {
41
41
  repo: string;
42
42
  routingKey: string;
43
43
  }, allowedEvents?: string[]): Promise<string>;
44
+ /**
45
+ * Look up a single trust entry by ID. Returns null if the row is
46
+ * missing. Callers (the admin HTTP route, in particular) use this
47
+ * to read a row's source/target routing keys before applying a
48
+ * scope check.
49
+ */
50
+ getById(id: string): Promise<TrustEntry | null>;
44
51
  /**
45
52
  * Remove a trust relationship by ID.
46
53
  */
@@ -23,6 +23,20 @@ export interface EventRouterConfig {
23
23
  retryMaxBackoffMs: number;
24
24
  /** Interval at which the leader-only retry scanner ticks (default: 10_000) */
25
25
  retryScanIntervalMs: number;
26
+ /**
27
+ * **Test-only.** Per-event-name fault injection: when `attempts <= N`,
28
+ * the EventRouter throws a synthetic dispatch error to drive the retry /
29
+ * DLQ path. Used by the fault-injection E2E to prove the lease + retry
30
+ * loop dispatches a real run when the inner dispatch eventually
31
+ * succeeds, and lands the row in the DLQ when N exceeds
32
+ * `maxDispatchAttempts`.
33
+ *
34
+ * Only honoured when `KICI_TEST_MODE=1` is set at config-load time —
35
+ * production deployments never see this knob even if the env var is
36
+ * planted by accident. Source the value from
37
+ * `KICI_TEST_EVENT_FAIL_FIRST_N` (a JSON object literal).
38
+ */
39
+ debugFailFirstNAttemptsByEvent?: Record<string, number>;
26
40
  }
27
41
  /**
28
42
  * Reason an event landed in the DLQ.
@@ -54,6 +68,25 @@ export interface StoredEvent {
54
68
  dlqAt: Date | null;
55
69
  dlqReason: DlqReason | null;
56
70
  }
71
+ /**
72
+ * Parse the `KICI_TEST_EVENT_FAIL_FIRST_N` JSON payload, gated by
73
+ * `KICI_TEST_MODE`. Returns the parsed map or `undefined` when:
74
+ *
75
+ * - `testMode` is false (the master switch). The JSON value is ignored
76
+ * entirely; production deployments never see fault-injection even if
77
+ * the per-event env var is accidentally planted.
78
+ * - the JSON value is absent / empty.
79
+ * - the JSON value is malformed or carries a wrong-shape entry. We
80
+ * refuse to fall back to a partial map: a typo on one line silently
81
+ * skipping that test would be confusing in CI logs.
82
+ *
83
+ * The accepted shape is `{ "<eventName>": <number> }`; non-string keys
84
+ * and non-number values are rejected. The numeric value is the inclusive
85
+ * upper bound of attempts to fail (so `1` fails the first attempt and
86
+ * succeeds on retry; `99` exceeds `maxDispatchAttempts` and lands the
87
+ * row in the DLQ).
88
+ */
89
+ export declare function parseFaultInjectionMap(testMode: boolean, raw: string | undefined): Record<string, number> | undefined;
57
90
  /**
58
91
  * Default event router configuration.
59
92
  */