@kici-dev/orchestrator 0.1.19 → 0.1.21

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 (41) hide show
  1. package/dist/agent/host-roster.d.ts +36 -1
  2. package/dist/agent/registry.d.ts +22 -0
  3. package/dist/app.d.ts +5 -0
  4. package/dist/cli/commands/source-manifest.d.ts +34 -0
  5. package/dist/cli/loopback-callback.d.ts +24 -0
  6. package/dist/cli/open-browser.d.ts +12 -0
  7. package/dist/cli.js +847 -205
  8. package/dist/cluster/coordinator.d.ts +5 -3
  9. package/dist/cluster/index.d.ts +2 -0
  10. package/dist/cluster/join-token.d.ts +27 -5
  11. package/dist/cluster/peer-auth-coordinator.d.ts +36 -0
  12. package/dist/cluster/peer-client.d.ts +34 -14
  13. package/dist/cluster/peer-credentials.d.ts +14 -2
  14. package/dist/cluster/peer-handler.d.ts +2 -2
  15. package/dist/cluster/rerouted-job-guard.d.ts +39 -0
  16. package/dist/db/migrations/043_rerouted_to_peer.d.ts +4 -0
  17. package/dist/db/migrations/044_check_mode.d.ts +4 -0
  18. package/dist/db/migrations/045_host_properties.d.ts +16 -0
  19. package/dist/db/migrations/046_join_token_consumed_by_instance.d.ts +17 -0
  20. package/dist/db/types.d.ts +33 -1
  21. package/dist/index.d.ts +5 -1
  22. package/dist/index.js +1168 -33
  23. package/dist/lockfile-validate.d.ts +24 -0
  24. package/dist/pipeline/dispatch-matched-workflow.d.ts +53 -1
  25. package/dist/pipeline/test-pipeline.d.ts +7 -0
  26. package/dist/providers/github/manifest-form.d.ts +15 -0
  27. package/dist/providers/github/manifest.d.ts +78 -0
  28. package/dist/reporting/execution-tracker.d.ts +10 -1
  29. package/dist/routes/admin-sources.d.ts +11 -0
  30. package/dist/routes/admin.d.ts +8 -0
  31. package/dist/secrets/pg-secret-store.d.ts +9 -0
  32. package/dist/server.js +3052 -2004
  33. package/dist/stale-detector/stale-run-detector.d.ts +8 -0
  34. package/dist/standalone.js +2837 -1817
  35. package/dist/worker/in-memory-job-queue.d.ts +17 -0
  36. package/dist/worker/peer-outbox.d.ts +36 -0
  37. package/dist/worker/worker-outbox-relay.d.ts +13 -0
  38. package/dist/ws/inventory-api.d.ts +17 -0
  39. package/installer-image-digests.json +3 -3
  40. package/package.json +4 -4
  41. package/sbom.spdx.json +77 -128
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import * as path$1 from "node:path";
5
5
  import path, { join, resolve } from "node:path";
6
6
  import * as fs$1 from "node:fs";
7
- import fs, { chmodSync, closeSync, createReadStream, createWriteStream, existsSync, openSync, promises, readFileSync, realpathSync, statSync, unwatchFile, watchFile, writeFileSync } from "node:fs";
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
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
10
  import crypto, { createDecipheriv, createHash, hkdfSync, randomBytes, randomUUID } from "node:crypto";
@@ -12,22 +12,24 @@ import { createInterface } from "node:readline";
12
12
  import { access, chmod, constants, copyFile, link, mkdir, open, readFile, readdir, rm, writeFile } from "node:fs/promises";
13
13
  import { parse, stringify } from "yaml";
14
14
  import { z } from "zod";
15
- import os from "node:os";
15
+ import os, { platform, tmpdir } from "node:os";
16
16
  import { getDatabaseCollationDrift, refreshDatabaseCollationVersion, reindexDatabaseConcurrently } from "@kici-dev/shared/db-collation";
17
17
  import { sql } from "kysely";
18
18
  import { Migrator } from "kysely/migration";
19
19
  import "jsonpath-plus";
20
- import { execFile, execFileSync, execSync, spawn } from "node:child_process";
20
+ import { exec, execFile, execFileSync, execSync, spawn } from "node:child_process";
21
21
  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
+ import { Octokit } from "@octokit/rest";
24
+ import { createAppAuth } from "@octokit/auth-app";
25
+ import http, { createServer } from "node:http";
22
26
  import { ZipArchive } from "archiver";
23
27
  import { confirm, input, password, select } from "@inquirer/prompts";
24
28
  import JSZip from "jszip";
25
29
  import { pipeline } from "node:stream/promises";
26
30
  import "pg";
27
- 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, scalerAgentLabels, secretAuditLogWarmSqlCase } from "@kici-dev/engine";
28
31
  import { DASHBOARD_WRITE_OPERATIONS, DASHBOARD_WRITE_OPERATIONS_BY_NAME, DashboardWriteCategory, DashboardWriteSensitivity } from "@kici-dev/engine/protocol/dashboard-write-operations";
29
32
  import { CLUSTER_NAME_FORMAT_MESSAGE } from "@kici-dev/engine/protocol/cluster-name";
30
- import http from "node:http";
31
33
  import { PassThrough } from "node:stream";
32
34
  import Docker from "dockerode";
33
35
  var __defProp = Object.defineProperty;
@@ -1554,8 +1556,8 @@ var init_client = __esmMin((() => {
1554
1556
  //#region src/db/migrations/001_initial.ts
1555
1557
  init_client();
1556
1558
  var _001_initial_exports = /* @__PURE__ */ __exportAll({
1557
- down: () => down$41,
1558
- up: () => up$41
1559
+ down: () => down$45,
1560
+ up: () => up$45
1559
1561
  });
1560
1562
  /**
1561
1563
  * Squashed initial migration -- creates the complete Orchestrator database schema.
@@ -2247,7 +2249,7 @@ const DDL_STATEMENTS = [
2247
2249
  `ALTER TABLE ONLY public.held_runs
2248
2250
  ADD CONSTRAINT held_runs_environment_id_fkey FOREIGN KEY (environment_id) REFERENCES public.environments(id);`
2249
2251
  ];
2250
- async function up$41(db) {
2252
+ async function up$45(db) {
2251
2253
  for (const stmt of DDL_STATEMENTS) await sql.raw(stmt).execute(db);
2252
2254
  await sql`
2253
2255
  INSERT INTO cluster_meta (key, value)
@@ -2269,7 +2271,7 @@ async function up$41(db) {
2269
2271
  * Rollback drops everything created above. Uses CASCADE on table drops to cut
2270
2272
  * through the FK graph without relying on exact topological order.
2271
2273
  */
2272
- async function down$41(db) {
2274
+ async function down$45(db) {
2273
2275
  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);
2274
2276
  for (const table of [
2275
2277
  "workflow_registrations",
@@ -2316,8 +2318,8 @@ async function down$41(db) {
2316
2318
  //#endregion
2317
2319
  //#region src/db/migrations/002_config_versions_key_version.ts
2318
2320
  var _002_config_versions_key_version_exports = /* @__PURE__ */ __exportAll({
2319
- down: () => down$40,
2320
- up: () => up$40
2321
+ down: () => down$44,
2322
+ up: () => up$44
2321
2323
  });
2322
2324
  /**
2323
2325
  * Add key_version column to config_versions so that sensitive-field encryption
@@ -2331,13 +2333,13 @@ var _002_config_versions_key_version_exports = /* @__PURE__ */ __exportAll({
2331
2333
  * No index is needed: rotation does a full-table scan; reads are by
2332
2334
  * `version` primary key and never filter on `key_version`.
2333
2335
  */
2334
- async function up$40(db) {
2336
+ async function up$44(db) {
2335
2337
  await sql`
2336
2338
  ALTER TABLE public.config_versions
2337
2339
  ADD COLUMN key_version integer NOT NULL DEFAULT 1
2338
2340
  `.execute(db);
2339
2341
  }
2340
- async function down$40(db) {
2342
+ async function down$44(db) {
2341
2343
  await sql`
2342
2344
  ALTER TABLE public.config_versions
2343
2345
  DROP COLUMN key_version
@@ -2346,8 +2348,8 @@ async function down$40(db) {
2346
2348
  //#endregion
2347
2349
  //#region src/db/migrations/003_access_log.ts
2348
2350
  var _003_access_log_exports = /* @__PURE__ */ __exportAll({
2349
- down: () => down$39,
2350
- up: () => up$39
2351
+ down: () => down$43,
2352
+ up: () => up$43
2351
2353
  });
2352
2354
  /**
2353
2355
  * Access log: one row per read or orchestrator-admin mutation attributable
@@ -2371,7 +2373,7 @@ var _003_access_log_exports = /* @__PURE__ */ __exportAll({
2371
2373
  * Retention is TTL-based via expires_at; packages/orchestrator/src/queue/
2372
2374
  * cleanup.ts picks up the prune pass.
2373
2375
  */
2374
- async function up$39(db) {
2376
+ async function up$43(db) {
2375
2377
  await sql`
2376
2378
  CREATE TABLE public.access_log (
2377
2379
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
@@ -2409,28 +2411,28 @@ async function up$39(db) {
2409
2411
  ON public.access_log (actor_type, actor_id, created_at DESC)
2410
2412
  `.execute(db);
2411
2413
  }
2412
- async function down$39(db) {
2414
+ async function down$43(db) {
2413
2415
  await sql`DROP TABLE IF EXISTS public.access_log`.execute(db);
2414
2416
  }
2415
2417
  //#endregion
2416
2418
  //#region src/db/migrations/004_rename_bundle_to_source.ts
2417
2419
  var _004_rename_bundle_to_source_exports = /* @__PURE__ */ __exportAll({
2418
- down: () => down$38,
2419
- up: () => up$38
2420
+ down: () => down$42,
2421
+ up: () => up$42
2420
2422
  });
2421
- async function up$38(db) {
2423
+ async function up$42(db) {
2422
2424
  await db.schema.alterTable("dispatch_queue").renameColumn("bundle_url", "source_tar_url").execute();
2423
2425
  await db.schema.alterTable("dispatch_queue").renameColumn("bundle_hash", "source_tar_hash").execute();
2424
2426
  }
2425
- async function down$38(db) {
2427
+ async function down$42(db) {
2426
2428
  await db.schema.alterTable("dispatch_queue").renameColumn("source_tar_url", "bundle_url").execute();
2427
2429
  await db.schema.alterTable("dispatch_queue").renameColumn("source_tar_hash", "bundle_hash").execute();
2428
2430
  }
2429
2431
  //#endregion
2430
2432
  //#region src/db/migrations/005_cold_store_chunk_counter.ts
2431
2433
  var _005_cold_store_chunk_counter_exports = /* @__PURE__ */ __exportAll({
2432
- down: () => down$37,
2433
- up: () => up$37
2434
+ down: () => down$41,
2435
+ up: () => up$41
2434
2436
  });
2435
2437
  /**
2436
2438
  * Cold-store chunk counter table.
@@ -2447,7 +2449,7 @@ var _005_cold_store_chunk_counter_exports = /* @__PURE__ */ __exportAll({
2447
2449
  *
2448
2450
  * sections 5 and 8.
2449
2451
  */
2450
- async function up$37(db) {
2452
+ async function up$41(db) {
2451
2453
  await sql`
2452
2454
  CREATE TABLE public.cold_store_chunk_counts (
2453
2455
  db TEXT NOT NULL,
@@ -2465,14 +2467,14 @@ async function up$37(db) {
2465
2467
  ON public.cold_store_chunk_counts (db, table_name)
2466
2468
  `.execute(db);
2467
2469
  }
2468
- async function down$37(db) {
2470
+ async function down$41(db) {
2469
2471
  await sql`DROP TABLE IF EXISTS public.cold_store_chunk_counts`.execute(db);
2470
2472
  }
2471
2473
  //#endregion
2472
2474
  //#region src/db/migrations/006_runs_jobs_steps_archived_at.ts
2473
2475
  var _006_runs_jobs_steps_archived_at_exports = /* @__PURE__ */ __exportAll({
2474
- down: () => down$36,
2475
- up: () => up$36
2476
+ down: () => down$40,
2477
+ up: () => up$40
2476
2478
  });
2477
2479
  /**
2478
2480
  * `execution_runs` / `execution_jobs` / `execution_steps` cold-store
@@ -2508,7 +2510,7 @@ var _006_runs_jobs_steps_archived_at_exports = /* @__PURE__ */ __exportAll({
2508
2510
  * - `idx_execution_jobs_routing_key_created (routing_key, created_at)`
2509
2511
  * - `idx_execution_steps_routing_key_created (routing_key, created_at)`
2510
2512
  */
2511
- async function up$36(db) {
2513
+ async function up$40(db) {
2512
2514
  await sql`
2513
2515
  ALTER TABLE public.execution_runs
2514
2516
  ADD COLUMN archived_at TIMESTAMPTZ NULL,
@@ -2553,7 +2555,7 @@ async function up$36(db) {
2553
2555
  ON public.execution_steps (routing_key, created_at)
2554
2556
  `.execute(db);
2555
2557
  }
2556
- async function down$36(db) {
2558
+ async function down$40(db) {
2557
2559
  await sql`DROP INDEX IF EXISTS public.idx_execution_steps_routing_key_created`.execute(db);
2558
2560
  await sql`
2559
2561
  ALTER TABLE public.execution_steps
@@ -2578,8 +2580,8 @@ async function down$36(db) {
2578
2580
  //#endregion
2579
2581
  //#region src/db/migrations/007_audit_logs_archived_at.ts
2580
2582
  var _007_audit_logs_archived_at_exports = /* @__PURE__ */ __exportAll({
2581
- down: () => down$35,
2582
- up: () => up$35
2583
+ down: () => down$39,
2584
+ up: () => up$39
2583
2585
  });
2584
2586
  /**
2585
2587
  * `secret_audit_log` and `access_log` cold-store schema additions, plus
@@ -2619,7 +2621,7 @@ var _007_audit_logs_archived_at_exports = /* @__PURE__ */ __exportAll({
2619
2621
  * `down()` would not have meaningful retention bounds. Acceptable for
2620
2622
  * staging.
2621
2623
  */
2622
- async function up$35(db) {
2624
+ async function up$39(db) {
2623
2625
  await sql`
2624
2626
  ALTER TABLE public.secret_audit_log
2625
2627
  ADD COLUMN archived_at TIMESTAMPTZ NULL,
@@ -2640,7 +2642,7 @@ async function up$35(db) {
2640
2642
  DROP COLUMN IF EXISTS expires_at
2641
2643
  `.execute(db);
2642
2644
  }
2643
- async function down$35(db) {
2645
+ async function down$39(db) {
2644
2646
  await sql`
2645
2647
  ALTER TABLE public.access_log
2646
2648
  ADD COLUMN expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '90 days')
@@ -2668,8 +2670,8 @@ async function down$35(db) {
2668
2670
  //#endregion
2669
2671
  //#region src/db/migrations/008_event_log_archived_at.ts
2670
2672
  var _008_event_log_archived_at_exports = /* @__PURE__ */ __exportAll({
2671
- down: () => down$34,
2672
- up: () => up$34
2673
+ down: () => down$38,
2674
+ up: () => up$38
2673
2675
  });
2674
2676
  /**
2675
2677
  * `event_log` cold-store schema additions plus removal of the
@@ -2707,7 +2709,7 @@ var _008_event_log_archived_at_exports = /* @__PURE__ */ __exportAll({
2707
2709
  * — best effort; rows inserted between `up()` and a hypothetical
2708
2710
  * `down()` would not have meaningful retention bounds.
2709
2711
  */
2710
- async function up$34(db) {
2712
+ async function up$38(db) {
2711
2713
  await sql`
2712
2714
  ALTER TABLE public.event_log
2713
2715
  ADD COLUMN archived_at TIMESTAMPTZ NULL,
@@ -2723,7 +2725,7 @@ async function up$34(db) {
2723
2725
  DROP COLUMN IF EXISTS expires_at
2724
2726
  `.execute(db);
2725
2727
  }
2726
- async function down$34(db) {
2728
+ async function down$38(db) {
2727
2729
  await sql`
2728
2730
  ALTER TABLE public.event_log
2729
2731
  ADD COLUMN expires_at TIMESTAMPTZ NOT NULL DEFAULT (now() + INTERVAL '30 days')
@@ -2742,8 +2744,8 @@ async function down$34(db) {
2742
2744
  //#endregion
2743
2745
  //#region src/db/migrations/009_access_log_trigram.ts
2744
2746
  var _009_access_log_trigram_exports = /* @__PURE__ */ __exportAll({
2745
- down: () => down$33,
2746
- up: () => up$33
2747
+ down: () => down$37,
2748
+ up: () => up$37
2747
2749
  });
2748
2750
  /**
2749
2751
  * Trigram (pg_trgm) index on access_log.error_message for the federated
@@ -2756,7 +2758,7 @@ var _009_access_log_trigram_exports = /* @__PURE__ */ __exportAll({
2756
2758
  * EXISTS` are both safe to re-run. No CONCURRENTLY because Kysely runs
2757
2759
  * migrations inside a transaction; the lock is brief on a sampled table.
2758
2760
  */
2759
- async function up$33(db) {
2761
+ async function up$37(db) {
2760
2762
  await sql`CREATE EXTENSION IF NOT EXISTS pg_trgm`.execute(db);
2761
2763
  await sql`
2762
2764
  CREATE INDEX IF NOT EXISTS access_log_error_message_trgm_idx
@@ -2765,14 +2767,14 @@ async function up$33(db) {
2765
2767
  WHERE error_message IS NOT NULL
2766
2768
  `.execute(db);
2767
2769
  }
2768
- async function down$33(db) {
2770
+ async function down$37(db) {
2769
2771
  await sql`DROP INDEX IF EXISTS public.access_log_error_message_trgm_idx`.execute(db);
2770
2772
  }
2771
2773
  //#endregion
2772
2774
  //#region src/db/migrations/010_cold_store_chunks.ts
2773
2775
  var _010_cold_store_chunks_exports = /* @__PURE__ */ __exportAll({
2774
- down: () => down$32,
2775
- up: () => up$32
2776
+ down: () => down$36,
2777
+ up: () => up$36
2776
2778
  });
2777
2779
  /**
2778
2780
  * Cold-store chunk index — Phase 2 (cold-store purge).
@@ -2804,7 +2806,7 @@ var _010_cold_store_chunks_exports = /* @__PURE__ */ __exportAll({
2804
2806
  * forever. Adapters that don't opt into per-bucket archival via
2805
2807
  * `coldTtlDays` don't insert here either.
2806
2808
  */
2807
- async function up$32(db) {
2809
+ async function up$36(db) {
2808
2810
  await sql`
2809
2811
  CREATE TABLE public.cold_store_chunks (
2810
2812
  db TEXT NOT NULL,
@@ -2831,14 +2833,14 @@ async function up$32(db) {
2831
2833
  ON public.cold_store_chunks (db, table_name, tenant_id, archived_at DESC)
2832
2834
  `.execute(db);
2833
2835
  }
2834
- async function down$32(db) {
2836
+ async function down$36(db) {
2835
2837
  await sql`DROP TABLE IF EXISTS public.cold_store_chunks`.execute(db);
2836
2838
  }
2837
2839
  //#endregion
2838
2840
  //#region src/db/migrations/011_drop_source_secrets_notify.ts
2839
2841
  var _011_drop_source_secrets_notify_exports = /* @__PURE__ */ __exportAll({
2840
- down: () => down$31,
2841
- up: () => up$31
2842
+ down: () => down$35,
2843
+ up: () => up$35
2842
2844
  });
2843
2845
  /**
2844
2846
  * Drop the `source_secrets_change_trigger` and the
@@ -2857,11 +2859,11 @@ var _011_drop_source_secrets_notify_exports = /* @__PURE__ */ __exportAll({
2857
2859
  * in `001_initial.ts`. They wake up no consumer until the `WebhookSecretManager`
2858
2860
  * is restored, so this migration is safe to roll back.
2859
2861
  */
2860
- async function up$31(db) {
2862
+ async function up$35(db) {
2861
2863
  await sql`DROP TRIGGER IF EXISTS source_secrets_change_trigger ON public.scoped_secrets`.execute(db);
2862
2864
  await sql`DROP FUNCTION IF EXISTS public.notify_source_secrets_change() CASCADE`.execute(db);
2863
2865
  }
2864
- async function down$31(db) {
2866
+ async function down$35(db) {
2865
2867
  await sql`
2866
2868
  CREATE OR REPLACE FUNCTION public.notify_source_secrets_change() RETURNS trigger
2867
2869
  LANGUAGE plpgsql
@@ -2900,8 +2902,8 @@ async function down$31(db) {
2900
2902
  //#endregion
2901
2903
  //#region src/db/migrations/012_peer_credentials_active_uniq.ts
2902
2904
  var _012_peer_credentials_active_uniq_exports = /* @__PURE__ */ __exportAll({
2903
- down: () => down$30,
2904
- up: () => up$30
2905
+ down: () => down$34,
2906
+ up: () => up$34
2905
2907
  });
2906
2908
  /**
2907
2909
  * Add a partial unique index on `peer_credentials (instance_id) WHERE
@@ -2927,7 +2929,7 @@ var _012_peer_credentials_active_uniq_exports = /* @__PURE__ */ __exportAll({
2927
2929
  * `down()` only drops the index; it does NOT undo the dedupe (there's no
2928
2930
  * safe way to recreate revoked rows, and the dedupe is monotonic).
2929
2931
  */
2930
- async function up$30(db) {
2932
+ async function up$34(db) {
2931
2933
  await sql`
2932
2934
  UPDATE public.peer_credentials
2933
2935
  SET revoked_at = NOW()
@@ -2945,14 +2947,14 @@ async function up$30(db) {
2945
2947
  WHERE revoked_at IS NULL
2946
2948
  `.execute(db);
2947
2949
  }
2948
- async function down$30(db) {
2950
+ async function down$34(db) {
2949
2951
  await sql`DROP INDEX IF EXISTS public.peer_credentials_active_uniq`.execute(db);
2950
2952
  }
2951
2953
  //#endregion
2952
2954
  //#region src/db/migrations/013_execution_log_bytes.ts
2953
2955
  var _013_execution_log_bytes_exports = /* @__PURE__ */ __exportAll({
2954
- down: () => down$29,
2955
- up: () => up$29
2956
+ down: () => down$33,
2957
+ up: () => up$33
2956
2958
  });
2957
2959
  /**
2958
2960
  * Add `log_bytes BIGINT NOT NULL DEFAULT 0` columns to `execution_runs` and
@@ -2971,7 +2973,7 @@ var _013_execution_log_bytes_exports = /* @__PURE__ */ __exportAll({
2971
2973
  *
2972
2974
  * Idempotent (`ADD COLUMN IF NOT EXISTS`).
2973
2975
  */
2974
- async function up$29(db) {
2976
+ async function up$33(db) {
2975
2977
  await sql`
2976
2978
  ALTER TABLE public.execution_runs
2977
2979
  ADD COLUMN IF NOT EXISTS log_bytes BIGINT NOT NULL DEFAULT 0
@@ -2981,15 +2983,15 @@ async function up$29(db) {
2981
2983
  ADD COLUMN IF NOT EXISTS log_bytes BIGINT NOT NULL DEFAULT 0
2982
2984
  `.execute(db);
2983
2985
  }
2984
- async function down$29(db) {
2986
+ async function down$33(db) {
2985
2987
  await sql`ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS log_bytes`.execute(db);
2986
2988
  await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS log_bytes`.execute(db);
2987
2989
  }
2988
2990
  //#endregion
2989
2991
  //#region src/db/migrations/014_kici_events_lease_retry.ts
2990
2992
  var _014_kici_events_lease_retry_exports = /* @__PURE__ */ __exportAll({
2991
- down: () => down$28,
2992
- up: () => up$28
2993
+ down: () => down$32,
2994
+ up: () => up$32
2993
2995
  });
2994
2996
  /**
2995
2997
  * Add lease + retry + DLQ columns to `kici_events` so the EventRouter can
@@ -3023,7 +3025,7 @@ var _014_kici_events_lease_retry_exports = /* @__PURE__ */ __exportAll({
3023
3025
  *
3024
3026
  * Idempotent (`ADD COLUMN IF NOT EXISTS` + `CREATE INDEX IF NOT EXISTS`).
3025
3027
  */
3026
- async function up$28(db) {
3028
+ async function up$32(db) {
3027
3029
  await sql`
3028
3030
  ALTER TABLE public.kici_events
3029
3031
  ADD COLUMN IF NOT EXISTS claimed_at TIMESTAMPTZ,
@@ -3054,7 +3056,7 @@ async function up$28(db) {
3054
3056
  WHERE dlq_at IS NOT NULL
3055
3057
  `.execute(db);
3056
3058
  }
3057
- async function down$28(db) {
3059
+ async function down$32(db) {
3058
3060
  await sql`DROP INDEX IF EXISTS public.idx_kici_events_dlq`.execute(db);
3059
3061
  await sql`DROP INDEX IF EXISTS public.idx_kici_events_lease_expired`.execute(db);
3060
3062
  await sql`DROP INDEX IF EXISTS public.idx_kici_events_retry_due`.execute(db);
@@ -3072,8 +3074,8 @@ async function down$28(db) {
3072
3074
  //#endregion
3073
3075
  //#region src/db/migrations/015_org_settings_customer_scoped.ts
3074
3076
  var _015_org_settings_customer_scoped_exports = /* @__PURE__ */ __exportAll({
3075
- down: () => down$27,
3076
- up: () => up$27
3077
+ down: () => down$31,
3078
+ up: () => up$31
3077
3079
  });
3078
3080
  /**
3079
3081
  * Org-scope `org_settings` and qualify each glob entry by source.
@@ -3101,7 +3103,7 @@ var _015_org_settings_customer_scoped_exports = /* @__PURE__ */ __exportAll({
3101
3103
  * Idempotent: a re-run on an already-migrated DB sees `customer_id` exists
3102
3104
  * and the list columns are already jsonb, so it is a no-op.
3103
3105
  */
3104
- async function up$27(db) {
3106
+ async function up$31(db) {
3105
3107
  if ((await sql`
3106
3108
  SELECT EXISTS (
3107
3109
  SELECT 1 FROM information_schema.columns
@@ -3226,7 +3228,7 @@ async function up$27(db) {
3226
3228
  await sql`DROP TABLE _org_settings_merged`.execute(db);
3227
3229
  await sql`DROP TABLE _org_settings_stage`.execute(db);
3228
3230
  }
3229
- async function down$27(db) {
3231
+ async function down$31(db) {
3230
3232
  if (!(await sql`
3231
3233
  SELECT EXISTS (
3232
3234
  SELECT 1 FROM information_schema.columns
@@ -3251,8 +3253,8 @@ async function down$27(db) {
3251
3253
  //#endregion
3252
3254
  //#region src/db/migrations/016_org_settings_allow_http_npm.ts
3253
3255
  var _016_org_settings_allow_http_npm_exports = /* @__PURE__ */ __exportAll({
3254
- down: () => down$26,
3255
- up: () => up$26
3256
+ down: () => down$30,
3257
+ up: () => up$30
3256
3258
  });
3257
3259
  /**
3258
3260
  * Add `org_settings.allow_http_npm_registries boolean NOT NULL DEFAULT false`.
@@ -3265,7 +3267,7 @@ var _016_org_settings_allow_http_npm_exports = /* @__PURE__ */ __exportAll({
3265
3267
  *
3266
3268
  * Idempotent: a re-run on a DB that already has the column is a no-op.
3267
3269
  */
3268
- async function up$26(db) {
3270
+ async function up$30(db) {
3269
3271
  if ((await sql`
3270
3272
  SELECT EXISTS (
3271
3273
  SELECT 1 FROM information_schema.columns
@@ -3279,7 +3281,7 @@ async function up$26(db) {
3279
3281
  ADD COLUMN allow_http_npm_registries boolean NOT NULL DEFAULT false
3280
3282
  `.execute(db);
3281
3283
  }
3282
- async function down$26(db) {
3284
+ async function down$30(db) {
3283
3285
  await sql`
3284
3286
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS allow_http_npm_registries
3285
3287
  `.execute(db);
@@ -3287,8 +3289,8 @@ async function down$26(db) {
3287
3289
  //#endregion
3288
3290
  //#region src/db/migrations/017_org_id_widen.ts
3289
3291
  var _017_org_id_widen_exports = /* @__PURE__ */ __exportAll({
3290
- down: () => down$25,
3291
- up: () => up$25
3292
+ down: () => down$29,
3293
+ up: () => up$29
3292
3294
  });
3293
3295
  /**
3294
3296
  * Widen every orchestrator-side `org_id varchar(12)` column to
@@ -3328,17 +3330,17 @@ const ORG_ID_TABLES$1 = [
3328
3330
  "held_runs",
3329
3331
  "scoped_secrets"
3330
3332
  ];
3331
- async function up$25(db) {
3333
+ async function up$29(db) {
3332
3334
  for (const table of ORG_ID_TABLES$1) await sql.raw(`ALTER TABLE public.${table} ALTER COLUMN org_id TYPE varchar(16)`).execute(db);
3333
3335
  }
3334
- async function down$25(db) {
3336
+ async function down$29(db) {
3335
3337
  for (const table of ORG_ID_TABLES$1) await sql.raw(`ALTER TABLE public.${table} ALTER COLUMN org_id TYPE varchar(12)`).execute(db);
3336
3338
  }
3337
3339
  //#endregion
3338
3340
  //#region src/db/migrations/018_org_id_prefix_backfill.ts
3339
3341
  var _018_org_id_prefix_backfill_exports = /* @__PURE__ */ __exportAll({
3340
- down: () => down$24,
3341
- up: () => up$24
3342
+ down: () => down$28,
3343
+ up: () => up$28
3342
3344
  });
3343
3345
  /**
3344
3346
  * Prefix every orchestrator-side tenant string with `org_` to align
@@ -3383,19 +3385,19 @@ const CUSTOMER_ID_TABLES = [
3383
3385
  "workflow_registrations",
3384
3386
  "org_settings"
3385
3387
  ];
3386
- async function up$24(db) {
3388
+ async function up$28(db) {
3387
3389
  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);
3388
3390
  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);
3389
3391
  }
3390
- async function down$24(db) {
3392
+ async function down$28(db) {
3391
3393
  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);
3392
3394
  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);
3393
3395
  }
3394
3396
  //#endregion
3395
3397
  //#region src/db/migrations/019_generic_sources_change_notify.ts
3396
3398
  var _019_generic_sources_change_notify_exports = /* @__PURE__ */ __exportAll({
3397
- down: () => down$23,
3398
- up: () => up$23
3399
+ down: () => down$27,
3400
+ up: () => up$27
3399
3401
  });
3400
3402
  /**
3401
3403
  * Add a Postgres trigger on `generic_webhook_sources` that emits
@@ -3418,7 +3420,7 @@ var _019_generic_sources_change_notify_exports = /* @__PURE__ */ __exportAll({
3418
3420
  * (`notify_sources_change()` + `sources_change_trigger`, defined in
3419
3421
  * `001_initial.ts`).
3420
3422
  */
3421
- async function up$23(db) {
3423
+ async function up$27(db) {
3422
3424
  await sql`
3423
3425
  CREATE FUNCTION public.notify_generic_sources_change() RETURNS trigger
3424
3426
  LANGUAGE plpgsql
@@ -3439,15 +3441,15 @@ async function up$23(db) {
3439
3441
  FOR EACH ROW EXECUTE FUNCTION public.notify_generic_sources_change()
3440
3442
  `.execute(db);
3441
3443
  }
3442
- async function down$23(db) {
3444
+ async function down$27(db) {
3443
3445
  await sql`DROP TRIGGER IF EXISTS generic_sources_change_trigger ON public.generic_webhook_sources`.execute(db);
3444
3446
  await sql`DROP FUNCTION IF EXISTS public.notify_generic_sources_change()`.execute(db);
3445
3447
  }
3446
3448
  //#endregion
3447
3449
  //#region src/db/migrations/020_org_settings_dashboard_write_policy.ts
3448
3450
  var _020_org_settings_dashboard_write_policy_exports = /* @__PURE__ */ __exportAll({
3449
- down: () => down$22,
3450
- up: () => up$22
3451
+ down: () => down$26,
3452
+ up: () => up$26
3451
3453
  });
3452
3454
  /**
3453
3455
  * Add `org_settings.dashboard_write_policy jsonb NOT NULL DEFAULT '{}'`.
@@ -3462,7 +3464,7 @@ var _020_org_settings_dashboard_write_policy_exports = /* @__PURE__ */ __exportA
3462
3464
  *
3463
3465
  * Idempotent: a re-run on a DB that already has the column is a no-op.
3464
3466
  */
3465
- async function up$22(db) {
3467
+ async function up$26(db) {
3466
3468
  if ((await sql`
3467
3469
  SELECT EXISTS (
3468
3470
  SELECT 1 FROM information_schema.columns
@@ -3476,7 +3478,7 @@ async function up$22(db) {
3476
3478
  ADD COLUMN dashboard_write_policy jsonb NOT NULL DEFAULT '{}'::jsonb
3477
3479
  `.execute(db);
3478
3480
  }
3479
- async function down$22(db) {
3481
+ async function down$26(db) {
3480
3482
  await sql`
3481
3483
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS dashboard_write_policy
3482
3484
  `.execute(db);
@@ -3484,8 +3486,8 @@ async function down$22(db) {
3484
3486
  //#endregion
3485
3487
  //#region src/db/migrations/021_check_run_tracking.ts
3486
3488
  var _021_check_run_tracking_exports = /* @__PURE__ */ __exportAll({
3487
- down: () => down$21,
3488
- up: () => up$21
3489
+ down: () => down$25,
3490
+ up: () => up$25
3489
3491
  });
3490
3492
  /**
3491
3493
  * Add `check_run_tracking` table for HA-safe check-run state persistence.
@@ -3509,7 +3511,7 @@ var _021_check_run_tracking_exports = /* @__PURE__ */ __exportAll({
3509
3511
  *
3510
3512
  * Idempotent: a re-run on a DB that already has the table is a no-op.
3511
3513
  */
3512
- async function up$21(db) {
3514
+ async function up$25(db) {
3513
3515
  if ((await sql`
3514
3516
  SELECT EXISTS (
3515
3517
  SELECT 1 FROM information_schema.tables
@@ -3540,14 +3542,14 @@ async function up$21(db) {
3540
3542
  WHERE run_id IS NOT NULL
3541
3543
  `.execute(db);
3542
3544
  }
3543
- async function down$21(db) {
3545
+ async function down$25(db) {
3544
3546
  await sql`DROP TABLE IF EXISTS public.check_run_tracking`.execute(db);
3545
3547
  }
3546
3548
  //#endregion
3547
3549
  //#region src/db/migrations/022_scaler_manager_state.ts
3548
3550
  var _022_scaler_manager_state_exports = /* @__PURE__ */ __exportAll({
3549
- down: () => down$20,
3550
- up: () => up$20
3551
+ down: () => down$24,
3552
+ up: () => up$24
3551
3553
  });
3552
3554
  /**
3553
3555
  * Add three tables persisting `ScalerManager` per-coord state:
@@ -3574,7 +3576,7 @@ var _022_scaler_manager_state_exports = /* @__PURE__ */ __exportAll({
3574
3576
  * Idempotent: a re-run on a DB that already has any of these tables
3575
3577
  * leaves the existing one alone.
3576
3578
  */
3577
- async function up$20(db) {
3579
+ async function up$24(db) {
3578
3580
  const tableExists = async (name) => {
3579
3581
  return (await sql`
3580
3582
  SELECT EXISTS (
@@ -3624,7 +3626,7 @@ async function up$20(db) {
3624
3626
  `.execute(db);
3625
3627
  }
3626
3628
  }
3627
- async function down$20(db) {
3629
+ async function down$24(db) {
3628
3630
  await sql`DROP TABLE IF EXISTS public.scaler_reservations`.execute(db);
3629
3631
  await sql`DROP TABLE IF EXISTS public.scaler_agent_jobs`.execute(db);
3630
3632
  await sql`DROP TABLE IF EXISTS public.scaler_spawning_agents`.execute(db);
@@ -3632,8 +3634,8 @@ async function down$20(db) {
3632
3634
  //#endregion
3633
3635
  //#region src/db/migrations/023_dispatch_queue_recovery_deadline.ts
3634
3636
  var _023_dispatch_queue_recovery_deadline_exports = /* @__PURE__ */ __exportAll({
3635
- down: () => down$19,
3636
- up: () => up$19
3637
+ down: () => down$23,
3638
+ up: () => up$23
3637
3639
  });
3638
3640
  /**
3639
3641
  * Add `dispatch_queue.recovery_deadline TIMESTAMPTZ` and
@@ -3657,7 +3659,7 @@ var _023_dispatch_queue_recovery_deadline_exports = /* @__PURE__ */ __exportAll(
3657
3659
  * Idempotent: re-running on a DB that already has either column is a
3658
3660
  * no-op.
3659
3661
  */
3660
- async function up$19(db) {
3662
+ async function up$23(db) {
3661
3663
  const colExists = async (name) => {
3662
3664
  return (await sql`
3663
3665
  SELECT EXISTS (
@@ -3682,7 +3684,7 @@ async function up$19(db) {
3682
3684
  WHERE recovery_deadline IS NOT NULL
3683
3685
  `.execute(db);
3684
3686
  }
3685
- async function down$19(db) {
3687
+ async function down$23(db) {
3686
3688
  await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_recovery_deadline`.execute(db);
3687
3689
  await sql`
3688
3690
  ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS recovery_agent_id
@@ -3694,8 +3696,8 @@ async function down$19(db) {
3694
3696
  //#endregion
3695
3697
  //#region src/db/migrations/024_dispatch_queue_provisioning_error.ts
3696
3698
  var _024_dispatch_queue_provisioning_error_exports = /* @__PURE__ */ __exportAll({
3697
- down: () => down$18,
3698
- up: () => up$18
3699
+ down: () => down$22,
3700
+ up: () => up$22
3699
3701
  });
3700
3702
  /**
3701
3703
  * Add `dispatch_queue.last_provisioning_error TEXT` recording the most
@@ -3711,7 +3713,7 @@ var _024_dispatch_queue_provisioning_error_exports = /* @__PURE__ */ __exportAll
3711
3713
  *
3712
3714
  * Idempotent: re-running on a DB that already has the column is a no-op.
3713
3715
  */
3714
- async function up$18(db) {
3716
+ async function up$22(db) {
3715
3717
  const colExists = async (name) => {
3716
3718
  return (await sql`
3717
3719
  SELECT EXISTS (
@@ -3727,7 +3729,7 @@ async function up$18(db) {
3727
3729
  ADD COLUMN last_provisioning_error TEXT
3728
3730
  `.execute(db);
3729
3731
  }
3730
- async function down$18(db) {
3732
+ async function down$22(db) {
3731
3733
  await sql`
3732
3734
  ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS last_provisioning_error
3733
3735
  `.execute(db);
@@ -3735,8 +3737,8 @@ async function down$18(db) {
3735
3737
  //#endregion
3736
3738
  //#region src/db/migrations/025_init_failure.ts
3737
3739
  var _025_init_failure_exports = /* @__PURE__ */ __exportAll({
3738
- down: () => down$17,
3739
- up: () => up$17
3740
+ down: () => down$21,
3741
+ up: () => up$21
3740
3742
  });
3741
3743
  /**
3742
3744
  * Add `init_failure jsonb` columns to `execution_runs` and `execution_jobs`.
@@ -3750,7 +3752,7 @@ var _025_init_failure_exports = /* @__PURE__ */ __exportAll({
3750
3752
  * Idempotent: re-running on a DB that already has either column is a no-op
3751
3753
  * for that column.
3752
3754
  */
3753
- async function up$17(db) {
3755
+ async function up$21(db) {
3754
3756
  const colExists = async (table, name) => {
3755
3757
  return (await sql`
3756
3758
  SELECT EXISTS (
@@ -3770,7 +3772,7 @@ async function up$17(db) {
3770
3772
  ADD COLUMN init_failure JSONB DEFAULT NULL
3771
3773
  `.execute(db);
3772
3774
  }
3773
- async function down$17(db) {
3775
+ async function down$21(db) {
3774
3776
  await sql`
3775
3777
  ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS init_failure
3776
3778
  `.execute(db);
@@ -3781,8 +3783,8 @@ async function down$17(db) {
3781
3783
  //#endregion
3782
3784
  //#region src/db/migrations/026_event_log_lockfile_corrupt.ts
3783
3785
  var _026_event_log_lockfile_corrupt_exports = /* @__PURE__ */ __exportAll({
3784
- down: () => down$16,
3785
- up: () => up$16
3786
+ down: () => down$20,
3787
+ up: () => up$20
3786
3788
  });
3787
3789
  /**
3788
3790
  * Extend the event_log.status CHECK constraint with 'lockfile_corrupt' so the
@@ -3791,7 +3793,7 @@ var _026_event_log_lockfile_corrupt_exports = /* @__PURE__ */ __exportAll({
3791
3793
  *
3792
3794
  * Idempotent: the DROP ... IF EXISTS / re-ADD pair re-runs cleanly.
3793
3795
  */
3794
- async function up$16(db) {
3796
+ async function up$20(db) {
3795
3797
  await sql`ALTER TABLE event_log DROP CONSTRAINT IF EXISTS event_log_status_check`.execute(db);
3796
3798
  await sql`
3797
3799
  ALTER TABLE event_log ADD CONSTRAINT event_log_status_check
@@ -3801,7 +3803,7 @@ async function up$16(db) {
3801
3803
  ])))
3802
3804
  `.execute(db);
3803
3805
  }
3804
- async function down$16(db) {
3806
+ async function down$20(db) {
3805
3807
  await sql`ALTER TABLE event_log DROP CONSTRAINT IF EXISTS event_log_status_check`.execute(db);
3806
3808
  await sql`
3807
3809
  ALTER TABLE event_log ADD CONSTRAINT event_log_status_check
@@ -3814,8 +3816,8 @@ async function down$16(db) {
3814
3816
  //#endregion
3815
3817
  //#region src/db/migrations/027_workflow_timeout.ts
3816
3818
  var _027_workflow_timeout_exports = /* @__PURE__ */ __exportAll({
3817
- down: () => down$15,
3818
- up: () => up$15
3819
+ down: () => down$19,
3820
+ up: () => up$19
3819
3821
  });
3820
3822
  /**
3821
3823
  * Add `workflow_timeout_ms integer` to `execution_runs`.
@@ -3833,13 +3835,13 @@ var _027_workflow_timeout_exports = /* @__PURE__ */ __exportAll({
3833
3835
  *
3834
3836
  * Idempotent: re-running on a DB that already has the column is a no-op.
3835
3837
  */
3836
- async function up$15(db) {
3838
+ async function up$19(db) {
3837
3839
  await sql`
3838
3840
  ALTER TABLE public.execution_runs
3839
3841
  ADD COLUMN IF NOT EXISTS workflow_timeout_ms INTEGER DEFAULT NULL
3840
3842
  `.execute(db);
3841
3843
  }
3842
- async function down$15(db) {
3844
+ async function down$19(db) {
3843
3845
  await sql`
3844
3846
  ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS workflow_timeout_ms
3845
3847
  `.execute(db);
@@ -3847,8 +3849,8 @@ async function down$15(db) {
3847
3849
  //#endregion
3848
3850
  //#region src/db/migrations/028_org_settings_user_cache.ts
3849
3851
  var _028_org_settings_user_cache_exports = /* @__PURE__ */ __exportAll({
3850
- down: () => down$14,
3851
- up: () => up$14
3852
+ down: () => down$18,
3853
+ up: () => up$18
3852
3854
  });
3853
3855
  /**
3854
3856
  * Add `org_settings.user_cache_quota_bytes bigint` and
@@ -3877,7 +3879,7 @@ async function columnExists(db, column) {
3877
3879
  ) AS exists
3878
3880
  `.execute(db)).rows[0]?.exists ?? false;
3879
3881
  }
3880
- async function up$14(db) {
3882
+ async function up$18(db) {
3881
3883
  if (!await columnExists(db, "user_cache_quota_bytes")) await sql`
3882
3884
  ALTER TABLE public.org_settings
3883
3885
  ADD COLUMN user_cache_quota_bytes bigint
@@ -3887,7 +3889,7 @@ async function up$14(db) {
3887
3889
  ADD COLUMN user_cache_ttl_ms bigint
3888
3890
  `.execute(db);
3889
3891
  }
3890
- async function down$14(db) {
3892
+ async function down$18(db) {
3891
3893
  await sql`
3892
3894
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS user_cache_quota_bytes
3893
3895
  `.execute(db);
@@ -3898,8 +3900,8 @@ async function down$14(db) {
3898
3900
  //#endregion
3899
3901
  //#region src/db/migrations/029_dispatch_queue_attempts.ts
3900
3902
  var _029_dispatch_queue_attempts_exports = /* @__PURE__ */ __exportAll({
3901
- down: () => down$13,
3902
- up: () => up$13
3903
+ down: () => down$17,
3904
+ up: () => up$17
3903
3905
  });
3904
3906
  /**
3905
3907
  * Add `dispatch_queue.dispatch_attempts INT NOT NULL DEFAULT 0`.
@@ -3913,7 +3915,7 @@ var _029_dispatch_queue_attempts_exports = /* @__PURE__ */ __exportAll({
3913
3915
  *
3914
3916
  * Idempotent: re-running on a DB that already has the column is a no-op.
3915
3917
  */
3916
- async function up$13(db) {
3918
+ async function up$17(db) {
3917
3919
  if (!((await sql`
3918
3920
  SELECT EXISTS (
3919
3921
  SELECT 1 FROM information_schema.columns
@@ -3926,7 +3928,7 @@ async function up$13(db) {
3926
3928
  ADD COLUMN dispatch_attempts INT NOT NULL DEFAULT 0
3927
3929
  `.execute(db);
3928
3930
  }
3929
- async function down$13(db) {
3931
+ async function down$17(db) {
3930
3932
  await sql`
3931
3933
  ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS dispatch_attempts
3932
3934
  `.execute(db);
@@ -3934,8 +3936,8 @@ async function down$13(db) {
3934
3936
  //#endregion
3935
3937
  //#region src/db/migrations/030_held_runs_env_set_null.ts
3936
3938
  var _030_held_runs_env_set_null_exports = /* @__PURE__ */ __exportAll({
3937
- down: () => down$12,
3938
- up: () => up$12
3939
+ down: () => down$16,
3940
+ up: () => up$16
3939
3941
  });
3940
3942
  /**
3941
3943
  * held_runs.environment_id becomes nullable with ON DELETE SET NULL so
@@ -3946,14 +3948,14 @@ var _030_held_runs_env_set_null_exports = /* @__PURE__ */ __exportAll({
3946
3948
  * Idempotent: dropping the NOT NULL and the constraint are both no-ops on a
3947
3949
  * re-run, and the constraint is re-created with the SET NULL action.
3948
3950
  */
3949
- async function up$12(db) {
3951
+ async function up$16(db) {
3950
3952
  await sql`ALTER TABLE public.held_runs ALTER COLUMN environment_id DROP NOT NULL`.execute(db);
3951
3953
  await sql`ALTER TABLE public.held_runs DROP CONSTRAINT IF EXISTS held_runs_environment_id_fkey`.execute(db);
3952
3954
  await sql`ALTER TABLE public.held_runs
3953
3955
  ADD CONSTRAINT held_runs_environment_id_fkey
3954
3956
  FOREIGN KEY (environment_id) REFERENCES public.environments(id) ON DELETE SET NULL`.execute(db);
3955
3957
  }
3956
- async function down$12(db) {
3958
+ async function down$16(db) {
3957
3959
  await sql`ALTER TABLE public.held_runs DROP CONSTRAINT IF EXISTS held_runs_environment_id_fkey`.execute(db);
3958
3960
  await sql`ALTER TABLE public.held_runs
3959
3961
  ADD CONSTRAINT held_runs_environment_id_fkey
@@ -3963,8 +3965,8 @@ async function down$12(db) {
3963
3965
  //#endregion
3964
3966
  //#region src/db/migrations/031_dispatch_queue_ack_deadline.ts
3965
3967
  var _031_dispatch_queue_ack_deadline_exports = /* @__PURE__ */ __exportAll({
3966
- down: () => down$11,
3967
- up: () => up$11
3968
+ down: () => down$15,
3969
+ up: () => up$15
3968
3970
  });
3969
3971
  /**
3970
3972
  * Add `dispatch_queue.ack_deadline TIMESTAMPTZ` and
@@ -3981,7 +3983,7 @@ var _031_dispatch_queue_ack_deadline_exports = /* @__PURE__ */ __exportAll({
3981
3983
  *
3982
3984
  * Idempotent: re-running on a DB that already has either column is a no-op.
3983
3985
  */
3984
- async function up$11(db) {
3986
+ async function up$15(db) {
3985
3987
  const colExists = async (name) => {
3986
3988
  return (await sql`
3987
3989
  SELECT EXISTS (
@@ -4006,7 +4008,7 @@ async function up$11(db) {
4006
4008
  WHERE ack_deadline IS NOT NULL
4007
4009
  `.execute(db);
4008
4010
  }
4009
- async function down$11(db) {
4011
+ async function down$15(db) {
4010
4012
  await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_ack_deadline`.execute(db);
4011
4013
  await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS ack_agent_id`.execute(db);
4012
4014
  await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS ack_deadline`.execute(db);
@@ -4014,8 +4016,8 @@ async function down$11(db) {
4014
4016
  //#endregion
4015
4017
  //#region src/db/migrations/032_org_settings_dispatch_ack_timeout.ts
4016
4018
  var _032_org_settings_dispatch_ack_timeout_exports = /* @__PURE__ */ __exportAll({
4017
- down: () => down$10,
4018
- up: () => up$10
4019
+ down: () => down$14,
4020
+ up: () => up$14
4019
4021
  });
4020
4022
  /**
4021
4023
  * Add `org_settings.dispatch_ack_timeout_ms BIGINT` (nullable).
@@ -4027,7 +4029,7 @@ var _032_org_settings_dispatch_ack_timeout_exports = /* @__PURE__ */ __exportAll
4027
4029
  *
4028
4030
  * Idempotent: a re-run on a DB that already has the column is a no-op.
4029
4031
  */
4030
- async function up$10(db) {
4032
+ async function up$14(db) {
4031
4033
  if ((await sql`
4032
4034
  SELECT EXISTS (
4033
4035
  SELECT 1 FROM information_schema.columns
@@ -4041,7 +4043,7 @@ async function up$10(db) {
4041
4043
  ADD COLUMN dispatch_ack_timeout_ms BIGINT
4042
4044
  `.execute(db);
4043
4045
  }
4044
- async function down$10(db) {
4046
+ async function down$14(db) {
4045
4047
  await sql`
4046
4048
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS dispatch_ack_timeout_ms
4047
4049
  `.execute(db);
@@ -4049,8 +4051,8 @@ async function down$10(db) {
4049
4051
  //#endregion
4050
4052
  //#region src/db/migrations/033_org_settings_approval.ts
4051
4053
  var _033_org_settings_approval_exports = /* @__PURE__ */ __exportAll({
4052
- down: () => down$9,
4053
- up: () => up$9
4054
+ down: () => down$13,
4055
+ up: () => up$13
4054
4056
  });
4055
4057
  /**
4056
4058
  * Add the two approval-policy columns to `org_settings`:
@@ -4067,7 +4069,7 @@ var _033_org_settings_approval_exports = /* @__PURE__ */ __exportAll({
4067
4069
  * and the orchestrator admin route. Idempotent: a re-run on a DB that already
4068
4070
  * has the columns is a no-op (each column is guarded independently).
4069
4071
  */
4070
- async function up$9(db) {
4072
+ async function up$13(db) {
4071
4073
  const colExists = async (column) => {
4072
4074
  return (await sql`
4073
4075
  SELECT EXISTS (
@@ -4087,7 +4089,7 @@ async function up$9(db) {
4087
4089
  ADD COLUMN allow_self_approval BOOLEAN NOT NULL DEFAULT true
4088
4090
  `.execute(db);
4089
4091
  }
4090
- async function down$9(db) {
4092
+ async function down$13(db) {
4091
4093
  await sql`
4092
4094
  ALTER TABLE public.org_settings DROP COLUMN IF EXISTS approval_expiry_seconds
4093
4095
  `.execute(db);
@@ -4098,8 +4100,8 @@ async function down$9(db) {
4098
4100
  //#endregion
4099
4101
  //#region src/db/migrations/034_held_runs_generalize.ts
4100
4102
  var _034_held_runs_generalize_exports = /* @__PURE__ */ __exportAll({
4101
- down: () => down$8,
4102
- up: () => up$8
4103
+ down: () => down$12,
4104
+ up: () => up$12
4103
4105
  });
4104
4106
  /**
4105
4107
  * Generalize `held_runs` from an environment-only hold into the unified
@@ -4121,7 +4123,7 @@ var _034_held_runs_generalize_exports = /* @__PURE__ */ __exportAll({
4121
4123
  * New `held_run_approvals` table: one row per approver decision, FK to
4122
4124
  * `held_runs.id` (uuid) with ON DELETE CASCADE.
4123
4125
  */
4124
- async function up$8(db) {
4126
+ async function up$12(db) {
4125
4127
  const colExists = async (column) => {
4126
4128
  return (await sql`
4127
4129
  SELECT EXISTS (
@@ -4154,7 +4156,7 @@ async function up$8(db) {
4154
4156
  ON public.held_run_approvals USING btree (held_run_id)
4155
4157
  `.execute(db);
4156
4158
  }
4157
- async function down$8(db) {
4159
+ async function down$12(db) {
4158
4160
  await sql`DROP TABLE IF EXISTS public.held_run_approvals`.execute(db);
4159
4161
  await sql`ALTER TABLE public.held_runs DROP COLUMN IF EXISTS approval_requirement`.execute(db);
4160
4162
  await sql`ALTER TABLE public.held_runs DROP COLUMN IF EXISTS trigger_source`.execute(db);
@@ -4164,8 +4166,8 @@ async function down$8(db) {
4164
4166
  //#endregion
4165
4167
  //#region src/db/migrations/035_pending_workflow_contexts.ts
4166
4168
  var _035_pending_workflow_contexts_exports = /* @__PURE__ */ __exportAll({
4167
- down: () => down$7,
4168
- up: () => up$7
4169
+ down: () => down$11,
4170
+ up: () => up$11
4169
4171
  });
4170
4172
  /**
4171
4173
  * Pending workflow dispatch context — backs resume of a workflow whose install
@@ -4174,7 +4176,7 @@ var _035_pending_workflow_contexts_exports = /* @__PURE__ */ __exportAll({
4174
4176
  * wait-timer expiry, concurrency slot free). The row is deleted once the resume
4175
4177
  * dispatch has been kicked off.
4176
4178
  */
4177
- async function up$7(db) {
4179
+ async function up$11(db) {
4178
4180
  await sql`
4179
4181
  CREATE TABLE IF NOT EXISTS public.pending_workflow_contexts (
4180
4182
  run_id text PRIMARY KEY,
@@ -4184,14 +4186,14 @@ async function up$7(db) {
4184
4186
  )
4185
4187
  `.execute(db);
4186
4188
  }
4187
- async function down$7(db) {
4189
+ async function down$11(db) {
4188
4190
  await sql`DROP TABLE IF EXISTS public.pending_workflow_contexts`.execute(db);
4189
4191
  }
4190
4192
  //#endregion
4191
4193
  //#region src/db/migrations/036_attestations.ts
4192
4194
  var _036_attestations_exports = /* @__PURE__ */ __exportAll({
4193
- down: () => down$6,
4194
- up: () => up$6
4195
+ down: () => down$10,
4196
+ up: () => up$10
4195
4197
  });
4196
4198
  /**
4197
4199
  * Add the `attestations` table for build-provenance bundles.
@@ -4204,7 +4206,7 @@ var _036_attestations_exports = /* @__PURE__ */ __exportAll({
4204
4206
  *
4205
4207
  * Idempotent: a re-run on a DB that already has the table is a no-op.
4206
4208
  */
4207
- async function up$6(db) {
4209
+ async function up$10(db) {
4208
4210
  if ((await sql`
4209
4211
  SELECT EXISTS (
4210
4212
  SELECT 1 FROM information_schema.tables
@@ -4230,14 +4232,14 @@ async function up$6(db) {
4230
4232
  ON public.attestations (run_id, job_id)
4231
4233
  `.execute(db);
4232
4234
  }
4233
- async function down$6(db) {
4235
+ async function down$10(db) {
4234
4236
  await sql`DROP TABLE IF EXISTS public.attestations`.execute(db);
4235
4237
  }
4236
4238
  //#endregion
4237
4239
  //#region src/db/migrations/037_generic_sources_provider_type_local.ts
4238
4240
  var _037_generic_sources_provider_type_local_exports = /* @__PURE__ */ __exportAll({
4239
- down: () => down$5,
4240
- up: () => up$5
4241
+ down: () => down$9,
4242
+ up: () => up$9
4241
4243
  });
4242
4244
  /**
4243
4245
  * Replace the `generic_webhook_sources.provider_type` CHECK constraint so it
@@ -4254,7 +4256,7 @@ var _037_generic_sources_provider_type_local_exports = /* @__PURE__ */ __exportA
4254
4256
  * Idempotent: the constraint is dropped IF EXISTS and recreated; the data
4255
4257
  * backfill is a plain UPDATE that is a no-op once no `'internal'` rows remain.
4256
4258
  */
4257
- async function up$5(db) {
4259
+ async function up$9(db) {
4258
4260
  await sql`
4259
4261
  ALTER TABLE public.generic_webhook_sources
4260
4262
  DROP CONSTRAINT IF EXISTS generic_webhook_sources_provider_type_check
@@ -4270,7 +4272,7 @@ async function up$5(db) {
4270
4272
  CHECK (provider_type = ANY (ARRAY['generic'::text, 'local'::text]))
4271
4273
  `.execute(db);
4272
4274
  }
4273
- async function down$5(db) {
4275
+ async function down$9(db) {
4274
4276
  await sql`
4275
4277
  ALTER TABLE public.generic_webhook_sources
4276
4278
  DROP CONSTRAINT IF EXISTS generic_webhook_sources_provider_type_check
@@ -4289,8 +4291,8 @@ async function down$5(db) {
4289
4291
  //#endregion
4290
4292
  //#region src/db/migrations/038_remote_sources.ts
4291
4293
  var _038_remote_sources_exports = /* @__PURE__ */ __exportAll({
4292
- down: () => down$4,
4293
- up: () => up$4
4294
+ down: () => down$8,
4295
+ up: () => up$8
4294
4296
  });
4295
4297
  /**
4296
4298
  * `remote_sources` anchors a Platform-relayed `kici run remote` to its real
@@ -4302,7 +4304,7 @@ var _038_remote_sources_exports = /* @__PURE__ */ __exportAll({
4302
4304
  *
4303
4305
  * Idempotent: a re-run on a DB that already has the table is a no-op.
4304
4306
  */
4305
- async function up$4(db) {
4307
+ async function up$8(db) {
4306
4308
  if ((await sql`
4307
4309
  SELECT EXISTS (
4308
4310
  SELECT 1 FROM information_schema.tables
@@ -4321,14 +4323,14 @@ async function up$4(db) {
4321
4323
  )
4322
4324
  `.execute(db);
4323
4325
  }
4324
- async function down$4(db) {
4326
+ async function down$8(db) {
4325
4327
  await sql`DROP TABLE IF EXISTS public.remote_sources`.execute(db);
4326
4328
  }
4327
4329
  //#endregion
4328
4330
  //#region src/db/migrations/039_host_roster.ts
4329
4331
  var _039_host_roster_exports = /* @__PURE__ */ __exportAll({
4330
- down: () => down$3,
4331
- up: () => up$3
4332
+ down: () => down$7,
4333
+ up: () => up$7
4332
4334
  });
4333
4335
  /**
4334
4336
  * `host_roster` is KiCI's declared inventory: one durable row per agent the
@@ -4345,7 +4347,7 @@ var _039_host_roster_exports = /* @__PURE__ */ __exportAll({
4345
4347
  *
4346
4348
  * Idempotent: a re-run on a DB that already has the table is a no-op.
4347
4349
  */
4348
- async function up$3(db) {
4350
+ async function up$7(db) {
4349
4351
  if ((await sql`
4350
4352
  SELECT EXISTS (
4351
4353
  SELECT 1 FROM information_schema.tables
@@ -4375,14 +4377,14 @@ async function up$3(db) {
4375
4377
  await sql`CREATE INDEX idx_host_roster_reap
4376
4378
  ON public.host_roster (lifecycle_class, last_seen)`.execute(db);
4377
4379
  }
4378
- async function down$3(db) {
4380
+ async function down$7(db) {
4379
4381
  await sql`DROP TABLE IF EXISTS public.host_roster`.execute(db);
4380
4382
  }
4381
4383
  //#endregion
4382
4384
  //#region src/db/migrations/040_runsonall_pin.ts
4383
4385
  var _040_runsonall_pin_exports = /* @__PURE__ */ __exportAll({
4384
- down: () => down$2,
4385
- up: () => up$2
4386
+ down: () => down$6,
4387
+ up: () => up$6
4386
4388
  });
4387
4389
  /**
4388
4390
  * Add the `runsOnAll` host fan-out columns:
@@ -4399,7 +4401,7 @@ var _040_runsonall_pin_exports = /* @__PURE__ */ __exportAll({
4399
4401
  *
4400
4402
  * Idempotent: re-running on a DB that already has any column is a no-op.
4401
4403
  */
4402
- async function colExists$2(db, table, name) {
4404
+ async function colExists$4(db, table, name) {
4403
4405
  return (await sql`
4404
4406
  SELECT EXISTS (
4405
4407
  SELECT 1 FROM information_schema.columns
@@ -4409,18 +4411,18 @@ async function colExists$2(db, table, name) {
4409
4411
  ) AS exists
4410
4412
  `.execute(db)).rows[0]?.exists ?? false;
4411
4413
  }
4412
- async function up$2(db) {
4413
- if (!await colExists$2(db, "dispatch_queue", "pinned_agent_id")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN pinned_agent_id TEXT`.execute(db);
4414
- if (!await colExists$2(db, "execution_jobs", "base_job_name")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN base_job_name TEXT`.execute(db);
4415
- if (!await colExists$2(db, "execution_jobs", "variant_kind")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN variant_kind TEXT`.execute(db);
4416
- if (!await colExists$2(db, "execution_jobs", "variant_label")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN variant_label TEXT`.execute(db);
4414
+ async function up$6(db) {
4415
+ if (!await colExists$4(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$4(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$4(db, "execution_jobs", "variant_kind")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN variant_kind TEXT`.execute(db);
4418
+ if (!await colExists$4(db, "execution_jobs", "variant_label")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN variant_label TEXT`.execute(db);
4417
4419
  await sql`
4418
4420
  CREATE INDEX IF NOT EXISTS idx_dispatch_queue_pinned_agent
4419
4421
  ON public.dispatch_queue (pinned_agent_id)
4420
4422
  WHERE pinned_agent_id IS NOT NULL
4421
4423
  `.execute(db);
4422
4424
  }
4423
- async function down$2(db) {
4425
+ async function down$6(db) {
4424
4426
  await sql`DROP INDEX IF EXISTS public.idx_dispatch_queue_pinned_agent`.execute(db);
4425
4427
  await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS pinned_agent_id`.execute(db);
4426
4428
  await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS variant_label`.execute(db);
@@ -4430,8 +4432,8 @@ async function down$2(db) {
4430
4432
  //#endregion
4431
4433
  //#region src/db/migrations/041_wave_gated.ts
4432
4434
  var _041_wave_gated_exports = /* @__PURE__ */ __exportAll({
4433
- down: () => down$1,
4434
- up: () => up$1
4435
+ down: () => down$5,
4436
+ up: () => up$5
4435
4437
  });
4436
4438
  /**
4437
4439
  * Add the rolling fan-out wave-gate columns:
@@ -4451,7 +4453,7 @@ var _041_wave_gated_exports = /* @__PURE__ */ __exportAll({
4451
4453
  *
4452
4454
  * Idempotent: re-running on a DB that already has the columns is a no-op.
4453
4455
  */
4454
- async function colExists$1(db, table, name) {
4456
+ async function colExists$3(db, table, name) {
4455
4457
  return (await sql`
4456
4458
  SELECT EXISTS (
4457
4459
  SELECT 1 FROM information_schema.columns
@@ -4461,16 +4463,16 @@ async function colExists$1(db, table, name) {
4461
4463
  ) AS exists
4462
4464
  `.execute(db)).rows[0]?.exists ?? false;
4463
4465
  }
4464
- async function up$1(db) {
4465
- if (!await colExists$1(db, "execution_jobs", "wave_gated")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_gated boolean NOT NULL DEFAULT false`.execute(db);
4466
- if (!await colExists$1(db, "execution_jobs", "wave_max_parallel")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_max_parallel integer`.execute(db);
4467
- if (!await colExists$1(db, "execution_jobs", "wave_fail_fast")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_fail_fast boolean`.execute(db);
4466
+ async function up$5(db) {
4467
+ if (!await colExists$3(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$3(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$3(db, "execution_jobs", "wave_fail_fast")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN wave_fail_fast boolean`.execute(db);
4468
4470
  await sql`
4469
4471
  CREATE INDEX IF NOT EXISTS idx_execution_jobs_wave
4470
4472
  ON public.execution_jobs (run_id, base_job_name, wave_gated)
4471
4473
  `.execute(db);
4472
4474
  }
4473
- async function down$1(db) {
4475
+ async function down$5(db) {
4474
4476
  await sql`DROP INDEX IF EXISTS public.idx_execution_jobs_wave`.execute(db);
4475
4477
  await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_fail_fast`.execute(db);
4476
4478
  await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS wave_max_parallel`.execute(db);
@@ -4479,8 +4481,8 @@ async function down$1(db) {
4479
4481
  //#endregion
4480
4482
  //#region src/db/migrations/042_dispatch_queue_patterns.ts
4481
4483
  var _042_dispatch_queue_patterns_exports = /* @__PURE__ */ __exportAll({
4482
- down: () => down,
4483
- up: () => up
4484
+ down: () => down$4,
4485
+ up: () => up$4
4484
4486
  });
4485
4487
  /**
4486
4488
  * Add pattern columns to dispatch_queue. Exact labels stay in runs_on_labels /
@@ -4496,6 +4498,76 @@ var _042_dispatch_queue_patterns_exports = /* @__PURE__ */ __exportAll({
4496
4498
  *
4497
4499
  * Idempotent: re-running on a DB that already has the columns is a no-op.
4498
4500
  */
4501
+ async function colExists$2(db, table, name) {
4502
+ return (await sql`
4503
+ SELECT EXISTS (
4504
+ SELECT 1 FROM information_schema.columns
4505
+ WHERE table_schema = 'public'
4506
+ AND table_name = ${table}
4507
+ AND column_name = ${name}
4508
+ ) AS exists
4509
+ `.execute(db)).rows[0]?.exists ?? false;
4510
+ }
4511
+ async function up$4(db) {
4512
+ if (!await colExists$2(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$2(db, "dispatch_queue", "exclude_patterns")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN exclude_patterns jsonb NOT NULL DEFAULT '[]'::jsonb`.execute(db);
4514
+ }
4515
+ async function down$4(db) {
4516
+ await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS exclude_patterns`.execute(db);
4517
+ await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS runs_on_patterns`.execute(db);
4518
+ }
4519
+ //#endregion
4520
+ //#region src/db/migrations/043_rerouted_to_peer.ts
4521
+ var _043_rerouted_to_peer_exports = /* @__PURE__ */ __exportAll({
4522
+ down: () => down$3,
4523
+ up: () => up$3
4524
+ });
4525
+ /**
4526
+ * Add a nullable `rerouted_to_peer text` marker to execution_jobs. Non-null
4527
+ * means the job was rerouted to a remote worker peer (the value is that
4528
+ * peer's instance id); NULL means the job runs locally. The run-recovery
4529
+ * sweepers read it so a rerouted job whose worker is still connected is not
4530
+ * prematurely failed.
4531
+ *
4532
+ * Idempotent: re-running on a DB that already has the column is a no-op.
4533
+ */
4534
+ async function colExists$1(db, table, name) {
4535
+ return (await sql`
4536
+ SELECT EXISTS (
4537
+ SELECT 1 FROM information_schema.columns
4538
+ WHERE table_schema = 'public'
4539
+ AND table_name = ${table}
4540
+ AND column_name = ${name}
4541
+ ) AS exists
4542
+ `.execute(db)).rows[0]?.exists ?? false;
4543
+ }
4544
+ async function up$3(db) {
4545
+ if (!await colExists$1(db, "execution_jobs", "rerouted_to_peer")) await sql`ALTER TABLE public.execution_jobs ADD COLUMN rerouted_to_peer text`.execute(db);
4546
+ }
4547
+ async function down$3(db) {
4548
+ await sql`ALTER TABLE public.execution_jobs DROP COLUMN IF EXISTS rerouted_to_peer`.execute(db);
4549
+ }
4550
+ //#endregion
4551
+ //#region src/db/migrations/044_check_mode.ts
4552
+ var _044_check_mode_exports = /* @__PURE__ */ __exportAll({
4553
+ down: () => down$2,
4554
+ up: () => up$2
4555
+ });
4556
+ /**
4557
+ * Add idempotent check-mode columns:
4558
+ *
4559
+ * - `execution_runs.check_mode text` — the run's mode (`apply` | `check` |
4560
+ * `check-fail-on-drift`). NULL means a legacy/apply run.
4561
+ * - `execution_steps.check_outcome text` — the per-step `CheckStepOutcome`
4562
+ * (`skipped` | `applied` | `declined` | `dry-run` | `no_check`). NULL when
4563
+ * the step ran without a check mode.
4564
+ * - `execution_steps.drift_summary text` — the human-readable `summarize(drift)`
4565
+ * line. NULL when no drift.
4566
+ * - `execution_steps.drift jsonb` — the structured drift value. NULL when no drift.
4567
+ *
4568
+ * All nullable. Idempotent: re-running on a DB that already has the columns is
4569
+ * a no-op.
4570
+ */
4499
4571
  async function colExists(db, table, name) {
4500
4572
  return (await sql`
4501
4573
  SELECT EXISTS (
@@ -4506,13 +4578,68 @@ async function colExists(db, table, name) {
4506
4578
  ) AS exists
4507
4579
  `.execute(db)).rows[0]?.exists ?? false;
4508
4580
  }
4581
+ async function up$2(db) {
4582
+ if (!await colExists(db, "execution_runs", "check_mode")) await sql`ALTER TABLE public.execution_runs ADD COLUMN check_mode text`.execute(db);
4583
+ if (!await colExists(db, "execution_steps", "check_outcome")) await sql`ALTER TABLE public.execution_steps ADD COLUMN check_outcome text`.execute(db);
4584
+ if (!await colExists(db, "execution_steps", "drift_summary")) await sql`ALTER TABLE public.execution_steps ADD COLUMN drift_summary text`.execute(db);
4585
+ if (!await colExists(db, "execution_steps", "drift")) await sql`ALTER TABLE public.execution_steps ADD COLUMN drift jsonb`.execute(db);
4586
+ }
4587
+ async function down$2(db) {
4588
+ await sql`ALTER TABLE public.execution_steps DROP COLUMN IF EXISTS drift`.execute(db);
4589
+ await sql`ALTER TABLE public.execution_steps DROP COLUMN IF EXISTS drift_summary`.execute(db);
4590
+ await sql`ALTER TABLE public.execution_steps DROP COLUMN IF EXISTS check_outcome`.execute(db);
4591
+ await sql`ALTER TABLE public.execution_runs DROP COLUMN IF EXISTS check_mode`.execute(db);
4592
+ }
4593
+ //#endregion
4594
+ //#region src/db/migrations/045_host_properties.ts
4595
+ var _045_host_properties_exports = /* @__PURE__ */ __exportAll({
4596
+ down: () => down$1,
4597
+ up: () => up$1
4598
+ });
4599
+ /**
4600
+ * Add the typed host-vars dimension to the host roster:
4601
+ *
4602
+ * - `host_roster.host_properties jsonb NOT NULL DEFAULT '{}'` — the typed
4603
+ * property bag (`string | number | boolean` values) reported by the agent at
4604
+ * registration and/or declared by the operator (`kici-admin host declare
4605
+ * --prop`). Labels stay the flat-string grouping dimension; properties are
4606
+ * the separate queryable host-vars dimension.
4607
+ *
4608
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`): re-running on a DB that already has
4609
+ * the column is a no-op. Staging data is preserved (additive column).
4610
+ */
4611
+ async function up$1(db) {
4612
+ await sql`ALTER TABLE public.host_roster
4613
+ ADD COLUMN IF NOT EXISTS host_properties jsonb NOT NULL DEFAULT '{}'::jsonb`.execute(db);
4614
+ }
4615
+ async function down$1(db) {
4616
+ await sql`ALTER TABLE public.host_roster DROP COLUMN IF EXISTS host_properties`.execute(db);
4617
+ }
4618
+ //#endregion
4619
+ //#region src/db/migrations/046_join_token_consumed_by_instance.ts
4620
+ var _046_join_token_consumed_by_instance_exports = /* @__PURE__ */ __exportAll({
4621
+ down: () => down,
4622
+ up: () => up
4623
+ });
4624
+ /**
4625
+ * Record the joining peer's instanceId at first consumption of a join token:
4626
+ *
4627
+ * - `join_tokens.consumed_by_instance text` — the `instanceId` of the peer that
4628
+ * consumed the token. This is the *joining peer*, distinct from `consumed_by`
4629
+ * (the coordinator that processed the claim). It lets a token be reused by the
4630
+ * same peer instance until `expires_at`, so a peer that lost its credential
4631
+ * (transient outage / deleted credential file) self-heals by re-presenting the
4632
+ * join token already in its env — no operator action, no cluster redeploy.
4633
+ *
4634
+ * Idempotent (`ADD COLUMN IF NOT EXISTS`): re-running on a DB that already has
4635
+ * the column is a no-op. Staging data is preserved (additive nullable column).
4636
+ */
4509
4637
  async function up(db) {
4510
- if (!await colExists(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);
4511
- if (!await colExists(db, "dispatch_queue", "exclude_patterns")) await sql`ALTER TABLE public.dispatch_queue ADD COLUMN exclude_patterns jsonb NOT NULL DEFAULT '[]'::jsonb`.execute(db);
4638
+ await sql`ALTER TABLE public.join_tokens
4639
+ ADD COLUMN IF NOT EXISTS consumed_by_instance text`.execute(db);
4512
4640
  }
4513
4641
  async function down(db) {
4514
- await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS exclude_patterns`.execute(db);
4515
- await sql`ALTER TABLE public.dispatch_queue DROP COLUMN IF EXISTS runs_on_patterns`.execute(db);
4642
+ await sql`ALTER TABLE public.join_tokens DROP COLUMN IF EXISTS consumed_by_instance`.execute(db);
4516
4643
  }
4517
4644
  //#endregion
4518
4645
  //#region src/db/migration-provider.ts
@@ -4560,7 +4687,11 @@ function createMigrationProvider() {
4560
4687
  "039_host_roster": _039_host_roster_exports,
4561
4688
  "040_runsonall_pin": _040_runsonall_pin_exports,
4562
4689
  "041_wave_gated": _041_wave_gated_exports,
4563
- "042_dispatch_queue_patterns": _042_dispatch_queue_patterns_exports
4690
+ "042_dispatch_queue_patterns": _042_dispatch_queue_patterns_exports,
4691
+ "043_rerouted_to_peer": _043_rerouted_to_peer_exports,
4692
+ "044_check_mode": _044_check_mode_exports,
4693
+ "045_host_properties": _045_host_properties_exports,
4694
+ "046_join_token_consumed_by_instance": _046_join_token_consumed_by_instance_exports
4564
4695
  };
4565
4696
  } };
4566
4697
  }
@@ -5187,6 +5318,400 @@ function installPostReceiveHook(repoPath, script) {
5187
5318
  return hookPath;
5188
5319
  }
5189
5320
  //#endregion
5321
+ //#region src/providers/github/auth.ts
5322
+ /**
5323
+ * GitHub clone token provider and authentication utilities.
5324
+ *
5325
+ * Implements the CloneTokenProvider interface from @kici-dev/engine for GitHub.
5326
+ * Uses GitHub App installation tokens for repository access.
5327
+ */
5328
+ /**
5329
+ * Create an Octokit instance authenticated as a GitHub App installation.
5330
+ *
5331
+ * Uses @octokit/auth-app to handle:
5332
+ * - JWT generation for app-level authentication
5333
+ * - Installation token creation and automatic refresh
5334
+ *
5335
+ * Each call creates a new Octokit instance scoped to the specific installation.
5336
+ * The auth-app strategy handles token caching internally per Octokit instance.
5337
+ *
5338
+ * @param config - GitHub App credentials (appId + privateKey)
5339
+ * @param installationId - GitHub App installation ID for the target account/org
5340
+ * @returns Octokit instance with auto-refreshing installation tokens
5341
+ */
5342
+ function createInstallationOctokit(config, installationId) {
5343
+ return new Octokit({
5344
+ authStrategy: createAppAuth,
5345
+ auth: {
5346
+ appId: config.appId,
5347
+ privateKey: config.privateKey,
5348
+ installationId
5349
+ }
5350
+ });
5351
+ }
5352
+ //#endregion
5353
+ //#region src/providers/github/manifest.ts
5354
+ /**
5355
+ * GitHub App Manifest flow helpers. The manifest encodes KiCI's exact
5356
+ * permissions + events + webhook config so the operator never picks them by
5357
+ * hand — GitHub creates a correctly-configured App from this object in one
5358
+ * click. See docs.github.com "Registering a GitHub App from a manifest".
5359
+ *
5360
+ * The webhook secret is NOT part of the manifest: GitHub generates it during
5361
+ * registration and returns it on the conversion response, so both GitHub and
5362
+ * the Platform end up sharing the same secret with zero operator effort.
5363
+ */
5364
+ function buildGithubAppManifest(input) {
5365
+ return {
5366
+ name: input.name,
5367
+ url: "https://kici.dev",
5368
+ hook_attributes: {
5369
+ url: input.webhookUrl,
5370
+ active: true
5371
+ },
5372
+ redirect_url: input.redirectUrl,
5373
+ ...input.setupUrl ? { setup_url: input.setupUrl } : {},
5374
+ public: false,
5375
+ default_permissions: {
5376
+ contents: "read",
5377
+ metadata: "read",
5378
+ pull_requests: "read",
5379
+ checks: "write",
5380
+ members: "read"
5381
+ },
5382
+ default_events: [
5383
+ "push",
5384
+ "pull_request",
5385
+ "check_run",
5386
+ "check_suite"
5387
+ ]
5388
+ };
5389
+ }
5390
+ /**
5391
+ * Exchange the short-lived manifest `code` for the App's id, private key, and
5392
+ * webhook secret. Runs server-to-server directly against GitHub — the private
5393
+ * key never transits the Platform.
5394
+ */
5395
+ async function convertManifestCode(code, deps = {}) {
5396
+ const { data } = await (deps.octokit ?? new Octokit()).request("POST /app-manifests/{code}/conversions", { code });
5397
+ const d = data;
5398
+ if (!d.webhook_secret) throw new Error("GitHub returned no webhook secret for the new App — cannot verify inbound events. Re-run the setup, or configure a webhook secret manually with source update.");
5399
+ return {
5400
+ appId: String(d.id),
5401
+ slug: d.slug,
5402
+ privateKey: d.pem,
5403
+ webhookSecret: d.webhook_secret,
5404
+ clientId: d.client_id,
5405
+ clientSecret: d.client_secret,
5406
+ htmlUrl: d.html_url
5407
+ };
5408
+ }
5409
+ function appOctokitFor(creds) {
5410
+ return new Octokit({
5411
+ authStrategy: createAppAuth,
5412
+ auth: {
5413
+ appId: creds.appId,
5414
+ privateKey: creds.privateKey
5415
+ }
5416
+ });
5417
+ }
5418
+ /**
5419
+ * Poll GitHub (as the App, via a JWT) until at least one installation exists,
5420
+ * returning the first installation's id + account login. Throws on timeout.
5421
+ */
5422
+ async function waitForInstallation(creds, opts) {
5423
+ const now = opts.now ?? Date.now;
5424
+ const octokit = opts.appOctokit ?? appOctokitFor(creds);
5425
+ const deadline = now() + opts.timeoutMs;
5426
+ for (;;) {
5427
+ const { data } = await octokit.request("GET /app/installations", { per_page: 1 });
5428
+ const installs = data;
5429
+ if (Array.isArray(installs) && installs.length > 0) return {
5430
+ installationId: installs[0].id,
5431
+ accountLogin: installs[0].account?.login ?? ""
5432
+ };
5433
+ if (now() >= deadline) throw new Error("Timed out waiting for the GitHub App to be installed");
5434
+ await new Promise((r) => setTimeout(r, opts.pollMs));
5435
+ }
5436
+ }
5437
+ /**
5438
+ * Mint an installation token from the captured private key and confirm the App
5439
+ * can reach repos — proves the key works end-to-end (the same path the agent's
5440
+ * clone uses at runtime).
5441
+ */
5442
+ async function verifyRepoAccess(creds, installationId, deps = {}) {
5443
+ const { data } = await (deps.octokit ?? createInstallationOctokit(creds, installationId)).request("GET /installation/repositories", { per_page: 1 });
5444
+ return { repoCount: data.total_count };
5445
+ }
5446
+ //#endregion
5447
+ //#region src/providers/github/manifest-form.ts
5448
+ /**
5449
+ * GitHub's create-from-manifest flow is an HTML form POST (the `manifest`
5450
+ * JSON cannot ride in a query string). These helpers render the auto-submitting
5451
+ * form and the headless code-display page. Pure string builders — no IO — so
5452
+ * both the CLI loopback server and the static marketing-site page can reuse the
5453
+ * same shapes.
5454
+ */
5455
+ function escapeHtml(s) {
5456
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
5457
+ }
5458
+ function manifestCreateUrl(githubOrg) {
5459
+ return githubOrg ? `https://github.com/organizations/${githubOrg}/settings/apps/new` : "https://github.com/settings/apps/new";
5460
+ }
5461
+ function renderManifestFormHtml(opts) {
5462
+ return `<!doctype html><html><head><meta charset="utf-8"><title>Create your KiCI GitHub App</title></head>
5463
+ <body>
5464
+ <p>Redirecting you to GitHub to create your KiCI GitHub App…</p>
5465
+ <form id="f" method="post" action="${escapeHtml(`${opts.createUrl}?state=${encodeURIComponent(opts.state)}`)}">
5466
+ <input type="hidden" name="manifest" value="${escapeHtml(opts.manifestJson)}">
5467
+ <noscript><button type="submit">Continue to GitHub</button></noscript>
5468
+ </form>
5469
+ <script>document.getElementById('f').submit();<\/script>
5470
+ </body></html>`;
5471
+ }
5472
+ function renderCodeDisplayHtml(code) {
5473
+ return `<!doctype html><html><head><meta charset="utf-8"><title>KiCI — copy your setup code</title></head>
5474
+ <body>
5475
+ <p>App created. Copy this code back into the <code>kici-admin</code> prompt:</p>
5476
+ <pre id="c" style="font-size:1.2rem;padding:12px;border:1px solid #ccc">${escapeHtml(code)}</pre>
5477
+ <button onclick="navigator.clipboard.writeText(document.getElementById('c').textContent)">Copy</button>
5478
+ </body></html>`;
5479
+ }
5480
+ //#endregion
5481
+ //#region src/cli/loopback-callback.ts
5482
+ function startManifestLoopback(opts) {
5483
+ return new Promise((resolveServer) => {
5484
+ let outcome;
5485
+ const waiters = [];
5486
+ const resolveCode = (value) => {
5487
+ if (outcome) return;
5488
+ outcome = {
5489
+ ok: true,
5490
+ value
5491
+ };
5492
+ for (const w of waiters) w.resolve(value);
5493
+ };
5494
+ const rejectCode = (error) => {
5495
+ if (outcome) return;
5496
+ outcome = {
5497
+ ok: false,
5498
+ error
5499
+ };
5500
+ for (const w of waiters) w.reject(error);
5501
+ };
5502
+ const server = createServer((req, res) => {
5503
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
5504
+ if (url.pathname === "/cb") {
5505
+ const code = url.searchParams.get("code") ?? "";
5506
+ const state = url.searchParams.get("state") ?? "";
5507
+ res.writeHead(200, { "content-type": "text/html" });
5508
+ res.end(renderCodeDisplayHtml(code));
5509
+ if (state !== opts.state) rejectCode(/* @__PURE__ */ new Error("OAuth state mismatch — aborting"));
5510
+ else resolveCode({
5511
+ code,
5512
+ state
5513
+ });
5514
+ return;
5515
+ }
5516
+ res.writeHead(200, { "content-type": "text/html" });
5517
+ res.end(renderManifestFormHtml({
5518
+ createUrl: opts.createUrl,
5519
+ state: opts.state,
5520
+ manifestJson: opts.manifestJson
5521
+ }));
5522
+ });
5523
+ server.on("clientError", () => {});
5524
+ server.listen(0, "127.0.0.1", () => {
5525
+ const base = `http://127.0.0.1:${server.address().port}`;
5526
+ resolveServer({
5527
+ redirectUrl: `${base}/cb`,
5528
+ formUrl: `${base}/`,
5529
+ waitForCode: (timeoutMs) => new Promise((resolve, reject) => {
5530
+ if (outcome) {
5531
+ outcome.ok ? resolve(outcome.value) : reject(outcome.error);
5532
+ return;
5533
+ }
5534
+ const timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Timed out waiting for GitHub callback")), timeoutMs);
5535
+ waiters.push({
5536
+ resolve: (v) => {
5537
+ clearTimeout(timer);
5538
+ resolve(v);
5539
+ },
5540
+ reject: (e) => {
5541
+ clearTimeout(timer);
5542
+ reject(e);
5543
+ }
5544
+ });
5545
+ }),
5546
+ close: () => server.close()
5547
+ });
5548
+ });
5549
+ });
5550
+ }
5551
+ //#endregion
5552
+ //#region src/cli/open-browser.ts
5553
+ /**
5554
+ * Open a URL in the operator's default browser, best-effort. Used by the GitHub
5555
+ * App manifest setup flow to launch the create / install pages. Failure is
5556
+ * non-fatal — the caller always prints the URL too, so a headless host (or a
5557
+ * blocked launcher) just falls back to copy-paste.
5558
+ *
5559
+ * `KICI_BROWSER_CMD` overrides the launcher: `none` suppresses it entirely
5560
+ * (E2E / headless capture), any other value is run with `{url}` substituted —
5561
+ * mirroring the `kici login` convention.
5562
+ */
5563
+ function openBrowserBestEffort(url) {
5564
+ const override = process.env.KICI_BROWSER_CMD;
5565
+ if (override === "none") return;
5566
+ if (override) {
5567
+ exec(override.replace("{url}", url), () => {});
5568
+ return;
5569
+ }
5570
+ exec(platform() === "darwin" ? `open ${JSON.stringify(url)}` : platform() === "win32" ? `start "" ${JSON.stringify(url)}` : `xdg-open ${JSON.stringify(url)}`, () => {});
5571
+ }
5572
+ //#endregion
5573
+ //#region src/cli/commands/source-manifest.ts
5574
+ /**
5575
+ * `kici-admin source add github --manifest` — the one-click GitHub App setup
5576
+ * flow. Drives GitHub's App Manifest flow end-to-end: builds a pre-configured
5577
+ * manifest (KiCI's exact permissions/events/webhook URL), hands it to GitHub via
5578
+ * an auto-submitting form, catches the returned short-lived setup code over a
5579
+ * localhost loopback (or copy-paste in --no-browser mode), exchanges it for the
5580
+ * App's id + private key + webhook secret, then reuses the existing source
5581
+ * storage + Platform-registration path.
5582
+ *
5583
+ * The private-key-bearing conversion happens entirely here on the orchestrator
5584
+ * host — it never transits the Platform (sovereignty invariant).
5585
+ */
5586
+ /** Static marketing-site page the CLI points at in headless paste-code mode. */
5587
+ const STATIC_CALLBACK_URL = "https://kici.dev/gh-manifest-callback";
5588
+ /** How long to wait for the operator to click "Create" on GitHub. */
5589
+ const CREATE_TIMEOUT_MS = 5 * 6e4;
5590
+ /** How long to wait for the operator to install the App on at least one repo. */
5591
+ const INSTALL_TIMEOUT_MS = 5 * 6e4;
5592
+ const INSTALL_POLL_MS = 3e3;
5593
+ function defaultReadLine(prompt) {
5594
+ const rl = createInterface({
5595
+ input: process.stdin,
5596
+ output: process.stdout
5597
+ });
5598
+ return new Promise((resolve) => {
5599
+ rl.question(prompt, (answer) => {
5600
+ rl.close();
5601
+ resolve(answer.trim());
5602
+ });
5603
+ });
5604
+ }
5605
+ function defaultWriteRecoveryFile(appId, pem) {
5606
+ const file = join(mkdtempSync(join(tmpdir(), "kici-gh-app-")), `app-${appId}.private-key.pem`);
5607
+ writeFileSync(file, pem, { mode: 384 });
5608
+ return file;
5609
+ }
5610
+ const realManifestSetupDeps = {
5611
+ startLoopback: startManifestLoopback,
5612
+ openBrowser: openBrowserBestEffort,
5613
+ readLine: defaultReadLine,
5614
+ convert: (code) => convertManifestCode(code),
5615
+ waitForInstallation,
5616
+ verifyRepoAccess,
5617
+ writeRecoveryFile: defaultWriteRecoveryFile
5618
+ };
5619
+ /** Resolve the org-scoped webhook URL via the pre-flight route; abort if null. */
5620
+ async function resolveWebhookUrl(client) {
5621
+ const pre = await client.get("/api/v1/admin/sources/github-webhook-url");
5622
+ if (!pre.webhookUrl) throw new Error(`Cannot resolve the GitHub webhook URL${pre.webhookNote ? ` (${pre.webhookNote})` : ""}. The manifest flow needs it to configure the App. Ensure the orchestrator is connected to the Platform and has identified its org, then retry.`);
5623
+ return pre.webhookUrl;
5624
+ }
5625
+ /**
5626
+ * Catch the short-lived setup code: over the loopback (browser mode) or by
5627
+ * printing the static-site URL and reading the pasted code (headless mode).
5628
+ */
5629
+ async function captureCode(opts, deps, manifestJson, state, redirectUrl, createUrl, loopback) {
5630
+ if (opts.noBrowser) {
5631
+ const url = `${STATIC_CALLBACK_URL}#m=${Buffer.from(manifestJson, "utf-8").toString("base64url")}&state=${encodeURIComponent(state)}&createUrl=${encodeURIComponent(createUrl)}`;
5632
+ console.log("Open this URL in a browser to create your App:");
5633
+ console.log(` ${url}`);
5634
+ console.log("After clicking \"Create\", copy the setup code shown on the page.");
5635
+ const code = await deps.readLine("Paste the setup code here: ");
5636
+ if (!code) throw new Error("No setup code was provided");
5637
+ return code;
5638
+ }
5639
+ console.log("→ Opening GitHub to create your App…");
5640
+ await deps.openBrowser(loopback.formUrl);
5641
+ console.log(` (if your browser did not open, visit ${loopback.formUrl} )`);
5642
+ const { code } = await loopback.waitForCode(CREATE_TIMEOUT_MS);
5643
+ return code;
5644
+ }
5645
+ /** Store credentials + register; on failure, save the PEM so the App is not orphaned. */
5646
+ async function storeAndRegister(opts, client, deps, creds) {
5647
+ try {
5648
+ return await client.post("/api/v1/admin/sources", {
5649
+ provider: "github",
5650
+ name: opts.name,
5651
+ appId: creds.appId,
5652
+ privateKey: creds.privateKey,
5653
+ webhookSecret: creds.webhookSecret
5654
+ });
5655
+ } catch (err) {
5656
+ const file = deps.writeRecoveryFile(creds.appId, creds.privateKey);
5657
+ console.error("");
5658
+ console.error("⚠ The GitHub App was created but storing it on the orchestrator failed.");
5659
+ console.error(` App id: ${creds.appId}`);
5660
+ console.error(` Private key saved to: ${file}`);
5661
+ console.error(" Recover with:");
5662
+ console.error(` kici-admin source add github --name "${opts.name}" --app-id ${creds.appId} --private-key @${file} --webhook-secret ${creds.webhookSecret}`);
5663
+ throw err;
5664
+ }
5665
+ }
5666
+ async function runGithubManifestSetup(opts, client, deps = realManifestSetupDeps) {
5667
+ const webhookUrl = await resolveWebhookUrl(client);
5668
+ const state = randomBytes(16).toString("hex");
5669
+ const createUrl = manifestCreateUrl(opts.githubOrg);
5670
+ const loopback = await deps.startLoopback({
5671
+ state,
5672
+ manifestJson: "{}",
5673
+ createUrl
5674
+ });
5675
+ try {
5676
+ const manifest = buildGithubAppManifest({
5677
+ name: opts.name,
5678
+ webhookUrl,
5679
+ redirectUrl: opts.noBrowser ? STATIC_CALLBACK_URL : loopback.redirectUrl
5680
+ });
5681
+ const manifestJson = JSON.stringify(manifest);
5682
+ loopback.close();
5683
+ const lb = await deps.startLoopback({
5684
+ state,
5685
+ manifestJson,
5686
+ createUrl
5687
+ });
5688
+ try {
5689
+ const code = await captureCode(opts, deps, manifestJson, state, lb.redirectUrl, createUrl, lb);
5690
+ const creds = await deps.convert(code);
5691
+ console.log(`→ ✓ App created (id ${creds.appId}), credentials captured`);
5692
+ const stored = await storeAndRegister(opts, client, deps, creds);
5693
+ console.log(`→ ✓ Stored on orchestrator (encrypted), registered as ${stored.routingKey}`);
5694
+ const installUrl = `https://github.com/apps/${creds.slug}/installations/new`;
5695
+ console.log(`→ Install the App on your repos: ${installUrl}`);
5696
+ if (!opts.noBrowser) await deps.openBrowser(installUrl);
5697
+ const { installationId, accountLogin } = await deps.waitForInstallation(creds, {
5698
+ timeoutMs: INSTALL_TIMEOUT_MS,
5699
+ pollMs: INSTALL_POLL_MS
5700
+ });
5701
+ console.log(`→ ✓ Installation detected (account ${accountLogin})`);
5702
+ const { repoCount } = await deps.verifyRepoAccess(creds, installationId);
5703
+ console.log(`→ ✓ Credentials verified (${repoCount} repositories reachable)`);
5704
+ console.log("");
5705
+ console.log(`GitHub App "${opts.name}" is live.`);
5706
+ console.log(` Webhook: ${webhookUrl}`);
5707
+ } finally {
5708
+ lb.close();
5709
+ }
5710
+ } finally {
5711
+ loopback.close();
5712
+ }
5713
+ }
5714
+ //#endregion
5190
5715
  //#region src/cli/commands/source.ts
5191
5716
  /**
5192
5717
  * Source management commands for kici-admin.
@@ -5375,8 +5900,25 @@ async function buildUniversalGitConfigFromOpts(opts) {
5375
5900
  function registerSourceCommands(program, getClient) {
5376
5901
  const src = program.command("source").description("Manage webhook sources");
5377
5902
  const add = src.command("add").description("Add a new webhook source");
5378
- add.command("github").description("Add a new GitHub App source").requiredOption("--name <name>", "Human-readable source name").requiredOption("--app-id <id>", "GitHub App ID").option("--private-key <pathOrValue>", "Private key (prefix with @ for file path)").option("--webhook-secret <secret>", "Webhook secret").option("--from-env <varName>", "Read private key from environment variable").option("--stdin", "Read private key from stdin").option("--json", "Emit raw JSON (the API response) instead of formatted text").action(async (opts) => {
5903
+ add.command("github").description("Add a new GitHub App source").requiredOption("--name <name>", "Human-readable source name").option("--app-id <id>", "GitHub App ID (omit when using --manifest)").option("--private-key <pathOrValue>", "Private key (prefix with @ for file path)").option("--webhook-secret <secret>", "Webhook secret").option("--from-env <varName>", "Read private key from environment variable").option("--stdin", "Read private key from stdin").option("--manifest", "One-click setup: create and configure a new GitHub App via the App Manifest flow").option("--no-browser", "Headless manifest setup: print a URL and paste the setup code back").option("--github-org <slug>", "Create the App under a GitHub org instead of your personal account").option("--json", "Emit raw JSON (the API response) instead of formatted text").action(async (opts) => {
5904
+ if (opts.manifest) {
5905
+ try {
5906
+ await runGithubManifestSetup({
5907
+ name: opts.name,
5908
+ noBrowser: !opts.browser,
5909
+ githubOrg: opts.githubOrg
5910
+ }, getClient());
5911
+ } catch (err) {
5912
+ console.error(`Error: ${toErrorMessage(err)}`);
5913
+ process.exit(1);
5914
+ }
5915
+ return;
5916
+ }
5379
5917
  try {
5918
+ if (!opts.appId) {
5919
+ console.error("Error: --app-id is required (or use --manifest for one-click setup)");
5920
+ process.exit(1);
5921
+ }
5380
5922
  const privateKey = await resolveSecret({
5381
5923
  value: opts.privateKey,
5382
5924
  stdin: opts.stdin,
@@ -10677,19 +11219,33 @@ var JoinTokenManager = class {
10677
11219
  * `peer_credentials_active_uniq` partial unique index).
10678
11220
  *
10679
11221
  * On a 0-row claim, a follow-up SELECT disambiguates not-found / expired
10680
- * / already-used so callers can branch on the specific reason — the
10681
- * recovery path in peer-handler.ts keys on the "already used" string
10682
- * specifically. The follow-up only fires on the unhappy path.
11222
+ * / already-used / reusable-by-same-instance so callers can branch on the
11223
+ * specific reason — the recovery path in peer-handler.ts keys on the
11224
+ * "already used" string specifically. The follow-up only fires on the
11225
+ * unhappy path.
11226
+ *
11227
+ * Self-healing reuse: a join token is re-consumable by the same joining
11228
+ * peer (`peerInstanceId`) until its `expires_at`. A peer that lost its
11229
+ * credential (transient outage / deleted credential file) re-presents the
11230
+ * still-valid join token already in its env; the coordinator accepts the
11231
+ * reuse and issues a fresh credential — no operator action, no cluster
11232
+ * redeploy. Reuse is bounded by BOTH `expires_at` AND the consuming
11233
+ * instanceId, so it never widens a leaked token's usefulness beyond the
11234
+ * instance that first consumed it.
10683
11235
  */
10684
- async validateAndConsumeToken(token, consumedBy) {
11236
+ async validateAndConsumeToken(token, consumedBy, peerInstanceId) {
10685
11237
  const parsed = parseToken(token);
10686
11238
  const keys = deriveKeys(Buffer.from(parsed.secretHex, "hex"));
10687
11239
  const updateResult = await this.deps.db.updateTable("join_tokens").set({
10688
11240
  consumed_at: /* @__PURE__ */ new Date(),
10689
- consumed_by: consumedBy
11241
+ consumed_by: consumedBy,
11242
+ consumed_by_instance: peerInstanceId
10690
11243
  }).where("token_hash", "=", keys.validationHash).where("consumed_at", "is", null).where("expires_at", ">", /* @__PURE__ */ new Date()).executeTakeFirst();
10691
11244
  if (Number(updateResult?.numUpdatedRows ?? 0n) > 0) {
10692
- joinTokenLogger.info("Consumed join token", { consumedBy });
11245
+ joinTokenLogger.info("Consumed join token", {
11246
+ consumedBy,
11247
+ peerInstanceId
11248
+ });
10693
11249
  return {
10694
11250
  routing: parsed.routing,
10695
11251
  keys
@@ -10697,8 +11253,21 @@ var JoinTokenManager = class {
10697
11253
  }
10698
11254
  const row = await this.deps.db.selectFrom("join_tokens").selectAll().where("token_hash", "=", keys.validationHash).executeTakeFirst();
10699
11255
  if (!row) throw new Error("Invalid join token");
10700
- if (row.consumed_at) throw new Error(TOKEN_ALREADY_USED_MESSAGE);
10701
- throw new Error("Join token has expired. Generate a new token with: kici admin create-join-token");
11256
+ if (new Date(row.expires_at).getTime() <= Date.now()) throw new Error("Join token has expired. Generate a new token with: kici admin create-join-token");
11257
+ if (row.consumed_at) {
11258
+ if (row.consumed_by_instance === peerInstanceId) {
11259
+ joinTokenLogger.info("Re-validated join token for returning instance", {
11260
+ consumedBy,
11261
+ peerInstanceId
11262
+ });
11263
+ return {
11264
+ routing: parsed.routing,
11265
+ keys
11266
+ };
11267
+ }
11268
+ throw new Error(TOKEN_ALREADY_USED_MESSAGE);
11269
+ }
11270
+ throw new Error("Invalid join token");
10702
11271
  }
10703
11272
  };
10704
11273
  /**
@@ -10765,14 +11334,24 @@ var PeerCredentialStore = class {
10765
11334
  this.db = db;
10766
11335
  }
10767
11336
  /**
10768
- * Save a new peer credential.
11337
+ * Save a new peer credential, revoking any prior active credential for the
11338
+ * same instanceId (the `peer_credentials_active_uniq` partial unique index
11339
+ * permits exactly one active credential per instanceId).
11340
+ *
11341
+ * Returns `{ revokedCount }` — how many previously-active credentials this
11342
+ * save revoked. Because a coordinator's peer-clients share one
11343
+ * identity-scoped credential, a `revokedCount > 0` here means every sibling
11344
+ * peer-client of `instanceId` that was authenticating with the old
11345
+ * credential is now invalidated and will be rejected on its next proof. The
11346
+ * caller logs this so a revoke that cascades sibling rejections is visible in
11347
+ * the orchestrator logs.
10769
11348
  */
10770
11349
  async save(opts) {
10771
11350
  const expiryDays = opts.expiryDays ?? DEFAULT_EXPIRY_DAYS;
10772
11351
  const expiresAt = new Date(Date.now() + expiryDays * 24 * 60 * 60 * 1e3);
10773
11352
  const runOnce = async () => {
10774
- await this.db.transaction().execute(async (trx) => {
10775
- await trx.updateTable("peer_credentials").set({ revoked_at: /* @__PURE__ */ new Date() }).where("instance_id", "=", opts.instanceId).where("revoked_at", "is", null).execute();
11353
+ return this.db.transaction().execute(async (trx) => {
11354
+ const revokeResult = await trx.updateTable("peer_credentials").set({ revoked_at: /* @__PURE__ */ new Date() }).where("instance_id", "=", opts.instanceId).where("revoked_at", "is", null).executeTakeFirst();
10776
11355
  await trx.insertInto("peer_credentials").values({
10777
11356
  instance_id: opts.instanceId,
10778
11357
  credential_hash: opts.credentialHash,
@@ -10781,15 +11360,13 @@ var PeerCredentialStore = class {
10781
11360
  source_token_hash: opts.sourceTokenHash ?? null,
10782
11361
  expires_at: expiresAt
10783
11362
  }).execute();
11363
+ return Number(revokeResult?.numUpdatedRows ?? 0n);
10784
11364
  });
10785
11365
  };
10786
11366
  try {
10787
- await runOnce();
11367
+ return { revokedCount: await runOnce() };
10788
11368
  } catch (err) {
10789
- if (err instanceof Error && "code" in err && err.code === "23505") {
10790
- await runOnce();
10791
- return;
10792
- }
11369
+ if (err instanceof Error && "code" in err && err.code === "23505") return { revokedCount: await runOnce() };
10793
11370
  throw err;
10794
11371
  }
10795
11372
  }
@@ -11005,6 +11582,23 @@ function deriveHostStatus(row, nowMs, graceMs) {
11005
11582
  return "unreachable";
11006
11583
  }
11007
11584
  /**
11585
+ * Normalize a stored `host_properties` value into a typed host-vars bag. The pg
11586
+ * driver returns parsed JSON for a `jsonb` column, but accept a JSON string too
11587
+ * (defensive for non-pg paths / tests). Non-object / null reads back as `{}`.
11588
+ */
11589
+ function parseHostProperties(value) {
11590
+ const parsed = typeof value === "string" ? safeJsonParse(value) : value;
11591
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
11592
+ return {};
11593
+ }
11594
+ function safeJsonParse(value) {
11595
+ try {
11596
+ return JSON.parse(value);
11597
+ } catch {
11598
+ return null;
11599
+ }
11600
+ }
11601
+ /**
11008
11602
  * Durable, cluster-shared roster of every agent the cluster has ever enrolled.
11009
11603
  *
11010
11604
  * The in-memory `AgentRegistry` reconciles into this table on register
@@ -11022,6 +11616,7 @@ var HostRosterStore = class {
11022
11616
  /** Idempotent upsert on agent_id; stamps connected_instance_id + last_seen. */
11023
11617
  async upsert(input) {
11024
11618
  const labelsJson = JSON.stringify(input.labels);
11619
+ const reportedJson = JSON.stringify(input.properties ?? {});
11025
11620
  await this.db.insertInto("host_roster").values({
11026
11621
  agent_id: input.agentId,
11027
11622
  token_id: input.tokenId,
@@ -11031,6 +11626,7 @@ var HostRosterStore = class {
11031
11626
  platform: input.platform,
11032
11627
  arch: input.arch,
11033
11628
  connected_instance_id: input.instanceId,
11629
+ host_properties: reportedJson,
11034
11630
  last_seen: sql`now()`,
11035
11631
  updated_at: sql`now()`
11036
11632
  }).onConflict((oc) => oc.column("agent_id").doUpdateSet({
@@ -11041,6 +11637,7 @@ var HostRosterStore = class {
11041
11637
  platform: input.platform,
11042
11638
  arch: input.arch,
11043
11639
  connected_instance_id: input.instanceId,
11640
+ host_properties: sql`COALESCE(host_roster.host_properties, '{}'::jsonb) || ${reportedJson}::jsonb`,
11044
11641
  last_seen: sql`now()`,
11045
11642
  updated_at: sql`now()`
11046
11643
  })).execute();
@@ -11065,6 +11662,7 @@ var HostRosterStore = class {
11065
11662
  labels: JSON.stringify(input.labels),
11066
11663
  hostname: input.hostname ?? null,
11067
11664
  connected_instance_id: null,
11665
+ host_properties: JSON.stringify(input.properties ?? {}),
11068
11666
  last_seen: sql`now()`,
11069
11667
  updated_at: sql`now()`
11070
11668
  }).onConflict((oc) => oc.column("agent_id").doNothing()).execute();
@@ -11099,13 +11697,51 @@ var HostRosterStore = class {
11099
11697
  connectedInstanceId: row.connected_instance_id,
11100
11698
  status: deriveHostStatus(row, now, graceMs),
11101
11699
  platform: row.platform,
11102
- arch: row.arch
11700
+ arch: row.arch,
11701
+ properties: parseHostProperties(row.host_properties)
11103
11702
  });
11104
11703
  }
11105
11704
  out.sort((a, b) => a.agentId.localeCompare(b.agentId));
11106
11705
  return out;
11107
11706
  }
11108
11707
  /**
11708
+ * Map a roster row to the canonical {@link HostInventoryEntry} — the queryable
11709
+ * shape returned by the `inventory.query`/`inventory.get` RPC and typed on the
11710
+ * SDK's `ctx.kici.inventory`. Status is derived the same way `findMatching`
11711
+ * derives it (live + fresh ⇒ ready; declared-but-absent static ⇒ unreachable;
11712
+ * ephemeral past ttl ⇒ stale).
11713
+ */
11714
+ toInventoryEntry(row, graceMs) {
11715
+ return {
11716
+ agentId: row.agent_id,
11717
+ labels: JSON.parse(row.labels),
11718
+ properties: parseHostProperties(row.host_properties),
11719
+ hostname: row.hostname,
11720
+ platform: row.platform,
11721
+ arch: row.arch,
11722
+ lifecycleClass: row.lifecycle_class,
11723
+ status: deriveHostStatus(row, Date.now(), graceMs),
11724
+ lastSeen: new Date(row.last_seen).toISOString()
11725
+ };
11726
+ }
11727
+ /**
11728
+ * Query the roster as canonical {@link HostInventoryEntry} records. With a
11729
+ * selector, reuses `findMatching`'s label filtering (server-side, glob/regex);
11730
+ * property filtering is done client-side in the workflow. Omit the selector ⇒
11731
+ * every host.
11732
+ */
11733
+ async queryInventory(selector, graceMs) {
11734
+ if (!selector || !selector.include && !selector.exclude) return (await this.listAll()).map((r) => this.toInventoryEntry(r, graceMs));
11735
+ const matched = await this.findMatching(selector.include ?? [], selector.exclude ?? [], graceMs);
11736
+ const byId = new Map(matched.map((m) => [m.agentId, m]));
11737
+ return (await this.listAll()).filter((r) => byId.has(r.agent_id)).map((r) => this.toInventoryEntry(r, graceMs));
11738
+ }
11739
+ /** Single-host inventory lookup; null when the agent is not in the roster. */
11740
+ async getInventory(agentId, graceMs) {
11741
+ const row = await this.get(agentId);
11742
+ return row ? this.toInventoryEntry(row, graceMs) : null;
11743
+ }
11744
+ /**
11109
11745
  * Count `static` (declared) hosts whose derived status is `unreachable` —
11110
11746
  * the "declared-but-absent" alarm population. Reuses the single-source
11111
11747
  * {@link deriveHostStatus} so the count never diverges from what
@@ -11126,6 +11762,10 @@ var HostRosterStore = class {
11126
11762
  };
11127
11763
  //#endregion
11128
11764
  //#region src/cli/commands/host.ts
11765
+ /** Collect a repeatable `--prop key=value` flag into an array. */
11766
+ function collectProp(value, previous) {
11767
+ return [...previous, value];
11768
+ }
11129
11769
  /**
11130
11770
  * The CLI reads the roster DB directly (no full orchestrator config load), so
11131
11771
  * it sources the grace window from the same env var the orchestrator config
@@ -11179,13 +11819,15 @@ function registerHostCommands(program) {
11179
11819
  process.exit(1);
11180
11820
  }
11181
11821
  });
11182
- 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").action(async (opts) => {
11822
+ 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) => {
11183
11823
  try {
11184
11824
  const labels = opts.labels ? String(opts.labels).split(",").map((s) => s.trim()).filter(Boolean) : [];
11825
+ const properties = parseHostPropertyAssignments(opts.prop);
11185
11826
  await withDb((db) => new HostRosterStore(db).declareStatic({
11186
11827
  agentId: opts.agentId,
11187
11828
  labels,
11188
- hostname: opts.hostname
11829
+ hostname: opts.hostname,
11830
+ properties
11189
11831
  }));
11190
11832
  console.log(`Declared static host: ${opts.agentId}`);
11191
11833
  } catch (err) {