@kici-dev/orchestrator 0.1.22 → 0.1.24

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 (62) hide show
  1. package/dist/agent/host-roster.d.ts +39 -3
  2. package/dist/agent/token-store.d.ts +33 -0
  3. package/dist/app.d.ts +3 -0
  4. package/dist/cache/pending-inits.d.ts +3 -2
  5. package/dist/cli/api-client.d.ts +8 -0
  6. package/dist/cli/commands/attestations-reverify.d.ts +18 -0
  7. package/dist/cli/commands/attestations.d.ts +3 -0
  8. package/dist/cli/commands/shared/versioned-upgrade.d.ts +52 -0
  9. package/dist/cli.js +2284 -442
  10. package/dist/config.d.ts +2 -0
  11. package/dist/dashboard/attestation-filters.d.ts +70 -0
  12. package/dist/dashboard/handler.d.ts +33 -1
  13. package/dist/db/migrations/051_binding_host_pattern.d.ts +18 -0
  14. package/dist/db/migrations/052_host_reach_metadata.d.ts +19 -0
  15. package/dist/db/migrations/053_agent_token_single_use.d.ts +17 -0
  16. package/dist/db/migrations/054_local_working_tree.d.ts +15 -0
  17. package/dist/db/migrations/055_agent_token_mandatory_labels.d.ts +18 -0
  18. package/dist/db/migrations/056_execution_jobs_environments.d.ts +17 -0
  19. package/dist/db/migrations/057_step_concurrency.d.ts +4 -0
  20. package/dist/db/migrations/058_access_log_agent_label.d.ts +4 -0
  21. package/dist/db/migrations/059_attestation_verdict.d.ts +4 -0
  22. package/dist/db/migrations/060_run_trigger_actor.d.ts +4 -0
  23. package/dist/db/types.d.ts +78 -0
  24. package/dist/environments/binding-store.d.ts +12 -3
  25. package/dist/environments/held-runs.d.ts +24 -0
  26. package/dist/environments/protection/aggregate.d.ts +43 -0
  27. package/dist/environments/protection/satisfiability.d.ts +64 -0
  28. package/dist/index.js +9 -0
  29. package/dist/metrics/prometheus.d.ts +2 -0
  30. package/dist/orchestrator-core.d.ts +8 -0
  31. package/dist/pipeline/dispatch-matched-workflow.d.ts +60 -3
  32. package/dist/pipeline/flatten-lock-steps.d.ts +11 -0
  33. package/dist/pipeline/inline-eval.d.ts +2 -1
  34. package/dist/pipeline/job-environments.d.ts +71 -0
  35. package/dist/pipeline/manual-schedule.d.ts +22 -0
  36. package/dist/pipeline/test-pipeline.d.ts +15 -0
  37. package/dist/provenance/trust-root.d.ts +24 -0
  38. package/dist/provenance/verify-at-ingest.d.ts +24 -0
  39. package/dist/reporting/agent-failure-category.d.ts +27 -0
  40. package/dist/reporting/agent-run-result-mapper.d.ts +13 -0
  41. package/dist/reporting/execution-tracker.d.ts +22 -2
  42. package/dist/reporting/log-writer.d.ts +19 -0
  43. package/dist/reporting/run-aggregator.d.ts +161 -0
  44. package/dist/reporting/step-log-reader.d.ts +39 -0
  45. package/dist/routes/admin-registrations.d.ts +8 -0
  46. package/dist/routes/admin-runs.d.ts +7 -0
  47. package/dist/scaler/manager.d.ts +17 -0
  48. package/dist/secrets/index.d.ts +1 -1
  49. package/dist/secrets/secret-resolver.d.ts +19 -6
  50. package/dist/server.js +21535 -19131
  51. package/dist/standalone.js +26544 -24580
  52. package/dist/storage/loopback-guard.d.ts +49 -0
  53. package/dist/ws/agent-handler.d.ts +15 -1
  54. package/dist/ws/bringup-api.d.ts +76 -0
  55. package/dist/ws/dashboard-fleet-handler.d.ts +11 -1
  56. package/dist/ws/fleet-runs-on-all.d.ts +3 -0
  57. package/dist/ws/platform-client.d.ts +39 -1
  58. package/dist/ws/test-relay-handlers.d.ts +9 -0
  59. package/installer-image-digests.json +3 -3
  60. package/package.json +6 -8
  61. package/sbom.spdx.json +57 -57
  62. package/dist/secrets/crypto.d.ts +0 -49
package/dist/cli.js CHANGED
@@ -2,27 +2,32 @@
2
2
  import { createRequire } from "node:module";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import * as path$1 from "node:path";
5
- import path, { join, resolve } from "node:path";
5
+ import path, { dirname, join, resolve } from "node:path";
6
6
  import * as fs$1 from "node:fs";
7
7
  import fs, { chmodSync, closeSync, createReadStream, createWriteStream, existsSync, mkdtempSync, openSync, promises, readFileSync, realpathSync, statSync, unwatchFile, watchFile, writeFileSync } from "node:fs";
8
8
  import { Command } from "commander";
9
- import { BaseColdStore, ChunkLru, addLogsToArchive, chunkObjectKey, clearDispatchQueueDirect, computeChunkId, computeMigrationsHash, createDb, createDbRole, createEnvironmentTemplateDirect, createLogger, createPool, createReadOnlyDbUser, decodeChunk, deleteEnvironmentDirect, dropAndCreateDatabase, emitKiciEventDirect, encodeKeySegment, ensureDatabase, formatBytes, formatUptime, isSchemaCurrent, listEnvironmentsDirect, listExecutionRunsDirect, listQueueDirect, listRegistrationsDirect, maskDatabaseUrl, parseDatabaseUrl, parseManifest, prunePeerCredentialsDirect, purgeScopedSecretsDirect, purgeSecretBackendsDirect, purgeStaleExecutionDirect, purgeStaleSourcesDirect, registerWorkflowManualDirect, resetRaftStateDirect, seedEnvironmentBindingDirect, seedEnvironmentDirect, setEnvironmentPolicyDirect, setEnvironmentSecretDirect, sha256, showEnvironmentDirect, showExecutionRunDirect, showQueueEntryDirect, showRegistrationDirect, storeMigrationContentHash, tablePrefix, toErrorMessage } from "@kici-dev/shared";
10
- import crypto, { createDecipheriv, createHash, hkdfSync, randomBytes, randomUUID } from "node:crypto";
9
+ import { BaseColdStore, ChunkLru, addLogsToArchive, chunkObjectKey, clearDispatchQueueDirect, computeChunkId, computeMigrationsHash, createDb, createDbRole, createEnvironmentTemplateDirect, createLogger, createPool, createReadOnlyDbUser, createS3Client, decodeChunk, deleteEnvironmentDirect, dropAndCreateDatabase, emitKiciEventDirect, encodeKeySegment, ensureDatabase, formatBytes, formatUptime, isSchemaCurrent, listEnvironmentsDirect, listExecutionRunsDirect, listQueueDirect, listRegistrationsDirect, maskDatabaseUrl, parseDatabaseUrl, parseManifest, prunePeerCredentialsDirect, purgeScopedSecretsDirect, purgeSecretBackendsDirect, purgeStaleExecutionDirect, purgeStaleSourcesDirect, registerWorkflowManualDirect, resetRaftStateDirect, seedEnvironmentBindingDirect, seedEnvironmentDirect, setEnvironmentPolicyDirect, setEnvironmentSecretDirect, sha256, showEnvironmentDirect, showExecutionRunDirect, showQueueEntryDirect, showRegistrationDirect, storeMigrationContentHash, tablePrefix, toErrorMessage } from "@kici-dev/shared";
10
+ import crypto, { createDecipheriv, createHash, createHmac, hkdfSync, randomBytes, randomUUID } from "node:crypto";
11
11
  import { createInterface } from "node:readline";
12
+ import { KICI_AGENT_ENV_PREFIX, KNOWN_ROLES, PRIVILEGED_ROOT_LABEL, ScalerBackendType, ScalerEventType, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES, WS_MAX_PAYLOAD_BYTES, accessLogWarmSqlCase, attestationVerifyStatusSchema, getAccessLogColdDays, getSecretAuditLogColdDays, matcherSatisfiedBy, minAccessLogWarmDays, minSecretAuditLogWarmDays, parseHostPropertyAssignments, scalerAgentLabels, secretAuditLogWarmSqlCase } from "@kici-dev/engine";
12
13
  import { access, chmod, constants, copyFile, link, mkdir, open, readFile, readdir, rm, writeFile } from "node:fs/promises";
13
14
  import { parse, stringify } from "yaml";
14
15
  import { z } from "zod";
15
- import os, { platform, tmpdir } from "node:os";
16
+ import os, { hostname, platform, tmpdir } from "node:os";
16
17
  import { getDatabaseCollationDrift, refreshDatabaseCollationVersion, reindexDatabaseConcurrently } from "@kici-dev/shared/db-collation";
17
18
  import { sql } from "kysely";
18
19
  import { Migrator } from "kysely/migration";
19
20
  import "jsonpath-plus";
20
21
  import { exec, execFile, execFileSync, execSync, spawn } from "node:child_process";
21
22
  import { promisify } from "node:util";
22
- import { KICI_AGENT_ENV_PREFIX, KNOWN_ROLES, ScalerBackendType, ScalerEventType, TERMINAL_JOB_STATES, TERMINAL_RUN_STATES, WS_MAX_PAYLOAD_BYTES, accessLogWarmSqlCase, getAccessLogColdDays, getSecretAuditLogColdDays, matcherSatisfiedBy, minAccessLogWarmDays, minSecretAuditLogWarmDays, parseHostPropertyAssignments, scalerAgentLabels, secretAuditLogWarmSqlCase } from "@kici-dev/engine";
23
23
  import { Octokit } from "@octokit/rest";
24
24
  import { createAppAuth } from "@octokit/auth-app";
25
25
  import http, { createServer } from "node:http";
26
+ import { LOGGER_ENV_VARS, defineEnv, validateUnknownKiciVars } from "@kici-dev/shared/env";
27
+ import { CopyObjectCommand, DeleteObjectCommand, GetObjectCommand, HeadObjectCommand, ListObjectsV2Command, PutObjectCommand } from "@aws-sdk/client-s3";
28
+ import { Upload } from "@aws-sdk/lib-storage";
29
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
30
+ import { verifyKiciBundle } from "@kici-dev/engine/provenance/verify";
26
31
  import { ZipArchive } from "archiver";
27
32
  import { confirm, input, password, select } from "@inquirer/prompts";
28
33
  import JSZip from "jszip";
@@ -370,6 +375,15 @@ var AdminApiClient = class {
370
375
  return this.request("GET", `/api/v1/admin/runs/${encodeURIComponent(runId)}/jobs${qs}`);
371
376
  }
372
377
  /**
378
+ * Fetch the machine-first, provenance-tagged structured run result: typed
379
+ * job DAG, per-step exit codes / durations / statuses, derived failure
380
+ * category. Untrusted fields are envelope-tagged; secret values are never
381
+ * returned (only secret-output key names).
382
+ */
383
+ async getRunStructured(runId) {
384
+ return this.request("GET", `/api/v1/admin/runs/${encodeURIComponent(runId)}/structured`);
385
+ }
386
+ /**
373
387
  * Fetch the scrub status of the run's ephemeral key. Never returns the
374
388
  * key material itself — only `{ exists, createdAt }`.
375
389
  */
@@ -797,13 +811,22 @@ function formatAgentTokenTable(tokens) {
797
811
  }
798
812
  function registerAgentCommands(program, getClient) {
799
813
  const agent = program.command("agent").description("Manage agent authentication tokens");
800
- agent.command("register").description("Create a static agent token").option("--labels <labels>", "Comma-separated agent labels (e.g. linux,x64)").action(async (opts) => {
814
+ agent.command("register").description("Create a static agent token").option("--labels <labels>", "Comma-separated agent labels (e.g. linux,x64)").option("--mandatory-label <label>", "Taint label the agent only accepts jobs demanding (repeatable). Also authorized as an advertised label.", (val, acc) => [...acc, val.trim()], []).option("--privileged-root", `Shorthand for --mandatory-label ${PRIVILEGED_ROOT_LABEL}: mint a confined root agent token (the agent must run as uid 0).`).action(async (opts) => {
801
815
  try {
802
- const labels = opts.labels ? opts.labels.split(",").map((l) => l.trim()) : void 0;
803
- const result = await getClient().createAgentToken({ labels });
816
+ const mandatorySet = new Set(opts.mandatoryLabel);
817
+ if (opts.privilegedRoot) mandatorySet.add(PRIVILEGED_ROOT_LABEL);
818
+ const mandatoryLabels = mandatorySet.size > 0 ? [...mandatorySet] : void 0;
819
+ const baseLabels = opts.labels ? opts.labels.split(",").map((l) => l.trim()) : [];
820
+ const labelSet = new Set([...baseLabels, ...mandatoryLabels ?? []]);
821
+ const labels = labelSet.size > 0 ? [...labelSet] : void 0;
822
+ const result = await getClient().createAgentToken({
823
+ labels,
824
+ mandatoryLabels
825
+ });
804
826
  console.log(`Agent token created successfully.`);
805
827
  console.log(`Token ID: ${result.id}`);
806
828
  console.log(`Token: ${result.token}`);
829
+ if (mandatoryLabels) console.log(`Taint: ${mandatoryLabels.join(", ")} (agent only accepts jobs demanding these)`);
807
830
  console.log("");
808
831
  console.log("WARNING: Save this token now -- it cannot be recovered after this point.");
809
832
  console.log("Set KICI_AGENT_TOKEN on the agent to use this token.");
@@ -1556,8 +1579,8 @@ var init_client = __esmMin((() => {
1556
1579
  //#region src/db/migrations/001_initial.ts
1557
1580
  init_client();
1558
1581
  var _001_initial_exports = /* @__PURE__ */ __exportAll({
1559
- down: () => down$49,
1560
- up: () => up$49
1582
+ down: () => down$59,
1583
+ up: () => up$59
1561
1584
  });
1562
1585
  /**
1563
1586
  * Squashed initial migration -- creates the complete Orchestrator database schema.
@@ -2249,7 +2272,7 @@ const DDL_STATEMENTS = [
2249
2272
  `ALTER TABLE ONLY public.held_runs
2250
2273
  ADD CONSTRAINT held_runs_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES public.environments(id);`
2251
2274
  ];
2252
- async function up$49(db) {
2275
+ async function up$59(db) {
2253
2276
  for (const stmt of DDL_STATEMENTS) await sql.raw(stmt).execute(db);
2254
2277
  await sql`
2255
2278
  INSERT INTO cluster_meta (key, value)
@@ -2271,7 +2294,7 @@ async function up$49(db) {
2271
2294
  * Rollback drops everything created above. Uses CASCADE on table drops to cut
2272
2295
  * through the FK graph without relying on exact topological order.
2273
2296
  */
2274
- async function down$49(db) {
2297
+ async function down$59(db) {
2275
2298
  for (const [trig, tbl] of [["source_secrets_change_trigger", "scoped_secrets"], ["sources_change_trigger", "sources"]]) await sql.raw(`DROP TRIGGER IF EXISTS ${trig} ON public.${tbl}`).execute(db);
2276
2299
  for (const table of [
2277
2300
  "workflow_registrations",
@@ -2318,8 +2341,8 @@ async function down$49(db) {
2318
2341
  //#endregion
2319
2342
  //#region src/db/migrations/002_config_versions_key_version.ts
2320
2343
  var _002_config_versions_key_version_exports = /* @__PURE__ */ __exportAll({
2321
- down: () => down$48,
2322
- up: () => up$48
2344
+ down: () => down$58,
2345
+ up: () => up$58
2323
2346
  });
2324
2347
  /**
2325
2348
  * Add key_version column to config_versions so that sensitive-field encryption
@@ -2333,13 +2356,13 @@ var _002_config_versions_key_version_exports = /* @__PURE__ */ __exportAll({
2333
2356
  * No index is needed: rotation does a full-table scan; reads are by
2334
2357
  * `version` primary key and never filter on `key_version`.
2335
2358
  */
2336
- async function up$48(db) {
2359
+ async function up$58(db) {
2337
2360
  await sql`
2338
2361
  ALTER TABLE public.config_versions
2339
2362
  ADD COLUMN key_version integer NOT NULL DEFAULT 1
2340
2363
  `.execute(db);
2341
2364
  }
2342
- async function down$48(db) {
2365
+ async function down$58(db) {
2343
2366
  await sql`
2344
2367
  ALTER TABLE public.config_versions
2345
2368
  DROP COLUMN key_version
@@ -2348,8 +2371,8 @@ async function down$48(db) {
2348
2371
  //#endregion
2349
2372
  //#region src/db/migrations/003_access_log.ts
2350
2373
  var _003_access_log_exports = /* @__PURE__ */ __exportAll({
2351
- down: () => down$47,
2352
- up: () => up$47
2374
+ down: () => down$57,
2375
+ up: () => up$57
2353
2376
  });
2354
2377
  /**
2355
2378
  * Access log: one row per read or orchestrator-admin mutation attributable
@@ -2373,7 +2396,7 @@ var _003_access_log_exports = /* @__PURE__ */ __exportAll({
2373
2396
  * Retention is TTL-based via expires_at; packages/orchestrator/src/queue/
2374
2397
  * cleanup.ts picks up the prune pass.
2375
2398
  */
2376
- async function up$47(db) {
2399
+ async function up$57(db) {
2377
2400
  await sql`
2378
2401
  CREATE TABLE public.access_log (
2379
2402
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -2411,28 +2434,28 @@ async function up$47(db) {
2411
2434
  ON public.access_log (actor_type, actor_id, created_at DESC)
2412
2435
  `.execute(db);
2413
2436
  }
2414
- async function down$47(db) {
2437
+ async function down$57(db) {
2415
2438
  await sql`DROP TABLE IF EXISTS public.access_log`.execute(db);
2416
2439
  }
2417
2440
  //#endregion
2418
2441
  //#region src/db/migrations/004_rename_bundle_to_source.ts
2419
2442
  var _004_rename_bundle_to_source_exports = /* @__PURE__ */ __exportAll({
2420
- down: () => down$46,
2421
- up: () => up$46
2443
+ down: () => down$56,
2444
+ up: () => up$56
2422
2445
  });
2423
- async function up$46(db) {
2446
+ async function up$56(db) {
2424
2447
  await db.schema.alterTable("dispatch_queue").renameColumn("bundle_url", "source_tar_url").execute();
2425
2448
  await db.schema.alterTable("dispatch_queue").renameColumn("bundle_hash", "source_tar_hash").execute();
2426
2449
  }
2427
- async function down$46(db) {
2450
+ async function down$56(db) {
2428
2451
  await db.schema.alterTable("dispatch_queue").renameColumn("source_tar_url", "bundle_url").execute();
2429
2452
  await db.schema.alterTable("dispatch_queue").renameColumn("source_tar_hash", "bundle_hash").execute();
2430
2453
  }
2431
2454
  //#endregion
2432
2455
  //#region src/db/migrations/005_cold_store_chunk_counter.ts
2433
2456
  var _005_cold_store_chunk_counter_exports = /* @__PURE__ */ __exportAll({
2434
- down: () => down$45,
2435
- up: () => up$45
2457
+ down: () => down$55,
2458
+ up: () => up$55
2436
2459
  });
2437
2460
  /**
2438
2461
  * Cold-store chunk counter table.
@@ -2449,7 +2472,7 @@ var _005_cold_store_chunk_counter_exports = /* @__PURE__ */ __exportAll({
2449
2472
  *
2450
2473
  * sections 5 and 8.
2451
2474
  */
2452
- async function up$45(db) {
2475
+ async function up$55(db) {
2453
2476
  await sql`
2454
2477
  CREATE TABLE public.cold_store_chunk_counts (
2455
2478
  db TEXT NOT NULL,
@@ -2467,14 +2490,14 @@ async function up$45(db) {
2467
2490
  ON public.cold_store_chunk_counts (db, table_name)
2468
2491
  `.execute(db);
2469
2492
  }
2470
- async function down$45(db) {
2493
+ async function down$55(db) {
2471
2494
  await sql`DROP TABLE IF EXISTS public.cold_store_chunk_counts`.execute(db);
2472
2495
  }
2473
2496
  //#endregion
2474
2497
  //#region src/db/migrations/006_runs_jobs_steps_archived_at.ts
2475
2498
  var _006_runs_jobs_steps_archived_at_exports = /* @__PURE__ */ __exportAll({
2476
- down: () => down$44,
2477
- up: () => up$44
2499
+ down: () => down$54,
2500
+ up: () => up$54
2478
2501
  });
2479
2502
  /**
2480
2503
  * `execution_runs` / `execution_jobs` / `execution_steps` cold-store
@@ -2510,7 +2533,7 @@ var _006_runs_jobs_steps_archived_at_exports = /* @__PURE__ */ __exportAll({
2510
2533
  * - `idx_execution_jobs_routing_key_created (routing_key, created_at)`
2511
2534
  * - `idx_execution_steps_routing_key_created (routing_key, created_at)`
2512
2535
  */
2513
- async function up$44(db) {
2536
+ async function up$54(db) {
2514
2537
  await sql`
2515
2538
  ALTER TABLE public.execution_runs
2516
2539
  ADD COLUMN archived_at TIMESTAMPTZ NULL,
@@ -2555,7 +2578,7 @@ async function up$44(db) {
2555
2578
  ON public.execution_steps (routing_key, created_at)
2556
2579
  `.execute(db);
2557
2580
  }
2558
- async function down$44(db) {
2581
+ async function down$54(db) {
2559
2582
  await sql`DROP INDEX IF EXISTS public.idx_execution_steps_routing_key_created`.execute(db);
2560
2583
  await sql`
2561
2584
  ALTER TABLE public.execution_steps
@@ -2580,8 +2603,8 @@ async function down$44(db) {
2580
2603
  //#endregion
2581
2604
  //#region src/db/migrations/007_audit_logs_archived_at.ts
2582
2605
  var _007_audit_logs_archived_at_exports = /* @__PURE__ */ __exportAll({
2583
- down: () => down$43,
2584
- up: () => up$43
2606
+ down: () => down$53,
2607
+ up: () => up$53
2585
2608
  });
2586
2609
  /**
2587
2610
  * `secret_audit_log` and `access_log` cold-store schema additions, plus
@@ -2621,7 +2644,7 @@ var _007_audit_logs_archived_at_exports = /* @__PURE__ */ __exportAll({
2621
2644
  * `down()` would not have meaningful retention bounds. Acceptable for
2622
2645
  * staging.
2623
2646
  */
2624
- async function up$43(db) {
2647
+ async function up$53(db) {
2625
2648
  await sql`
2626
2649
  ALTER TABLE public.secret_audit_log
2627
2650
  ADD COLUMN archived_at TIMESTAMPTZ NULL,
@@ -2642,7 +2665,7 @@ async function up$43(db) {
2642
2665
  DROP COLUMN IF EXISTS expires_at
2643
2666
  `.execute(db);
2644
2667
  }
2645
- async function down$43(db) {
2668
+ async function down$53(db) {
2646
2669
  await sql`
2647
2670
  ALTER TABLE public.access_log
2648
2671
  ADD COLUMN expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '90 days')
@@ -2670,8 +2693,8 @@ async function down$43(db) {
2670
2693
  //#endregion
2671
2694
  //#region src/db/migrations/008_event_log_archived_at.ts
2672
2695
  var _008_event_log_archived_at_exports = /* @__PURE__ */ __exportAll({
2673
- down: () => down$42,
2674
- up: () => up$42
2696
+ down: () => down$52,
2697
+ up: () => up$52
2675
2698
  });
2676
2699
  /**
2677
2700
  * `event_log` cold-store schema additions plus removal of the
@@ -2709,7 +2732,7 @@ var _008_event_log_archived_at_exports = /* @__PURE__ */ __exportAll({
2709
2732
  * — best effort; rows inserted between `up()` and a hypothetical
2710
2733
  * `down()` would not have meaningful retention bounds.
2711
2734
  */
2712
- async function up$42(db) {
2735
+ async function up$52(db) {
2713
2736
  await sql`
2714
2737
  ALTER TABLE public.event_log
2715
2738
  ADD COLUMN archived_at TIMESTAMPTZ NULL,
@@ -2725,7 +2748,7 @@ async function up$42(db) {
2725
2748
  DROP COLUMN IF EXISTS expires_at
2726
2749
  `.execute(db);
2727
2750
  }
2728
- async function down$42(db) {
2751
+ async function down$52(db) {
2729
2752
  await sql`
2730
2753
  ALTER TABLE public.event_log
2731
2754
  ADD COLUMN expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '30 days')
@@ -2744,8 +2767,8 @@ async function down$42(db) {
2744
2767
  //#endregion
2745
2768
  //#region src/db/migrations/009_access_log_trigram.ts
2746
2769
  var _009_access_log_trigram_exports = /* @__PURE__ */ __exportAll({
2747
- down: () => down$41,
2748
- up: () => up$41
2770
+ down: () => down$51,
2771
+ up: () => up$51
2749
2772
  });
2750
2773
  /**
2751
2774
  * Trigram (pg_trgm) index on access_log.error_message for the federated
@@ -2758,7 +2781,7 @@ var _009_access_log_trigram_exports = /* @__PURE__ */ __exportAll({
2758
2781
  * EXISTS` are both safe to re-run. No CONCURRENTLY because Kysely runs
2759
2782
  * migrations inside a transaction; the lock is brief on a sampled table.
2760
2783
  */
2761
- async function up$41(db) {
2784
+ async function up$51(db) {
2762
2785
  await sql`CREATE EXTENSION IF NOT EXISTS pg_trgm`.execute(db);
2763
2786
  await sql`
2764
2787
  CREATE INDEX IF NOT EXISTS access_log_error_message_trgm_idx
@@ -2767,14 +2790,14 @@ async function up$41(db) {
2767
2790
  WHERE error_message IS NOT NULL
2768
2791
  `.execute(db);
2769
2792
  }
2770
- async function down$41(db) {
2793
+ async function down$51(db) {
2771
2794
  await sql`DROP INDEX IF EXISTS public.access_log_error_message_trgm_idx`.execute(db);
2772
2795
  }
2773
2796
  //#endregion
2774
2797
  //#region src/db/migrations/010_cold_store_chunks.ts
2775
2798
  var _010_cold_store_chunks_exports = /* @__PURE__ */ __exportAll({
2776
- down: () => down$40,
2777
- up: () => up$40
2799
+ down: () => down$50,
2800
+ up: () => up$50
2778
2801
  });
2779
2802
  /**
2780
2803
  * Cold-store chunk index — Phase 2 (cold-store purge).
@@ -2806,7 +2829,7 @@ var _010_cold_store_chunks_exports = /* @__PURE__ */ __exportAll({
2806
2829
  * forever. Adapters that don't opt into per-bucket archival via
2807
2830
  * `coldTtlDays` don't insert here either.
2808
2831
  */
2809
- async function up$40(db) {
2832
+ async function up$50(db) {
2810
2833
  await sql`
2811
2834
  CREATE TABLE public.cold_store_chunks (
2812
2835
  db TEXT NOT NULL,
@@ -2833,14 +2856,14 @@ async function up$40(db) {
2833
2856
  ON public.cold_store_chunks (db, table_name, tenant_id, archived_at DESC)
2834
2857
  `.execute(db);
2835
2858
  }
2836
- async function down$40(db) {
2859
+ async function down$50(db) {
2837
2860
  await sql`DROP TABLE IF EXISTS public.cold_store_chunks`.execute(db);
2838
2861
  }
2839
2862
  //#endregion
2840
2863
  //#region src/db/migrations/011_drop_source_secrets_notify.ts
2841
2864
  var _011_drop_source_secrets_notify_exports = /* @__PURE__ */ __exportAll({
2842
- down: () => down$39,
2843
- up: () => up$39
2865
+ down: () => down$49,
2866
+ up: () => up$49
2844
2867
  });
2845
2868
  /**
2846
2869
  * Drop the `source_secrets_change_trigger` and the
@@ -2859,11 +2882,11 @@ var _011_drop_source_secrets_notify_exports = /* @__PURE__ */ __exportAll({
2859
2882
  * in `001_initial.ts`. They wake up no consumer until the `WebhookSecretManager`
2860
2883
  * is restored, so this migration is safe to roll back.
2861
2884
  */
2862
- async function up$39(db) {
2885
+ async function up$49(db) {
2863
2886
  await sql`DROP TRIGGER IF EXISTS source_secrets_change_trigger ON public.scoped_secrets`.execute(db);
2864
2887
  await sql`DROP FUNCTION IF EXISTS public.notify_source_secrets_change() CASCADE`.execute(db);
2865
2888
  }
2866
- async function down$39(db) {
2889
+ async function down$49(db) {
2867
2890
  await sql`
2868
2891
  CREATE OR REPLACE FUNCTION public.notify_source_secrets_change() RETURNS trigger
2869
2892
  LANGUAGE plpgsql
@@ -2902,8 +2925,8 @@ async function down$39(db) {
2902
2925
  //#endregion
2903
2926
  //#region src/db/migrations/012_peer_credentials_active_uniq.ts
2904
2927
  var _012_peer_credentials_active_uniq_exports = /* @__PURE__ */ __exportAll({
2905
- down: () => down$38,
2906
- up: () => up$38
2928
+ down: () => down$48,
2929
+ up: () => up$48
2907
2930
  });
2908
2931
  /**
2909
2932
  * Add a partial unique index on `peer_credentials (instance_id) WHERE
@@ -2929,7 +2952,7 @@ var _012_peer_credentials_active_uniq_exports = /* @__PURE__ */ __exportAll({
2929
2952
  * `down()` only drops the index; it does NOT undo the dedupe (there's no
2930
2953
  * safe way to recreate revoked rows, and the dedupe is monotonic).
2931
2954
  */
2932
- async function up$38(db) {
2955
+ async function up$48(db) {
2933
2956
  await sql`
2934
2957
  UPDATE public.peer_credentials
2935
2958
  SET revoked_at = NOW()
@@ -2947,14 +2970,14 @@ async function up$38(db) {
2947
2970
  WHERE revoked_at IS NULL
2948
2971
  `.execute(db);
2949
2972
  }
2950
- async function down$38(db) {
2973
+ async function down$48(db) {
2951
2974
  await sql`DROP INDEX IF EXISTS public.peer_credentials_active_uniq`.execute(db);
2952
2975
  }
2953
2976
  //#endregion
2954
2977
  //#region src/db/migrations/013_execution_log_bytes.ts
2955
2978
  var _013_execution_log_bytes_exports = /* @__PURE__ */ __exportAll({
2956
- down: () => down$37,
2957
- up: () => up$37
2979
+ down: () => down$47,
2980
+ up: () => up$47
2958
2981
  });
2959
2982
  /**
2960
2983
  * Add `log_bytes BIGINT NOT NULL DEFAULT 0` columns to `execution_runs` and
@@ -2973,7 +2996,7 @@ var _013_execution_log_bytes_exports = /* @__PURE__ */ __exportAll({
2973
2996
  *
2974
2997
  * Idempotent (`ADD COLUMN IF NOT EXISTS`).
2975
2998
  */
2976
- async function up$37(db) {
2999
+ async function up$47(db) {
2977
3000
  await sql`
2978
3001
  ALTER TABLE public.execution_runs
2979
3002
  ADD COLUMN IF NOT EXISTS log_bytes BIGINT NOT NULL DEFAULT 0
@@ -2983,15 +3006,15 @@ async function up$37(db) {
2983
3006
  ADD COLUMN IF NOT EXISTS log_bytes BIGINT NOT NULL DEFAULT 0
2984
3007
  `.execute(db);
2985
3008
  }
2986
- async function down$37(db) {
3009
+ async function down$47(db) {
2987
3010
  await sql`ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS log_bytes`.execute(db);
2988
3011
  await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS log_bytes`.execute(db);
2989
3012
  }
2990
3013
  //#endregion
2991
3014
  //#region src/db/migrations/014_kici_events_lease_retry.ts
2992
3015
  var _014_kici_events_lease_retry_exports = /* @__PURE__ */ __exportAll({
2993
- down: () => down$36,
2994
- up: () => up$36
3016
+ down: () => down$46,
3017
+ up: () => up$46
2995
3018
  });
2996
3019
  /**
2997
3020
  * Add lease + retry + DLQ columns to `kici_events` so the EventRouter can
@@ -3025,7 +3048,7 @@ var _014_kici_events_lease_retry_exports = /* @__PURE__ */ __exportAll({
3025
3048
  *
3026
3049
  * Idempotent (`ADD COLUMN IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS`).
3027
3050
  */
3028
- async function up$36(db) {
3051
+ async function up$46(db) {
3029
3052
  await sql`
3030
3053
  ALTER TABLE public.kici_events
3031
3054
  ADD COLUMN IF NOT EXISTS claimed_at TIMESTAMPTZ,
@@ -3056,7 +3079,7 @@ async function up$36(db) {
3056
3079
  WHERE dlq_at IS NOT NULL
3057
3080
  `.execute(db);
3058
3081
  }
3059
- async function down$36(db) {
3082
+ async function down$46(db) {
3060
3083
  await sql`DROP INDEX IF EXISTS public.idx_kici_events_dlq`.execute(db);
3061
3084
  await sql`DROP INDEX IF EXISTS public.idx_kici_events_lease_expired`.execute(db);
3062
3085
  await sql`DROP INDEX IF EXISTS public.idx_kici_events_retry_due`.execute(db);
@@ -3074,8 +3097,8 @@ async function down$36(db) {
3074
3097
  //#endregion
3075
3098
  //#region src/db/migrations/015_org_settings_customer_scoped.ts
3076
3099
  var _015_org_settings_customer_scoped_exports = /* @__PURE__ */ __exportAll({
3077
- down: () => down$35,
3078
- up: () => up$35
3100
+ down: () => down$45,
3101
+ up: () => up$45
3079
3102
  });
3080
3103
  /**
3081
3104
  * Org-scope `org_settings` and qualify each glob entry by source.
@@ -3103,7 +3126,7 @@ var _015_org_settings_customer_scoped_exports = /* @__PURE__ */ __exportAll({
3103
3126
  * Idempotent: a re-run on an already-migrated DB sees `customer_id` exists
3104
3127
  * and the list columns are already jsonb, so it is a no-op.
3105
3128
  */
3106
- async function up$35(db) {
3129
+ async function up$45(db) {
3107
3130
  if ((await sql`
3108
3131
  SELECT EXISTS (
3109
3132
  SELECT 1 FROM information_schema.columns
@@ -3228,7 +3251,7 @@ async function up$35(db) {
3228
3251
  await sql`DROP TABLE _org_settings_merged`.execute(db);
3229
3252
  await sql`DROP TABLE _org_settings_stage`.execute(db);
3230
3253
  }
3231
- async function down$35(db) {
3254
+ async function down$45(db) {
3232
3255
  if (!(await sql`
3233
3256
  SELECT EXISTS (
3234
3257
  SELECT 1 FROM information_schema.columns
@@ -3253,8 +3276,8 @@ async function down$35(db) {
3253
3276
  //#endregion
3254
3277
  //#region src/db/migrations/016_org_settings_allow_http_npm.ts
3255
3278
  var _016_org_settings_allow_http_npm_exports = /* @__PURE__ */ __exportAll({
3256
- down: () => down$34,
3257
- up: () => up$34
3279
+ down: () => down$44,
3280
+ up: () => up$44
3258
3281
  });
3259
3282
  /**
3260
3283
  * Add `org_settings.allow_http_npm_registries boolean NOT NULL DEFAULT false`.
@@ -3267,7 +3290,7 @@ var _016_org_settings_allow_http_npm_exports = /* @__PURE__ */ __exportAll({
3267
3290
  *
3268
3291
  * Idempotent: a re-run on a DB that already has the column is a no-op.
3269
3292
  */
3270
- async function up$34(db) {
3293
+ async function up$44(db) {
3271
3294
  if ((await sql`
3272
3295
  SELECT EXISTS (
3273
3296
  SELECT 1 FROM information_schema.columns
@@ -3281,7 +3304,7 @@ async function up$34(db) {
3281
3304
  ADD COLUMN allow_http_npm_registries boolean NOT NULL DEFAULT false
3282
3305
  `.execute(db);
3283
3306
  }
3284
- async function down$34(db) {
3307
+ async function down$44(db) {
3285
3308
  await sql`
3286
3309
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS allow_http_npm_registries
3287
3310
  `.execute(db);
@@ -3289,8 +3312,8 @@ async function down$34(db) {
3289
3312
  //#endregion
3290
3313
  //#region src/db/migrations/017_org_id_widen.ts
3291
3314
  var _017_org_id_widen_exports = /* @__PURE__ */ __exportAll({
3292
- down: () => down$33,
3293
- up: () => up$33
3315
+ down: () => down$43,
3316
+ up: () => up$43
3294
3317
  });
3295
3318
  /**
3296
3319
  * Widen every orchestrator-side `org_id varchar(12)` column to
@@ -3330,17 +3353,17 @@ const ORG_ID_TABLES$1 = [
3330
3353
  "held_runs",
3331
3354
  "scoped_secrets"
3332
3355
  ];
3333
- async function up$33(db) {
3356
+ async function up$43(db) {
3334
3357
  for (const table of ORG_ID_TABLES$1) await sql.raw(`ALTER TABLE public.${table} ALTER COLUMN org_id TYPE varchar(16)`).execute(db);
3335
3358
  }
3336
- async function down$33(db) {
3359
+ async function down$43(db) {
3337
3360
  for (const table of ORG_ID_TABLES$1) await sql.raw(`ALTER TABLE public.${table} ALTER COLUMN org_id TYPE varchar(12)`).execute(db);
3338
3361
  }
3339
3362
  //#endregion
3340
3363
  //#region src/db/migrations/018_org_id_prefix_backfill.ts
3341
3364
  var _018_org_id_prefix_backfill_exports = /* @__PURE__ */ __exportAll({
3342
- down: () => down$32,
3343
- up: () => up$32
3365
+ down: () => down$42,
3366
+ up: () => up$42
3344
3367
  });
3345
3368
  /**
3346
3369
  * Prefix every orchestrator-side tenant string with `org_` to align
@@ -3385,19 +3408,19 @@ const CUSTOMER_ID_TABLES = [
3385
3408
  "workflow_registrations",
3386
3409
  "org_settings"
3387
3410
  ];
3388
- async function up$32(db) {
3411
+ async function up$42(db) {
3389
3412
  for (const table of ORG_ID_TABLES) await sql.raw(`UPDATE public.${table} SET org_id = 'org_' || org_id WHERE org_id <> 'kici-admin' AND org_id NOT LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
3390
3413
  for (const table of CUSTOMER_ID_TABLES) await sql.raw(`UPDATE public.${table} SET customer_id = 'org_' || customer_id WHERE customer_id <> 'kici-admin' AND customer_id NOT LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
3391
3414
  }
3392
- async function down$32(db) {
3415
+ async function down$42(db) {
3393
3416
  for (const table of CUSTOMER_ID_TABLES) await sql.raw(`UPDATE public.${table} SET customer_id = substring(customer_id from 5) WHERE customer_id LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
3394
3417
  for (const table of ORG_ID_TABLES) await sql.raw(`UPDATE public.${table} SET org_id = substring(org_id from 5) WHERE org_id LIKE 'org\\_%' ESCAPE '\\'`).execute(db);
3395
3418
  }
3396
3419
  //#endregion
3397
3420
  //#region src/db/migrations/019_generic_sources_change_notify.ts
3398
3421
  var _019_generic_sources_change_notify_exports = /* @__PURE__ */ __exportAll({
3399
- down: () => down$31,
3400
- up: () => up$31
3422
+ down: () => down$41,
3423
+ up: () => up$41
3401
3424
  });
3402
3425
  /**
3403
3426
  * Add a Postgres trigger on `generic_webhook_sources` that emits
@@ -3420,7 +3443,7 @@ var _019_generic_sources_change_notify_exports = /* @__PURE__ */ __exportAll({
3420
3443
  * (`notify_sources_change()` + `sources_change_trigger`, defined in
3421
3444
  * `001_initial.ts`).
3422
3445
  */
3423
- async function up$31(db) {
3446
+ async function up$41(db) {
3424
3447
  await sql`
3425
3448
  CREATE FUNCTION public.notify_generic_sources_change() RETURNS trigger
3426
3449
  LANGUAGE plpgsql
@@ -3441,15 +3464,15 @@ async function up$31(db) {
3441
3464
  FOR EACH ROW EXECUTE FUNCTION public.notify_generic_sources_change()
3442
3465
  `.execute(db);
3443
3466
  }
3444
- async function down$31(db) {
3467
+ async function down$41(db) {
3445
3468
  await sql`DROP TRIGGER IF EXISTS generic_sources_change_trigger ON public.generic_webhook_sources`.execute(db);
3446
3469
  await sql`DROP FUNCTION IF EXISTS public.notify_generic_sources_change()`.execute(db);
3447
3470
  }
3448
3471
  //#endregion
3449
3472
  //#region src/db/migrations/020_org_settings_dashboard_write_policy.ts
3450
3473
  var _020_org_settings_dashboard_write_policy_exports = /* @__PURE__ */ __exportAll({
3451
- down: () => down$30,
3452
- up: () => up$30
3474
+ down: () => down$40,
3475
+ up: () => up$40
3453
3476
  });
3454
3477
  /**
3455
3478
  * Add `org_settings.dashboard_write_policy jsonb NOT NULL DEFAULT '{}'`.
@@ -3464,7 +3487,7 @@ var _020_org_settings_dashboard_write_policy_exports = /* @__PURE__ */ __exportA
3464
3487
  *
3465
3488
  * Idempotent: a re-run on a DB that already has the column is a no-op.
3466
3489
  */
3467
- async function up$30(db) {
3490
+ async function up$40(db) {
3468
3491
  if ((await sql`
3469
3492
  SELECT EXISTS (
3470
3493
  SELECT 1 FROM information_schema.columns
@@ -3478,7 +3501,7 @@ async function up$30(db) {
3478
3501
  ADD COLUMN dashboard_write_policy jsonb NOT NULL DEFAULT '{}'::jsonb
3479
3502
  `.execute(db);
3480
3503
  }
3481
- async function down$30(db) {
3504
+ async function down$40(db) {
3482
3505
  await sql`
3483
3506
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS dashboard_write_policy
3484
3507
  `.execute(db);
@@ -3486,8 +3509,8 @@ async function down$30(db) {
3486
3509
  //#endregion
3487
3510
  //#region src/db/migrations/021_check_run_tracking.ts
3488
3511
  var _021_check_run_tracking_exports = /* @__PURE__ */ __exportAll({
3489
- down: () => down$29,
3490
- up: () => up$29
3512
+ down: () => down$39,
3513
+ up: () => up$39
3491
3514
  });
3492
3515
  /**
3493
3516
  * Add `check_run_tracking` table for HA-safe check-run state persistence.
@@ -3511,7 +3534,7 @@ var _021_check_run_tracking_exports = /* @__PURE__ */ __exportAll({
3511
3534
  *
3512
3535
  * Idempotent: a re-run on a DB that already has the table is a no-op.
3513
3536
  */
3514
- async function up$29(db) {
3537
+ async function up$39(db) {
3515
3538
  if ((await sql`
3516
3539
  SELECT EXISTS (
3517
3540
  SELECT 1 FROM information_schema.tables
@@ -3542,14 +3565,14 @@ async function up$29(db) {
3542
3565
  WHERE run_id IS NOT NULL
3543
3566
  `.execute(db);
3544
3567
  }
3545
- async function down$29(db) {
3568
+ async function down$39(db) {
3546
3569
  await sql`DROP TABLE IF EXISTS public.check_run_tracking`.execute(db);
3547
3570
  }
3548
3571
  //#endregion
3549
3572
  //#region src/db/migrations/022_scaler_manager_state.ts
3550
3573
  var _022_scaler_manager_state_exports = /* @__PURE__ */ __exportAll({
3551
- down: () => down$28,
3552
- up: () => up$28
3574
+ down: () => down$38,
3575
+ up: () => up$38
3553
3576
  });
3554
3577
  /**
3555
3578
  * Add three tables persisting `ScalerManager` per-coord state:
@@ -3576,7 +3599,7 @@ var _022_scaler_manager_state_exports = /* @__PURE__ */ __exportAll({
3576
3599
  * Idempotent: a re-run on a DB that already has any of these tables
3577
3600
  * leaves the existing one alone.
3578
3601
  */
3579
- async function up$28(db) {
3602
+ async function up$38(db) {
3580
3603
  const tableExists = async (name) => {
3581
3604
  return (await sql`
3582
3605
  SELECT EXISTS (
@@ -3626,7 +3649,7 @@ async function up$28(db) {
3626
3649
  `.execute(db);
3627
3650
  }
3628
3651
  }
3629
- async function down$28(db) {
3652
+ async function down$38(db) {
3630
3653
  await sql`DROP TABLE IF EXISTS public.scaler_reservations`.execute(db);
3631
3654
  await sql`DROP TABLE IF EXISTS public.scaler_agent_jobs`.execute(db);
3632
3655
  await sql`DROP TABLE IF EXISTS public.scaler_spawning_agents`.execute(db);
@@ -3634,8 +3657,8 @@ async function down$28(db) {
3634
3657
  //#endregion
3635
3658
  //#region src/db/migrations/023_dispatch_queue_recovery_deadline.ts
3636
3659
  var _023_dispatch_queue_recovery_deadline_exports = /* @__PURE__ */ __exportAll({
3637
- down: () => down$27,
3638
- up: () => up$27
3660
+ down: () => down$37,
3661
+ up: () => up$37
3639
3662
  });
3640
3663
  /**
3641
3664
  * Add `dispatch_queue.recovery_deadline TIMESTAMPTZ` and
@@ -3659,7 +3682,7 @@ var _023_dispatch_queue_recovery_deadline_exports = /* @__PURE__ */ __exportAll(
3659
3682
  * Idempotent: re-running on a DB that already has either column is a
3660
3683
  * no-op.
3661
3684
  */
3662
- async function up$27(db) {
3685
+ async function up$37(db) {
3663
3686
  const colExists = async (name) => {
3664
3687
  return (await sql`
3665
3688
  SELECT EXISTS (
@@ -3684,7 +3707,7 @@ async function up$27(db) {
3684
3707
  WHERE recovery_deadline IS NOT NULL
3685
3708
  `.execute(db);
3686
3709
  }
3687
- async function down$27(db) {
3710
+ async function down$37(db) {
3688
3711
  await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_recovery_deadline`.execute(db);
3689
3712
  await sql`
3690
3713
  ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS recovery_agent_id
@@ -3696,8 +3719,8 @@ async function down$27(db) {
3696
3719
  //#endregion
3697
3720
  //#region src/db/migrations/024_dispatch_queue_provisioning_error.ts
3698
3721
  var _024_dispatch_queue_provisioning_error_exports = /* @__PURE__ */ __exportAll({
3699
- down: () => down$26,
3700
- up: () => up$26
3722
+ down: () => down$36,
3723
+ up: () => up$36
3701
3724
  });
3702
3725
  /**
3703
3726
  * Add `dispatch_queue.last_provisioning_error TEXT` recording the most
@@ -3713,7 +3736,7 @@ var _024_dispatch_queue_provisioning_error_exports = /* @__PURE__ */ __exportAll
3713
3736
  *
3714
3737
  * Idempotent: re-running on a DB that already has the column is a no-op.
3715
3738
  */
3716
- async function up$26(db) {
3739
+ async function up$36(db) {
3717
3740
  const colExists = async (name) => {
3718
3741
  return (await sql`
3719
3742
  SELECT EXISTS (
@@ -3729,7 +3752,7 @@ async function up$26(db) {
3729
3752
  ADD COLUMN last_provisioning_error TEXT
3730
3753
  `.execute(db);
3731
3754
  }
3732
- async function down$26(db) {
3755
+ async function down$36(db) {
3733
3756
  await sql`
3734
3757
  ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS last_provisioning_error
3735
3758
  `.execute(db);
@@ -3737,8 +3760,8 @@ async function down$26(db) {
3737
3760
  //#endregion
3738
3761
  //#region src/db/migrations/025_init_failure.ts
3739
3762
  var _025_init_failure_exports = /* @__PURE__ */ __exportAll({
3740
- down: () => down$25,
3741
- up: () => up$25
3763
+ down: () => down$35,
3764
+ up: () => up$35
3742
3765
  });
3743
3766
  /**
3744
3767
  * Add `init_failure jsonb` columns to `execution_runs` and `execution_jobs`.
@@ -3752,7 +3775,7 @@ var _025_init_failure_exports = /* @__PURE__ */ __exportAll({
3752
3775
  * Idempotent: re-running on a DB that already has either column is a no-op
3753
3776
  * for that column.
3754
3777
  */
3755
- async function up$25(db) {
3778
+ async function up$35(db) {
3756
3779
  const colExists = async (table, name) => {
3757
3780
  return (await sql`
3758
3781
  SELECT EXISTS (
@@ -3772,7 +3795,7 @@ async function up$25(db) {
3772
3795
  ADD COLUMN init_failure JSONB DEFAULT NULL
3773
3796
  `.execute(db);
3774
3797
  }
3775
- async function down$25(db) {
3798
+ async function down$35(db) {
3776
3799
  await sql`
3777
3800
  ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS init_failure
3778
3801
  `.execute(db);
@@ -3783,8 +3806,8 @@ async function down$25(db) {
3783
3806
  //#endregion
3784
3807
  //#region src/db/migrations/026_event_log_lockfile_corrupt.ts
3785
3808
  var _026_event_log_lockfile_corrupt_exports = /* @__PURE__ */ __exportAll({
3786
- down: () => down$24,
3787
- up: () => up$24
3809
+ down: () => down$34,
3810
+ up: () => up$34
3788
3811
  });
3789
3812
  /**
3790
3813
  * Extend the event_log.status CHECK constraint with 'lockfile_corrupt' so the
@@ -3793,7 +3816,7 @@ var _026_event_log_lockfile_corrupt_exports = /* @__PURE__ */ __exportAll({
3793
3816
  *
3794
3817
  * Idempotent: the DROP ... IF EXISTS / re-ADD pair re-runs cleanly.
3795
3818
  */
3796
- async function up$24(db) {
3819
+ async function up$34(db) {
3797
3820
  await sql`ALTER TABLE event_log DROP CONSTRAINT IF EXISTS event_log_status_check`.execute(db);
3798
3821
  await sql`
3799
3822
  ALTER TABLE event_log ADD CONSTRAINT event_log_status_check
@@ -3803,7 +3826,7 @@ async function up$24(db) {
3803
3826
  ])))
3804
3827
  `.execute(db);
3805
3828
  }
3806
- async function down$24(db) {
3829
+ async function down$34(db) {
3807
3830
  await sql`ALTER TABLE event_log DROP CONSTRAINT IF EXISTS event_log_status_check`.execute(db);
3808
3831
  await sql`
3809
3832
  ALTER TABLE event_log ADD CONSTRAINT event_log_status_check
@@ -3816,8 +3839,8 @@ async function down$24(db) {
3816
3839
  //#endregion
3817
3840
  //#region src/db/migrations/027_workflow_timeout.ts
3818
3841
  var _027_workflow_timeout_exports = /* @__PURE__ */ __exportAll({
3819
- down: () => down$23,
3820
- up: () => up$23
3842
+ down: () => down$33,
3843
+ up: () => up$33
3821
3844
  });
3822
3845
  /**
3823
3846
  * Add `workflow_timeout_ms integer` to `execution_runs`.
@@ -3835,13 +3858,13 @@ var _027_workflow_timeout_exports = /* @__PURE__ */ __exportAll({
3835
3858
  *
3836
3859
  * Idempotent: re-running on a DB that already has the column is a no-op.
3837
3860
  */
3838
- async function up$23(db) {
3861
+ async function up$33(db) {
3839
3862
  await sql`
3840
3863
  ALTER TABLE public.execution_runs
3841
3864
  ADD COLUMN IF NOT EXISTS workflow_timeout_ms INTEGER DEFAULT NULL
3842
3865
  `.execute(db);
3843
3866
  }
3844
- async function down$23(db) {
3867
+ async function down$33(db) {
3845
3868
  await sql`
3846
3869
  ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS workflow_timeout_ms
3847
3870
  `.execute(db);
@@ -3849,8 +3872,8 @@ async function down$23(db) {
3849
3872
  //#endregion
3850
3873
  //#region src/db/migrations/028_org_settings_user_cache.ts
3851
3874
  var _028_org_settings_user_cache_exports = /* @__PURE__ */ __exportAll({
3852
- down: () => down$22,
3853
- up: () => up$22
3875
+ down: () => down$32,
3876
+ up: () => up$32
3854
3877
  });
3855
3878
  /**
3856
3879
  * Add `org_settings.user_cache_quota_bytes bigint` and
@@ -3879,7 +3902,7 @@ async function columnExists(db, column) {
3879
3902
  ) AS exists
3880
3903
  `.execute(db)).rows[0]?.exists ?? false;
3881
3904
  }
3882
- async function up$22(db) {
3905
+ async function up$32(db) {
3883
3906
  if (!await columnExists(db, "user_cache_quota_bytes")) await sql`
3884
3907
  ALTER TABLE public.org_settings
3885
3908
  ADD COLUMN user_cache_quota_bytes bigint
@@ -3889,7 +3912,7 @@ async function up$22(db) {
3889
3912
  ADD COLUMN user_cache_ttl_ms bigint
3890
3913
  `.execute(db);
3891
3914
  }
3892
- async function down$22(db) {
3915
+ async function down$32(db) {
3893
3916
  await sql`
3894
3917
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS user_cache_quota_bytes
3895
3918
  `.execute(db);
@@ -3900,8 +3923,8 @@ async function down$22(db) {
3900
3923
  //#endregion
3901
3924
  //#region src/db/migrations/029_dispatch_queue_attempts.ts
3902
3925
  var _029_dispatch_queue_attempts_exports = /* @__PURE__ */ __exportAll({
3903
- down: () => down$21,
3904
- up: () => up$21
3926
+ down: () => down$31,
3927
+ up: () => up$31
3905
3928
  });
3906
3929
  /**
3907
3930
  * Add `dispatch_queue.dispatch_attempts INT NOT NULL DEFAULT 0`.
@@ -3915,7 +3938,7 @@ var _029_dispatch_queue_attempts_exports = /* @__PURE__ */ __exportAll({
3915
3938
  *
3916
3939
  * Idempotent: re-running on a DB that already has the column is a no-op.
3917
3940
  */
3918
- async function up$21(db) {
3941
+ async function up$31(db) {
3919
3942
  if (!((await sql`
3920
3943
  SELECT EXISTS (
3921
3944
  SELECT 1 FROM information_schema.columns
@@ -3928,7 +3951,7 @@ async function up$21(db) {
3928
3951
  ADD COLUMN dispatch_attempts INT NOT NULL DEFAULT 0
3929
3952
  `.execute(db);
3930
3953
  }
3931
- async function down$21(db) {
3954
+ async function down$31(db) {
3932
3955
  await sql`
3933
3956
  ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS dispatch_attempts
3934
3957
  `.execute(db);
@@ -3936,8 +3959,8 @@ async function down$21(db) {
3936
3959
  //#endregion
3937
3960
  //#region src/db/migrations/030_held_runs_env_set_null.ts
3938
3961
  var _030_held_runs_env_set_null_exports = /* @__PURE__ */ __exportAll({
3939
- down: () => down$20,
3940
- up: () => up$20
3962
+ down: () => down$30,
3963
+ up: () => up$30
3941
3964
  });
3942
3965
  /**
3943
3966
  * held_runs.environment_id becomes nullable with ON DELETE SET NULL so
@@ -3948,14 +3971,14 @@ var _030_held_runs_env_set_null_exports = /* @__PURE__ */ __exportAll({
3948
3971
  * Idempotent: dropping the NOT NULL and the constraint are both no-ops on a
3949
3972
  * re-run, and the constraint is re-created with the SET NULL action.
3950
3973
  */
3951
- async function up$20(db) {
3974
+ async function up$30(db) {
3952
3975
  await sql`ALTER TABLE public.held_runs ALTER COLUMN environment_id DROP NOT NULL`.execute(db);
3953
3976
  await sql`ALTER TABLE public.held_runs DROP CONSTRAINT IF EXISTS held_runs_environment_id_fkey`.execute(db);
3954
3977
  await sql`ALTER TABLE public.held_runs
3955
3978
  ADD CONSTRAINT held_runs_environment_id_fkey
3956
3979
  FOREIGN KEY (environment_id) REFERENCES public.environments(id) ON DELETE SET NULL`.execute(db);
3957
3980
  }
3958
- async function down$20(db) {
3981
+ async function down$30(db) {
3959
3982
  await sql`ALTER TABLE public.held_runs DROP CONSTRAINT IF EXISTS held_runs_environment_id_fkey`.execute(db);
3960
3983
  await sql`ALTER TABLE public.held_runs
3961
3984
  ADD CONSTRAINT held_runs_environment_id_fkey
@@ -3965,8 +3988,8 @@ async function down$20(db) {
3965
3988
  //#endregion
3966
3989
  //#region src/db/migrations/031_dispatch_queue_ack_deadline.ts
3967
3990
  var _031_dispatch_queue_ack_deadline_exports = /* @__PURE__ */ __exportAll({
3968
- down: () => down$19,
3969
- up: () => up$19
3991
+ down: () => down$29,
3992
+ up: () => up$29
3970
3993
  });
3971
3994
  /**
3972
3995
  * Add `dispatch_queue.ack_deadline TIMESTAMPTZ` and
@@ -3983,7 +4006,7 @@ var _031_dispatch_queue_ack_deadline_exports = /* @__PURE__ */ __exportAll({
3983
4006
  *
3984
4007
  * Idempotent: re-running on a DB that already has either column is a no-op.
3985
4008
  */
3986
- async function up$19(db) {
4009
+ async function up$29(db) {
3987
4010
  const colExists = async (name) => {
3988
4011
  return (await sql`
3989
4012
  SELECT EXISTS (
@@ -4008,7 +4031,7 @@ async function up$19(db) {
4008
4031
  WHERE ack_deadline IS NOT NULL
4009
4032
  `.execute(db);
4010
4033
  }
4011
- async function down$19(db) {
4034
+ async function down$29(db) {
4012
4035
  await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_ack_deadline`.execute(db);
4013
4036
  await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS ack_agent_id`.execute(db);
4014
4037
  await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS ack_deadline`.execute(db);
@@ -4016,8 +4039,8 @@ async function down$19(db) {
4016
4039
  //#endregion
4017
4040
  //#region src/db/migrations/032_org_settings_dispatch_ack_timeout.ts
4018
4041
  var _032_org_settings_dispatch_ack_timeout_exports = /* @__PURE__ */ __exportAll({
4019
- down: () => down$18,
4020
- up: () => up$18
4042
+ down: () => down$28,
4043
+ up: () => up$28
4021
4044
  });
4022
4045
  /**
4023
4046
  * Add `org_settings.dispatch_ack_timeout_ms BIGINT` (nullable).
@@ -4029,7 +4052,7 @@ var _032_org_settings_dispatch_ack_timeout_exports = /* @__PURE__ */ __exportAll
4029
4052
  *
4030
4053
  * Idempotent: a re-run on a DB that already has the column is a no-op.
4031
4054
  */
4032
- async function up$18(db) {
4055
+ async function up$28(db) {
4033
4056
  if ((await sql`
4034
4057
  SELECT EXISTS (
4035
4058
  SELECT 1 FROM information_schema.columns
@@ -4043,7 +4066,7 @@ async function up$18(db) {
4043
4066
  ADD COLUMN dispatch_ack_timeout_ms BIGINT
4044
4067
  `.execute(db);
4045
4068
  }
4046
- async function down$18(db) {
4069
+ async function down$28(db) {
4047
4070
  await sql`
4048
4071
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS dispatch_ack_timeout_ms
4049
4072
  `.execute(db);
@@ -4051,8 +4074,8 @@ async function down$18(db) {
4051
4074
  //#endregion
4052
4075
  //#region src/db/migrations/033_org_settings_approval.ts
4053
4076
  var _033_org_settings_approval_exports = /* @__PURE__ */ __exportAll({
4054
- down: () => down$17,
4055
- up: () => up$17
4077
+ down: () => down$27,
4078
+ up: () => up$27
4056
4079
  });
4057
4080
  /**
4058
4081
  * Add the two approval-policy columns to `org_settings`:
@@ -4069,7 +4092,7 @@ var _033_org_settings_approval_exports = /* @__PURE__ */ __exportAll({
4069
4092
  * and the orchestrator admin route. Idempotent: a re-run on a DB that already
4070
4093
  * has the columns is a no-op (each column is guarded independently).
4071
4094
  */
4072
- async function up$17(db) {
4095
+ async function up$27(db) {
4073
4096
  const colExists = async (column) => {
4074
4097
  return (await sql`
4075
4098
  SELECT EXISTS (
@@ -4089,7 +4112,7 @@ async function up$17(db) {
4089
4112
  ADD COLUMN allow_self_approval BOOLEAN NOT NULL DEFAULT true
4090
4113
  `.execute(db);
4091
4114
  }
4092
- async function down$17(db) {
4115
+ async function down$27(db) {
4093
4116
  await sql`
4094
4117
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS approval_expiry_seconds
4095
4118
  `.execute(db);
@@ -4100,8 +4123,8 @@ async function down$17(db) {
4100
4123
  //#endregion
4101
4124
  //#region src/db/migrations/034_held_runs_generalize.ts
4102
4125
  var _034_held_runs_generalize_exports = /* @__PURE__ */ __exportAll({
4103
- down: () => down$16,
4104
- up: () => up$16
4126
+ down: () => down$26,
4127
+ up: () => up$26
4105
4128
  });
4106
4129
  /**
4107
4130
  * Generalize `held_runs` from an environment-only hold into the unified
@@ -4123,7 +4146,7 @@ var _034_held_runs_generalize_exports = /* @__PURE__ */ __exportAll({
4123
4146
  * New `held_run_approvals` table: one row per approver decision, FK to
4124
4147
  * `held_runs.id` (uuid) with ON DELETE CASCADE.
4125
4148
  */
4126
- async function up$16(db) {
4149
+ async function up$26(db) {
4127
4150
  const colExists = async (column) => {
4128
4151
  return (await sql`
4129
4152
  SELECT EXISTS (
@@ -4156,7 +4179,7 @@ async function up$16(db) {
4156
4179
  ON public.held_run_approvals USING btree (held_run_id)
4157
4180
  `.execute(db);
4158
4181
  }
4159
- async function down$16(db) {
4182
+ async function down$26(db) {
4160
4183
  await sql`DROP TABLE IF EXISTS public.held_run_approvals`.execute(db);
4161
4184
  await sql`ALTER TABLE public.held_runs DROP COLUMN IF EXISTS approval_requirement`.execute(db);
4162
4185
  await sql`ALTER TABLE public.held_runs DROP COLUMN IF EXISTS trigger_source`.execute(db);
@@ -4166,8 +4189,8 @@ async function down$16(db) {
4166
4189
  //#endregion
4167
4190
  //#region src/db/migrations/035_pending_workflow_contexts.ts
4168
4191
  var _035_pending_workflow_contexts_exports = /* @__PURE__ */ __exportAll({
4169
- down: () => down$15,
4170
- up: () => up$15
4192
+ down: () => down$25,
4193
+ up: () => up$25
4171
4194
  });
4172
4195
  /**
4173
4196
  * Pending workflow dispatch context — backs resume of a workflow whose install
@@ -4176,7 +4199,7 @@ var _035_pending_workflow_contexts_exports = /* @__PURE__ */ __exportAll({
4176
4199
  * wait-timer expiry, concurrency slot free). The row is deleted once the resume
4177
4200
  * dispatch has been kicked off.
4178
4201
  */
4179
- async function up$15(db) {
4202
+ async function up$25(db) {
4180
4203
  await sql`
4181
4204
  CREATE TABLE IF NOT EXISTS public.pending_workflow_contexts (
4182
4205
  run_id text PRIMARY KEY,
@@ -4186,14 +4209,14 @@ async function up$15(db) {
4186
4209
  )
4187
4210
  `.execute(db);
4188
4211
  }
4189
- async function down$15(db) {
4212
+ async function down$25(db) {
4190
4213
  await sql`DROP TABLE IF EXISTS public.pending_workflow_contexts`.execute(db);
4191
4214
  }
4192
4215
  //#endregion
4193
4216
  //#region src/db/migrations/036_attestations.ts
4194
4217
  var _036_attestations_exports = /* @__PURE__ */ __exportAll({
4195
- down: () => down$14,
4196
- up: () => up$14
4218
+ down: () => down$24,
4219
+ up: () => up$24
4197
4220
  });
4198
4221
  /**
4199
4222
  * Add the `attestations` table for build-provenance bundles.
@@ -4206,7 +4229,7 @@ var _036_attestations_exports = /* @__PURE__ */ __exportAll({
4206
4229
  *
4207
4230
  * Idempotent: a re-run on a DB that already has the table is a no-op.
4208
4231
  */
4209
- async function up$14(db) {
4232
+ async function up$24(db) {
4210
4233
  if ((await sql`
4211
4234
  SELECT EXISTS (
4212
4235
  SELECT 1 FROM information_schema.tables
@@ -4232,15 +4255,15 @@ async function up$14(db) {
4232
4255
  ON public.attestations (run_id, job_id)
4233
4256
  `.execute(db);
4234
4257
  }
4235
- async function down$14(db) {
4258
+ async function down$24(db) {
4236
4259
  await sql`DROP TABLE IF EXISTS public.attestations`.execute(db);
4237
4260
  }
4238
4261
  //#endregion
4239
4262
  //#region src/db/migrations/037_generic_sources_provider_type_local.ts
4240
4263
  var _037_generic_sources_provider_type_local_exports = /* @__PURE__ */ __exportAll({
4241
- down: () => down$13,
4242
- up: () => up$13
4243
- });
4264
+ down: () => down$23,
4265
+ up: () => up$23
4266
+ });
4244
4267
  /**
4245
4268
  * Replace the `generic_webhook_sources.provider_type` CHECK constraint so it
4246
4269
  * permits `'local'` instead of `'internal'`, and migrate any carried-over
@@ -4256,7 +4279,7 @@ var _037_generic_sources_provider_type_local_exports = /* @__PURE__ */ __exportA
4256
4279
  * Idempotent: the constraint is dropped IF EXISTS and recreated; the data
4257
4280
  * backfill is a plain UPDATE that is a no-op once no `'internal'` rows remain.
4258
4281
  */
4259
- async function up$13(db) {
4282
+ async function up$23(db) {
4260
4283
  await sql`
4261
4284
  ALTER TABLE public.generic_webhook_sources
4262
4285
  DROP CONSTRAINT IF EXISTS generic_webhook_sources_provider_type_check
@@ -4272,7 +4295,7 @@ async function up$13(db) {
4272
4295
  CHECK (provider_type = ANY (ARRAY['generic'::text, 'local'::text]))
4273
4296
  `.execute(db);
4274
4297
  }
4275
- async function down$13(db) {
4298
+ async function down$23(db) {
4276
4299
  await sql`
4277
4300
  ALTER TABLE public.generic_webhook_sources
4278
4301
  DROP CONSTRAINT IF EXISTS generic_webhook_sources_provider_type_check
@@ -4291,8 +4314,8 @@ async function down$13(db) {
4291
4314
  //#endregion
4292
4315
  //#region src/db/migrations/038_remote_sources.ts
4293
4316
  var _038_remote_sources_exports = /* @__PURE__ */ __exportAll({
4294
- down: () => down$12,
4295
- up: () => up$12
4317
+ down: () => down$22,
4318
+ up: () => up$22
4296
4319
  });
4297
4320
  /**
4298
4321
  * `remote_sources` anchors a Platform-relayed `kici run remote` to its real
@@ -4304,7 +4327,7 @@ var _038_remote_sources_exports = /* @__PURE__ */ __exportAll({
4304
4327
  *
4305
4328
  * Idempotent: a re-run on a DB that already has the table is a no-op.
4306
4329
  */
4307
- async function up$12(db) {
4330
+ async function up$22(db) {
4308
4331
  if ((await sql`
4309
4332
  SELECT EXISTS (
4310
4333
  SELECT 1 FROM information_schema.tables
@@ -4323,14 +4346,14 @@ async function up$12(db) {
4323
4346
  )
4324
4347
  `.execute(db);
4325
4348
  }
4326
- async function down$12(db) {
4349
+ async function down$22(db) {
4327
4350
  await sql`DROP TABLE IF EXISTS public.remote_sources`.execute(db);
4328
4351
  }
4329
4352
  //#endregion
4330
4353
  //#region src/db/migrations/039_host_roster.ts
4331
4354
  var _039_host_roster_exports = /* @__PURE__ */ __exportAll({
4332
- down: () => down$11,
4333
- up: () => up$11
4355
+ down: () => down$21,
4356
+ up: () => up$21
4334
4357
  });
4335
4358
  /**
4336
4359
  * `host_roster` is KiCI's declared inventory: one durable row per agent the
@@ -4347,7 +4370,7 @@ var _039_host_roster_exports = /* @__PURE__ */ __exportAll({
4347
4370
  *
4348
4371
  * Idempotent: a re-run on a DB that already has the table is a no-op.
4349
4372
  */
4350
- async function up$11(db) {
4373
+ async function up$21(db) {
4351
4374
  if ((await sql`
4352
4375
  SELECT EXISTS (
4353
4376
  SELECT 1 FROM information_schema.tables
@@ -4377,14 +4400,14 @@ async function up$11(db) {
4377
4400
  await sql`CREATE INDEX idx_host_roster_reap
4378
4401
  ON public.host_roster (lifecycle_class, last_seen)`.execute(db);
4379
4402
  }
4380
- async function down$11(db) {
4403
+ async function down$21(db) {
4381
4404
  await sql`DROP TABLE IF EXISTS public.host_roster`.execute(db);
4382
4405
  }
4383
4406
  //#endregion
4384
4407
  //#region src/db/migrations/040_runsonall_pin.ts
4385
4408
  var _040_runsonall_pin_exports = /* @__PURE__ */ __exportAll({
4386
- down: () => down$10,
4387
- up: () => up$10
4409
+ down: () => down$20,
4410
+ up: () => up$20
4388
4411
  });
4389
4412
  /**
4390
4413
  * Add the `runsOnAll` host fan-out columns:
@@ -4401,7 +4424,7 @@ var _040_runsonall_pin_exports = /* @__PURE__ */ __exportAll({
4401
4424
  *
4402
4425
  * Idempotent: re-running on a DB that already has any column is a no-op.
4403
4426
  */
4404
- async function colExists$5(db, table, name) {
4427
+ async function colExists$9(db, table, name) {
4405
4428
  return (await sql`
4406
4429
  SELECT EXISTS (
4407
4430
  SELECT 1 FROM information_schema.columns
@@ -4411,18 +4434,18 @@ async function colExists$5(db, table, name) {
4411
4434
  ) AS exists
4412
4435
  `.execute(db)).rows[0]?.exists ?? false;
4413
4436
  }
4414
- async function up$10(db) {
4415
- if (!await colExists$5(db, "dispatch_queue", "pinned_agent_id")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN pinned_agent_id TEXT`.execute(db);
4416
- if (!await colExists$5(db, "execution_jobs", "base_job_name")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN base_job_name TEXT`.execute(db);
4417
- if (!await colExists$5(db, "execution_jobs", "variant_kind")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN variant_kind TEXT`.execute(db);
4418
- if (!await colExists$5(db, "execution_jobs", "variant_label")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN variant_label TEXT`.execute(db);
4437
+ async function up$20(db) {
4438
+ if (!await colExists$9(db, "dispatch_queue", "pinned_agent_id")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN pinned_agent_id TEXT`.execute(db);
4439
+ if (!await colExists$9(db, "execution_jobs", "base_job_name")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN base_job_name TEXT`.execute(db);
4440
+ if (!await colExists$9(db, "execution_jobs", "variant_kind")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN variant_kind TEXT`.execute(db);
4441
+ if (!await colExists$9(db, "execution_jobs", "variant_label")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN variant_label TEXT`.execute(db);
4419
4442
  await sql`
4420
4443
  CREATE INDEX IF NOT EXISTS idx_dispatch_queue_pinned_agent
4421
4444
  ON public.dispatch_queue (pinned_agent_id)
4422
4445
  WHERE pinned_agent_id IS NOT NULL
4423
4446
  `.execute(db);
4424
4447
  }
4425
- async function down$10(db) {
4448
+ async function down$20(db) {
4426
4449
  await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_pinned_agent`.execute(db);
4427
4450
  await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS pinned_agent_id`.execute(db);
4428
4451
  await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS variant_label`.execute(db);
@@ -4432,8 +4455,8 @@ async function down$10(db) {
4432
4455
  //#endregion
4433
4456
  //#region src/db/migrations/041_wave_gated.ts
4434
4457
  var _041_wave_gated_exports = /* @__PURE__ */ __exportAll({
4435
- down: () => down$9,
4436
- up: () => up$9
4458
+ down: () => down$19,
4459
+ up: () => up$19
4437
4460
  });
4438
4461
  /**
4439
4462
  * Add the rolling fan-out wave-gate columns:
@@ -4453,7 +4476,7 @@ var _041_wave_gated_exports = /* @__PURE__ */ __exportAll({
4453
4476
  *
4454
4477
  * Idempotent: re-running on a DB that already has the columns is a no-op.
4455
4478
  */
4456
- async function colExists$4(db, table, name) {
4479
+ async function colExists$8(db, table, name) {
4457
4480
  return (await sql`
4458
4481
  SELECT EXISTS (
4459
4482
  SELECT 1 FROM information_schema.columns
@@ -4463,16 +4486,16 @@ async function colExists$4(db, table, name) {
4463
4486
  ) AS exists
4464
4487
  `.execute(db)).rows[0]?.exists ?? false;
4465
4488
  }
4466
- async function up$9(db) {
4467
- if (!await colExists$4(db, "execution_jobs", "wave_gated")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_gated boolean NOT NULL DEFAULT false`.execute(db);
4468
- if (!await colExists$4(db, "execution_jobs", "wave_max_parallel")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_max_parallel integer`.execute(db);
4469
- if (!await colExists$4(db, "execution_jobs", "wave_fail_fast")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_fail_fast boolean`.execute(db);
4489
+ async function up$19(db) {
4490
+ if (!await colExists$8(db, "execution_jobs", "wave_gated")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_gated boolean NOT NULL DEFAULT false`.execute(db);
4491
+ if (!await colExists$8(db, "execution_jobs", "wave_max_parallel")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_max_parallel integer`.execute(db);
4492
+ if (!await colExists$8(db, "execution_jobs", "wave_fail_fast")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_fail_fast boolean`.execute(db);
4470
4493
  await sql`
4471
4494
  CREATE INDEX IF NOT EXISTS idx_execution_jobs_wave
4472
4495
  ON public.execution_jobs (run_id, base_job_name, wave_gated)
4473
4496
  `.execute(db);
4474
4497
  }
4475
- async function down$9(db) {
4498
+ async function down$19(db) {
4476
4499
  await sql`DROP INDEX IF EXISTS public.idx_execution_jobs_wave`.execute(db);
4477
4500
  await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_fail_fast`.execute(db);
4478
4501
  await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_max_parallel`.execute(db);
@@ -4481,8 +4504,8 @@ async function down$9(db) {
4481
4504
  //#endregion
4482
4505
  //#region src/db/migrations/042_dispatch_queue_patterns.ts
4483
4506
  var _042_dispatch_queue_patterns_exports = /* @__PURE__ */ __exportAll({
4484
- down: () => down$8,
4485
- up: () => up$8
4507
+ down: () => down$18,
4508
+ up: () => up$18
4486
4509
  });
4487
4510
  /**
4488
4511
  * Add pattern columns to dispatch_queue. Exact labels stay in runs_on_labels /
@@ -4498,7 +4521,7 @@ var _042_dispatch_queue_patterns_exports = /* @__PURE__ */ __exportAll({
4498
4521
  *
4499
4522
  * Idempotent: re-running on a DB that already has the columns is a no-op.
4500
4523
  */
4501
- async function colExists$3(db, table, name) {
4524
+ async function colExists$7(db, table, name) {
4502
4525
  return (await sql`
4503
4526
  SELECT EXISTS (
4504
4527
  SELECT 1 FROM information_schema.columns
@@ -4508,19 +4531,19 @@ async function colExists$3(db, table, name) {
4508
4531
  ) AS exists
4509
4532
  `.execute(db)).rows[0]?.exists ?? false;
4510
4533
  }
4511
- async function up$8(db) {
4512
- if (!await colExists$3(db, "dispatch_queue", "runs_on_patterns")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN runs_on_patterns jsonb NOT NULL DEFAULT '[]'::jsonb`.execute(db);
4513
- if (!await colExists$3(db, "dispatch_queue", "exclude_patterns")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN exclude_patterns jsonb NOT NULL DEFAULT '[]'::jsonb`.execute(db);
4534
+ async function up$18(db) {
4535
+ if (!await colExists$7(db, "dispatch_queue", "runs_on_patterns")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN runs_on_patterns jsonb NOT NULL DEFAULT '[]'::jsonb`.execute(db);
4536
+ if (!await colExists$7(db, "dispatch_queue", "exclude_patterns")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN exclude_patterns jsonb NOT NULL DEFAULT '[]'::jsonb`.execute(db);
4514
4537
  }
4515
- async function down$8(db) {
4538
+ async function down$18(db) {
4516
4539
  await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS exclude_patterns`.execute(db);
4517
4540
  await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS runs_on_patterns`.execute(db);
4518
4541
  }
4519
4542
  //#endregion
4520
4543
  //#region src/db/migrations/043_rerouted_to_peer.ts
4521
4544
  var _043_rerouted_to_peer_exports = /* @__PURE__ */ __exportAll({
4522
- down: () => down$7,
4523
- up: () => up$7
4545
+ down: () => down$17,
4546
+ up: () => up$17
4524
4547
  });
4525
4548
  /**
4526
4549
  * Add a nullable `rerouted_to_peer text` marker to execution_jobs. Non-null
@@ -4531,7 +4554,7 @@ var _043_rerouted_to_peer_exports = /* @__PURE__ */ __exportAll({
4531
4554
  *
4532
4555
  * Idempotent: re-running on a DB that already has the column is a no-op.
4533
4556
  */
4534
- async function colExists$2(db, table, name) {
4557
+ async function colExists$6(db, table, name) {
4535
4558
  return (await sql`
4536
4559
  SELECT EXISTS (
4537
4560
  SELECT 1 FROM information_schema.columns
@@ -4541,17 +4564,17 @@ async function colExists$2(db, table, name) {
4541
4564
  ) AS exists
4542
4565
  `.execute(db)).rows[0]?.exists ?? false;
4543
4566
  }
4544
- async function up$7(db) {
4545
- if (!await colExists$2(db, "execution_jobs", "rerouted_to_peer")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN rerouted_to_peer text`.execute(db);
4567
+ async function up$17(db) {
4568
+ if (!await colExists$6(db, "execution_jobs", "rerouted_to_peer")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN rerouted_to_peer text`.execute(db);
4546
4569
  }
4547
- async function down$7(db) {
4570
+ async function down$17(db) {
4548
4571
  await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS rerouted_to_peer`.execute(db);
4549
4572
  }
4550
4573
  //#endregion
4551
4574
  //#region src/db/migrations/044_check_mode.ts
4552
4575
  var _044_check_mode_exports = /* @__PURE__ */ __exportAll({
4553
- down: () => down$6,
4554
- up: () => up$6
4576
+ down: () => down$16,
4577
+ up: () => up$16
4555
4578
  });
4556
4579
  /**
4557
4580
  * Add idempotent check-mode columns:
@@ -4568,7 +4591,7 @@ var _044_check_mode_exports = /* @__PURE__ */ __exportAll({
4568
4591
  * All nullable. Idempotent: re-running on a DB that already has the columns is
4569
4592
  * a no-op.
4570
4593
  */
4571
- async function colExists$1(db, table, name) {
4594
+ async function colExists$5(db, table, name) {
4572
4595
  return (await sql`
4573
4596
  SELECT EXISTS (
4574
4597
  SELECT 1 FROM information_schema.columns
@@ -4578,13 +4601,13 @@ async function colExists$1(db, table, name) {
4578
4601
  ) AS exists
4579
4602
  `.execute(db)).rows[0]?.exists ?? false;
4580
4603
  }
4581
- async function up$6(db) {
4582
- if (!await colExists$1(db, "execution_runs", "check_mode")) await sql`ALTER TABLE public.execution_runs ADD COLUMN check_mode text`.execute(db);
4583
- if (!await colExists$1(db, "execution_steps", "check_outcome")) await sql`ALTER TABLE public.execution_steps ADD COLUMN check_outcome text`.execute(db);
4584
- if (!await colExists$1(db, "execution_steps", "drift_summary")) await sql`ALTER TABLE public.execution_steps ADD COLUMN drift_summary text`.execute(db);
4585
- if (!await colExists$1(db, "execution_steps", "drift")) await sql`ALTER TABLE public.execution_steps ADD COLUMN drift jsonb`.execute(db);
4604
+ async function up$16(db) {
4605
+ if (!await colExists$5(db, "execution_runs", "check_mode")) await sql`ALTER TABLE public.execution_runs ADD COLUMN check_mode text`.execute(db);
4606
+ if (!await colExists$5(db, "execution_steps", "check_outcome")) await sql`ALTER TABLE public.execution_steps ADD COLUMN check_outcome text`.execute(db);
4607
+ if (!await colExists$5(db, "execution_steps", "drift_summary")) await sql`ALTER TABLE public.execution_steps ADD COLUMN drift_summary text`.execute(db);
4608
+ if (!await colExists$5(db, "execution_steps", "drift")) await sql`ALTER TABLE public.execution_steps ADD COLUMN drift jsonb`.execute(db);
4586
4609
  }
4587
- async function down$6(db) {
4610
+ async function down$16(db) {
4588
4611
  await sql`ALTER TABLE public.execution_steps DROP COLUMN IF EXISTS drift`.execute(db);
4589
4612
  await sql`ALTER TABLE public.execution_steps DROP COLUMN IF EXISTS drift_summary`.execute(db);
4590
4613
  await sql`ALTER TABLE public.execution_steps DROP COLUMN IF EXISTS check_outcome`.execute(db);
@@ -4593,8 +4616,8 @@ async function down$6(db) {
4593
4616
  //#endregion
4594
4617
  //#region src/db/migrations/045_host_properties.ts
4595
4618
  var _045_host_properties_exports = /* @__PURE__ */ __exportAll({
4596
- down: () => down$5,
4597
- up: () => up$5
4619
+ down: () => down$15,
4620
+ up: () => up$15
4598
4621
  });
4599
4622
  /**
4600
4623
  * Add the typed host-vars dimension to the host roster:
@@ -4608,18 +4631,18 @@ var _045_host_properties_exports = /* @__PURE__ */ __exportAll({
4608
4631
  * Idempotent (`ADD COLUMN IF NOT EXISTS`): re-running on a DB that already has
4609
4632
  * the column is a no-op. Staging data is preserved (additive column).
4610
4633
  */
4611
- async function up$5(db) {
4634
+ async function up$15(db) {
4612
4635
  await sql`ALTER TABLE public.host_roster
4613
4636
  ADD COLUMN IF NOT EXISTS host_properties jsonb NOT NULL DEFAULT '{}'::jsonb`.execute(db);
4614
4637
  }
4615
- async function down$5(db) {
4638
+ async function down$15(db) {
4616
4639
  await sql`ALTER TABLE public.host_roster DROP COLUMN IF EXISTS host_properties`.execute(db);
4617
4640
  }
4618
4641
  //#endregion
4619
4642
  //#region src/db/migrations/046_join_token_consumed_by_instance.ts
4620
4643
  var _046_join_token_consumed_by_instance_exports = /* @__PURE__ */ __exportAll({
4621
- down: () => down$4,
4622
- up: () => up$4
4644
+ down: () => down$14,
4645
+ up: () => up$14
4623
4646
  });
4624
4647
  /**
4625
4648
  * Record the joining peer's instanceId at first consumption of a join token:
@@ -4634,18 +4657,18 @@ var _046_join_token_consumed_by_instance_exports = /* @__PURE__ */ __exportAll({
4634
4657
  * Idempotent (`ADD COLUMN IF NOT EXISTS`): re-running on a DB that already has
4635
4658
  * the column is a no-op. Staging data is preserved (additive nullable column).
4636
4659
  */
4637
- async function up$4(db) {
4660
+ async function up$14(db) {
4638
4661
  await sql`ALTER TABLE public.join_tokens
4639
4662
  ADD COLUMN IF NOT EXISTS consumed_by_instance text`.execute(db);
4640
4663
  }
4641
- async function down$4(db) {
4664
+ async function down$14(db) {
4642
4665
  await sql`ALTER TABLE public.join_tokens DROP COLUMN IF EXISTS consumed_by_instance`.execute(db);
4643
4666
  }
4644
4667
  //#endregion
4645
4668
  //#region src/db/migrations/047_needs_run_on.ts
4646
4669
  var _047_needs_run_on_exports = /* @__PURE__ */ __exportAll({
4647
- down: () => down$3,
4648
- up: () => up$3
4670
+ down: () => down$13,
4671
+ up: () => up$13
4649
4672
  });
4650
4673
  /**
4651
4674
  * Migrate `execution_job_needs` from the binary `if_failed` policy to a
@@ -4670,7 +4693,7 @@ const ALL_TERMINAL_JSON = JSON.stringify([
4670
4693
  "drift_dropped"
4671
4694
  ]);
4672
4695
  const SUCCESS_ONLY_JSON = JSON.stringify(["success"]);
4673
- async function colExists(db, table, name) {
4696
+ async function colExists$4(db, table, name) {
4674
4697
  return (await sql`
4675
4698
  SELECT EXISTS (
4676
4699
  SELECT 1 FROM information_schema.columns
@@ -4681,12 +4704,12 @@ async function colExists(db, table, name) {
4681
4704
  `.execute(db)).rows[0]?.exists ?? false;
4682
4705
  }
4683
4706
  const SUCCESS_ONLY_DEFAULT = `'${SUCCESS_ONLY_JSON}'`;
4684
- async function up$3(db) {
4685
- if (!await colExists(db, "execution_job_needs", "run_on")) await sql`
4707
+ async function up$13(db) {
4708
+ if (!await colExists$4(db, "execution_job_needs", "run_on")) await sql`
4686
4709
  ALTER TABLE public.execution_job_needs
4687
4710
  ADD COLUMN run_on text NOT NULL DEFAULT ${sql.raw(SUCCESS_ONLY_DEFAULT)}
4688
4711
  `.execute(db);
4689
- if (await colExists(db, "execution_job_needs", "if_failed")) {
4712
+ if (await colExists$4(db, "execution_job_needs", "if_failed")) {
4690
4713
  await sql`
4691
4714
  UPDATE public.execution_job_needs
4692
4715
  SET run_on = ${ALL_TERMINAL_JSON}
@@ -4700,12 +4723,12 @@ async function up$3(db) {
4700
4723
  await sql`ALTER TABLE public.execution_job_needs DROP COLUMN if_failed`.execute(db);
4701
4724
  }
4702
4725
  }
4703
- async function down$3(db) {
4704
- if (!await colExists(db, "execution_job_needs", "if_failed")) await sql`
4726
+ async function down$13(db) {
4727
+ if (!await colExists$4(db, "execution_job_needs", "if_failed")) await sql`
4705
4728
  ALTER TABLE public.execution_job_needs
4706
4729
  ADD COLUMN if_failed text NOT NULL DEFAULT 'skip'
4707
4730
  `.execute(db);
4708
- if (await colExists(db, "execution_job_needs", "run_on")) {
4731
+ if (await colExists$4(db, "execution_job_needs", "run_on")) {
4709
4732
  await sql`
4710
4733
  UPDATE public.execution_job_needs
4711
4734
  SET if_failed = CASE WHEN run_on = ${SUCCESS_ONLY_JSON} THEN 'skip' ELSE 'run' END
@@ -4716,8 +4739,8 @@ async function down$3(db) {
4716
4739
  //#endregion
4717
4740
  //#region src/db/migrations/048_host_reboot_pending.ts
4718
4741
  var _048_host_reboot_pending_exports = /* @__PURE__ */ __exportAll({
4719
- down: () => down$2,
4720
- up: () => up$2
4742
+ down: () => down$12,
4743
+ up: () => up$12
4721
4744
  });
4722
4745
  /**
4723
4746
  * Add `host_roster.reboot_pending_until timestamptz NULL` — the persisted
@@ -4734,18 +4757,18 @@ var _048_host_reboot_pending_exports = /* @__PURE__ */ __exportAll({
4734
4757
  * Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
4735
4758
  * preserved.
4736
4759
  */
4737
- async function up$2(db) {
4760
+ async function up$12(db) {
4738
4761
  await sql`ALTER TABLE public.host_roster
4739
4762
  ADD COLUMN IF NOT EXISTS reboot_pending_until timestamptz`.execute(db);
4740
4763
  }
4741
- async function down$2(db) {
4764
+ async function down$12(db) {
4742
4765
  await sql`ALTER TABLE public.host_roster DROP COLUMN IF EXISTS reboot_pending_until`.execute(db);
4743
4766
  }
4744
4767
  //#endregion
4745
4768
  //#region src/db/migrations/049_held_runs_payload.ts
4746
4769
  var _049_held_runs_payload_exports = /* @__PURE__ */ __exportAll({
4747
- down: () => down$1,
4748
- up: () => up$1
4770
+ down: () => down$11,
4771
+ up: () => up$11
4749
4772
  });
4750
4773
  /**
4751
4774
  * Add `held_runs.payload jsonb NULL` — the drift payload captured when a
@@ -4757,18 +4780,18 @@ var _049_held_runs_payload_exports = /* @__PURE__ */ __exportAll({
4757
4780
  * Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
4758
4781
  * preserved.
4759
4782
  */
4760
- async function up$1(db) {
4783
+ async function up$11(db) {
4761
4784
  await sql`ALTER TABLE public.held_runs
4762
4785
  ADD COLUMN IF NOT EXISTS payload jsonb`.execute(db);
4763
4786
  }
4764
- async function down$1(db) {
4787
+ async function down$11(db) {
4765
4788
  await sql`ALTER TABLE public.held_runs DROP COLUMN IF EXISTS payload`.execute(db);
4766
4789
  }
4767
4790
  //#endregion
4768
4791
  //#region src/db/migrations/050_sources_slug.ts
4769
4792
  var _050_sources_slug_exports = /* @__PURE__ */ __exportAll({
4770
- down: () => down,
4771
- up: () => up
4793
+ down: () => down$10,
4794
+ up: () => up$10
4772
4795
  });
4773
4796
  /**
4774
4797
  * Add `sources.slug TEXT NULL` — the GitHub App slug (the URL-safe identifier
@@ -4783,141 +4806,491 @@ var _050_sources_slug_exports = /* @__PURE__ */ __exportAll({
4783
4806
  * Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
4784
4807
  * preserved.
4785
4808
  */
4786
- async function up(db) {
4809
+ async function up$10(db) {
4787
4810
  await sql`ALTER TABLE public.sources
4788
4811
  ADD COLUMN IF NOT EXISTS slug text`.execute(db);
4789
4812
  }
4790
- async function down(db) {
4813
+ async function down$10(db) {
4791
4814
  await sql`ALTER TABLE public.sources DROP COLUMN IF EXISTS slug`.execute(db);
4792
4815
  }
4793
4816
  //#endregion
4794
- //#region src/db/migration-provider.ts
4795
- function createMigrationProvider() {
4796
- return { async getMigrations() {
4797
- return {
4798
- "001_initial": _001_initial_exports,
4799
- "002_config_versions_key_version": _002_config_versions_key_version_exports,
4800
- "003_access_log": _003_access_log_exports,
4801
- "004_rename_bundle_to_source": _004_rename_bundle_to_source_exports,
4802
- "005_cold_store_chunk_counter": _005_cold_store_chunk_counter_exports,
4803
- "006_runs_jobs_steps_archived_at": _006_runs_jobs_steps_archived_at_exports,
4804
- "007_audit_logs_archived_at": _007_audit_logs_archived_at_exports,
4805
- "008_event_log_archived_at": _008_event_log_archived_at_exports,
4806
- "009_access_log_trigram": _009_access_log_trigram_exports,
4807
- "010_cold_store_chunks": _010_cold_store_chunks_exports,
4808
- "011_drop_source_secrets_notify": _011_drop_source_secrets_notify_exports,
4809
- "012_peer_credentials_active_uniq": _012_peer_credentials_active_uniq_exports,
4810
- "013_execution_log_bytes": _013_execution_log_bytes_exports,
4811
- "014_kici_events_lease_retry": _014_kici_events_lease_retry_exports,
4812
- "015_org_settings_customer_scoped": _015_org_settings_customer_scoped_exports,
4813
- "016_org_settings_allow_http_npm": _016_org_settings_allow_http_npm_exports,
4814
- "017_org_id_widen": _017_org_id_widen_exports,
4815
- "018_org_id_prefix_backfill": _018_org_id_prefix_backfill_exports,
4816
- "019_generic_sources_change_notify": _019_generic_sources_change_notify_exports,
4817
- "020_org_settings_dashboard_write_policy": _020_org_settings_dashboard_write_policy_exports,
4818
- "021_check_run_tracking": _021_check_run_tracking_exports,
4819
- "022_scaler_manager_state": _022_scaler_manager_state_exports,
4820
- "023_dispatch_queue_recovery_deadline": _023_dispatch_queue_recovery_deadline_exports,
4821
- "024_dispatch_queue_provisioning_error": _024_dispatch_queue_provisioning_error_exports,
4822
- "025_init_failure": _025_init_failure_exports,
4823
- "026_event_log_lockfile_corrupt": _026_event_log_lockfile_corrupt_exports,
4824
- "027_workflow_timeout": _027_workflow_timeout_exports,
4825
- "028_org_settings_user_cache": _028_org_settings_user_cache_exports,
4826
- "029_dispatch_queue_attempts": _029_dispatch_queue_attempts_exports,
4827
- "030_held_runs_env_set_null": _030_held_runs_env_set_null_exports,
4828
- "031_dispatch_queue_ack_deadline": _031_dispatch_queue_ack_deadline_exports,
4829
- "032_org_settings_dispatch_ack_timeout": _032_org_settings_dispatch_ack_timeout_exports,
4830
- "033_org_settings_approval": _033_org_settings_approval_exports,
4831
- "034_held_runs_generalize": _034_held_runs_generalize_exports,
4832
- "035_pending_workflow_contexts": _035_pending_workflow_contexts_exports,
4833
- "036_attestations": _036_attestations_exports,
4834
- "037_generic_sources_provider_type_local": _037_generic_sources_provider_type_local_exports,
4835
- "038_remote_sources": _038_remote_sources_exports,
4836
- "039_host_roster": _039_host_roster_exports,
4837
- "040_runsonall_pin": _040_runsonall_pin_exports,
4838
- "041_wave_gated": _041_wave_gated_exports,
4839
- "042_dispatch_queue_patterns": _042_dispatch_queue_patterns_exports,
4840
- "043_rerouted_to_peer": _043_rerouted_to_peer_exports,
4841
- "044_check_mode": _044_check_mode_exports,
4842
- "045_host_properties": _045_host_properties_exports,
4843
- "046_join_token_consumed_by_instance": _046_join_token_consumed_by_instance_exports,
4844
- "047_needs_run_on": _047_needs_run_on_exports,
4845
- "048_host_reboot_pending": _048_host_reboot_pending_exports,
4846
- "049_held_runs_payload": _049_held_runs_payload_exports,
4847
- "050_sources_slug": _050_sources_slug_exports
4848
- };
4849
- } };
4817
+ //#region src/db/migrations/051_binding_host_pattern.ts
4818
+ var _051_binding_host_pattern_exports = /* @__PURE__ */ __exportAll({
4819
+ down: () => down$9,
4820
+ up: () => up$9
4821
+ });
4822
+ /**
4823
+ * Add a per-host dimension to environment secret bindings:
4824
+ *
4825
+ * - `environment_bindings.host_pattern text NOT NULL DEFAULT '**'` — the host
4826
+ * selector a binding applies to (exact / glob / regex, matched against a
4827
+ * fan-out child's agentId / hostname / labels). `'**'` matches every host, so
4828
+ * existing rows are backfilled to `'**'` and keep their fleet-wide behaviour.
4829
+ * - The binding unique key widens from `(environment_id, scope_pattern)` to
4830
+ * `(environment_id, scope_pattern, host_pattern)` so the same scope can carry
4831
+ * distinct per-host selectors.
4832
+ *
4833
+ * Idempotent: re-running on a DB that already has the column / index is a no-op.
4834
+ * Additive — staging data is preserved.
4835
+ */
4836
+ async function up$9(db) {
4837
+ await sql`ALTER TABLE public.environment_bindings
4838
+ ADD COLUMN IF NOT EXISTS host_pattern text NOT NULL DEFAULT '**'`.execute(db);
4839
+ await sql`ALTER TABLE public.environment_bindings
4840
+ DROP CONSTRAINT IF EXISTS environment_bindings_env_id_scope_unique`.execute(db);
4841
+ await sql`CREATE UNIQUE INDEX IF NOT EXISTS environment_bindings_env_scope_host_uniq
4842
+ ON public.environment_bindings (environment_id, scope_pattern, host_pattern)`.execute(db);
4843
+ }
4844
+ async function down$9(db) {
4845
+ await sql`DROP INDEX IF EXISTS public.environment_bindings_env_scope_host_uniq`.execute(db);
4846
+ await sql`ALTER TABLE public.environment_bindings
4847
+ DROP COLUMN IF EXISTS host_pattern`.execute(db);
4848
+ await sql`ALTER TABLE public.environment_bindings
4849
+ ADD CONSTRAINT environment_bindings_env_id_scope_unique UNIQUE (environment_id, scope_pattern)`.execute(db);
4850
4850
  }
4851
4851
  //#endregion
4852
- //#region src/db/migrator.ts
4852
+ //#region src/db/migrations/052_host_reach_metadata.ts
4853
+ var _052_host_reach_metadata_exports = /* @__PURE__ */ __exportAll({
4854
+ down: () => down$8,
4855
+ up: () => up$8
4856
+ });
4853
4857
  /**
4854
- * Auto-migrate module with PostgreSQL advisory lock for HA safety.
4858
+ * Add pre-agent reach metadata to the host roster so a declared host (no agent
4859
+ * yet) can be reached over SSH for bootstrap bring-up:
4855
4860
  *
4856
- * Uses pg_advisory_lock to ensure only one orchestrator instance runs
4857
- * migrations at a time, preventing conflicts in multi-instance deployments.
4861
+ * - `host_roster.address text NULL` IP / hostname to SSH to.
4862
+ * - `host_roster.ssh_user text NULL` SSH login user (defaults to `root` at use site).
4863
+ * - `host_roster.ssh_port int NULL` — SSH port (defaults to 22 at use site).
4864
+ * - `host_roster.ssh_key_secret text NULL` — scoped-secret ref (`scope/key`)
4865
+ * holding the bring-up private key. The orchestrator resolves it server-side;
4866
+ * the key never lives in the roster.
4867
+ *
4868
+ * All nullable: a host with no reach metadata simply cannot be bootstrapped and
4869
+ * behaves exactly as before. Idempotent (`ADD COLUMN IF NOT EXISTS`); additive,
4870
+ * so staging data is preserved.
4858
4871
  */
4859
- const logger$10 = createLogger({ prefix: "migrate" });
4860
- const ADVISORY_LOCK_KEY = 543210001;
4872
+ async function up$8(db) {
4873
+ await sql`ALTER TABLE public.host_roster
4874
+ ADD COLUMN IF NOT EXISTS address text,
4875
+ ADD COLUMN IF NOT EXISTS ssh_user text,
4876
+ ADD COLUMN IF NOT EXISTS ssh_port integer,
4877
+ ADD COLUMN IF NOT EXISTS ssh_key_secret text`.execute(db);
4878
+ }
4879
+ async function down$8(db) {
4880
+ await sql`ALTER TABLE public.host_roster
4881
+ DROP COLUMN IF EXISTS address,
4882
+ DROP COLUMN IF EXISTS ssh_user,
4883
+ DROP COLUMN IF EXISTS ssh_port,
4884
+ DROP COLUMN IF EXISTS ssh_key_secret`.execute(db);
4885
+ }
4886
+ //#endregion
4887
+ //#region src/db/migrations/053_agent_token_single_use.ts
4888
+ var _053_agent_token_single_use_exports = /* @__PURE__ */ __exportAll({
4889
+ down: () => down$7,
4890
+ up: () => up$7
4891
+ });
4861
4892
  /**
4862
- * Run all pending migrations with an advisory lock for concurrency safety.
4893
+ * Add a single-use marker to agent tokens for the bootstrap (init-runner)
4894
+ * bring-up flow:
4863
4895
  *
4864
- * Acquires pg_advisory_lock before migrating to prevent concurrent
4865
- * migration attempts from multiple orchestrator instances. The lock
4866
- * is always released in the finally block, even on error.
4896
+ * - `agent_tokens.consumed_at timestamptz NULL` set the first time a
4897
+ * single-use bootstrap token is consumed (at `agent.register`). A second
4898
+ * register with the same token is rejected. NULL = never consumed (the
4899
+ * default for every existing static / ephemeral token, which stay reusable
4900
+ * until expiry).
4901
+ *
4902
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
4903
+ * preserved.
4867
4904
  */
4868
- async function runMigrations(opts) {
4869
- const client = await opts.pool.connect();
4870
- try {
4871
- logger$10.info("Acquiring migration lock...");
4872
- await client.query("SELECT pg_advisory_lock($1)", [ADVISORY_LOCK_KEY]);
4873
- logger$10.info("Migration lock acquired");
4874
- const migrator = new Migrator({
4875
- db: opts.db,
4876
- provider: createMigrationProvider()
4877
- });
4878
- const start = Date.now();
4879
- const { results, error } = await migrator.migrateToLatest();
4880
- for (const result of results ?? []) if (result.status === "Success") logger$10.info(`${result.migrationName} ... OK (${Date.now() - start}ms)`);
4881
- else if (result.status === "Error") logger$10.error(`${result.migrationName} ... FAILED`);
4882
- if (error) throw error;
4883
- const applied = (results ?? []).filter((r) => r.status === "Success");
4884
- if (applied.length === 0) logger$10.info("Database schema is up to date");
4885
- else logger$10.info(`Applied ${applied.length} migration(s)`);
4886
- const hash = await computeMigrationsHash(createMigrationProvider());
4887
- await storeMigrationContentHash(opts.pool, hash);
4888
- return results ?? [];
4889
- } finally {
4890
- await client.query("SELECT pg_advisory_unlock($1)", [ADVISORY_LOCK_KEY]);
4891
- client.release();
4892
- }
4905
+ async function up$7(db) {
4906
+ await sql`ALTER TABLE public.agent_tokens
4907
+ ADD COLUMN IF NOT EXISTS consumed_at timestamptz`.execute(db);
4908
+ }
4909
+ async function down$7(db) {
4910
+ await sql`ALTER TABLE public.agent_tokens
4911
+ DROP COLUMN IF EXISTS consumed_at`.execute(db);
4893
4912
  }
4894
4913
  //#endregion
4895
- //#region src/cli/commands/db.ts
4914
+ //#region src/db/migrations/054_local_working_tree.ts
4915
+ var _054_local_working_tree_exports = /* @__PURE__ */ __exportAll({
4916
+ down: () => down$6,
4917
+ up: () => up$6
4918
+ });
4896
4919
  /**
4897
- * Database management commands for kici-admin.
4920
+ * Mark runs that executed an uploaded local working tree (`kici run remote`):
4898
4921
  *
4899
- * db migrate Run pending migrations (HTTP orchestrator must be up)
4900
- * db fresh DROP + CREATE + migrate + store content hash (direct DB)
4901
- * db ensure <name> CREATE DATABASE IF NOT EXISTS (direct DB)
4902
- * db create-role CREATE ROLE LOGIN [CREATEDB] (direct DB)
4903
- * db create-readonly-user Read-only role + GRANT SELECT (direct DB)
4904
- * db check-schema Compare bundled migrations vs live schema (direct DB)
4905
- * db collation-check Check pg_database.datcollversion vs running libc (direct DB)
4906
- * db reindex REINDEX DATABASE CONCURRENTLY (direct DB)
4907
- * db refresh-collation-version ALTER DATABASE REFRESH COLLATION VERSION (direct DB)
4922
+ * - `execution_runs.local_working_tree boolean NOT NULL DEFAULT false` true
4923
+ * for runs that ran a developer's local working tree from the CLI (inline
4924
+ * lock file). The dashboard renders a "Local machine" badge for these runs
4925
+ * and avoids building an external repository link from the repo identifier.
4908
4926
  *
4909
- * The direct-DB subcommands cannot go through HTTP because the target database
4910
- * may not exist yet or is about to be dropped. They follow the same pattern
4911
- * as `kici-admin peer create-token` — open a pool from KICI_DATABASE_URL /
4912
- * DATABASE_URL / --database-url.
4927
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`); additive with a default, so staging
4928
+ * data is preserved.
4913
4929
  */
4914
- function resolveDatabaseUrl$2(explicit) {
4915
- const url = explicit ?? process.env.KICI_DATABASE_URL;
4916
- if (!url) throw new Error("Database URL required. Pass --database-url or set KICI_DATABASE_URL.");
4917
- return url;
4930
+ async function up$6(db) {
4931
+ await sql`ALTER TABLE public.execution_runs
4932
+ ADD COLUMN IF NOT EXISTS local_working_tree boolean NOT NULL DEFAULT false`.execute(db);
4918
4933
  }
4919
- async function confirmInteractive$1(prompt, expected) {
4920
- if (!process.stdin.isTTY) return false;
4934
+ async function down$6(db) {
4935
+ await sql`ALTER TABLE public.execution_runs
4936
+ DROP COLUMN IF EXISTS local_working_tree`.execute(db);
4937
+ }
4938
+ //#endregion
4939
+ //#region src/db/migrations/055_agent_token_mandatory_labels.ts
4940
+ var _055_agent_token_mandatory_labels_exports = /* @__PURE__ */ __exportAll({
4941
+ down: () => down$5,
4942
+ up: () => up$5
4943
+ });
4944
+ /**
4945
+ * Add a token-bound mandatory-label taint to agent tokens:
4946
+ *
4947
+ * - `agent_tokens.mandatory_labels text NULL` — a JSON-encoded `string[]` of
4948
+ * Kubernetes-taint-style gate labels the token authorizes. When a static
4949
+ * agent registers with this token, the set becomes the agent's registry-entry
4950
+ * `mandatoryLabels`: the agent only accepts a job when every label here
4951
+ * appears in the job's required labels. NULL = no taint (the default; the
4952
+ * agent accepts any job its advertised labels match), so every existing token
4953
+ * stays unconfined until re-minted.
4954
+ *
4955
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
4956
+ * preserved.
4957
+ */
4958
+ async function up$5(db) {
4959
+ await sql`ALTER TABLE public.agent_tokens
4960
+ ADD COLUMN IF NOT EXISTS mandatory_labels text`.execute(db);
4961
+ }
4962
+ async function down$5(db) {
4963
+ await sql`ALTER TABLE public.agent_tokens
4964
+ DROP COLUMN IF EXISTS mandatory_labels`.execute(db);
4965
+ }
4966
+ //#endregion
4967
+ //#region src/db/migrations/056_execution_jobs_environments.ts
4968
+ var _056_execution_jobs_environments_exports = /* @__PURE__ */ __exportAll({
4969
+ down: () => down$4,
4970
+ up: () => up$4
4971
+ });
4972
+ /**
4973
+ * Add a per-job bound deployment-environment list to `execution_jobs`:
4974
+ *
4975
+ * - `execution_jobs.environments text NULL` — a JSON-encoded `string[]` of the
4976
+ * ordered environment names a job binds (`environments: [...]`), in merge
4977
+ * order. Written at dispatch with the statically-resolved names (impure
4978
+ * dynamic elements as a `(dynamic)` placeholder), then overwritten with the
4979
+ * fully-resolved list when a deferred-init agent eval resolves dynamic
4980
+ * elements. NULL = the job binds no environment.
4981
+ *
4982
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`); additive, so staging data is
4983
+ * preserved.
4984
+ */
4985
+ async function up$4(db) {
4986
+ await sql`ALTER TABLE public.execution_jobs
4987
+ ADD COLUMN IF NOT EXISTS environments text`.execute(db);
4988
+ }
4989
+ async function down$4(db) {
4990
+ await sql`ALTER TABLE public.execution_jobs
4991
+ DROP COLUMN IF EXISTS environments`.execute(db);
4992
+ }
4993
+ //#endregion
4994
+ //#region src/db/migrations/057_step_concurrency.ts
4995
+ var _057_step_concurrency_exports = /* @__PURE__ */ __exportAll({
4996
+ down: () => down$3,
4997
+ up: () => up$3
4998
+ });
4999
+ /**
5000
+ * Add parallel step-group concurrency columns to `execution_steps`:
5001
+ *
5002
+ * - `concurrency_kind text` — the step's concurrency role (`sequential` |
5003
+ * `parallel-child` | `parallel-group`). NULL for an ordinary sequential step.
5004
+ * - `group_id text` — the parallel-group correlation id shared by a group's
5005
+ * children (e.g. `g0`). NULL for a sequential step.
5006
+ *
5007
+ * Both nullable, no backfill. Idempotent: re-running on a DB that already has the
5008
+ * columns is a no-op.
5009
+ */
5010
+ async function colExists$3(db, table, name) {
5011
+ return (await sql`
5012
+ SELECT EXISTS (
5013
+ SELECT 1 FROM information_schema.columns
5014
+ WHERE table_schema = 'public'
5015
+ AND table_name = ${table}
5016
+ AND column_name = ${name}
5017
+ ) AS exists
5018
+ `.execute(db)).rows[0]?.exists ?? false;
5019
+ }
5020
+ async function up$3(db) {
5021
+ if (!await colExists$3(db, "execution_steps", "concurrency_kind")) await sql`ALTER TABLE public.execution_steps ADD COLUMN concurrency_kind text`.execute(db);
5022
+ if (!await colExists$3(db, "execution_steps", "group_id")) await sql`ALTER TABLE public.execution_steps ADD COLUMN group_id text`.execute(db);
5023
+ }
5024
+ async function down$3(db) {
5025
+ await sql`ALTER TABLE public.execution_steps DROP COLUMN IF EXISTS group_id`.execute(db);
5026
+ await sql`ALTER TABLE public.execution_steps DROP COLUMN IF EXISTS concurrency_kind`.execute(db);
5027
+ }
5028
+ //#endregion
5029
+ //#region src/db/migrations/058_access_log_agent_label.ts
5030
+ var _058_access_log_agent_label_exports = /* @__PURE__ */ __exportAll({
5031
+ down: () => down$2,
5032
+ up: () => up$2
5033
+ });
5034
+ /**
5035
+ * Add `access_log.agent_label text` — the human-set name of the agent that
5036
+ * performed the action, when the actor authenticated with an agent-kind PAT.
5037
+ *
5038
+ * Queryable column (not just baked into `actor_meta`) so an operator can filter
5039
+ * the access log by agent. NULL for ordinary human / API-key / system actors.
5040
+ * Nullable, no backfill. Idempotent.
5041
+ */
5042
+ async function colExists$2(db, table, name) {
5043
+ return (await sql`
5044
+ SELECT EXISTS (
5045
+ SELECT 1 FROM information_schema.columns
5046
+ WHERE table_schema = 'public'
5047
+ AND table_name = ${table}
5048
+ AND column_name = ${name}
5049
+ ) AS exists
5050
+ `.execute(db)).rows[0]?.exists ?? false;
5051
+ }
5052
+ async function up$2(db) {
5053
+ if (!await colExists$2(db, "access_log", "agent_label")) await sql`ALTER TABLE public.access_log ADD COLUMN agent_label text`.execute(db);
5054
+ }
5055
+ async function down$2(db) {
5056
+ await sql`ALTER TABLE public.access_log DROP COLUMN IF EXISTS agent_label`.execute(db);
5057
+ }
5058
+ //#endregion
5059
+ //#region src/db/migrations/059_attestation_verdict.ts
5060
+ var _059_attestation_verdict_exports = /* @__PURE__ */ __exportAll({
5061
+ down: () => down$1,
5062
+ up: () => up$1
5063
+ });
5064
+ /**
5065
+ * Add server-side verification verdict columns to `attestations`, plus the
5066
+ * lookup indexes the org-wide attestations browser queries against.
5067
+ *
5068
+ * The orchestrator verifies each provenance bundle at ingest and records the
5069
+ * verdict here so the dashboard list shows trustworthy badges with no per-row
5070
+ * bundle fetch:
5071
+ * - `verify_status` — one of `verified` / `failed` / `unverifiable` / `pending`
5072
+ * (mirrors `attestationVerifyStatusSchema.enum.*` in `@kici-dev/engine`).
5073
+ * `DEFAULT 'pending'` covers rows written before a verdict is computed.
5074
+ * - `verify_reason` — first failure code from `verifyKiciBundle` (nullable).
5075
+ * - `verified_at` — when the verdict was recorded (nullable).
5076
+ *
5077
+ * Idempotent: guarded on the `verify_status` column's existence.
5078
+ */
5079
+ async function colExists$1(db, table, name) {
5080
+ return (await sql`
5081
+ SELECT EXISTS (
5082
+ SELECT 1 FROM information_schema.columns
5083
+ WHERE table_schema = 'public'
5084
+ AND table_name = ${table}
5085
+ AND column_name = ${name}
5086
+ ) AS exists
5087
+ `.execute(db)).rows[0]?.exists ?? false;
5088
+ }
5089
+ async function up$1(db) {
5090
+ if (await colExists$1(db, "attestations", "verify_status")) return;
5091
+ await sql`
5092
+ ALTER TABLE public.attestations
5093
+ ADD COLUMN verify_status TEXT NOT NULL DEFAULT 'pending',
5094
+ ADD COLUMN verify_reason TEXT,
5095
+ ADD COLUMN verified_at TIMESTAMPTZ
5096
+ `.execute(db);
5097
+ await sql`CREATE INDEX idx_attestations_subject_digest ON public.attestations (subject_digest)`.execute(db);
5098
+ await sql`CREATE INDEX idx_attestations_subject_name ON public.attestations (subject_name)`.execute(db);
5099
+ await sql`CREATE INDEX idx_attestations_verify_status ON public.attestations (verify_status)`.execute(db);
5100
+ await sql`CREATE INDEX idx_attestations_created_at ON public.attestations (created_at)`.execute(db);
5101
+ }
5102
+ async function down$1(db) {
5103
+ await sql`DROP INDEX IF EXISTS idx_attestations_created_at`.execute(db);
5104
+ await sql`DROP INDEX IF EXISTS idx_attestations_verify_status`.execute(db);
5105
+ await sql`DROP INDEX IF EXISTS idx_attestations_subject_name`.execute(db);
5106
+ await sql`DROP INDEX IF EXISTS idx_attestations_subject_digest`.execute(db);
5107
+ await sql`
5108
+ ALTER TABLE public.attestations
5109
+ DROP COLUMN IF EXISTS verify_status,
5110
+ DROP COLUMN IF EXISTS verify_reason,
5111
+ DROP COLUMN IF EXISTS verified_at
5112
+ `.execute(db);
5113
+ }
5114
+ //#endregion
5115
+ //#region src/db/migrations/060_run_trigger_actor.ts
5116
+ var _060_run_trigger_actor_exports = /* @__PURE__ */ __exportAll({
5117
+ down: () => down,
5118
+ up: () => up
5119
+ });
5120
+ /**
5121
+ * Add the triggering-actor columns to `execution_runs`:
5122
+ *
5123
+ * - `trigger_actor_provider` — the origin provider of the actor identity
5124
+ * (`github` today; provider-generic so GitLab/Bitbucket extend later).
5125
+ * - `trigger_actor_username` — the provider login of the person who triggered
5126
+ * the run (e.g. the GitHub pusher / PR author).
5127
+ * - `trigger_actor_user_id` — the immutable provider user id (mirrors
5128
+ * `identity_links.provider_user_id`), preferred over the mutable username
5129
+ * when resolving the actor to a KiCI user.
5130
+ *
5131
+ * Captured for ALL event types (push/tag/PR/comment), unlike the PR-only
5132
+ * `contributor_username`. Independently useful for a dashboard "triggered by
5133
+ * @x" and audit, beyond actor notifications. All nullable, no backfill,
5134
+ * idempotent.
5135
+ */
5136
+ async function colExists(db, table, name) {
5137
+ return (await sql`
5138
+ SELECT EXISTS (
5139
+ SELECT 1 FROM information_schema.columns
5140
+ WHERE table_schema = 'public'
5141
+ AND table_name = ${table}
5142
+ AND column_name = ${name}
5143
+ ) AS exists
5144
+ `.execute(db)).rows[0]?.exists ?? false;
5145
+ }
5146
+ async function up(db) {
5147
+ if (!await colExists(db, "execution_runs", "trigger_actor_provider")) await sql`ALTER TABLE public.execution_runs ADD COLUMN trigger_actor_provider text`.execute(db);
5148
+ if (!await colExists(db, "execution_runs", "trigger_actor_username")) await sql`ALTER TABLE public.execution_runs ADD COLUMN trigger_actor_username text`.execute(db);
5149
+ if (!await colExists(db, "execution_runs", "trigger_actor_user_id")) await sql`ALTER TABLE public.execution_runs ADD COLUMN trigger_actor_user_id text`.execute(db);
5150
+ }
5151
+ async function down(db) {
5152
+ await sql`ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS trigger_actor_provider`.execute(db);
5153
+ await sql`ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS trigger_actor_username`.execute(db);
5154
+ await sql`ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS trigger_actor_user_id`.execute(db);
5155
+ }
5156
+ //#endregion
5157
+ //#region src/db/migration-provider.ts
5158
+ function createMigrationProvider() {
5159
+ return { async getMigrations() {
5160
+ return {
5161
+ "001_initial": _001_initial_exports,
5162
+ "002_config_versions_key_version": _002_config_versions_key_version_exports,
5163
+ "003_access_log": _003_access_log_exports,
5164
+ "004_rename_bundle_to_source": _004_rename_bundle_to_source_exports,
5165
+ "005_cold_store_chunk_counter": _005_cold_store_chunk_counter_exports,
5166
+ "006_runs_jobs_steps_archived_at": _006_runs_jobs_steps_archived_at_exports,
5167
+ "007_audit_logs_archived_at": _007_audit_logs_archived_at_exports,
5168
+ "008_event_log_archived_at": _008_event_log_archived_at_exports,
5169
+ "009_access_log_trigram": _009_access_log_trigram_exports,
5170
+ "010_cold_store_chunks": _010_cold_store_chunks_exports,
5171
+ "011_drop_source_secrets_notify": _011_drop_source_secrets_notify_exports,
5172
+ "012_peer_credentials_active_uniq": _012_peer_credentials_active_uniq_exports,
5173
+ "013_execution_log_bytes": _013_execution_log_bytes_exports,
5174
+ "014_kici_events_lease_retry": _014_kici_events_lease_retry_exports,
5175
+ "015_org_settings_customer_scoped": _015_org_settings_customer_scoped_exports,
5176
+ "016_org_settings_allow_http_npm": _016_org_settings_allow_http_npm_exports,
5177
+ "017_org_id_widen": _017_org_id_widen_exports,
5178
+ "018_org_id_prefix_backfill": _018_org_id_prefix_backfill_exports,
5179
+ "019_generic_sources_change_notify": _019_generic_sources_change_notify_exports,
5180
+ "020_org_settings_dashboard_write_policy": _020_org_settings_dashboard_write_policy_exports,
5181
+ "021_check_run_tracking": _021_check_run_tracking_exports,
5182
+ "022_scaler_manager_state": _022_scaler_manager_state_exports,
5183
+ "023_dispatch_queue_recovery_deadline": _023_dispatch_queue_recovery_deadline_exports,
5184
+ "024_dispatch_queue_provisioning_error": _024_dispatch_queue_provisioning_error_exports,
5185
+ "025_init_failure": _025_init_failure_exports,
5186
+ "026_event_log_lockfile_corrupt": _026_event_log_lockfile_corrupt_exports,
5187
+ "027_workflow_timeout": _027_workflow_timeout_exports,
5188
+ "028_org_settings_user_cache": _028_org_settings_user_cache_exports,
5189
+ "029_dispatch_queue_attempts": _029_dispatch_queue_attempts_exports,
5190
+ "030_held_runs_env_set_null": _030_held_runs_env_set_null_exports,
5191
+ "031_dispatch_queue_ack_deadline": _031_dispatch_queue_ack_deadline_exports,
5192
+ "032_org_settings_dispatch_ack_timeout": _032_org_settings_dispatch_ack_timeout_exports,
5193
+ "033_org_settings_approval": _033_org_settings_approval_exports,
5194
+ "034_held_runs_generalize": _034_held_runs_generalize_exports,
5195
+ "035_pending_workflow_contexts": _035_pending_workflow_contexts_exports,
5196
+ "036_attestations": _036_attestations_exports,
5197
+ "037_generic_sources_provider_type_local": _037_generic_sources_provider_type_local_exports,
5198
+ "038_remote_sources": _038_remote_sources_exports,
5199
+ "039_host_roster": _039_host_roster_exports,
5200
+ "040_runsonall_pin": _040_runsonall_pin_exports,
5201
+ "041_wave_gated": _041_wave_gated_exports,
5202
+ "042_dispatch_queue_patterns": _042_dispatch_queue_patterns_exports,
5203
+ "043_rerouted_to_peer": _043_rerouted_to_peer_exports,
5204
+ "044_check_mode": _044_check_mode_exports,
5205
+ "045_host_properties": _045_host_properties_exports,
5206
+ "046_join_token_consumed_by_instance": _046_join_token_consumed_by_instance_exports,
5207
+ "047_needs_run_on": _047_needs_run_on_exports,
5208
+ "048_host_reboot_pending": _048_host_reboot_pending_exports,
5209
+ "049_held_runs_payload": _049_held_runs_payload_exports,
5210
+ "050_sources_slug": _050_sources_slug_exports,
5211
+ "051_binding_host_pattern": _051_binding_host_pattern_exports,
5212
+ "052_host_reach_metadata": _052_host_reach_metadata_exports,
5213
+ "053_agent_token_single_use": _053_agent_token_single_use_exports,
5214
+ "054_local_working_tree": _054_local_working_tree_exports,
5215
+ "055_agent_token_mandatory_labels": _055_agent_token_mandatory_labels_exports,
5216
+ "056_execution_jobs_environments": _056_execution_jobs_environments_exports,
5217
+ "057_step_concurrency": _057_step_concurrency_exports,
5218
+ "058_access_log_agent_label": _058_access_log_agent_label_exports,
5219
+ "059_attestation_verdict": _059_attestation_verdict_exports,
5220
+ "060_run_trigger_actor": _060_run_trigger_actor_exports
5221
+ };
5222
+ } };
5223
+ }
5224
+ //#endregion
5225
+ //#region src/db/migrator.ts
5226
+ /**
5227
+ * Auto-migrate module with PostgreSQL advisory lock for HA safety.
5228
+ *
5229
+ * Uses pg_advisory_lock to ensure only one orchestrator instance runs
5230
+ * migrations at a time, preventing conflicts in multi-instance deployments.
5231
+ */
5232
+ const logger$12 = createLogger({ prefix: "migrate" });
5233
+ const ADVISORY_LOCK_KEY = 543210001;
5234
+ /**
5235
+ * Run all pending migrations with an advisory lock for concurrency safety.
5236
+ *
5237
+ * Acquires pg_advisory_lock before migrating to prevent concurrent
5238
+ * migration attempts from multiple orchestrator instances. The lock
5239
+ * is always released in the finally block, even on error.
5240
+ */
5241
+ async function runMigrations(opts) {
5242
+ const client = await opts.pool.connect();
5243
+ try {
5244
+ logger$12.info("Acquiring migration lock...");
5245
+ await client.query("SELECT pg_advisory_lock($1)", [ADVISORY_LOCK_KEY]);
5246
+ logger$12.info("Migration lock acquired");
5247
+ const migrator = new Migrator({
5248
+ db: opts.db,
5249
+ provider: createMigrationProvider()
5250
+ });
5251
+ const start = Date.now();
5252
+ const { results, error } = await migrator.migrateToLatest();
5253
+ for (const result of results ?? []) if (result.status === "Success") logger$12.info(`${result.migrationName} ... OK (${Date.now() - start}ms)`);
5254
+ else if (result.status === "Error") logger$12.error(`${result.migrationName} ... FAILED`);
5255
+ if (error) throw error;
5256
+ const applied = (results ?? []).filter((r) => r.status === "Success");
5257
+ if (applied.length === 0) logger$12.info("Database schema is up to date");
5258
+ else logger$12.info(`Applied ${applied.length} migration(s)`);
5259
+ const hash = await computeMigrationsHash(createMigrationProvider());
5260
+ await storeMigrationContentHash(opts.pool, hash);
5261
+ return results ?? [];
5262
+ } finally {
5263
+ await client.query("SELECT pg_advisory_unlock($1)", [ADVISORY_LOCK_KEY]);
5264
+ client.release();
5265
+ }
5266
+ }
5267
+ //#endregion
5268
+ //#region src/cli/commands/db.ts
5269
+ /**
5270
+ * Database management commands for kici-admin.
5271
+ *
5272
+ * db migrate Run pending migrations (HTTP — orchestrator must be up)
5273
+ * db fresh DROP + CREATE + migrate + store content hash (direct DB)
5274
+ * db ensure <name> CREATE DATABASE IF NOT EXISTS (direct DB)
5275
+ * db create-role CREATE ROLE LOGIN [CREATEDB] (direct DB)
5276
+ * db create-readonly-user Read-only role + GRANT SELECT (direct DB)
5277
+ * db check-schema Compare bundled migrations vs live schema (direct DB)
5278
+ * db collation-check Check pg_database.datcollversion vs running libc (direct DB)
5279
+ * db reindex REINDEX DATABASE CONCURRENTLY (direct DB)
5280
+ * db refresh-collation-version ALTER DATABASE REFRESH COLLATION VERSION (direct DB)
5281
+ *
5282
+ * The direct-DB subcommands cannot go through HTTP because the target database
5283
+ * may not exist yet or is about to be dropped. They follow the same pattern
5284
+ * as `kici-admin peer create-token` — open a pool from KICI_DATABASE_URL /
5285
+ * DATABASE_URL / --database-url.
5286
+ */
5287
+ function resolveDatabaseUrl$3(explicit) {
5288
+ const url = explicit ?? process.env.KICI_DATABASE_URL;
5289
+ if (!url) throw new Error("Database URL required. Pass --database-url or set KICI_DATABASE_URL.");
5290
+ return url;
5291
+ }
5292
+ async function confirmInteractive$2(prompt, expected) {
5293
+ if (!process.stdin.isTTY) return false;
4921
5294
  process.stderr.write(prompt);
4922
5295
  const rl = createInterface({
4923
5296
  input: process.stdin,
@@ -4958,10 +5331,10 @@ function registerDbCommands(program, getClient) {
4958
5331
  });
4959
5332
  db.command("fresh").description("DROP + CREATE the orchestrator DB, run migrations, record content hash").option("--database-url <url>", "Target database URL (else KICI_DATABASE_URL / DATABASE_URL)").requiredOption("--confirm", "Explicit confirmation (destructive)").option("--yes", "Skip interactive confirmation (for scripted use)").action(async (opts) => {
4960
5333
  try {
4961
- const url = resolveDatabaseUrl$2(opts.databaseUrl);
5334
+ const url = resolveDatabaseUrl$3(opts.databaseUrl);
4962
5335
  const { dbName } = parseDatabaseUrl(url);
4963
5336
  if (!opts.yes) {
4964
- if (!await confirmInteractive$1(`About to DROP + RECREATE database "${dbName}". Type "${dbName}" to confirm: `, dbName)) {
5337
+ if (!await confirmInteractive$2(`About to DROP + RECREATE database "${dbName}". Type "${dbName}" to confirm: `, dbName)) {
4965
5338
  console.error("Aborted.");
4966
5339
  process.exit(1);
4967
5340
  }
@@ -4989,7 +5362,7 @@ function registerDbCommands(program, getClient) {
4989
5362
  });
4990
5363
  db.command("ensure <name>").description("CREATE DATABASE IF NOT EXISTS (idempotent)").option("--database-url <url>", "Admin DB URL (else KICI_DATABASE_URL / DATABASE_URL)").option("--owner <role>", "DB owner role (default: URL user). Pass when the admin connection is privileged but the new DB should be owned by a separate non-privileged role.").option("--revoke-connect-public", "After ensure, REVOKE CONNECT ON DATABASE \"<name>\" FROM PUBLIC (recommended on shared clusters).").option("--grant-connect-role <role>", "After ensure (and any --revoke-connect-public), GRANT CONNECT ON DATABASE \"<name>\" TO \"<role>\". Repeatable.", (val, acc) => acc.concat([val]), []).action(async (name, opts) => {
4991
5364
  try {
4992
- const baseUrl = resolveDatabaseUrl$2(opts.databaseUrl);
5365
+ const baseUrl = resolveDatabaseUrl$3(opts.databaseUrl);
4993
5366
  const url = new URL(baseUrl);
4994
5367
  url.pathname = `/${name}`;
4995
5368
  const targetUrl = url.toString();
@@ -5008,7 +5381,7 @@ function registerDbCommands(program, getClient) {
5008
5381
  });
5009
5382
  db.command("create-role").description("CREATE / ALTER ROLE with LOGIN [+ CREATEDB] (idempotent)").option("--database-url <url>", "Admin DB URL (else KICI_DATABASE_URL / DATABASE_URL)").requiredOption("--user <name>", "Role name to create or update").requiredOption("--password <password>", "Role password (raw — quote as needed)").option("--createdb", "Grant CREATEDB to the new role", false).action(async (opts) => {
5010
5383
  try {
5011
- const { adminUrl } = parseDatabaseUrl(resolveDatabaseUrl$2(opts.databaseUrl));
5384
+ const { adminUrl } = parseDatabaseUrl(resolveDatabaseUrl$3(opts.databaseUrl));
5012
5385
  logInvocation(`create-role ${opts.user}`, adminUrl);
5013
5386
  const outcome = await createDbRole(adminUrl, {
5014
5387
  username: opts.user,
@@ -5023,7 +5396,7 @@ function registerDbCommands(program, getClient) {
5023
5396
  });
5024
5397
  db.command("create-readonly-user").description("Create a read-only role with SELECT on all tables + default privileges").option("--database-url <url>", "Target DB URL (must connect as owner)").requiredOption("--user <name>", "Read-only role name").requiredOption("--password <password>", "Role password").action(async (opts) => {
5025
5398
  try {
5026
- const url = resolveDatabaseUrl$2(opts.databaseUrl);
5399
+ const url = resolveDatabaseUrl$3(opts.databaseUrl);
5027
5400
  const { dbName } = parseDatabaseUrl(url);
5028
5401
  logInvocation(`create-readonly-user ${opts.user}`, url);
5029
5402
  const outcome = await createReadOnlyDbUser(url, {
@@ -5038,7 +5411,7 @@ function registerDbCommands(program, getClient) {
5038
5411
  });
5039
5412
  db.command("check-schema").description("Compare bundled migrations vs live schema. Exit 2 on drift.").option("--database-url <url>", "Target DB URL (else KICI_DATABASE_URL / DATABASE_URL)").option("--json", "Emit JSON instead of a human-readable line", false).action(async (opts) => {
5040
5413
  try {
5041
- const pool = createPool(resolveDatabaseUrl$2(opts.databaseUrl));
5414
+ const pool = createPool(resolveDatabaseUrl$3(opts.databaseUrl));
5042
5415
  try {
5043
5416
  const status = await isSchemaCurrent(pool, createMigrationProvider());
5044
5417
  if (opts.json) process.stdout.write(JSON.stringify(status) + "\n");
@@ -5055,7 +5428,7 @@ function registerDbCommands(program, getClient) {
5055
5428
  });
5056
5429
  db.command("collation-check").description("Compare pg_database.datcollversion against the running libc collation version. Exit 2 on drift.").option("--database-url <url>", "Target DB URL (else KICI_DATABASE_URL / DATABASE_URL)").option("--json", "Emit JSON instead of a human-readable line", false).action(async (opts) => {
5057
5430
  try {
5058
- const url = resolveDatabaseUrl$2(opts.databaseUrl);
5431
+ const url = resolveDatabaseUrl$3(opts.databaseUrl);
5059
5432
  const { dbName } = parseDatabaseUrl(url);
5060
5433
  const pool = createPool(url);
5061
5434
  try {
@@ -5085,7 +5458,7 @@ function registerDbCommands(program, getClient) {
5085
5458
  });
5086
5459
  db.command("reindex").description("REINDEX DATABASE CONCURRENTLY <db>. Rebuilds every index under the running libc collation rules. Non-blocking but takes minutes + ~2× temp disk.").option("--database-url <url>", "Target DB URL (else KICI_DATABASE_URL / DATABASE_URL)").requiredOption("--confirm", "Explicit confirmation (destructive — long-running)").requiredOption("--reason <text>", "Reason (recorded in stderr banner)").action(async (opts) => {
5087
5460
  try {
5088
- const url = resolveDatabaseUrl$2(opts.databaseUrl);
5461
+ const url = resolveDatabaseUrl$3(opts.databaseUrl);
5089
5462
  const { dbName } = parseDatabaseUrl(url);
5090
5463
  logInvocation(`reindex ${dbName} (${opts.reason})`, url);
5091
5464
  const pool = createPool(url);
@@ -5102,7 +5475,7 @@ function registerDbCommands(program, getClient) {
5102
5475
  });
5103
5476
  db.command("refresh-collation-version").description("ALTER DATABASE <db> REFRESH COLLATION VERSION. Metadata-only bump; pair with db reindex after a libc-base image rebuild.").option("--database-url <url>", "Target DB URL (else KICI_DATABASE_URL / DATABASE_URL)").requiredOption("--reason <text>", "Reason (recorded in stderr banner)").action(async (opts) => {
5104
5477
  try {
5105
- const url = resolveDatabaseUrl$2(opts.databaseUrl);
5478
+ const url = resolveDatabaseUrl$3(opts.databaseUrl);
5106
5479
  const { dbName } = parseDatabaseUrl(url);
5107
5480
  logInvocation(`refresh-collation-version ${dbName} (${opts.reason})`, url);
5108
5481
  const pool = createPool(url);
@@ -6540,7 +6913,7 @@ async function getRemoteSource(db, orgId) {
6540
6913
  //#endregion
6541
6914
  //#region src/cli/commands/remote-source.ts
6542
6915
  init_client();
6543
- function resolveDatabaseUrl$1(explicit) {
6916
+ function resolveDatabaseUrl$2(explicit) {
6544
6917
  const url = explicit ?? process.env.KICI_DATABASE_URL;
6545
6918
  if (!url) throw new Error("Database URL required. Pass --database-url or set KICI_DATABASE_URL.");
6546
6919
  return url;
@@ -6549,7 +6922,7 @@ function registerRemoteSourceCommands(program) {
6549
6922
  program.command("remote-source").description("Inspect the auto-provisioned remote-source org anchor").command("show <orgId>").description("Print the remote_sources anchor row for an org (routing key remote:<orgId>).").option("--database-url <url>", "Orchestrator DB URL (else KICI_DATABASE_URL)").option("--format <format>", "Output format: json|table", "table").action(async (orgId, opts) => {
6550
6923
  let url;
6551
6924
  try {
6552
- url = resolveDatabaseUrl$1(opts.databaseUrl);
6925
+ url = resolveDatabaseUrl$2(opts.databaseUrl);
6553
6926
  } catch (err) {
6554
6927
  console.error(`Error: ${toErrorMessage(err)}`);
6555
6928
  process.exit(1);
@@ -6911,6 +7284,64 @@ function registerRunsCommands(program, getClient) {
6911
7284
  process.exit(1);
6912
7285
  }
6913
7286
  });
7287
+ runs.command("structured <runId>").description("Show the provenance-tagged structured run result (agent read path; /structured)").option("--json", "Emit the raw AgentRunResult (untrusted envelopes preserved)").action(async (runId, opts) => {
7288
+ try {
7289
+ const result = await getClient().getRunStructured(runId);
7290
+ if (opts.json) {
7291
+ console.log(JSON.stringify(result, null, 2));
7292
+ return;
7293
+ }
7294
+ const uv = (v) => v == null ? "-" : typeof v === "object" ? v.value : String(v);
7295
+ console.log(`Run: ${result.runId}`);
7296
+ console.log(` Workflow: ${uv(result.workflowName)}`);
7297
+ console.log(` Status: ${result.status}`);
7298
+ console.log(` Failure: ${result.failureCategory ?? "-"}`);
7299
+ if (result.failureReason) console.log(` Reason: ${uv(result.failureReason)}`);
7300
+ console.log(` Repo: ${uv(result.repoIdentifier)}`);
7301
+ console.log(` Ref: ${uv(result.ref)}`);
7302
+ console.log(` SHA: ${result.sha}`);
7303
+ console.log(` Duration: ${formatDuration(result.durationMs)}`);
7304
+ if (result.jobs.length === 0) {
7305
+ console.log("\nNo jobs.");
7306
+ return;
7307
+ }
7308
+ console.log("");
7309
+ const jobHeaders = [
7310
+ "job",
7311
+ "status",
7312
+ "failed_steps"
7313
+ ];
7314
+ const jobRows = result.jobs.map((j) => [
7315
+ uv(j.jobName),
7316
+ j.status,
7317
+ String(j.steps.filter((s) => s.exitCode != null && s.exitCode !== 0).length)
7318
+ ]);
7319
+ console.log(renderTable$2(jobHeaders, jobRows));
7320
+ for (const job of result.jobs) {
7321
+ if (job.steps.length === 0) continue;
7322
+ console.log("");
7323
+ console.log(`Steps for ${uv(job.jobName)}:`);
7324
+ const stepHeaders = [
7325
+ "#",
7326
+ "name",
7327
+ "status",
7328
+ "exit",
7329
+ "duration"
7330
+ ];
7331
+ const stepRows = job.steps.map((s) => [
7332
+ String(s.stepIndex),
7333
+ uv(s.stepName),
7334
+ s.status,
7335
+ s.exitCode != null ? String(s.exitCode) : "-",
7336
+ formatDuration(s.durationMs)
7337
+ ]);
7338
+ console.log(renderTable$2(stepHeaders, stepRows));
7339
+ }
7340
+ } catch (err) {
7341
+ console.error(`Error: ${toErrorMessage(err)}`);
7342
+ process.exit(1);
7343
+ }
7344
+ });
6914
7345
  runs.command("jobs <runId>").description("List jobs for a run (dogfooded via /api/v1/admin/runs/:runId/jobs)").option("--include-steps", "Embed step list inside each job (default false)").option("--json", "Emit raw JSON instead of a table").action(async (runId, opts) => {
6915
7346
  try {
6916
7347
  const response = await getClient().getRunJobs(runId, { includeSteps: Boolean(opts.includeSteps) });
@@ -6961,67 +7392,1298 @@ function registerRunsCommands(program, getClient) {
6961
7392
  console.log(renderTable$2(stepHeaders, stepRows));
6962
7393
  }
6963
7394
  } catch (err) {
6964
- console.error(`Error: ${toErrorMessage(err)}`);
6965
- process.exit(1);
7395
+ console.error(`Error: ${toErrorMessage(err)}`);
7396
+ process.exit(1);
7397
+ }
7398
+ });
7399
+ runs.command("ephemeral-key <runId>").description("Show whether the run-ephemeral key has been scrubbed yet").option("--json", "Emit raw JSON instead of plain text").action(async (runId, opts) => {
7400
+ try {
7401
+ const response = await getClient().getRunEphemeralKey(runId);
7402
+ if (opts.json) {
7403
+ console.log(JSON.stringify(response, null, 2));
7404
+ return;
7405
+ }
7406
+ if (response.exists) {
7407
+ console.log(`exists: true`);
7408
+ console.log(`created_at: ${response.createdAt}`);
7409
+ } else {
7410
+ console.log(`exists: false`);
7411
+ console.log(`created_at: -`);
7412
+ }
7413
+ } catch (err) {
7414
+ console.error(`Error: ${toErrorMessage(err)}`);
7415
+ process.exit(1);
7416
+ }
7417
+ });
7418
+ runs.command("secret-outputs <runId>").description("List per-job secret outputs (masked by default; --reveal decrypts and audits)").option("--output-key <key>", "Filter to a single output_key").option("--reveal", "Decrypt and print plaintext values. Audited with actor=secret-outputs.reveal; requires secret.reveal permission").option("--json", "Emit raw JSON instead of a table").action(async (runId, opts) => {
7419
+ try {
7420
+ if (opts.reveal) process.stderr.write("⚠ --reveal will decrypt secret values; this call is recorded in secret_audit_log.\n");
7421
+ const response = await getClient().getRunSecretOutputs(runId, {
7422
+ outputKey: opts.outputKey,
7423
+ reveal: Boolean(opts.reveal)
7424
+ });
7425
+ if (opts.json) {
7426
+ console.log(JSON.stringify(response, null, 2));
7427
+ return;
7428
+ }
7429
+ if (response.outputs.length === 0) {
7430
+ console.log("No secret outputs for this run.");
7431
+ return;
7432
+ }
7433
+ const headers = opts.reveal ? [
7434
+ "job_id",
7435
+ "output_key",
7436
+ "value",
7437
+ "created_at"
7438
+ ] : [
7439
+ "job_id",
7440
+ "output_key",
7441
+ "masked",
7442
+ "created_at"
7443
+ ];
7444
+ const rows = response.outputs.map((o) => opts.reveal ? [
7445
+ o.jobId,
7446
+ o.outputKey,
7447
+ o.value ?? o.revealError ?? "(decrypt failed)",
7448
+ o.createdAt
7449
+ ] : [
7450
+ o.jobId,
7451
+ o.outputKey,
7452
+ String(o.masked),
7453
+ o.createdAt
7454
+ ]);
7455
+ console.log(renderTable$2(headers, rows));
7456
+ } catch (err) {
7457
+ console.error(`Error: ${toErrorMessage(err)}`);
7458
+ process.exit(1);
7459
+ }
7460
+ });
7461
+ }
7462
+ //#endregion
7463
+ //#region src/config.ts
7464
+ /**
7465
+ * Orchestrator configuration.
7466
+ *
7467
+ * This is a thin backward-compatible wrapper around the new config resolution
7468
+ * chain (config/resolver.ts). It preserves the existing synchronous loadConfig()
7469
+ * API so that no other files need changing.
7470
+ *
7471
+ * The new config system lives in config/ and supports:
7472
+ * - YAML config files (config/loader.ts)
7473
+ * - Shared DB config store (config/shared-store.ts)
7474
+ * - 4-layer resolution: env > YAML > DB > defaults (config/resolver.ts)
7475
+ * - KICI_ env var mapping (config/env-overlay.ts)
7476
+ *
7477
+ * Provider configuration (GitHub Apps, etc.) is managed via the `sources` table
7478
+ * and PgSecretStore, not through config. See SourceStore and SourceManager.
7479
+ */
7480
+ const baseSchema = z.object({
7481
+ mode: z.enum([
7482
+ "platform",
7483
+ "hybrid",
7484
+ "independent"
7485
+ ]).default("platform"),
7486
+ port: z.coerce.number().default(4e3),
7487
+ basePath: z.string().default("/"),
7488
+ tlsCertPath: z.string().optional(),
7489
+ platformUrl: z.string().optional(),
7490
+ platformToken: z.string().optional(),
7491
+ /**
7492
+ * Base URL of the user-facing dashboard. Used to build `details_url`
7493
+ * on GitHub Check Runs and similar outbound URLs that reach public
7494
+ * surfaces — the `oal_<12-char>` public alias is appended to this
7495
+ * base. When unset, no `details_url` is emitted (preserving today's
7496
+ * behavior). Trailing slash optional.
7497
+ */
7498
+ dashboardUrl: z.string().optional(),
7499
+ /**
7500
+ * Provenance trust root (the OIDC issuer) used to verify build-provenance
7501
+ * bundles. The live process learns this over the Platform `auth.success`
7502
+ * connect message; this config/env value is the source for the CLI backfill
7503
+ * (`kici-admin attestations reverify`), which has no live handshake. When
7504
+ * unset, the orchestrator records attestation verdicts as `unverifiable`.
7505
+ */
7506
+ provenanceIssuer: z.string().optional(),
7507
+ /**
7508
+ * Public base URL at which this orchestrator's own webhook ingress is
7509
+ * reachable (independent/hybrid self-serve generic webhooks:
7510
+ * `<base>/webhook/<customerId>/generic/<sourceId>`). Used by
7511
+ * `kici-admin source add` to print a generic source's webhook URL. GitHub-App
7512
+ * ingress is Platform-relayed, so GitHub URLs come from the Platform's
7513
+ * `source.register.ack`, not this value. Trailing slash optional.
7514
+ */
7515
+ webhookPublicUrl: z.string().optional(),
7516
+ databaseUrl: z.string().default(""),
7517
+ lockfileCacheMax: z.coerce.number().default(500),
7518
+ lockfileCacheTtlMs: z.coerce.number().default(36e5),
7519
+ queueMaxDepth: z.coerce.number().default(1e3),
7520
+ queueTimeoutMs: z.coerce.number().default(36e5),
7521
+ /**
7522
+ * Operator-facing backpressure warning threshold for the dispatch queue.
7523
+ * When pending-queue depth stays at or above this value for at least two
7524
+ * consecutive refresher ticks (~10s), the orchestrator emits a
7525
+ * `logger.warn` pointing operators at the per-label Grafana panel so
7526
+ * they can identify which label pool is starved. `0` disables the
7527
+ * warner entirely — metrics are still exported, but the periodic warn
7528
+ * is silenced. See docs/operator/monitoring.md for tuning guidance.
7529
+ */
7530
+ queueBackpressureThreshold: z.coerce.number().default(100),
7531
+ workerConcurrency: z.coerce.number().default(5),
7532
+ concurrencyWaitTimeoutMs: z.coerce.number().int().min(1e3).default(36e5),
7533
+ dispatchAckTimeoutMs: z.coerce.number().int().min(1e3).default(1e4),
7534
+ cacheStorageType: z.enum(["s3", "filesystem"]).optional(),
7535
+ cacheStoragePath: z.string().optional(),
7536
+ cacheStorageS3Bucket: z.string().optional(),
7537
+ cacheStorageS3Prefix: z.string().default(""),
7538
+ cacheStorageS3Region: z.string().optional(),
7539
+ cacheStorageS3Endpoint: z.string().optional(),
7540
+ cacheStorageS3ExternalEndpoint: z.string().optional(),
7541
+ cacheStorageS3UploadEndpoint: z.string().optional(),
7542
+ cacheStorageS3ForcePathStyle: z.enum(["true", "false"]).transform((v) => v === "true").optional(),
7543
+ cacheStorageFsPath: z.string().optional(),
7544
+ cacheStorageFsBaseUrl: z.string().optional(),
7545
+ logStorageS3Bucket: z.string().optional(),
7546
+ cacheTtlDays: z.coerce.number().default(30),
7547
+ cacheBuildTimeoutMs: z.coerce.number().default(6e5),
7548
+ cacheMaxTarballBytes: z.coerce.number().default(524288e3),
7549
+ userCacheQuotaBytes: z.coerce.number().int().positive().default(5 * 1024 * 1024 * 1024).describe("Cluster-wide default per-org byte quota for the user-facing cache (ctx.cache). A per-org override in org_settings.user_cache_quota_bytes (set via `kici-admin org-settings user-cache set-quota`) takes precedence when present."),
7550
+ userCacheTtlMs: z.coerce.number().int().positive().default(10080 * 60 * 1e3).describe("Cluster-wide default per-entry TTL (ms) for the user-facing cache. A per-org override in org_settings.user_cache_ttl_ms (set via `kici-admin org-settings user-cache set-ttl`) takes precedence when present."),
7551
+ webhookPayloadDir: z.string().optional(),
7552
+ dataDir: z.string().optional(),
7553
+ scalerConfigPath: z.string().optional(),
7554
+ scalerConfigDir: z.string().optional(),
7555
+ machineLedgerDir: z.string().optional(),
7556
+ staleDetectorScanIntervalMs: z.coerce.number().default(6e4),
7557
+ staleDetectorThresholdMultiplier: z.coerce.number().default(2),
7558
+ jobHeartbeatIntervalMs: z.coerce.number().default(6e4),
7559
+ githubAppNameRefreshIntervalMs: z.coerce.number().default(864e5),
7560
+ secretKey: z.string().optional(),
7561
+ secretKeyFile: z.string().optional(),
7562
+ secretKeyOld: z.string().optional(),
7563
+ secretKeyFileOld: z.string().optional(),
7564
+ bootstrapAdminToken: z.string().optional(),
7565
+ pgCustomerSecrets: z.enum(["true", "false"]).default("true").transform((v) => v === "true"),
7566
+ agentAuth: z.enum(["token", "none"]).default("token"),
7567
+ agentTokenTtlMs: z.coerce.number().default(36e5),
7568
+ rosterGraceMs: z.coerce.number().int().min(1e3).default(3e5),
7569
+ rosterTtlMs: z.coerce.number().int().min(1e3).default(18e5),
7570
+ hostRebootDeadlineMs: z.coerce.number().int().min(1e3).default(9e5),
7571
+ orchestratorHostAgentId: z.string().optional(),
7572
+ maxFanoutHosts: z.coerce.number().int().min(1).default(1024),
7573
+ eventRouterMaxChainDepth: z.coerce.number().default(10),
7574
+ eventRouterRateLimitPerWorkflowPerMinute: z.coerce.number().default(100),
7575
+ eventRouterEventTtlSeconds: z.coerce.number().default(604800),
7576
+ eventRouterCleanupIntervalMs: z.coerce.number().default(36e5),
7577
+ eventRouterMaxDispatchAttempts: z.coerce.number().default(5),
7578
+ eventRouterLeaseDurationMs: z.coerce.number().default(6e4),
7579
+ eventRouterRetryBaseBackoffMs: z.coerce.number().default(5e3),
7580
+ eventRouterRetryMaxBackoffMs: z.coerce.number().default(3e5),
7581
+ eventRouterRetryScanIntervalMs: z.coerce.number().default(1e4),
7582
+ /**
7583
+ * **Test-only.** Master switch for fault-injection knobs in the event
7584
+ * dispatch pipeline. When `false` (default) the orchestrator ignores
7585
+ * every test-only knob below, even if its env var is set. Pair with
7586
+ * `KICI_TEST_EVENT_FAIL_FIRST_N` to drive the E2E retry / DLQ
7587
+ * scenarios. Production deployments leave this at its default.
7588
+ */
7589
+ testMode: z.string().default("0").transform((v) => v === "1" || v.toLowerCase() === "true"),
7590
+ /**
7591
+ * **Test-only.** JSON map of `{ "<eventName>": <N> }` instructing the
7592
+ * EventRouter to throw a synthetic dispatch error while
7593
+ * `event.attempts <= N`. Ignored unless `KICI_TEST_MODE=1`.
7594
+ */
7595
+ testEventFailFirstN: z.string().optional(),
7596
+ eventLogMaxPayloadBytes: z.coerce.number().default(5 * 1024 * 1024),
7597
+ logLevel: z.enum([
7598
+ "debug",
7599
+ "info",
7600
+ "warn",
7601
+ "error"
7602
+ ]).default("info"),
7603
+ nodeEnv: z.enum([
7604
+ "development",
7605
+ "production",
7606
+ "test"
7607
+ ]).default("development"),
7608
+ autoMigrate: z.string().default("true").transform((v) => v !== "false"),
7609
+ agentMaxReconnectDelayMs: z.coerce.number().default(6e4),
7610
+ skipS3SentinelValidation: z.string().default("false").transform((v) => v === "true"),
7611
+ otelExporterOtlpEndpoint: z.string().optional(),
7612
+ clusterName: z.string().optional(),
7613
+ cluster: z.object({
7614
+ /** This orchestrator's unique instance ID. Default: random UUID. */
7615
+ instanceId: z.string().optional().default(() => randomUUID()),
7616
+ /** This orchestrator's address for peers to connect to. Required when peers are configured. */
7617
+ address: z.string().optional(),
7618
+ /** Join token for cluster bootstrap. */
7619
+ joinToken: z.string().optional(),
7620
+ /** Path to peer credential file. */
7621
+ credentialFile: z.string().default("~/.kici/peer-credential"),
7622
+ /** Auto-rotate credentials. */
7623
+ autoRotateCredentials: z.boolean().default(false),
7624
+ /** Static peer addresses for independent mode. Comma-separated URLs. */
7625
+ peers: z.string().optional().transform((v) => v ? v.split(",").map((s) => s.trim()) : []),
7626
+ /** Raft election timeout minimum in ms. Default: 5000. */
7627
+ raftElectionTimeoutMinMs: z.coerce.number().default(5e3),
7628
+ /** Raft election timeout maximum in ms. Default: 10000. */
7629
+ raftElectionTimeoutMaxMs: z.coerce.number().default(1e4),
7630
+ /** Raft leader heartbeat interval in ms. Default: 2000. */
7631
+ raftHeartbeatMs: z.coerce.number().default(2e3),
7632
+ /** Peer heartbeat interval in ms (inventory broadcast). Default: 30000. */
7633
+ peerHeartbeatIntervalMs: z.coerce.number().default(3e4),
7634
+ /** Maximum peer reconnect delay in ms. Default: 60000. */
7635
+ peerMaxReconnectDelayMs: z.coerce.number().default(6e4),
7636
+ /** Cluster role: coordinator (full orchestrator) or worker (delegated execution). Default: coordinator. */
7637
+ role: z.enum(["coordinator", "worker"]).default("coordinator"),
7638
+ /** URL of the coordinator to connect to when role=worker. Single-coord mode. */
7639
+ coordinatorUrl: z.string().optional(),
7640
+ /**
7641
+ * URLs of all coordinators to connect to when role=worker (comma-separated).
7642
+ * Worker maintains one outbound PeerClient per coord so every coord can
7643
+ * route work to it. Takes precedence over coordinatorUrl when both are set.
7644
+ */
7645
+ coordinatorUrls: z.string().optional().transform((v) => v ? v.split(",").map((s) => s.trim()).filter(Boolean) : []),
7646
+ /** Stale peer timeout in ms (2 missed heartbeats at 30s = 60s default). */
7647
+ peerStaleTimeoutMs: z.coerce.number().default(6e4),
7648
+ /** Grace period before dormant-mode self-election (0 peers). Default: 60000ms.
7649
+ * Prevents false self-election during the peer discovery window. */
7650
+ electionGracePeriodMs: z.coerce.number().default(6e4),
7651
+ /** Single-node deployment mode. When true, election grace period is bypassed
7652
+ * for immediate self-election. Default: false. */
7653
+ singleNode: z.union([z.boolean(), z.string()]).default(false).transform((v) => typeof v === "boolean" ? v : v === "true"),
7654
+ /** Trusted proxy IPs/CIDRs for X-Forwarded-For/X-Real-IP extraction. Comma-separated. */
7655
+ trustedProxies: z.string().default("").transform((v) => v ? v.split(",").map((s) => s.trim()) : [])
7656
+ }).prefault({})
7657
+ });
7658
+ /**
7659
+ * Env-var definition for the orchestrator. Exported so the docs generator and
7660
+ * the deploy-stg pre-validator can re-parse without going through process.env.
7661
+ *
7662
+ * Note: passes the inner `baseSchema` (a ZodObject) so describe() can walk
7663
+ * `.shape`, but uses the outer `configSchema` (with .superRefine) as the parser
7664
+ * so cross-field rules still fire.
7665
+ */
7666
+ const envDef = defineEnv({
7667
+ service: "orchestrator",
7668
+ schema: baseSchema,
7669
+ parser: baseSchema.superRefine((data, ctx) => {
7670
+ const isWorker = data.cluster.role === "worker";
7671
+ if (isWorker && !data.cluster.coordinatorUrl && (!data.cluster.coordinatorUrls || data.cluster.coordinatorUrls.length === 0)) ctx.addIssue({
7672
+ code: z.ZodIssueCode.custom,
7673
+ message: "KICI_CLUSTER_COORDINATOR_URL or KICI_CLUSTER_COORDINATOR_URLS is required when KICI_CLUSTER_ROLE=worker",
7674
+ path: ["cluster", "coordinatorUrls"]
7675
+ });
7676
+ if (!isWorker && !data.databaseUrl) ctx.addIssue({
7677
+ code: z.ZodIssueCode.custom,
7678
+ message: "KICI_DATABASE_URL is required for coordinator mode",
7679
+ path: ["databaseUrl"]
7680
+ });
7681
+ if (!isWorker && (data.mode === "platform" || data.mode === "hybrid")) {
7682
+ if (!data.platformUrl) ctx.addIssue({
7683
+ code: z.ZodIssueCode.custom,
7684
+ message: "KICI_PLATFORM_URL is required when KICI_MODE is platform or hybrid",
7685
+ path: ["platformUrl"]
7686
+ });
7687
+ if (!data.platformToken) ctx.addIssue({
7688
+ code: z.ZodIssueCode.custom,
7689
+ message: "KICI_PLATFORM_TOKEN is required when KICI_MODE is platform or hybrid",
7690
+ path: ["platformToken"]
7691
+ });
7692
+ }
7693
+ if (data.cacheStorageType === "s3" && !data.cacheStorageS3Bucket) ctx.addIssue({
7694
+ code: z.ZodIssueCode.custom,
7695
+ message: "KICI_STORAGE_BUCKET is required when KICI_STORAGE_TYPE is s3",
7696
+ path: ["cacheStorageS3Bucket"]
7697
+ });
7698
+ if (data.cacheStorageType === "filesystem") {
7699
+ if (!data.cacheStorageFsPath) ctx.addIssue({
7700
+ code: z.ZodIssueCode.custom,
7701
+ message: "KICI_STORAGE_FS_PATH is required when KICI_STORAGE_TYPE is filesystem",
7702
+ path: ["cacheStorageFsPath"]
7703
+ });
7704
+ else if (!data.cacheStorageFsPath.startsWith("/")) ctx.addIssue({
7705
+ code: z.ZodIssueCode.custom,
7706
+ message: "KICI_STORAGE_FS_PATH must be an absolute path",
7707
+ path: ["cacheStorageFsPath"]
7708
+ });
7709
+ }
7710
+ if (data.cluster.peers.length > 0 && !data.cluster.address) ctx.addIssue({
7711
+ code: z.ZodIssueCode.custom,
7712
+ message: "KICI_CLUSTER_ADDRESS is required when KICI_CLUSTER_PEERS is set (peers need to know where to connect back)",
7713
+ path: ["cluster", "address"]
7714
+ });
7715
+ }),
7716
+ envMap: {
7717
+ mode: "KICI_MODE",
7718
+ port: "KICI_PORT",
7719
+ basePath: "KICI_BASE_PATH",
7720
+ tlsCertPath: "KICI_SERVER_TLS_CERT_PATH",
7721
+ platformUrl: "KICI_PLATFORM_URL",
7722
+ platformToken: "KICI_PLATFORM_TOKEN",
7723
+ dashboardUrl: "KICI_DASHBOARD_URL",
7724
+ provenanceIssuer: "KICI_PROVENANCE_ISSUER",
7725
+ webhookPublicUrl: "KICI_WEBHOOK_PUBLIC_URL",
7726
+ databaseUrl: "KICI_DATABASE_URL",
7727
+ cacheStorageType: "KICI_STORAGE_TYPE",
7728
+ cacheStoragePath: "KICI_STORAGE_PATH",
7729
+ cacheStorageS3Bucket: "KICI_STORAGE_BUCKET",
7730
+ cacheStorageS3Prefix: "KICI_STORAGE_PREFIX",
7731
+ cacheStorageS3Region: "KICI_STORAGE_REGION",
7732
+ cacheStorageS3Endpoint: "KICI_STORAGE_ENDPOINT",
7733
+ cacheStorageS3ExternalEndpoint: "KICI_STORAGE_EXTERNAL_ENDPOINT",
7734
+ cacheStorageS3UploadEndpoint: "KICI_STORAGE_UPLOAD_ENDPOINT",
7735
+ cacheStorageS3ForcePathStyle: "KICI_STORAGE_FORCE_PATH_STYLE",
7736
+ cacheStorageFsPath: "KICI_STORAGE_FS_PATH",
7737
+ cacheStorageFsBaseUrl: "KICI_STORAGE_FS_BASE_URL",
7738
+ logStorageS3Bucket: "KICI_STORAGE_LOG_BUCKET",
7739
+ cacheTtlDays: "KICI_CACHE_TTL_DAYS",
7740
+ cacheBuildTimeoutMs: "KICI_CACHE_BUILD_TIMEOUT_MS",
7741
+ cacheMaxTarballBytes: "KICI_CACHE_MAX_TARBALL_BYTES",
7742
+ userCacheQuotaBytes: "KICI_USER_CACHE_QUOTA_BYTES",
7743
+ userCacheTtlMs: "KICI_USER_CACHE_TTL_MS",
7744
+ lockfileCacheMax: "KICI_LOCKFILE_CACHE_MAX",
7745
+ lockfileCacheTtlMs: "KICI_LOCKFILE_CACHE_TTL_MS",
7746
+ queueMaxDepth: "KICI_QUEUE_MAX_DEPTH",
7747
+ queueTimeoutMs: "KICI_QUEUE_TIMEOUT_MS",
7748
+ queueBackpressureThreshold: "KICI_QUEUE_BACKPRESSURE_THRESHOLD",
7749
+ workerConcurrency: "KICI_WORKER_CONCURRENCY",
7750
+ concurrencyWaitTimeoutMs: "KICI_CONCURRENCY_WAIT_TIMEOUT_MS",
7751
+ dispatchAckTimeoutMs: "KICI_DISPATCH_ACK_TIMEOUT_MS",
7752
+ webhookPayloadDir: "KICI_WEBHOOK_PAYLOAD_DIR",
7753
+ dataDir: "KICI_DATA_DIR",
7754
+ scalerConfigPath: "KICI_SCALER_CONFIG_PATH",
7755
+ scalerConfigDir: "KICI_SCALER_CONFIG_DIR",
7756
+ machineLedgerDir: "KICI_MACHINE_LEDGER_DIR",
7757
+ staleDetectorScanIntervalMs: "KICI_STALE_DETECTOR_SCAN_INTERVAL_MS",
7758
+ staleDetectorThresholdMultiplier: "KICI_STALE_DETECTOR_THRESHOLD_MULTIPLIER",
7759
+ jobHeartbeatIntervalMs: "KICI_JOB_HEARTBEAT_INTERVAL_MS",
7760
+ githubAppNameRefreshIntervalMs: "KICI_GITHUB_APP_NAME_REFRESH_INTERVAL_MS",
7761
+ secretKey: "KICI_SECRET_KEY",
7762
+ secretKeyFile: "KICI_SECRET_KEY_FILE",
7763
+ secretKeyOld: "KICI_SECRET_KEY_OLD",
7764
+ secretKeyFileOld: "KICI_SECRET_KEY_FILE_OLD",
7765
+ bootstrapAdminToken: "KICI_BOOTSTRAP_ADMIN_TOKEN",
7766
+ pgCustomerSecrets: "KICI_PG_CUSTOMER_SECRETS",
7767
+ agentAuth: "KICI_AGENT_AUTH",
7768
+ agentTokenTtlMs: "KICI_AGENT_TOKEN_TTL_MS",
7769
+ rosterGraceMs: "KICI_ROSTER_GRACE_MS",
7770
+ rosterTtlMs: "KICI_ROSTER_TTL_MS",
7771
+ hostRebootDeadlineMs: "KICI_HOST_REBOOT_DEADLINE_MS",
7772
+ orchestratorHostAgentId: "KICI_ORCHESTRATOR_HOST_AGENT_ID",
7773
+ maxFanoutHosts: "KICI_MAX_FANOUT_HOSTS",
7774
+ eventRouterMaxChainDepth: "KICI_EVENT_ROUTER_MAX_CHAIN_DEPTH",
7775
+ eventRouterRateLimitPerWorkflowPerMinute: "KICI_EVENT_ROUTER_RATE_LIMIT_PER_WORKFLOW_PER_MINUTE",
7776
+ eventRouterEventTtlSeconds: "KICI_EVENT_ROUTER_EVENT_TTL_SECONDS",
7777
+ eventRouterCleanupIntervalMs: "KICI_EVENT_ROUTER_CLEANUP_INTERVAL_MS",
7778
+ eventRouterMaxDispatchAttempts: "KICI_EVENT_ROUTER_MAX_DISPATCH_ATTEMPTS",
7779
+ eventRouterLeaseDurationMs: "KICI_EVENT_ROUTER_LEASE_DURATION_MS",
7780
+ eventRouterRetryBaseBackoffMs: "KICI_EVENT_ROUTER_RETRY_BASE_BACKOFF_MS",
7781
+ eventRouterRetryMaxBackoffMs: "KICI_EVENT_ROUTER_RETRY_MAX_BACKOFF_MS",
7782
+ eventRouterRetryScanIntervalMs: "KICI_EVENT_ROUTER_RETRY_SCAN_INTERVAL_MS",
7783
+ testMode: "KICI_TEST_MODE",
7784
+ testEventFailFirstN: "KICI_TEST_EVENT_FAIL_FIRST_N",
7785
+ eventLogMaxPayloadBytes: "KICI_EVENT_LOG_MAX_PAYLOAD_BYTES",
7786
+ logLevel: "KICI_LOG_LEVEL",
7787
+ nodeEnv: "NODE_ENV",
7788
+ autoMigrate: "KICI_AUTO_MIGRATE",
7789
+ agentMaxReconnectDelayMs: "KICI_AGENT_MAX_RECONNECT_DELAY_MS",
7790
+ skipS3SentinelValidation: "KICI_SKIP_S3_SENTINEL_VALIDATION",
7791
+ otelExporterOtlpEndpoint: "OTEL_EXPORTER_OTLP_ENDPOINT",
7792
+ clusterName: "KICI_CLUSTER_NAME",
7793
+ cluster: {
7794
+ instanceId: "KICI_CLUSTER_INSTANCE_ID",
7795
+ address: "KICI_CLUSTER_ADDRESS",
7796
+ joinToken: "KICI_CLUSTER_JOIN_TOKEN",
7797
+ credentialFile: "KICI_CLUSTER_CREDENTIAL_FILE",
7798
+ peers: "KICI_CLUSTER_PEERS",
7799
+ raftElectionTimeoutMinMs: "KICI_CLUSTER_RAFT_ELECTION_TIMEOUT_MIN_MS",
7800
+ raftElectionTimeoutMaxMs: "KICI_CLUSTER_RAFT_ELECTION_TIMEOUT_MAX_MS",
7801
+ raftHeartbeatMs: "KICI_CLUSTER_RAFT_HEARTBEAT_MS",
7802
+ peerHeartbeatIntervalMs: "KICI_CLUSTER_PEER_HEARTBEAT_INTERVAL_MS",
7803
+ peerMaxReconnectDelayMs: "KICI_CLUSTER_PEER_MAX_RECONNECT_DELAY_MS",
7804
+ role: "KICI_CLUSTER_ROLE",
7805
+ coordinatorUrl: "KICI_CLUSTER_COORDINATOR_URL",
7806
+ coordinatorUrls: "KICI_CLUSTER_COORDINATOR_URLS",
7807
+ peerStaleTimeoutMs: "KICI_CLUSTER_PEER_STALE_TIMEOUT_MS",
7808
+ electionGracePeriodMs: "KICI_CLUSTER_ELECTION_GRACE_PERIOD_MS",
7809
+ singleNode: "KICI_CLUSTER_SINGLE_NODE",
7810
+ trustedProxies: "KICI_CLUSTER_TRUSTED_PROXIES"
7811
+ }
7812
+ }
7813
+ });
7814
+ /**
7815
+ * Load orchestrator configuration from environment variables.
7816
+ *
7817
+ * This is the LEGACY synchronous config loader. It reads directly from process.env
7818
+ * using the env var names declared in `envDef` (KICI_DATABASE_URL, KICI_PORT, etc.).
7819
+ *
7820
+ * Provider configuration (GitHub Apps) is no longer loaded from env vars.
7821
+ * Use the sources table and SourceManager instead.
7822
+ *
7823
+ * For new deployments, use resolveLocalConfig() + resolveFullConfig() from
7824
+ * config/resolver.ts which support YAML files, KICI_ env var prefixes, and
7825
+ * the shared DB config store.
7826
+ */
7827
+ /**
7828
+ * Cold-store env vars consumed by `OrchestratorColdStore` directly via
7829
+ * `process.env` (not threaded through the AppConfig schema). Registered
7830
+ * here so the unknown-KICI_* validator at boot doesn't reject them.
7831
+ * Per-table tuning mirrors `knownTables` in `orchestrator-cold-store.ts`.
7832
+ */
7833
+ const COLD_STORE_ENV_VARS = [
7834
+ "KICI_COLD_STORE_ENABLED",
7835
+ "KICI_COLD_STORE_BUCKET",
7836
+ "KICI_COLD_STORE_PREFIX",
7837
+ "KICI_COLD_STORE_REGION",
7838
+ "KICI_COLD_STORE_ENDPOINT",
7839
+ "KICI_COLD_STORE_EXTERNAL_ENDPOINT",
7840
+ "KICI_COLD_STORE_FORCE_PATH_STYLE",
7841
+ "KICI_COLD_STORE_S3_CONCURRENCY",
7842
+ "KICI_COLD_STORE_EXECUTION_RUNS_WARM_TTL_DAYS",
7843
+ "KICI_COLD_STORE_EXECUTION_RUNS_MIN_WARM_TENANT_BYTES",
7844
+ "KICI_COLD_STORE_EXECUTION_RUNS_MIN_CHUNK_BYTES",
7845
+ "KICI_COLD_STORE_EXECUTION_RUNS_MAX_CHUNK_BYTES",
7846
+ "KICI_COLD_STORE_EXECUTION_RUNS_MAX_ROWS_PER_CYCLE",
7847
+ "KICI_COLD_STORE_EXECUTION_RUNS_ENABLED",
7848
+ "KICI_COLD_STORE_EXECUTION_JOBS_WARM_TTL_DAYS",
7849
+ "KICI_COLD_STORE_EXECUTION_JOBS_MIN_WARM_TENANT_BYTES",
7850
+ "KICI_COLD_STORE_EXECUTION_JOBS_MIN_CHUNK_BYTES",
7851
+ "KICI_COLD_STORE_EXECUTION_JOBS_MAX_CHUNK_BYTES",
7852
+ "KICI_COLD_STORE_EXECUTION_JOBS_MAX_ROWS_PER_CYCLE",
7853
+ "KICI_COLD_STORE_EXECUTION_JOBS_ENABLED",
7854
+ "KICI_COLD_STORE_EXECUTION_STEPS_WARM_TTL_DAYS",
7855
+ "KICI_COLD_STORE_EXECUTION_STEPS_MIN_WARM_TENANT_BYTES",
7856
+ "KICI_COLD_STORE_EXECUTION_STEPS_MIN_CHUNK_BYTES",
7857
+ "KICI_COLD_STORE_EXECUTION_STEPS_MAX_CHUNK_BYTES",
7858
+ "KICI_COLD_STORE_EXECUTION_STEPS_MAX_ROWS_PER_CYCLE",
7859
+ "KICI_COLD_STORE_EXECUTION_STEPS_ENABLED",
7860
+ "KICI_COLD_STORE_SECRET_AUDIT_LOG_WARM_TTL_DAYS",
7861
+ "KICI_COLD_STORE_SECRET_AUDIT_LOG_MIN_WARM_TENANT_BYTES",
7862
+ "KICI_COLD_STORE_SECRET_AUDIT_LOG_MIN_CHUNK_BYTES",
7863
+ "KICI_COLD_STORE_SECRET_AUDIT_LOG_MAX_CHUNK_BYTES",
7864
+ "KICI_COLD_STORE_SECRET_AUDIT_LOG_MAX_ROWS_PER_CYCLE",
7865
+ "KICI_COLD_STORE_SECRET_AUDIT_LOG_ENABLED",
7866
+ "KICI_COLD_STORE_ACCESS_LOG_WARM_TTL_DAYS",
7867
+ "KICI_COLD_STORE_ACCESS_LOG_MIN_WARM_TENANT_BYTES",
7868
+ "KICI_COLD_STORE_ACCESS_LOG_MIN_CHUNK_BYTES",
7869
+ "KICI_COLD_STORE_ACCESS_LOG_MAX_CHUNK_BYTES",
7870
+ "KICI_COLD_STORE_ACCESS_LOG_MAX_ROWS_PER_CYCLE",
7871
+ "KICI_COLD_STORE_ACCESS_LOG_ENABLED",
7872
+ "KICI_COLD_STORE_EVENT_LOG_WARM_TTL_DAYS",
7873
+ "KICI_COLD_STORE_EVENT_LOG_MIN_WARM_TENANT_BYTES",
7874
+ "KICI_COLD_STORE_EVENT_LOG_MIN_CHUNK_BYTES",
7875
+ "KICI_COLD_STORE_EVENT_LOG_MAX_CHUNK_BYTES",
7876
+ "KICI_COLD_STORE_EVENT_LOG_MAX_ROWS_PER_CYCLE",
7877
+ "KICI_COLD_STORE_EVENT_LOG_ENABLED"
7878
+ ];
7879
+ /**
7880
+ * Deployment-identity env vars injected by the installer (and the staging
7881
+ * deploy) so the orchestrator can report its own deployment shape in
7882
+ * `source.register`. Read directly from `process.env` by the deployment reader,
7883
+ * not threaded through the AppConfig schema — registered here so the
7884
+ * unknown-KICI_* validator at boot doesn't reject them.
7885
+ */
7886
+ const DEPLOY_IDENTITY_ENV_VARS = [
7887
+ "KICI_DEPLOY_MODE",
7888
+ "KICI_DEPLOY_CONTAINER",
7889
+ "KICI_DEPLOY_CONTAINER_RUNTIME"
7890
+ ];
7891
+ function loadConfig() {
7892
+ const data = envDef.parse();
7893
+ validateUnknownKiciVars([
7894
+ ...envDef.listKnownEnvVars(),
7895
+ ...LOGGER_ENV_VARS,
7896
+ ...COLD_STORE_ENV_VARS,
7897
+ ...DEPLOY_IDENTITY_ENV_VARS
7898
+ ]);
7899
+ const instanceId = data.cluster.instanceId || `${hostname()}-${randomUUID().slice(0, 8)}`;
7900
+ const userCacheStorage = {
7901
+ userCacheQuotaBytes: data.userCacheQuotaBytes,
7902
+ userCacheTtlMs: data.userCacheTtlMs
7903
+ };
7904
+ let storage;
7905
+ if (data.cacheStorageType === "s3") storage = {
7906
+ type: "s3",
7907
+ bucket: data.cacheStorageS3Bucket,
7908
+ prefix: data.cacheStorageS3Prefix,
7909
+ region: data.cacheStorageS3Region,
7910
+ endpoint: data.cacheStorageS3Endpoint,
7911
+ externalEndpoint: data.cacheStorageS3ExternalEndpoint,
7912
+ uploadEndpoint: data.cacheStorageS3UploadEndpoint,
7913
+ forcePathStyle: data.cacheStorageS3ForcePathStyle,
7914
+ logBucket: data.logStorageS3Bucket,
7915
+ ...userCacheStorage
7916
+ };
7917
+ else if (data.cacheStorageType === "filesystem") storage = {
7918
+ type: "filesystem",
7919
+ fsBasePath: data.cacheStorageFsPath,
7920
+ fsBaseUrl: data.cacheStorageFsBaseUrl,
7921
+ ...userCacheStorage
7922
+ };
7923
+ else storage = { ...userCacheStorage };
7924
+ return {
7925
+ ...data,
7926
+ instanceId,
7927
+ storage
7928
+ };
7929
+ }
7930
+ //#endregion
7931
+ //#region src/storage/s3.ts
7932
+ /**
7933
+ * S3-backed cache storage.
7934
+ *
7935
+ * Stores cached items in S3 with custom metadata headers for TTL tracking.
7936
+ * Uses AWS SDK v3 modular imports -- only loaded when S3 storage is configured.
7937
+ *
7938
+ * Metadata storage:
7939
+ * x-amz-meta-created-at -- ISO timestamp of creation
7940
+ * x-amz-meta-last-accessed-at -- ISO timestamp of last access
7941
+ *
7942
+ * TTL is enforced lazily: expired items are deleted on access.
7943
+ * Pre-signed URLs are generated for getUrl() with 15-minute expiry.
7944
+ */
7945
+ /** Pre-signed download URL expiry in seconds (15 minutes) */
7946
+ const PRESIGNED_URL_EXPIRY_SECONDS = 900;
7947
+ /** Pre-signed upload URL expiry in seconds (30 minutes -- uploads take longer) */
7948
+ const UPLOAD_URL_EXPIRY_SECONDS = 1800;
7949
+ var S3CacheStorage = class {
7950
+ client;
7951
+ /** Separate client for pre-signed URL generation (uses externalEndpoint if configured). */
7952
+ presignClient;
7953
+ /** Separate client for the host CLI's pre-signed upload URL (uses uploadEndpoint if configured). */
7954
+ uploadPresignClient;
7955
+ bucket;
7956
+ prefix;
7957
+ ttlMs;
7958
+ constructor(options) {
7959
+ this.bucket = options.bucket;
7960
+ this.prefix = options.prefix;
7961
+ this.ttlMs = options.ttlMs;
7962
+ this.client = createS3Client(options);
7963
+ if (options.externalEndpoint) this.presignClient = createS3Client({
7964
+ ...options,
7965
+ endpoint: options.externalEndpoint
7966
+ });
7967
+ else this.presignClient = this.client;
7968
+ if (options.uploadEndpoint) this.uploadPresignClient = createS3Client({
7969
+ ...options,
7970
+ endpoint: options.uploadEndpoint
7971
+ });
7972
+ else this.uploadPresignClient = this.client;
7973
+ }
7974
+ /** Build the full S3 object key from a cache key. */
7975
+ objectKey(key) {
7976
+ return `${this.prefix}${key}`;
7977
+ }
7978
+ /** Check if metadata indicates an expired item, honoring a per-op TTL override. */
7979
+ isExpired(meta, ttlMsOverride) {
7980
+ const ttl = ttlMsOverride ?? this.ttlMs;
7981
+ const lastAccessed = new Date(meta.lastAccessedAt).getTime();
7982
+ return Date.now() - lastAccessed > ttl;
7983
+ }
7984
+ /**
7985
+ * Read metadata from an S3 object's custom headers.
7986
+ * Returns null if the object doesn't exist (NoSuchKey/NotFound).
7987
+ */
7988
+ async readMeta(key) {
7989
+ try {
7990
+ const head = await this.client.send(new HeadObjectCommand({
7991
+ Bucket: this.bucket,
7992
+ Key: this.objectKey(key)
7993
+ }));
7994
+ const createdAt = head.Metadata?.["created-at"] ?? "";
7995
+ const lastAccessedAt = head.Metadata?.["last-accessed-at"] ?? "";
7996
+ if (!createdAt || !lastAccessedAt) return null;
7997
+ return {
7998
+ createdAt,
7999
+ lastAccessedAt
8000
+ };
8001
+ } catch (err) {
8002
+ if (this.isNotFoundError(err)) return null;
8003
+ throw err;
8004
+ }
8005
+ }
8006
+ /**
8007
+ * Update metadata on an S3 object by copying it to itself.
8008
+ * This is the standard S3 pattern for updating metadata without re-uploading data.
8009
+ */
8010
+ async updateMeta(key, meta) {
8011
+ const objectKey = this.objectKey(key);
8012
+ await this.client.send(new CopyObjectCommand({
8013
+ Bucket: this.bucket,
8014
+ Key: objectKey,
8015
+ CopySource: `${this.bucket}/${objectKey}`,
8016
+ MetadataDirective: "REPLACE",
8017
+ Metadata: {
8018
+ "created-at": meta.createdAt,
8019
+ "last-accessed-at": meta.lastAccessedAt
8020
+ }
8021
+ }));
8022
+ }
8023
+ /** Check if an error is a "not found" error (NoSuchKey, NotFound, 404). */
8024
+ isNotFoundError(err) {
8025
+ if (typeof err !== "object" || err === null) return false;
8026
+ const name = err.name;
8027
+ if (name === "NoSuchKey" || name === "NotFound") return true;
8028
+ return err.$metadata?.httpStatusCode === 404;
8029
+ }
8030
+ async put(key, data) {
8031
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8032
+ const body = typeof data === "string" ? Buffer.from(data) : data;
8033
+ await new Upload({
8034
+ client: this.client,
8035
+ params: {
8036
+ Bucket: this.bucket,
8037
+ Key: this.objectKey(key),
8038
+ Body: body,
8039
+ Metadata: {
8040
+ "created-at": now,
8041
+ "last-accessed-at": now
8042
+ }
8043
+ }
8044
+ }).done();
8045
+ }
8046
+ async get(key, ttlMsOverride) {
8047
+ const meta = await this.readMeta(key);
8048
+ if (!meta) return null;
8049
+ if (this.isExpired(meta, ttlMsOverride)) {
8050
+ await this.deleteObject(key);
8051
+ return null;
8052
+ }
8053
+ let data;
8054
+ try {
8055
+ const response = await this.client.send(new GetObjectCommand({
8056
+ Bucket: this.bucket,
8057
+ Key: this.objectKey(key)
8058
+ }));
8059
+ if (!response.Body) return null;
8060
+ data = Buffer.from(await response.Body.transformToByteArray());
8061
+ } catch (err) {
8062
+ if (this.isNotFoundError(err)) return null;
8063
+ throw err;
8064
+ }
8065
+ try {
8066
+ meta.lastAccessedAt = (/* @__PURE__ */ new Date()).toISOString();
8067
+ await this.updateMeta(key, meta);
8068
+ } catch {}
8069
+ return data;
8070
+ }
8071
+ async has(key, ttlMsOverride) {
8072
+ const meta = await this.readMeta(key);
8073
+ if (!meta) return false;
8074
+ if (this.isExpired(meta, ttlMsOverride)) {
8075
+ await this.deleteObject(key);
8076
+ return false;
8077
+ }
8078
+ return true;
8079
+ }
8080
+ async delete(key) {
8081
+ const existed = await this.readMeta(key) !== null;
8082
+ await this.deleteObject(key);
8083
+ return existed;
8084
+ }
8085
+ async touch(key) {
8086
+ const meta = await this.readMeta(key);
8087
+ if (!meta) return;
8088
+ meta.lastAccessedAt = (/* @__PURE__ */ new Date()).toISOString();
8089
+ await this.updateMeta(key, meta);
8090
+ }
8091
+ async getUrl(key, ttlMsOverride) {
8092
+ const meta = await this.readMeta(key);
8093
+ if (!meta) return null;
8094
+ if (this.isExpired(meta, ttlMsOverride)) {
8095
+ await this.deleteObject(key);
8096
+ return null;
8097
+ }
8098
+ return await getSignedUrl(this.presignClient, new GetObjectCommand({
8099
+ Bucket: this.bucket,
8100
+ Key: this.objectKey(key)
8101
+ }), { expiresIn: PRESIGNED_URL_EXPIRY_SECONDS });
8102
+ }
8103
+ async getUploadUrl(key) {
8104
+ const objectKey = this.objectKey(key);
8105
+ return getSignedUrl(this.presignClient, new PutObjectCommand({
8106
+ Bucket: this.bucket,
8107
+ Key: objectKey
8108
+ }), { expiresIn: UPLOAD_URL_EXPIRY_SECONDS });
8109
+ }
8110
+ async getInternalUploadUrl(key) {
8111
+ const objectKey = this.objectKey(key);
8112
+ return getSignedUrl(this.uploadPresignClient, new PutObjectCommand({
8113
+ Bucket: this.bucket,
8114
+ Key: objectKey
8115
+ }), { expiresIn: UPLOAD_URL_EXPIRY_SECONDS });
8116
+ }
8117
+ async initMeta(key) {
8118
+ const objectKey = this.objectKey(key);
8119
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8120
+ await this.client.send(new CopyObjectCommand({
8121
+ Bucket: this.bucket,
8122
+ Key: objectKey,
8123
+ CopySource: `${this.bucket}/${objectKey}`,
8124
+ MetadataDirective: "REPLACE",
8125
+ Metadata: {
8126
+ "created-at": now,
8127
+ "last-accessed-at": now
8128
+ }
8129
+ }));
8130
+ }
8131
+ async list(subPrefix) {
8132
+ const fullPrefix = this.objectKey(subPrefix);
8133
+ const items = [];
8134
+ let token;
8135
+ do {
8136
+ const resp = await this.client.send(new ListObjectsV2Command({
8137
+ Bucket: this.bucket,
8138
+ Prefix: fullPrefix,
8139
+ ContinuationToken: token
8140
+ }));
8141
+ for (const obj of resp.Contents ?? []) {
8142
+ if (!obj.Key) continue;
8143
+ items.push({
8144
+ key: obj.Key.slice(this.prefix.length),
8145
+ created: obj.LastModified ? obj.LastModified.getTime() : 0
8146
+ });
8147
+ }
8148
+ token = resp.IsTruncated ? resp.NextContinuationToken : void 0;
8149
+ } while (token);
8150
+ items.sort((a, b) => b.created - a.created);
8151
+ return items.map((i) => i.key);
8152
+ }
8153
+ async getMetadata(key) {
8154
+ return this.readMeta(key);
8155
+ }
8156
+ async copy(srcKey, destKey) {
8157
+ const srcObj = this.objectKey(srcKey);
8158
+ const destObj = this.objectKey(destKey);
8159
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8160
+ await this.client.send(new CopyObjectCommand({
8161
+ Bucket: this.bucket,
8162
+ Key: destObj,
8163
+ CopySource: `${this.bucket}/${srcObj}`,
8164
+ MetadataDirective: "REPLACE",
8165
+ Metadata: {
8166
+ "created-at": now,
8167
+ "last-accessed-at": now
8168
+ }
8169
+ }));
8170
+ }
8171
+ /** Delete an S3 object. Idempotent (doesn't error if missing). */
8172
+ async deleteObject(key) {
8173
+ try {
8174
+ await this.client.send(new DeleteObjectCommand({
8175
+ Bucket: this.bucket,
8176
+ Key: this.objectKey(key)
8177
+ }));
8178
+ } catch (err) {
8179
+ if (!this.isNotFoundError(err)) throw err;
8180
+ }
8181
+ }
8182
+ };
8183
+ //#endregion
8184
+ //#region src/storage/sign-url.ts
8185
+ /**
8186
+ * HMAC-signed URL tokens for the filesystem cache backend.
8187
+ *
8188
+ * Pre-signed S3 URLs are not available for filesystem storage, so the
8189
+ * filesystem backend mints HMAC-SHA256 tokens that the orchestrator's HTTP
8190
+ * blob route verifies before serving / writing a file. Token shape:
8191
+ *
8192
+ * `${expiresUnixMs}.${hexSig}`
8193
+ *
8194
+ * Signature input: `${method}:${key}:${expiresUnixMs}` with HMAC-SHA256 keyed
8195
+ * by a per-orchestrator-process secret (random 32-byte hex at boot, persists
8196
+ * for the lifetime of the process). Tokens become invalid on orchestrator
8197
+ * restart — fine for the filesystem backend's E2E / single-host use case.
8198
+ */
8199
+ /** Default URL lifetime: 1 hour (matches S3 backend's PUT-URL lifetime). */
8200
+ const DEFAULT_SIGN_URL_TTL_MS = 3600 * 1e3;
8201
+ function computeSig(secret, method, key, expiresMs) {
8202
+ const hmac = createHmac("sha256", secret);
8203
+ hmac.update(`${method}:${key}:${expiresMs}`);
8204
+ return hmac.digest("hex");
8205
+ }
8206
+ /**
8207
+ * Mint a signed token for the given (method, key, expiry) triple.
8208
+ */
8209
+ function signToken(secret, method, key, ttlMs = DEFAULT_SIGN_URL_TTL_MS) {
8210
+ const expiresMs = Date.now() + ttlMs;
8211
+ return {
8212
+ token: `${expiresMs}.${computeSig(secret, method, key, expiresMs)}`,
8213
+ expiresMs
8214
+ };
8215
+ }
8216
+ //#endregion
8217
+ //#region src/storage/filesystem.ts
8218
+ /**
8219
+ * Filesystem-backed cache storage.
8220
+ *
8221
+ * Mirrors the `CacheStorage` interface that `S3CacheStorage` provides, but
8222
+ * stores blobs as files under a configured base directory. Pre-signed URLs
8223
+ * are replaced by HMAC-signed `http://<orchUrl>/api/v1/cache/blob/...` URLs
8224
+ * — see `sign-url.ts` for the token mechanics and `app.ts` for the route.
8225
+ *
8226
+ * Intended for single-host deployments and E2E sandboxes where standing up
8227
+ * an S3-compatible service is overkill. Production should still use S3.
8228
+ *
8229
+ * Metadata is co-located as a sibling JSON file (`<key>.meta.json`) so the
8230
+ * data file is byte-identical to what the agent receives over HTTP. TTL is
8231
+ * enforced lazily on access — same model as the S3 backend.
8232
+ */
8233
+ const DEFAULT_ROUTE_PREFIX = "/api/v1/cache/blob/";
8234
+ var FilesystemCacheStorage = class {
8235
+ basePath;
8236
+ ttlMs;
8237
+ baseUrl;
8238
+ signingSecret;
8239
+ routePrefix;
8240
+ constructor(options) {
8241
+ this.basePath = options.basePath;
8242
+ this.ttlMs = options.ttlMs;
8243
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
8244
+ this.signingSecret = options.signingSecret;
8245
+ this.routePrefix = (options.routePrefix ?? DEFAULT_ROUTE_PREFIX).replace(/\/?$/, "/");
8246
+ }
8247
+ /**
8248
+ * Map a cache key to its on-disk path. Keys may contain `/`, which becomes a
8249
+ * directory separator. Reject keys containing `..` segments or NUL bytes to
8250
+ * prevent path traversal — the same defence the route layer enforces too.
8251
+ */
8252
+ resolvePath(key) {
8253
+ if (key.includes("\0") || key.split("/").some((seg) => seg === ".." || seg === "")) throw new Error(`Invalid cache key: ${JSON.stringify(key)}`);
8254
+ return join(this.basePath, key);
8255
+ }
8256
+ /** Public for the HTTP route. The route validates the key before calling. */
8257
+ pathFor(key) {
8258
+ return this.resolvePath(key);
8259
+ }
8260
+ metaPath(key) {
8261
+ return `${this.resolvePath(key)}.meta.json`;
8262
+ }
8263
+ isExpired(meta, ttlMsOverride) {
8264
+ const ttl = ttlMsOverride ?? this.ttlMs;
8265
+ const lastAccessed = new Date(meta.lastAccessedAt).getTime();
8266
+ return Date.now() - lastAccessed > ttl;
8267
+ }
8268
+ async readMeta(key) {
8269
+ try {
8270
+ const raw = await promises.readFile(this.metaPath(key), "utf-8");
8271
+ const parsed = JSON.parse(raw);
8272
+ if (typeof parsed.createdAt !== "string" || typeof parsed.lastAccessedAt !== "string") return null;
8273
+ return parsed;
8274
+ } catch (err) {
8275
+ if (isNotFoundFsError(err)) return null;
8276
+ throw err;
8277
+ }
8278
+ }
8279
+ async writeMeta(key, meta) {
8280
+ const path = this.metaPath(key);
8281
+ await promises.mkdir(dirname(path), { recursive: true });
8282
+ await writeFileAtomic(path, JSON.stringify(meta));
8283
+ }
8284
+ async put(key, data) {
8285
+ const path = this.resolvePath(key);
8286
+ await promises.mkdir(dirname(path), { recursive: true });
8287
+ await writeFileAtomic(path, typeof data === "string" ? Buffer.from(data) : data);
8288
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8289
+ await this.writeMeta(key, {
8290
+ createdAt: now,
8291
+ lastAccessedAt: now
8292
+ });
8293
+ }
8294
+ async get(key, ttlMsOverride) {
8295
+ const meta = await this.readMeta(key);
8296
+ if (!meta) return null;
8297
+ if (this.isExpired(meta, ttlMsOverride)) {
8298
+ await this.deleteFiles(key);
8299
+ return null;
8300
+ }
8301
+ let data;
8302
+ try {
8303
+ data = await promises.readFile(this.resolvePath(key));
8304
+ } catch (err) {
8305
+ if (isNotFoundFsError(err)) return null;
8306
+ throw err;
6966
8307
  }
6967
- });
6968
- runs.command("ephemeral-key <runId>").description("Show whether the run-ephemeral key has been scrubbed yet").option("--json", "Emit raw JSON instead of plain text").action(async (runId, opts) => {
6969
8308
  try {
6970
- const response = await getClient().getRunEphemeralKey(runId);
6971
- if (opts.json) {
6972
- console.log(JSON.stringify(response, null, 2));
6973
- return;
6974
- }
6975
- if (response.exists) {
6976
- console.log(`exists: true`);
6977
- console.log(`created_at: ${response.createdAt}`);
6978
- } else {
6979
- console.log(`exists: false`);
6980
- console.log(`created_at: -`);
6981
- }
8309
+ meta.lastAccessedAt = (/* @__PURE__ */ new Date()).toISOString();
8310
+ await this.writeMeta(key, meta);
8311
+ } catch {}
8312
+ return data;
8313
+ }
8314
+ async has(key, ttlMsOverride) {
8315
+ const meta = await this.readMeta(key);
8316
+ if (!meta) return false;
8317
+ if (this.isExpired(meta, ttlMsOverride)) {
8318
+ await this.deleteFiles(key);
8319
+ return false;
8320
+ }
8321
+ return true;
8322
+ }
8323
+ async delete(key) {
8324
+ const existed = await this.readMeta(key) !== null;
8325
+ await this.deleteFiles(key);
8326
+ return existed;
8327
+ }
8328
+ async touch(key) {
8329
+ const meta = await this.readMeta(key);
8330
+ if (!meta) return;
8331
+ meta.lastAccessedAt = (/* @__PURE__ */ new Date()).toISOString();
8332
+ await this.writeMeta(key, meta);
8333
+ }
8334
+ async getUrl(key, ttlMsOverride) {
8335
+ const meta = await this.readMeta(key);
8336
+ if (!meta) return null;
8337
+ if (this.isExpired(meta, ttlMsOverride)) {
8338
+ await this.deleteFiles(key);
8339
+ return null;
8340
+ }
8341
+ return this.signedUrl("GET", key);
8342
+ }
8343
+ async getUploadUrl(key) {
8344
+ return this.signedUrl("PUT", key);
8345
+ }
8346
+ async getInternalUploadUrl(key) {
8347
+ return this.signedUrl("PUT", key);
8348
+ }
8349
+ async initMeta(key) {
8350
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8351
+ await this.writeMeta(key, {
8352
+ createdAt: now,
8353
+ lastAccessedAt: now
8354
+ });
8355
+ }
8356
+ async list(subPrefix) {
8357
+ const root = this.resolvePath(subPrefix.replace(/\/$/, ""));
8358
+ let entries;
8359
+ try {
8360
+ entries = await this.walkDataFiles(root, subPrefix.replace(/\/$/, ""));
6982
8361
  } catch (err) {
6983
- console.error(`Error: ${toErrorMessage(err)}`);
6984
- process.exit(1);
8362
+ if (isNotFoundFsError(err)) return [];
8363
+ throw err;
8364
+ }
8365
+ entries.sort((a, b) => b.mtime - a.mtime);
8366
+ return entries.map((e) => e.key);
8367
+ }
8368
+ async getMetadata(key) {
8369
+ return this.readMeta(key);
8370
+ }
8371
+ async copy(srcKey, destKey) {
8372
+ const srcPath = this.resolvePath(srcKey);
8373
+ const destPath = this.resolvePath(destKey);
8374
+ await promises.mkdir(dirname(destPath), { recursive: true });
8375
+ await promises.copyFile(srcPath, destPath);
8376
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8377
+ await this.writeMeta(destKey, {
8378
+ createdAt: now,
8379
+ lastAccessedAt: now
8380
+ });
8381
+ }
8382
+ /**
8383
+ * Recursively collect data files under `dir`, returning each as a cache key
8384
+ * relative to `basePath` paired with its mtime. Metadata sidecars
8385
+ * (`*.meta.json`) and the atomic-write temp files (`*.tmp-*`) are skipped so
8386
+ * `list()` only surfaces real cache objects.
8387
+ */
8388
+ async walkDataFiles(dir, keyPrefix) {
8389
+ const out = [];
8390
+ const dirents = await promises.readdir(dir, { withFileTypes: true });
8391
+ for (const dirent of dirents) {
8392
+ const childPath = join(dir, dirent.name);
8393
+ const childKey = keyPrefix ? `${keyPrefix}/${dirent.name}` : dirent.name;
8394
+ if (dirent.isDirectory()) {
8395
+ out.push(...await this.walkDataFiles(childPath, childKey));
8396
+ continue;
8397
+ }
8398
+ if (dirent.name.endsWith(".meta.json") || /\.tmp-[0-9a-f]+$/.test(dirent.name)) continue;
8399
+ const stat = await promises.stat(childPath);
8400
+ out.push({
8401
+ key: childKey,
8402
+ mtime: stat.mtimeMs
8403
+ });
6985
8404
  }
8405
+ return out;
8406
+ }
8407
+ signedUrl(method, key) {
8408
+ const { token } = signToken(this.signingSecret, method, key);
8409
+ const encodedKey = key.split("/").map((seg) => encodeURIComponent(seg)).join("/");
8410
+ return `${this.baseUrl}${this.routePrefix}${encodedKey}?sig=${encodeURIComponent(token)}`;
8411
+ }
8412
+ async deleteFiles(key) {
8413
+ await promises.rm(this.resolvePath(key), { force: true });
8414
+ await promises.rm(this.metaPath(key), { force: true });
8415
+ }
8416
+ };
8417
+ function isNotFoundFsError(err) {
8418
+ return typeof err === "object" && err !== null && err.code === "ENOENT";
8419
+ }
8420
+ /**
8421
+ * Write a file atomically: write to a temp sibling, fsync, rename. Avoids
8422
+ * partial reads if the orchestrator crashes mid-write. The temp suffix is
8423
+ * randomised so concurrent writers don't collide on the same temp name.
8424
+ */
8425
+ async function writeFileAtomic(path, data) {
8426
+ const tmp = `${path}.tmp-${randomBytes(6).toString("hex")}`;
8427
+ const handle = await promises.open(tmp, "w");
8428
+ try {
8429
+ await handle.writeFile(data);
8430
+ await handle.sync();
8431
+ } finally {
8432
+ await handle.close();
8433
+ }
8434
+ await promises.rename(tmp, path);
8435
+ }
8436
+ //#endregion
8437
+ //#region src/storage/index.ts
8438
+ /**
8439
+ * Create a cache storage backend from configuration.
8440
+ */
8441
+ function createCacheStorage(config) {
8442
+ if (config.type === "s3") return new S3CacheStorage({
8443
+ bucket: config.bucket,
8444
+ prefix: config.prefix,
8445
+ ttlMs: config.ttlMs,
8446
+ region: config.region,
8447
+ endpoint: config.endpoint,
8448
+ externalEndpoint: config.externalEndpoint,
8449
+ uploadEndpoint: config.uploadEndpoint,
8450
+ forcePathStyle: config.forcePathStyle
6986
8451
  });
6987
- runs.command("secret-outputs <runId>").description("List per-job secret outputs (masked by default; --reveal decrypts and audits)").option("--output-key <key>", "Filter to a single output_key").option("--reveal", "Decrypt and print plaintext values. Audited with actor=secret-outputs.reveal; requires secret.reveal permission").option("--json", "Emit raw JSON instead of a table").action(async (runId, opts) => {
8452
+ if (config.type === "filesystem") return new FilesystemCacheStorage({
8453
+ basePath: config.basePath,
8454
+ ttlMs: config.ttlMs,
8455
+ baseUrl: config.baseUrl,
8456
+ signingSecret: config.signingSecret
8457
+ });
8458
+ throw new Error(`Unknown cache storage type: ${config.type}`);
8459
+ }
8460
+ //#endregion
8461
+ //#region src/provenance/trust-root.ts
8462
+ /**
8463
+ * Derive the JWKS URI for a provenance OIDC issuer. Identical to the Platform's
8464
+ * own derivation (`packages/platform/src/dashboard/routes/runs.ts`) so a bundle
8465
+ * verified client-side and at ingest resolve the same key set.
8466
+ */
8467
+ function deriveJwksUri(issuer) {
8468
+ return `${issuer.replace(/\/+$/, "")}/.well-known/jwks.json`;
8469
+ }
8470
+ function createProvenanceTrustRoot(opts = {}) {
8471
+ const fetchImpl = opts.fetchImpl ?? fetch;
8472
+ const ttlMs = opts.ttlMs ?? 5 * 6e4;
8473
+ let issuer = opts.issuer ?? null;
8474
+ let cache = null;
8475
+ async function fetchJwks() {
8476
+ if (!issuer) return null;
6988
8477
  try {
6989
- if (opts.reveal) process.stderr.write("⚠ --reveal will decrypt secret values; this call is recorded in secret_audit_log.\n");
6990
- const response = await getClient().getRunSecretOutputs(runId, {
6991
- outputKey: opts.outputKey,
6992
- reveal: Boolean(opts.reveal)
6993
- });
6994
- if (opts.json) {
6995
- console.log(JSON.stringify(response, null, 2));
6996
- return;
8478
+ const res = await fetchImpl(deriveJwksUri(issuer));
8479
+ if (!res.ok) return null;
8480
+ const jwks = await res.json();
8481
+ cache = {
8482
+ jwks,
8483
+ at: Date.now()
8484
+ };
8485
+ return jwks;
8486
+ } catch {
8487
+ return null;
8488
+ }
8489
+ }
8490
+ function hasKid(jwks, kid) {
8491
+ if (!kid || !jwks) return true;
8492
+ return jwks.keys.some((k) => k.kid === kid);
8493
+ }
8494
+ return {
8495
+ getIssuer: () => issuer,
8496
+ getJwks: async (kid) => {
8497
+ if (cache && Date.now() - cache.at < ttlMs && hasKid(cache.jwks, kid)) return cache.jwks;
8498
+ return await fetchJwks();
8499
+ },
8500
+ setIssuer: (next) => {
8501
+ if (next !== issuer) cache = null;
8502
+ issuer = next;
8503
+ }
8504
+ };
8505
+ }
8506
+ //#endregion
8507
+ //#region src/provenance/verify-at-ingest.ts
8508
+ /**
8509
+ * Compute the verification verdict for a stored provenance bundle at ingest.
8510
+ *
8511
+ * Fail-closed: a missing trust root, unfetchable JWKS, missing storage, or an
8512
+ * unreadable bundle all yield `unverifiable` (never silently `verified`). A
8513
+ * bundle that verifies false yields `failed` with the first failure code. Any
8514
+ * thrown error is caught and recorded as `unverifiable` — verification never
8515
+ * fails the upload.
8516
+ */
8517
+ async function computeAttestationVerdict(opts) {
8518
+ const { trustRoot, storage, storageKey } = opts;
8519
+ const status = attestationVerifyStatusSchema.enum;
8520
+ try {
8521
+ const issuer = trustRoot?.getIssuer() ?? null;
8522
+ if (!issuer) return {
8523
+ verifyStatus: status.unverifiable,
8524
+ verifyReason: "no_issuer_configured",
8525
+ verifiedAt: null
8526
+ };
8527
+ if (!storage) return {
8528
+ verifyStatus: status.unverifiable,
8529
+ verifyReason: "no_storage",
8530
+ verifiedAt: null
8531
+ };
8532
+ const jwks = await trustRoot.getJwks();
8533
+ if (!jwks) return {
8534
+ verifyStatus: status.unverifiable,
8535
+ verifyReason: "jwks_fetch_failed",
8536
+ verifiedAt: null
8537
+ };
8538
+ const raw = await storage.get(storageKey);
8539
+ if (!raw) return {
8540
+ verifyStatus: status.unverifiable,
8541
+ verifyReason: "bundle_unreadable",
8542
+ verifiedAt: null
8543
+ };
8544
+ const result = await verifyKiciBundle({
8545
+ bundle: JSON.parse(raw.toString("utf8")),
8546
+ jwks,
8547
+ expectedIssuer: issuer
8548
+ });
8549
+ return {
8550
+ verifyStatus: result.verified ? status.verified : status.failed,
8551
+ verifyReason: result.verified ? null : result.failures[0] ?? "verification_failed",
8552
+ verifiedAt: /* @__PURE__ */ new Date()
8553
+ };
8554
+ } catch (err) {
8555
+ opts.logWarn?.(toErrorMessage(err));
8556
+ return {
8557
+ verifyStatus: status.unverifiable,
8558
+ verifyReason: "verify_error",
8559
+ verifiedAt: null
8560
+ };
8561
+ }
8562
+ }
8563
+ //#endregion
8564
+ //#region src/cli/commands/attestations-reverify.ts
8565
+ const logger$9 = createLogger({ prefix: "kici-admin-attestations" });
8566
+ /**
8567
+ * Backfill / refresh attestation verdicts. By default it re-evaluates only rows
8568
+ * that have no usable verdict yet (`pending` / `unverifiable`) — the rollout
8569
+ * path for rows recorded before verify-at-ingest, or before an org configured
8570
+ * provenance. With `--all` it re-evaluates every row. Idempotent: re-running
8571
+ * over already-verified rows (when `--all`) recomputes the same verdict.
8572
+ */
8573
+ async function reverifyAttestations(db, trustRoot, storage, opts) {
8574
+ let query = db.selectFrom("attestations").select([
8575
+ "id",
8576
+ "storage_key",
8577
+ "verify_status"
8578
+ ]);
8579
+ if (!opts.all) query = query.where("verify_status", "in", ["pending", "unverifiable"]);
8580
+ const rows = await query.execute();
8581
+ let updated = 0;
8582
+ for (const row of rows) {
8583
+ const verdict = await computeAttestationVerdict({
8584
+ trustRoot,
8585
+ storage,
8586
+ storageKey: row.storage_key,
8587
+ logWarn: (reason) => logger$9.warn("reverify: verification error", {
8588
+ attestationId: row.id,
8589
+ reason
8590
+ })
8591
+ });
8592
+ await db.updateTable("attestations").set({
8593
+ verify_status: verdict.verifyStatus,
8594
+ verify_reason: verdict.verifyReason,
8595
+ verified_at: verdict.verifiedAt
8596
+ }).where("id", "=", row.id).execute();
8597
+ updated += 1;
8598
+ logger$9.info("reverify: updated attestation verdict", {
8599
+ attestationId: row.id,
8600
+ from: row.verify_status,
8601
+ to: verdict.verifyStatus
8602
+ });
8603
+ }
8604
+ return {
8605
+ updated,
8606
+ scanned: rows.length
8607
+ };
8608
+ }
8609
+ //#endregion
8610
+ //#region src/cli/commands/attestations.ts
8611
+ /**
8612
+ * `kici-admin attestations` — orchestrator-DB-direct provenance maintenance.
8613
+ *
8614
+ * attestations reverify [--all] Recompute stored verdicts (verify-at-ingest
8615
+ * backfill). Default scope: pending /
8616
+ * unverifiable rows; `--all` re-evaluates
8617
+ * every row. Direct DB + object storage.
8618
+ *
8619
+ * Classified `kici-admin` (orchestrator DB plane) per .claude/rules/platform-admin.md.
8620
+ */
8621
+ init_client();
8622
+ const logger$8 = createLogger({ prefix: "kici-admin-attestations" });
8623
+ function resolveDatabaseUrl$1(explicit) {
8624
+ const url = explicit ?? process.env.KICI_DATABASE_URL;
8625
+ if (!url) throw new Error("Database URL required. Pass --database-url or set KICI_DATABASE_URL.");
8626
+ return url;
8627
+ }
8628
+ /** Build the provenance object-storage handle from config (s3 / filesystem). */
8629
+ function buildStorage(config) {
8630
+ if (config.storage?.type === "s3") return createCacheStorage({
8631
+ type: "s3",
8632
+ bucket: config.storage.bucket,
8633
+ prefix: config.storage.prefix ?? "",
8634
+ ttlMs: config.cacheTtlDays * 864e5,
8635
+ region: config.storage.region,
8636
+ endpoint: config.storage.endpoint,
8637
+ externalEndpoint: config.storage.externalEndpoint,
8638
+ uploadEndpoint: config.storage.uploadEndpoint,
8639
+ forcePathStyle: config.storage.forcePathStyle
8640
+ });
8641
+ if (config.storage?.type === "filesystem") return createCacheStorage({
8642
+ type: "filesystem",
8643
+ basePath: config.storage.fsBasePath,
8644
+ ttlMs: config.cacheTtlDays * 864e5,
8645
+ baseUrl: config.storage.fsBaseUrl ?? `http://127.0.0.1:${config.port}`,
8646
+ signingSecret: "kici-admin-reverify"
8647
+ });
8648
+ }
8649
+ async function confirmInteractive$1(prompt) {
8650
+ const rl = createInterface({
8651
+ input: process.stdin,
8652
+ output: process.stderr
8653
+ });
8654
+ try {
8655
+ const answer = await new Promise((resolve) => rl.question(prompt, resolve));
8656
+ return answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes";
8657
+ } finally {
8658
+ rl.close();
8659
+ }
8660
+ }
8661
+ function registerAttestationsCommands(program) {
8662
+ program.command("attestations").description("Provenance-attestation maintenance (orchestrator DB)").command("reverify").description("Recompute stored attestation verdicts (verify-at-ingest backfill)").option("--all", "Re-evaluate every attestation (default: only pending/unverifiable)").option("--database-url <url>", "Orchestrator DB URL (else KICI_DATABASE_URL)").option("--yes", "Skip the --all confirmation prompt").action(async (opts) => {
8663
+ try {
8664
+ const config = loadConfig();
8665
+ const url = resolveDatabaseUrl$1(opts.databaseUrl ?? (config.databaseUrl || void 0));
8666
+ if (opts.all && !opts.yes) {
8667
+ if (!await confirmInteractive$1("Re-evaluate the verdict of EVERY attestation (including already-verified)? [y/N] ")) {
8668
+ console.error("Aborted.");
8669
+ process.exit(1);
8670
+ }
6997
8671
  }
6998
- if (response.outputs.length === 0) {
6999
- console.log("No secret outputs for this run.");
7000
- return;
8672
+ const pool = createPool(url);
8673
+ const db = createDb$1(pool);
8674
+ const storage = buildStorage(config);
8675
+ const trustRoot = createProvenanceTrustRoot({ issuer: config.provenanceIssuer ?? null });
8676
+ try {
8677
+ const { updated, scanned } = await reverifyAttestations(db, trustRoot, storage, { all: !!opts.all });
8678
+ logger$8.info("attestations reverify complete", {
8679
+ updated,
8680
+ scanned
8681
+ });
8682
+ process.stderr.write(`Reverified ${updated} of ${scanned} attestation(s).\n`);
8683
+ } finally {
8684
+ await db.destroy();
8685
+ await pool.end().catch(() => {});
7001
8686
  }
7002
- const headers = opts.reveal ? [
7003
- "job_id",
7004
- "output_key",
7005
- "value",
7006
- "created_at"
7007
- ] : [
7008
- "job_id",
7009
- "output_key",
7010
- "masked",
7011
- "created_at"
7012
- ];
7013
- const rows = response.outputs.map((o) => opts.reveal ? [
7014
- o.jobId,
7015
- o.outputKey,
7016
- o.value ?? o.revealError ?? "(decrypt failed)",
7017
- o.createdAt
7018
- ] : [
7019
- o.jobId,
7020
- o.outputKey,
7021
- String(o.masked),
7022
- o.createdAt
7023
- ]);
7024
- console.log(renderTable$2(headers, rows));
7025
8687
  } catch (err) {
7026
8688
  console.error(`Error: ${toErrorMessage(err)}`);
7027
8689
  process.exit(1);
@@ -9744,6 +11406,44 @@ function getInstallBase(platform, name) {
9744
11406
  case "windows": return `C:\\Program Files\\KiCI\\${name}${sep}`;
9745
11407
  }
9746
11408
  }
11409
+ /**
11410
+ * Build the interactive `--pick` choices, newest-first (lexicographic, matching
11411
+ * the rest of the versioned-directory handling). The currently-active version is
11412
+ * labeled `(current)` and disabled so it cannot be selected — picking it would be
11413
+ * a no-op restart.
11414
+ */
11415
+ function buildPickChoices(versions, current) {
11416
+ return [...versions].sort().reverse().map((v) => ({
11417
+ value: v,
11418
+ name: v === current ? `${v} (current)` : v,
11419
+ disabled: v === current ? "current version" : false
11420
+ }));
11421
+ }
11422
+ /**
11423
+ * The versions a `--pick` switch may target: every installed version except the
11424
+ * currently-active one. Empty when only the current version (or nothing) is
11425
+ * installed — the caller turns that into a "nothing to pick" error.
11426
+ */
11427
+ function selectablePickTargets(versions, current) {
11428
+ return versions.filter((v) => v !== current);
11429
+ }
11430
+ /**
11431
+ * Validate that `--pick` is not combined with a source/mode flag it conflicts
11432
+ * with. `--pick` activates an already-installed version, so it never downloads
11433
+ * (`--from`/`--url`), never takes an explicit `--version`, and is its own mode
11434
+ * (not `--rollback`/`--cleanup`). Returns an error message, or null when OK.
11435
+ */
11436
+ function checkPickFlagConflicts(opts) {
11437
+ if (!opts.pick) return null;
11438
+ const conflicts = [];
11439
+ if (opts.from) conflicts.push("--from");
11440
+ if (opts.url) conflicts.push("--url");
11441
+ if (opts.version) conflicts.push("--version");
11442
+ if (opts.rollback) conflicts.push("--rollback");
11443
+ if (opts.cleanup) conflicts.push("--cleanup");
11444
+ if (conflicts.length === 0) return null;
11445
+ return `--pick cannot be combined with ${conflicts.join(", ")}. --pick activates an already-installed version; it never downloads or takes an explicit version.`;
11446
+ }
9747
11447
  /** Check if the platform is Windows. */
9748
11448
  function isWindows(platform) {
9749
11449
  return platform === "windows";
@@ -9901,8 +11601,17 @@ async function performVersionedUpgrade(component, opts) {
9901
11601
  console.error("Error: root privileges required to upgrade system-level services");
9902
11602
  process.exit(1);
9903
11603
  }
11604
+ const pickConflict = checkPickFlagConflicts(opts);
11605
+ if (pickConflict) {
11606
+ console.error(`Error: ${pickConflict}`);
11607
+ process.exit(1);
11608
+ }
11609
+ if (opts.pick) {
11610
+ await handlePick(component, platform, installBase, config, manager, resolvedInstance, opts);
11611
+ return;
11612
+ }
9904
11613
  if (opts.rollback) {
9905
- await handleRollback(component, platform, installBase, config, manager, opts);
11614
+ await handleRollback(component, platform, installBase, config, manager, resolvedInstance, opts);
9906
11615
  return;
9907
11616
  }
9908
11617
  if (opts.cleanup) {
@@ -10061,9 +11770,89 @@ async function performNpmSourceUpgrade(component, config, resolvedInstance, mana
10061
11770
  console.log(`Upgrade complete: service "${config.name}" is now running version ${verdict.version}.`);
10062
11771
  }
10063
11772
  /**
11773
+ * Handle `--pick`: interactively choose an already-installed version and switch
11774
+ * the active version pointer to it. Lists every installed version (the active
11775
+ * one shown disabled), confirms the change, then delegates to
11776
+ * {@link switchToInstalledVersion}.
11777
+ */
11778
+ async function handlePick(component, platform, installBase, config, manager, resolvedInstance, opts) {
11779
+ if (!process.stdin.isTTY) {
11780
+ console.error("Error: --pick requires an interactive terminal; pass an explicit archive (--from/--url) or use --rollback for non-interactive switching.");
11781
+ process.exit(1);
11782
+ }
11783
+ const versions = listInstalledVersions(installBase, component);
11784
+ const current = getCurrentVersion(installBase, component, platform);
11785
+ if (selectablePickTargets(versions, current).length === 0) {
11786
+ const only = current ?? versions[0];
11787
+ console.error(only ? `Error: only one version installed (${only}), nothing to pick.` : `Error: no installed versions found for ${component}.`);
11788
+ process.exit(1);
11789
+ }
11790
+ const target = await select({
11791
+ message: `Select a ${component} version to activate:`,
11792
+ choices: buildPickChoices(versions, current)
11793
+ });
11794
+ if (!opts.yes) {
11795
+ console.log(`This will switch "${config.name}":`);
11796
+ if (current) console.log(` Current version: ${current}`);
11797
+ console.log(` Pick target: ${target}`);
11798
+ console.log(` Install path: ${path.join(installBase, `${component}-${target}`)}`);
11799
+ console.log(" The service will be stopped during the switch.");
11800
+ console.log("");
11801
+ if (!await confirm$2("Proceed with switch?")) {
11802
+ console.log("Pick cancelled.");
11803
+ return;
11804
+ }
11805
+ }
11806
+ await switchToInstalledVersion({
11807
+ component,
11808
+ platform,
11809
+ installBase,
11810
+ config,
11811
+ manager,
11812
+ resolvedInstance,
11813
+ targetVersion: target
11814
+ });
11815
+ console.log("");
11816
+ console.log(`Switched: ${current ?? "(unknown)"} -> ${target}`);
11817
+ }
11818
+ /**
11819
+ * Stop the service, switch the active version pointer to an already-installed
11820
+ * versioned directory (atomic symlink on Unix; uninstall→install + version file
11821
+ * on Windows), restart, and persist the new `kiciVersion` into the manifest.
11822
+ *
11823
+ * Shared by `--rollback` (switch to the previous version) and `--pick` (switch
11824
+ * to any chosen installed version).
11825
+ */
11826
+ async function switchToInstalledVersion(args) {
11827
+ const { component, platform, installBase, config, manager, resolvedInstance, targetVersion } = args;
11828
+ console.log("Stopping service...");
11829
+ if ((await manager.status(config)).state === "running") {
11830
+ await manager.stop(config);
11831
+ console.log("Service stopped.");
11832
+ }
11833
+ if (isWindows(platform)) {
11834
+ const launcherPath = path.join(installBase, `${component}-${targetVersion}`, getLauncherName(component, platform));
11835
+ config.executablePath = launcherPath;
11836
+ await manager.uninstall(config);
11837
+ await manager.install(config);
11838
+ writeCurrentVersion(installBase, component, targetVersion);
11839
+ console.log(`Service registration updated to ${launcherPath}`);
11840
+ } else {
11841
+ updateSymlinkAtomic(installBase, component, targetVersion);
11842
+ console.log(`Symlink updated: ${path.join(installBase, component)} -> ${component}-${targetVersion}`);
11843
+ }
11844
+ console.log("Starting service...");
11845
+ await manager.start(config);
11846
+ console.log("Service started.");
11847
+ writeManifest(resolvedInstance.instanceDir, {
11848
+ ...resolvedInstance.manifest,
11849
+ kiciVersion: targetVersion
11850
+ });
11851
+ }
11852
+ /**
10064
11853
  * Handle --rollback: switch symlink to the previous version and restart.
10065
11854
  */
10066
- async function handleRollback(component, platform, installBase, config, manager, opts) {
11855
+ async function handleRollback(component, platform, installBase, config, manager, resolvedInstance, opts) {
10067
11856
  const versions = listInstalledVersions(installBase, component);
10068
11857
  if (versions.length < 2) {
10069
11858
  console.error("Error: no previous version available for rollback");
@@ -10096,25 +11885,15 @@ async function handleRollback(component, platform, installBase, config, manager,
10096
11885
  return;
10097
11886
  }
10098
11887
  }
10099
- console.log("Stopping service...");
10100
- if ((await manager.status(config)).state === "running") {
10101
- await manager.stop(config);
10102
- console.log("Service stopped.");
10103
- }
10104
- if (isWindows(platform)) {
10105
- const launcherPath = path.join(installBase, `${component}-${previousVersion}`, getLauncherName(component, platform));
10106
- config.executablePath = launcherPath;
10107
- await manager.uninstall(config);
10108
- await manager.install(config);
10109
- writeCurrentVersion(installBase, component, previousVersion);
10110
- console.log(`Service registration updated to ${launcherPath}`);
10111
- } else {
10112
- updateSymlinkAtomic(installBase, component, previousVersion);
10113
- console.log(`Symlink updated: ${path.join(installBase, component)} -> ${component}-${previousVersion}`);
10114
- }
10115
- console.log("Starting service...");
10116
- await manager.start(config);
10117
- console.log("Service started.");
11888
+ await switchToInstalledVersion({
11889
+ component,
11890
+ platform,
11891
+ installBase,
11892
+ config,
11893
+ manager,
11894
+ resolvedInstance,
11895
+ targetVersion: previousVersion
11896
+ });
10118
11897
  console.log("");
10119
11898
  console.log(`Rollback complete: ${currentVersion} -> ${previousVersion}`);
10120
11899
  }
@@ -10864,7 +12643,7 @@ function registerLogsCommand(parent) {
10864
12643
  //#endregion
10865
12644
  //#region src/cli/commands/orchestrator-service/upgrade.ts
10866
12645
  function registerUpgradeCommand(parent) {
10867
- parent.command("upgrade").description("Upgrade orchestrator to a new version using versioned directory layout").option("--platform <type>", "Service platform (systemd|launchd|windows|compose)").option("--instance-dir <path>", "Deploy folder of the instance to upgrade").option("--name <name>", "Service name (no default — must resolve via flag/CWD)").option("--from <path>", "Path to package archive (.tar.gz or .zip)").option("--url <url>", "URL to download package archive from").option("--version <version>", "Target version string (e.g., 0.3.0)").option("--yes", "Skip confirmation prompt").option("--force", "Overwrite existing versioned directory").option("--cleanup", "Remove old versions (keeps current and previous)").option("--rollback", "Roll back to the previous version").action(async (opts) => {
12646
+ parent.command("upgrade").description("Upgrade orchestrator to a new version using versioned directory layout").option("--platform <type>", "Service platform (systemd|launchd|windows|compose)").option("--instance-dir <path>", "Deploy folder of the instance to upgrade").option("--name <name>", "Service name (no default — must resolve via flag/CWD)").option("--from <path>", "Path to package archive (.tar.gz or .zip)").option("--url <url>", "URL to download package archive from").option("--version <version>", "Target version string (e.g., 0.3.0)").option("--yes", "Skip confirmation prompt").option("--force", "Overwrite existing versioned directory").option("--cleanup", "Remove old versions (keeps current and previous)").option("--rollback", "Roll back to the previous version").option("--pick", "Interactively pick an installed version to activate").action(async (opts) => {
10868
12647
  await performVersionedUpgrade("orchestrator", opts);
10869
12648
  });
10870
12649
  }
@@ -11364,7 +13143,7 @@ function registerAgentLogsCommand(parent) {
11364
13143
  //#endregion
11365
13144
  //#region src/cli/commands/agent-service/upgrade.ts
11366
13145
  function registerAgentUpgradeCommand(parent) {
11367
- parent.command("upgrade").description("Upgrade agent to a new version using versioned directory layout").option("--platform <type>", "Service platform (systemd|launchd|windows|compose)").option("--instance-dir <path>", "Deploy folder of the instance to upgrade").option("--name <name>", "Service name (no default — must resolve via flag/CWD)").option("--from <path>", "Path to package archive (.tar.gz or .zip)").option("--url <url>", "URL to download package archive from").option("--version <version>", "Target version string (e.g., 0.3.0)").option("--yes", "Skip confirmation prompt").option("--force", "Overwrite existing versioned directory").option("--cleanup", "Remove old versions (keeps current and previous)").option("--rollback", "Roll back to the previous version").action(async (opts) => {
13146
+ parent.command("upgrade").description("Upgrade agent to a new version using versioned directory layout").option("--platform <type>", "Service platform (systemd|launchd|windows|compose)").option("--instance-dir <path>", "Deploy folder of the instance to upgrade").option("--name <name>", "Service name (no default — must resolve via flag/CWD)").option("--from <path>", "Path to package archive (.tar.gz or .zip)").option("--url <url>", "URL to download package archive from").option("--version <version>", "Target version string (e.g., 0.3.0)").option("--yes", "Skip confirmation prompt").option("--force", "Overwrite existing versioned directory").option("--cleanup", "Remove old versions (keeps current and previous)").option("--rollback", "Roll back to the previous version").option("--pick", "Interactively pick an installed version to activate").action(async (opts) => {
11368
13147
  await performVersionedUpgrade("agent", opts);
11369
13148
  });
11370
13149
  }
@@ -11911,19 +13690,67 @@ var HostRosterStore = class {
11911
13690
  async stampLastSeen(agentId, instanceId) {
11912
13691
  await this.db.updateTable("host_roster").set({ last_seen: sql`now()` }).where("agent_id", "=", agentId).where("connected_instance_id", "=", instanceId).execute();
11913
13692
  }
11914
- /** Operator pre-declare of a static host before its agent dials in. */
13693
+ /**
13694
+ * Operator pre-declare of a static host. A re-declare of an existing agent_id
13695
+ * converges the operator-owned columns (labels, hostname, reach,
13696
+ * host_properties) to the newly-declared values while preserving the
13697
+ * agent-reported liveness/identity columns (connected_instance_id, last_seen,
13698
+ * platform, arch, lifecycle_class, token_id, reboot_pending_until). The
13699
+ * optional reach fields (`address`/`sshUser`/`sshPort`/`sshKeySecret`) carry
13700
+ * how to SSH to the host for bootstrap bring-up. Each operator field is
13701
+ * preserve-on-omit: an omitted field keeps the existing column value.
13702
+ *
13703
+ * Returns `{ created: true }` when the declare inserted a new row,
13704
+ * `{ created: false }` when it converged an existing one.
13705
+ */
11915
13706
  async declareStatic(input) {
11916
- await this.db.insertInto("host_roster").values({
13707
+ const labelsBind = input.labels === void 0 ? null : JSON.stringify(input.labels);
13708
+ const propsBind = input.properties === void 0 ? null : JSON.stringify(input.properties);
13709
+ return { created: !!(await this.db.insertInto("host_roster").values({
11917
13710
  agent_id: input.agentId,
11918
13711
  token_id: null,
11919
13712
  lifecycle_class: "static",
11920
- labels: JSON.stringify(input.labels),
13713
+ labels: JSON.stringify(input.labels ?? []),
11921
13714
  hostname: input.hostname ?? null,
11922
13715
  connected_instance_id: null,
11923
13716
  host_properties: JSON.stringify(input.properties ?? {}),
13717
+ address: input.address ?? null,
13718
+ ssh_user: input.sshUser ?? null,
13719
+ ssh_port: input.sshPort ?? null,
13720
+ ssh_key_secret: input.sshKeySecret ?? null,
11924
13721
  last_seen: sql`now()`,
11925
13722
  updated_at: sql`now()`
11926
- }).onConflict((oc) => oc.column("agent_id").doNothing()).execute();
13723
+ }).onConflict((oc) => oc.column("agent_id").doUpdateSet({
13724
+ labels: sql`COALESCE(${labelsBind}, host_roster.labels)`,
13725
+ hostname: sql`COALESCE(${input.hostname ?? null}, host_roster.hostname)`,
13726
+ host_properties: sql`COALESCE(host_roster.host_properties, '{}'::jsonb) || COALESCE(${propsBind}::jsonb, '{}'::jsonb)`,
13727
+ address: sql`COALESCE(${input.address ?? null}, host_roster.address)`,
13728
+ ssh_user: sql`COALESCE(${input.sshUser ?? null}, host_roster.ssh_user)`,
13729
+ ssh_port: sql`COALESCE(${input.sshPort ?? null}, host_roster.ssh_port)`,
13730
+ ssh_key_secret: sql`COALESCE(${input.sshKeySecret ?? null}, host_roster.ssh_key_secret)`,
13731
+ updated_at: sql`now()`
13732
+ })).returning(sql`(xmax = 0)`.as("created")).executeTakeFirstOrThrow()).created };
13733
+ }
13734
+ /**
13735
+ * Read a host's pre-agent reach metadata. Returns null when the host is not
13736
+ * in the roster; a host declared without reach fields reads back with nulls.
13737
+ */
13738
+ async getReach(agentId) {
13739
+ const row = await this.db.selectFrom("host_roster").select([
13740
+ "agent_id",
13741
+ "address",
13742
+ "ssh_user",
13743
+ "ssh_port",
13744
+ "ssh_key_secret"
13745
+ ]).where("agent_id", "=", agentId).executeTakeFirst();
13746
+ if (!row) return null;
13747
+ return {
13748
+ agentId: row.agent_id,
13749
+ address: row.address,
13750
+ sshUser: row.ssh_user,
13751
+ sshPort: row.ssh_port,
13752
+ sshKeySecret: row.ssh_key_secret
13753
+ };
11927
13754
  }
11928
13755
  async get(agentId) {
11929
13756
  return await this.db.selectFrom("host_roster").selectAll().where("agent_id", "=", agentId).executeTakeFirst() ?? null;
@@ -12113,15 +13940,24 @@ function registerHostCommands(program) {
12113
13940
  process.exit(1);
12114
13941
  }
12115
13942
  });
12116
- host.command("declare").description("Pre-declare a static host before it connects").requiredOption("--agent-id <id>", "Agent id the host will register as").option("--labels <labels>", "Comma-separated labels", "").option("--hostname <name>", "Hostname").option("--prop <key=value>", "Typed host property (repeatable; true/false ⇒ boolean, numeric ⇒ number)", collectProp, []).action(async (opts) => {
13943
+ host.command("declare").description("Pre-declare a static host before it connects").requiredOption("--agent-id <id>", "Agent id the host will register as").option("--labels <labels>", "Comma-separated labels", "").option("--hostname <name>", "Hostname").option("--prop <key=value>", "Typed host property (repeatable; true/false ⇒ boolean, numeric ⇒ number)", collectProp, []).option("--address <host>", "Pre-agent SSH reach address (IP / hostname) for bootstrap").option("--ssh-user <user>", "SSH login user for bootstrap bring-up").option("--ssh-port <port>", "SSH port for bootstrap bring-up").option("--ssh-key-secret <ref>", "Scoped-secret ref (scope/key) holding the bring-up private key").action(async (opts) => {
12117
13944
  try {
12118
13945
  const labels = opts.labels ? String(opts.labels).split(",").map((s) => s.trim()).filter(Boolean) : [];
12119
13946
  const properties = parseHostPropertyAssignments(opts.prop);
13947
+ let sshPort;
13948
+ if (opts.sshPort !== void 0) {
13949
+ sshPort = Number(opts.sshPort);
13950
+ if (!Number.isInteger(sshPort) || sshPort <= 0) throw new Error(`--ssh-port must be a positive integer (got "${opts.sshPort}")`);
13951
+ }
12120
13952
  await withDb((db) => new HostRosterStore(db).declareStatic({
12121
13953
  agentId: opts.agentId,
12122
13954
  labels,
12123
13955
  hostname: opts.hostname,
12124
- properties
13956
+ properties,
13957
+ address: opts.address,
13958
+ sshUser: opts.sshUser,
13959
+ sshPort,
13960
+ sshKeySecret: opts.sshKeySecret
12125
13961
  }));
12126
13962
  console.log(`Declared static host: ${opts.agentId}`);
12127
13963
  } catch (err) {
@@ -12817,7 +14653,10 @@ function printShowEnvironment(res) {
12817
14653
  } else console.log("variables: none");
12818
14654
  if (res.bindings.length > 0) {
12819
14655
  console.log(`bindings (${res.bindings.length}):`);
12820
- for (const b of res.bindings) console.log(` ${b.scope_pattern}`);
14656
+ for (const b of res.bindings) {
14657
+ const host = b.host_pattern && b.host_pattern !== "**" ? ` (host: ${b.host_pattern})` : "";
14658
+ console.log(` ${b.scope_pattern}${host}`);
14659
+ }
12821
14660
  } else console.log("bindings: none");
12822
14661
  }
12823
14662
  function registerEnvironmentCommands(program, getClient) {
@@ -12852,17 +14691,19 @@ function registerEnvironmentCommands(program, getClient) {
12852
14691
  process.exit(1);
12853
14692
  }
12854
14693
  });
12855
- env.command("bind").description("Upsert an environment_bindings row (scope_pattern → environment)").requiredOption("--org <id>", "Org ID").requiredOption("--env <name>", "Environment name").requiredOption("--scope <pattern>", "Scope pattern (e.g. \"staging\" or \"aws/prod/**\")").option("--database-url <url>", "Use direct DB access instead of HTTP (offline mode)").option("--json", "Emit JSON output").action(async (opts) => {
14694
+ env.command("bind").description("Upsert an environment_bindings row (scope_pattern → environment)").requiredOption("--org <id>", "Org ID").requiredOption("--env <name>", "Environment name").requiredOption("--scope <pattern>", "Scope pattern (e.g. \"staging\" or \"aws/prod/**\")").option("--host <pattern>", "Host selector (exact/glob/regex over agentId/host/labels); \"**\" = all hosts", "**").option("--database-url <url>", "Use direct DB access instead of HTTP (offline mode)").option("--json", "Emit JSON output").action(async (opts) => {
12856
14695
  try {
12857
14696
  const dbUrl = resolveDirectDbUrl$4(opts.databaseUrl);
12858
14697
  const payload = {
12859
14698
  orgId: opts.org,
12860
14699
  envName: opts.env,
12861
- scopePattern: opts.scope
14700
+ scopePattern: opts.scope,
14701
+ hostPattern: opts.host
12862
14702
  };
12863
14703
  const result = dbUrl ? await seedEnvironmentBindingDirect(dbUrl, payload) : await getClient().post(`/api/v1/admin/environments/${encodeURIComponent(opts.env)}/bind`, {
12864
14704
  orgId: opts.org,
12865
- scopePattern: opts.scope
14705
+ scopePattern: opts.scope,
14706
+ hostPattern: opts.host
12866
14707
  });
12867
14708
  if (opts.json) console.log(JSON.stringify(result));
12868
14709
  else console.log(`environment bind: created=${result.created}${dbUrl ? " (direct)" : ""}`);
@@ -18822,6 +20663,7 @@ function buildProgram() {
18822
20663
  registerRemoteSourceCommands(program);
18823
20664
  registerWorkflowCommands(program, getClient);
18824
20665
  registerRunsCommands(program, getClient);
20666
+ registerAttestationsCommands(program);
18825
20667
  registerEventLogCommands(program, getClient);
18826
20668
  registerAccessLogCommands(program, getClient);
18827
20669
  registerBackendCommands(program, getClient);