@kici-dev/orchestrator 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (183) hide show
  1. package/dist/__test-helpers__/fake-scaler-state-store.d.ts +24 -0
  2. package/dist/__test-helpers__/mock-db-query.d.ts +130 -0
  3. package/dist/__test-helpers__/mock-db.d.ts +27 -0
  4. package/dist/agent/agent-version.d.ts +34 -0
  5. package/dist/agent/dispatcher.d.ts +151 -13
  6. package/dist/agent/host-roster.d.ts +37 -5
  7. package/dist/agent/registry.d.ts +2 -0
  8. package/dist/app.d.ts +56 -7
  9. package/dist/approvals/apply-decision.d.ts +67 -19
  10. package/dist/approvals/held-run-release-wiring.d.ts +45 -0
  11. package/dist/approvals/independent-wiring.d.ts +55 -0
  12. package/dist/approvals/resume-router.d.ts +9 -0
  13. package/dist/approvals/triggerer-subject.d.ts +37 -0
  14. package/dist/cache/dep-cache.d.ts +54 -10
  15. package/dist/cache/global-eval-round-cache.d.ts +95 -0
  16. package/dist/cache/index.d.ts +3 -0
  17. package/dist/cache/pending-global-evals.d.ts +42 -0
  18. package/dist/cache/pending-inits.d.ts +10 -0
  19. package/dist/cli/api-client.d.ts +18 -0
  20. package/dist/cli/commands/cluster-settings.d.ts +52 -3
  21. package/dist/cli/commands/diagnose.d.ts +6 -0
  22. package/dist/cli/commands/held-run.d.ts +58 -0
  23. package/dist/cli/commands/runs.d.ts +1 -0
  24. package/dist/cli/commands/trust-policy.d.ts +117 -12
  25. package/dist/cli/kici-admin.d.ts +10 -0
  26. package/dist/cli.js +2770 -938
  27. package/dist/cluster/cluster-identity.d.ts +3 -3
  28. package/dist/cluster/cluster-settings-reader.d.ts +59 -1
  29. package/dist/cluster/coordinator.d.ts +44 -0
  30. package/dist/cluster/index.d.ts +4 -0
  31. package/dist/cluster/peer-handler.d.ts +19 -0
  32. package/dist/cluster/peer-registry.d.ts +17 -0
  33. package/dist/cluster/plan-headroom-store.d.ts +26 -0
  34. package/dist/cluster/worker-eviction.d.ts +69 -0
  35. package/dist/cold-store/load-event-log-range.d.ts +10 -3
  36. package/dist/cold-store/load-secret-audit-log-range.d.ts +4 -3
  37. package/dist/cold-store/tables/secret-audit-log.d.ts +4 -3
  38. package/dist/config.d.ts +48 -21
  39. package/dist/content-requirements-cache.d.ts +55 -0
  40. package/dist/contexts/held-runs.d.ts +123 -16
  41. package/dist/contexts/protection/aggregate.d.ts +3 -2
  42. package/dist/contexts/protection/branch-gate.d.ts +15 -0
  43. package/dist/contexts/protection/concurrency-gate.d.ts +8 -2
  44. package/dist/contexts/protection/pipeline.d.ts +18 -0
  45. package/dist/contexts/release-queued-holds.d.ts +35 -0
  46. package/dist/dashboard/attestation-filters.d.ts +8 -0
  47. package/dist/dashboard/handler.d.ts +6 -24
  48. package/dist/db/migrations/109_cluster_settings_cache_knobs.d.ts +4 -0
  49. package/dist/db/migrations/110_cluster_settings_global_eval_knobs.d.ts +4 -0
  50. package/dist/db/migrations/111_cluster_settings_global_eval_wait.d.ts +4 -0
  51. package/dist/db/migrations/112_execution_runs_workflow_repo.d.ts +4 -0
  52. package/dist/db/migrations/113_execution_runs_workflow_repo_index.d.ts +30 -0
  53. package/dist/db/migrations/114_ingest_queue_claim.d.ts +4 -0
  54. package/dist/db/migrations/115_global_workflows_cluster_switch.d.ts +9 -0
  55. package/dist/db/migrations/116_org_plan_headroom.d.ts +4 -0
  56. package/dist/db/migrations/117_invoke_gate_proxy_jobs.d.ts +4 -0
  57. package/dist/db/migrations/118_invoke_gate_scheduling.d.ts +4 -0
  58. package/dist/db/migrations/119_scaler_ha_ownership.d.ts +4 -0
  59. package/dist/db/migrations/120_cluster_settings_scaler_reaper.d.ts +4 -0
  60. package/dist/db/migrations/121_org_settings_allow_untrusted_dockerfile_builds.d.ts +19 -0
  61. package/dist/db/migrations/122_host_roster_scaler_managed.d.ts +25 -0
  62. package/dist/db/migrations/123_workflow_registrations_default_branch.d.ts +24 -0
  63. package/dist/db/migrations/124_cluster_settings_provision_backoff.d.ts +4 -0
  64. package/dist/db/migrations/125_org_trust_directory.d.ts +30 -0
  65. package/dist/db/migrations/126_held_runs_posted_pending_check.d.ts +28 -0
  66. package/dist/db/migrations/127_org_trust_policy_approval_expiry_seconds.d.ts +26 -0
  67. package/dist/db/migrations/128_execution_runs_global_eval_round.d.ts +23 -0
  68. package/dist/db/migrations/129_execution_runs_dispatch_routing_key.d.ts +24 -0
  69. package/dist/db/migrations/130_scaler_provision_outcomes.d.ts +32 -0
  70. package/dist/db/types.d.ts +428 -12
  71. package/dist/entry-helpers.d.ts +28 -0
  72. package/dist/events/event-emitter.d.ts +22 -0
  73. package/dist/events/event-router.d.ts +87 -1
  74. package/dist/events/types.d.ts +2 -23
  75. package/dist/git/credential-broker.d.ts +94 -0
  76. package/dist/git/inherited-secret.d.ts +24 -0
  77. package/dist/git/installation-token.d.ts +38 -0
  78. package/dist/git/job-context.d.ts +13 -0
  79. package/dist/index.js +324 -84
  80. package/dist/metrics/agent-metrics-aggregator.d.ts +4 -4
  81. package/dist/metrics/prometheus.d.ts +170 -3
  82. package/dist/oidc/oidc-mint-registration.d.ts +7 -3
  83. package/dist/orchestrator-core.d.ts +91 -24
  84. package/dist/pipeline/content-filter.d.ts +71 -0
  85. package/dist/pipeline/direct-ingress-deps.d.ts +100 -0
  86. package/dist/pipeline/dispatch-matched-workflow.d.ts +521 -11
  87. package/dist/pipeline/global-eval-round.d.ts +307 -0
  88. package/dist/pipeline/internal-event-pipeline.d.ts +140 -0
  89. package/dist/pipeline/invoke-gate.d.ts +164 -0
  90. package/dist/pipeline/job-contexts.d.ts +16 -17
  91. package/dist/pipeline/needs-scheduler.d.ts +13 -0
  92. package/dist/pipeline/process-webhook.d.ts +126 -31
  93. package/dist/pipeline/processor.d.ts +282 -23
  94. package/dist/pipeline/rerun.d.ts +21 -0
  95. package/dist/pipeline/resume-workflow.d.ts +40 -7
  96. package/dist/pipeline/route-or-dispatch-jobs.d.ts +15 -0
  97. package/dist/pipeline/security-hold-check.d.ts +217 -0
  98. package/dist/pipeline/test-pipeline.d.ts +12 -0
  99. package/dist/pipeline/undispatched-hold-checks.d.ts +23 -0
  100. package/dist/pipeline/webhook-payload-store.d.ts +20 -0
  101. package/dist/policy/dashboard-write-policy-listener.d.ts +62 -0
  102. package/dist/policy/dashboard-write-policy.d.ts +14 -0
  103. package/dist/provenance/backfill-run.d.ts +10 -1
  104. package/dist/provider-registry.d.ts +47 -4
  105. package/dist/providers/github/check-status-poster.d.ts +33 -3
  106. package/dist/providers/github/commit-message.d.ts +20 -0
  107. package/dist/providers/github/file-contents.d.ts +40 -0
  108. package/dist/providers/github/index.d.ts +2 -2
  109. package/dist/providers/github/normalizer.d.ts +3 -2
  110. package/dist/providers/local/normalizer.d.ts +3 -5
  111. package/dist/providers/universal-git/config.d.ts +2 -0
  112. package/dist/providers/universal-git/index.d.ts +11 -2
  113. package/dist/providers/universal-git/normalizer.d.ts +10 -0
  114. package/dist/queue/cleanup.d.ts +7 -1
  115. package/dist/queue/job-queue.d.ts +112 -7
  116. package/dist/queue/terminalize-unroutable.d.ts +13 -0
  117. package/dist/registration/extractor.d.ts +3 -3
  118. package/dist/registration/registration-index.d.ts +7 -0
  119. package/dist/registration/registration-run-match.d.ts +47 -0
  120. package/dist/registration/registration-store.d.ts +22 -0
  121. package/dist/reporting/check-run-reporter.d.ts +146 -2
  122. package/dist/reporting/check-run-summary.d.ts +25 -1
  123. package/dist/reporting/execution-tracker.d.ts +324 -9
  124. package/dist/reporting/log-chunk-sink.d.ts +8 -5
  125. package/dist/reporting/run-aggregator.d.ts +4 -14
  126. package/dist/reporting/step-display-order.d.ts +43 -0
  127. package/dist/routes/admin-held-runs.d.ts +142 -0
  128. package/dist/routes/admin-org-settings.d.ts +5 -0
  129. package/dist/routes/admin-trust-policy.d.ts +34 -6
  130. package/dist/routes/admin.d.ts +15 -0
  131. package/dist/scaler/backend-factory.d.ts +55 -0
  132. package/dist/scaler/bare-metal-backend.d.ts +43 -5
  133. package/dist/scaler/claim-store.d.ts +119 -0
  134. package/dist/scaler/config.d.ts +4 -0
  135. package/dist/scaler/container-backend.d.ts +18 -4
  136. package/dist/scaler/container-routing.d.ts +23 -0
  137. package/dist/scaler/container-spawn.d.ts +28 -0
  138. package/dist/scaler/event-backend.d.ts +124 -0
  139. package/dist/scaler/event-provision-reaper.d.ts +350 -0
  140. package/dist/scaler/failure-tracker.d.ts +1 -1
  141. package/dist/scaler/firecracker-backend.d.ts +33 -6
  142. package/dist/scaler/index.d.ts +11 -1
  143. package/dist/scaler/label-matcher.d.ts +4 -3
  144. package/dist/scaler/manager.d.ts +905 -76
  145. package/dist/scaler/resolve-container-auth.d.ts +45 -0
  146. package/dist/scaler/scaler-events.d.ts +11 -0
  147. package/dist/scaler/scaler-state-store.d.ts +294 -10
  148. package/dist/scaler/types.d.ts +131 -13
  149. package/dist/scaler/warm-pool.d.ts +132 -29
  150. package/dist/security/comment-handler.d.ts +50 -11
  151. package/dist/security/global-workflow-policy.d.ts +52 -12
  152. package/dist/security/identity-link.d.ts +60 -0
  153. package/dist/security/lock-source.d.ts +11 -13
  154. package/dist/security/reduced-privilege-note.d.ts +59 -0
  155. package/dist/security/trust-directory-store.d.ts +233 -0
  156. package/dist/security/trust-policy-gate.d.ts +94 -59
  157. package/dist/security/trust-policy-store.d.ts +3 -0
  158. package/dist/security/trust-resolver.d.ts +14 -98
  159. package/dist/server.d.ts +11 -1
  160. package/dist/server.js +56329 -42234
  161. package/dist/stale-detector/gate-deadline-detector.d.ts +52 -0
  162. package/dist/stale-detector/stale-run-detector.d.ts +61 -3
  163. package/dist/standalone.js +37619 -23570
  164. package/dist/webhook/ingest-accept.d.ts +70 -0
  165. package/dist/webhook/ingest-overflow-buffer.d.ts +35 -4
  166. package/dist/webhook/ingest-overflow-replayer.d.ts +50 -6
  167. package/dist/worker/in-memory-job-queue.d.ts +1 -1
  168. package/dist/ws/agent-handler.d.ts +31 -4
  169. package/dist/ws/dashboard-context-handler.d.ts +28 -4
  170. package/dist/ws/dashboard-global-workflows-handler.d.ts +36 -9
  171. package/dist/ws/execution-status-frame.d.ts +32 -0
  172. package/dist/ws/failure-messages.d.ts +2 -0
  173. package/dist/ws/git-credential-relay.d.ts +39 -0
  174. package/dist/ws/oidc-token-relay.d.ts +6 -11
  175. package/dist/ws/platform-client.d.ts +68 -9
  176. package/dist/ws/test-relay-handlers.d.ts +35 -10
  177. package/installer-image-digests.json +3 -3
  178. package/package.json +24 -19
  179. package/sbom.spdx.json +868 -821
  180. package/dist/approvals/team-membership-lookup.d.ts +0 -13
  181. package/dist/pipeline/inline-eval.d.ts +0 -44
  182. package/dist/providers/github/contributor-resolver.d.ts +0 -30
  183. package/dist/security/contributor-cache.d.ts +0 -83
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ import { chmod, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
11
11
  import { Kysely, PostgresDialect, sql } from "kysely";
12
12
  import pg from "pg";
13
13
  import { z } from "zod";
14
- import { DEFAULT_APPROVAL_EXPIRY_HOURS, HoldScope, HoldType, TriggerSource, persistedHoldTypeSpellings } from "@kici-dev/engine";
14
+ import { DEFAULT_APPROVAL_EXPIRY_HOURS, DEFAULT_APPROVAL_EXPIRY_SECONDS, ForkPolicy, HoldScope, HoldType, TriggerSource, approvalExpiryHoursOf, approvalExpirySecondsOf, persistedHoldTypeSpellings } from "@kici-dev/engine";
15
15
  import { platform, tmpdir } from "node:os";
16
16
  import { createInterface } from "node:readline";
17
17
  import { Octokit } from "@octokit/rest";
@@ -302,7 +302,7 @@ var S3CacheStorage = class {
302
302
  * restart — fine for the filesystem backend's E2E / single-host use case.
303
303
  */
304
304
  /** Default URL lifetime: 1 hour (matches S3 backend's PUT-URL lifetime). */
305
- const DEFAULT_SIGN_URL_TTL_MS = 3600 * 1e3;
305
+ const DEFAULT_SIGN_URL_TTL_MS = 36e5;
306
306
  function computeSig(secret, method, key, expiresMs) {
307
307
  const hmac = createHmac("sha256", secret);
308
308
  hmac.update(`${method}:${key}:${expiresMs}`);
@@ -668,11 +668,13 @@ var SourceCache = class {
668
668
  /**
669
669
  * Dependency-specific cache layer wrapping CacheStorage.
670
670
  *
671
- * Stores and retrieves dependency tarballs keyed by lockfileHash + platform + arch.
672
- * Shared CacheStorage backend with SourceCache (same S3 bucket).
673
- * Refreshes TTL on reads (touch-on-read).
671
+ * Stores dependency tarballs under their own content hash, with a small pointer
672
+ * resolving lockfileHash + platform + arch to that hash. Shared CacheStorage
673
+ * backend with SourceCache (same S3 bucket). Refreshes TTL on reads
674
+ * (touch-on-read).
674
675
  *
675
- * Cache key format: deps/{platform}-{arch}/{lockfileHash}.tar.gz
676
+ * Keys: `deps/{platform}-{arch}/{depsHash}.tar.gz` (immutable) and
677
+ * `deps/{platform}-{arch}/{lockfileHash}.hash` (the pointer).
676
678
  */
677
679
  const logger$2 = createLogger({ prefix: "dep-cache" });
678
680
  /** Default max tarball size: 500MB */
@@ -680,11 +682,35 @@ const DEFAULT_MAX_TARBALL_BYTES = 524288e3;
680
682
  /** Default dependency-cache entry TTL: 30 days. */
681
683
  const DEFAULT_CACHE_TTL_DAYS = 30;
682
684
  const MS_PER_DAY = 864e5;
683
- /** Build cache key for dependency tarball: deps/{platform}-{arch}/{lockfileHash}.tar.gz */
684
- function depKey(lockfileHash, platform, arch) {
685
- return `deps/${platform}-${arch}/${lockfileHash}.tar.gz`;
685
+ /**
686
+ * Cache key for a dependency tarball, addressed by the tarball's OWN content
687
+ * hash: `deps/{platform}-{arch}/{depsHash}.tar.gz`.
688
+ *
689
+ * Content-addressing is what makes a mismatched pair unrepresentable. When the
690
+ * tarball lived at a lockfile-derived name, it and its companion `.hash` were
691
+ * two independently-written objects under keys that any two builds sharing a
692
+ * lockfile + platform + arch also share — so concurrent builders could leave the
693
+ * `.tar.gz` from one and the `.hash` from the other, and the reader failed
694
+ * verification durably. Naming the object by its own hash means every pair a
695
+ * reader can observe is self-consistent, and the bytes at a given key can never
696
+ * change after a URL is signed for it.
697
+ */
698
+ function depTarballKey(depsHash, platform, arch) {
699
+ return `deps/${platform}-${arch}/${depsHash}.tar.gz`;
686
700
  }
687
- var DepCache = class {
701
+ /**
702
+ * Cache key for the pointer that resolves a lockfile to the content hash of the
703
+ * tarball built from it: `deps/{platform}-{arch}/{lockfileHash}.hash`.
704
+ *
705
+ * This is the one mutable object in the scheme. A concurrent write replaces a
706
+ * pointer wholesale — there is no window in which it is half-written — so the
707
+ * worst a racing pair of builders can do is leave whichever pointer landed last,
708
+ * and both tarballs remain valid and fetchable.
709
+ */
710
+ function depPointerKey(lockfileHash, platform, arch) {
711
+ return `deps/${platform}-${arch}/${lockfileHash}.hash`;
712
+ }
713
+ var DepCache = class DepCache {
688
714
  storage;
689
715
  maxTarballBytes;
690
716
  cacheTtlDaysFallback;
@@ -706,10 +732,30 @@ var DepCache = class {
706
732
  if (!this.clusterSettings) return void 0;
707
733
  return await this.clusterSettings.getNumber("cache_ttl_days", this.cacheTtlDaysFallback) * MS_PER_DAY;
708
734
  }
709
- /** Check if a dep tarball exists in cache. */
735
+ /**
736
+ * Resolve a lockfile to the content hash of the tarball built from it, or
737
+ * null when no pointer exists. The single place the indirection is read.
738
+ */
739
+ async resolvePointer(lockfileHash, platform, arch, ttlMsOverride) {
740
+ return (await this.storage.get(depPointerKey(lockfileHash, platform, arch), ttlMsOverride))?.toString("utf-8").trim() || null;
741
+ }
742
+ /**
743
+ * Check if a dep tarball exists in cache.
744
+ *
745
+ * Both halves must be present: a pointer whose tarball has aged out is a miss,
746
+ * not a hit, or the caller skips a rebuild and dispatches a URL that 404s.
747
+ */
710
748
  async has(lockfileHash, platform, arch) {
711
- const key = depKey(lockfileHash, platform, arch);
712
- const exists = await this.storage.has(key, await this.resolveTtlMsOverride());
749
+ const ttlMsOverride = await this.resolveTtlMsOverride();
750
+ const depsHash = await this.resolvePointer(lockfileHash, platform, arch, ttlMsOverride);
751
+ if (!depsHash) {
752
+ logger$2.debug(`has(${lockfileHash}): false (no pointer)`, {
753
+ platform,
754
+ arch
755
+ });
756
+ return false;
757
+ }
758
+ const exists = await this.storage.has(depTarballKey(depsHash, platform, arch), ttlMsOverride);
713
759
  logger$2.debug(`has(${lockfileHash}): ${exists}`, {
714
760
  platform,
715
761
  arch
@@ -721,42 +767,40 @@ var DepCache = class {
721
767
  * Refreshes TTL on hit.
722
768
  */
723
769
  async getUrl(lockfileHash, platform, arch) {
724
- const key = depKey(lockfileHash, platform, arch);
725
- const url = await this.storage.getUrl(key, await this.resolveTtlMsOverride());
726
- if (url) {
727
- await this.storage.touch(key);
728
- logger$2.debug(`getUrl(${lockfileHash}): hit`, {
729
- platform,
730
- arch
731
- });
732
- } else logger$2.debug(`getUrl(${lockfileHash}): miss`, {
733
- platform,
734
- arch
735
- });
736
- return url;
770
+ return (await this.getUrlAndHash(lockfileHash, platform, arch))?.url ?? null;
737
771
  }
738
772
  /**
739
- * Get a pre-signed download URL and the tarball content hash (if available).
740
- * Returns null on cache miss. Hash may be undefined for old entries stored
741
- * before integrity tracking was added.
773
+ * Get a pre-signed download URL and the tarball content hash.
774
+ *
775
+ * Returns null on cache miss — including an entry with no pointer, which is
776
+ * unverifiable and so is deliberately not served.
742
777
  */
743
778
  async getUrlAndHash(lockfileHash, platform, arch) {
744
- const key = depKey(lockfileHash, platform, arch);
745
- const url = await this.storage.getUrl(key, await this.resolveTtlMsOverride());
779
+ const ttlMsOverride = await this.resolveTtlMsOverride();
780
+ const pointerKey = depPointerKey(lockfileHash, platform, arch);
781
+ const hash = (await this.storage.get(pointerKey, ttlMsOverride))?.toString("utf-8").trim() || void 0;
782
+ if (!hash) {
783
+ logger$2.debug(`getUrlAndHash(${lockfileHash}): miss (no pointer)`, {
784
+ platform,
785
+ arch
786
+ });
787
+ return null;
788
+ }
789
+ const key = depTarballKey(hash, platform, arch);
790
+ const url = await this.storage.getUrl(key, ttlMsOverride);
746
791
  if (!url) {
747
- logger$2.debug(`getUrlAndHash(${lockfileHash}): miss`, {
792
+ logger$2.debug(`getUrlAndHash(${lockfileHash}): miss (dangling pointer)`, {
748
793
  platform,
749
794
  arch
750
795
  });
751
796
  return null;
752
797
  }
753
798
  await this.storage.touch(key);
754
- const hashKey = `deps/${platform}-${arch}/${lockfileHash}.hash`;
755
- const hash = (await this.storage.get(hashKey))?.toString("utf-8") || void 0;
799
+ await this.storage.touch(pointerKey);
756
800
  logger$2.debug(`getUrlAndHash(${lockfileHash}): hit`, {
757
801
  platform,
758
802
  arch,
759
- hasHash: !!hash
803
+ hasHash: true
760
804
  });
761
805
  return {
762
806
  url,
@@ -766,9 +810,21 @@ var DepCache = class {
766
810
  /**
767
811
  * Get a pre-signed upload URL for direct agent-to-S3 upload.
768
812
  */
769
- async getUploadUrl(lockfileHash, platform, arch) {
770
- const key = depKey(lockfileHash, platform, arch);
771
- return this.storage.getUploadUrl(key);
813
+ async getUploadUrl(depsHash, platform, arch) {
814
+ return this.storage.getUploadUrl(depTarballKey(depsHash, platform, arch));
815
+ }
816
+ /**
817
+ * Publish the pointer that makes an uploaded tarball discoverable by lockfile.
818
+ *
819
+ * Called only after the agent confirms its upload completed. Publishing before
820
+ * the bytes land would let a reader follow the pointer to a missing object.
821
+ */
822
+ async publishPointer(lockfileHash, platform, arch, depsHash) {
823
+ await this.storage.put(depPointerKey(lockfileHash, platform, arch), depsHash);
824
+ logger$2.info(`publishPointer(${lockfileHash}) -> ${depsHash.slice(0, 12)}`, {
825
+ platform,
826
+ arch
827
+ });
772
828
  }
773
829
  /**
774
830
  * Store a dep tarball in cache.
@@ -777,12 +833,14 @@ var DepCache = class {
777
833
  async store(lockfileHash, platform, arch, tarballData) {
778
834
  const maxTarballBytes = this.clusterSettings ? await this.clusterSettings.getNumber("cache_max_tarball_bytes", this.maxTarballBytes) : this.maxTarballBytes;
779
835
  if (tarballData.length > maxTarballBytes) throw new Error(`Dep tarball exceeds max size: ${tarballData.length} bytes > ${maxTarballBytes} bytes limit`);
780
- const key = depKey(lockfileHash, platform, arch);
781
- await this.storage.put(key, tarballData);
836
+ const depsHash = DepCache.computeHash(tarballData);
837
+ await this.storage.put(depTarballKey(depsHash, platform, arch), tarballData);
838
+ await this.storage.put(depPointerKey(lockfileHash, platform, arch), depsHash);
782
839
  logger$2.info(`store: ${tarballData.length} bytes`, {
783
840
  lockfileHash,
784
841
  platform,
785
- arch
842
+ arch,
843
+ depsHash: depsHash.slice(0, 12)
786
844
  });
787
845
  }
788
846
  /**
@@ -792,10 +850,11 @@ var DepCache = class {
792
850
  static computeHash(data) {
793
851
  return sha256(data);
794
852
  }
795
- /** Remove a dep tarball from cache. */
853
+ /** Remove a dep tarball and its pointer from cache. */
796
854
  async remove(lockfileHash, platform, arch) {
797
- const key = depKey(lockfileHash, platform, arch);
798
- const removed = await this.storage.delete(key);
855
+ const depsHash = await this.resolvePointer(lockfileHash, platform, arch, await this.resolveTtlMsOverride());
856
+ await this.storage.delete(depPointerKey(lockfileHash, platform, arch));
857
+ const removed = depsHash ? await this.storage.delete(depTarballKey(depsHash, platform, arch)) : false;
799
858
  logger$2.info(`remove(${lockfileHash}): ${removed ? "removed" : "not found"}`, {
800
859
  platform,
801
860
  arch
@@ -846,13 +905,13 @@ const logger$1 = createLogger({ prefix: "user-cache" });
846
905
  * per-org override in `org_settings.user_cache_quota_bytes`. The cluster-wide
847
906
  * value is itself operator-configurable via KICI_USER_CACHE_QUOTA_BYTES.
848
907
  */
849
- const DEFAULT_USER_CACHE_QUOTA_BYTES = 5 * 1024 * 1024 * 1024;
908
+ const DEFAULT_USER_CACHE_QUOTA_BYTES = 5368709120;
850
909
  /**
851
910
  * Cluster-wide default entry TTL: 7 days. Fallback when an org has no per-org
852
911
  * override in `org_settings.user_cache_ttl_ms`. The cluster-wide value is
853
912
  * operator-configurable via KICI_USER_CACHE_TTL_MS.
854
913
  */
855
- const DEFAULT_USER_CACHE_TTL_MS = 10080 * 60 * 1e3;
914
+ const DEFAULT_USER_CACHE_TTL_MS = 6048e5;
856
915
  /** Tarball suffix for committed cache entries. */
857
916
  const TAR_SUFFIX = ".tar.gz";
858
917
  /** Matches a stem ending in the fixed-width discriminator this module appends. */
@@ -1280,11 +1339,13 @@ function mapRow(row) {
1280
1339
  * @param data - Credential data to persist
1281
1340
  */
1282
1341
  async function writeCredentialFile(filePath, data) {
1283
- await mkdir(dirname(filePath), {
1342
+ const dir = dirname(filePath);
1343
+ await mkdir(dir, {
1284
1344
  recursive: true,
1285
1345
  mode: 448
1286
1346
  });
1287
- await writeFile(filePath, JSON.stringify(data, null, 2) + "\n", { mode: 384 });
1347
+ const json = JSON.stringify(data, null, 2) + "\n";
1348
+ await writeFile(filePath, json, { mode: 384 });
1288
1349
  await chmod(filePath, 384);
1289
1350
  }
1290
1351
  /**
@@ -1312,10 +1373,11 @@ async function readCredentialFile(filePath) {
1312
1373
  * shared with the orchestrator.
1313
1374
  */
1314
1375
  async function createPeerCredentialStoreFromUrl(databaseUrl, opts) {
1315
- const db = new Kysely({ dialect: new PostgresDialect({ pool: new pg.Pool({
1376
+ const pool = new pg.Pool({
1316
1377
  connectionString: databaseUrl,
1317
1378
  max: opts?.maxConnections ?? 3
1318
- }) }) });
1379
+ });
1380
+ const db = new Kysely({ dialect: new PostgresDialect({ pool }) });
1319
1381
  return {
1320
1382
  store: new PeerCredentialStore(db),
1321
1383
  dispose: async () => {
@@ -1586,10 +1648,11 @@ var JoinTokenManager = class {
1586
1648
  * the real cluster DB.
1587
1649
  */
1588
1650
  function createJoinTokenManagerFromUrl(databaseUrl, opts) {
1589
- const db = new Kysely({ dialect: new PostgresDialect({ pool: new pg.Pool({
1651
+ const pool = new pg.Pool({
1590
1652
  connectionString: databaseUrl,
1591
1653
  max: opts?.maxConnections ?? 3
1592
- }) }) });
1654
+ });
1655
+ const db = new Kysely({ dialect: new PostgresDialect({ pool }) });
1593
1656
  return {
1594
1657
  manager: new JoinTokenManager({ db }),
1595
1658
  dispose: async () => {
@@ -1626,7 +1689,25 @@ function deriveKeys(secret) {
1626
1689
  }
1627
1690
  /**
1628
1691
  * Reason a run was held in the security queue. Persisted verbatim in
1629
- * `held_runs.reason` and switched on by `buildSecurityHoldSummary`.
1692
+ * `held_runs.reason` and whose vocabulary `buildSecurityHoldSummary` switches
1693
+ * on.
1694
+ *
1695
+ * Each value names why a run sits in the queue. The enum stays whole because
1696
+ * compiling code still names all four: `buildSecurityHoldSummary` renders one
1697
+ * branch per value. Dropping a member breaks that, and breaks the parity test
1698
+ * that pins `SECURITY_HOLD_JOB_IDS`' three policy-reason keys to this enum's
1699
+ * options — the engine constant cannot import the enum, so that coupling lives
1700
+ * only in the orchestrator's test. Nothing validates `held_runs.reason` against
1701
+ * this enum, so a stored value is not rejected anywhere.
1702
+ *
1703
+ * - `fork_pr` — the org trust policy's fork switch held the run.
1704
+ * - `context_trust` — a context's minimum-trust gate held the run.
1705
+ * - `workflow_modification` — deprecated; no longer raised. Modifications to
1706
+ * `.kici/` are surfaced on their own informational check and no longer feed
1707
+ * a policy arm. Removed at v1.0.0.
1708
+ * - `unknown_contributor` — deprecated; no longer raised. The policy turns on
1709
+ * whether the pull request came from a fork, not on who opened it. Removed
1710
+ * at v1.0.0.
1630
1711
  */
1631
1712
  const SecurityHoldReason = z.enum([
1632
1713
  "workflow_modification",
@@ -1640,9 +1721,19 @@ var HeldRunStore = class {
1640
1721
  constructor(db) {
1641
1722
  this.db = db;
1642
1723
  }
1643
- /** Create a new held run with pending status. */
1644
- async create(orgId, data) {
1645
- return this.db.insertInto("held_runs").values({
1724
+ /**
1725
+ * Create a new held run with pending status.
1726
+ *
1727
+ * `exec` is the executor the INSERT runs through, defaulting to the store's
1728
+ * own connection. A caller that writes something the row cannot exist without
1729
+ * — the job's pending dispatch context, without which the hold can never be
1730
+ * resumed — passes its enclosing transaction, so the two land or roll back
1731
+ * together. Handed in rather than taken from an ambient scope: Kysely has no
1732
+ * such scope, so a `this.db` insert inside a `db.transaction()` callback runs
1733
+ * on a different connection and commits on its own.
1734
+ */
1735
+ async create(orgId, data, exec = this.db) {
1736
+ return exec.insertInto("held_runs").values({
1646
1737
  org_id: orgId,
1647
1738
  run_id: data.runId,
1648
1739
  job_id: data.jobId,
@@ -1650,16 +1741,21 @@ var HeldRunStore = class {
1650
1741
  hold_type: data.holdType,
1651
1742
  queue_type: data.queueType ?? "context",
1652
1743
  reason: data.reason,
1653
- expires_at: data.expiresAt
1744
+ expires_at: data.expiresAt,
1745
+ posted_pending_check: false,
1746
+ ...data.scope !== void 0 && { hold_scope: data.scope },
1747
+ ...data.triggerSource !== void 0 && { trigger_source: data.triggerSource }
1654
1748
  }).returningAll().executeTakeFirstOrThrow();
1655
1749
  }
1656
1750
  /**
1657
1751
  * Create a generalized approval hold (workflow/job/step scope, explicit or
1658
1752
  * context trigger) carrying a normalized `ApprovalRequirement`. Returns
1659
1753
  * the created row.
1754
+ *
1755
+ * `exec` carries the same meaning it does on {@link create}.
1660
1756
  */
1661
- async createHold(orgId, data) {
1662
- return this.db.insertInto("held_runs").values({
1757
+ async createHold(orgId, data, exec = this.db) {
1758
+ return exec.insertInto("held_runs").values({
1663
1759
  org_id: orgId,
1664
1760
  run_id: data.runId,
1665
1761
  job_id: data.jobId,
@@ -1668,6 +1764,7 @@ var HeldRunStore = class {
1668
1764
  queue_type: data.queueType ?? "context",
1669
1765
  reason: data.requirement.reason,
1670
1766
  expires_at: new Date(data.requirement.expiresAt),
1767
+ posted_pending_check: false,
1671
1768
  hold_scope: data.scope,
1672
1769
  step_index: data.stepIndex ?? null,
1673
1770
  trigger_source: data.triggerSource,
@@ -1675,6 +1772,40 @@ var HeldRunStore = class {
1675
1772
  ...data.payload !== void 0 && { payload: JSON.stringify(data.payload) }
1676
1773
  }).returningAll().executeTakeFirstOrThrow();
1677
1774
  }
1775
+ /**
1776
+ * Record that the pending `KiCI Security` check reached the provider, for
1777
+ * every hold that gates it, so the settle that ends one knows it has a check
1778
+ * to terminalize.
1779
+ *
1780
+ * Written AFTER the post returns, never before. The two orders fail
1781
+ * differently and the failures are not equivalent: recording first and dying
1782
+ * before the post leaves a row claiming a check the commit does not have, and
1783
+ * the settle then CREATES one — a completed `KiCI Security` run appearing on a
1784
+ * commit nothing ever held. Recording second leaves the opposite residue, a
1785
+ * real pending check the settle declines to close, which is the same stuck
1786
+ * check the fire-and-forget post could already produce. A fabricated failing
1787
+ * check on a pull request is worse than a stuck one, so the write goes last.
1788
+ *
1789
+ * **One statement for all of them, not one per hold.** A commit's check run is
1790
+ * shared by every hold on it, and the settle asks the contention query which
1791
+ * of them still owns it. Marking them in a loop admits a PARTIAL mark: mark
1792
+ * the reviewer hold, fail on the security hold, and the security row keeps
1793
+ * `posted_pending_check: false` — so it is not counted as a contender, and
1794
+ * approving the reviewer hold terminalizes the shared check `success` while
1795
+ * the trust hold still gates the job. That is a fabricated PASSING check,
1796
+ * which is the worse direction, reached without any process dying: a
1797
+ * deadlock, a statement timeout or a lost connection between the two UPDATEs
1798
+ * is enough. A single `WHERE id IN (…)` either marks every hold or none.
1799
+ *
1800
+ * The residual window is therefore one statement issued immediately after the
1801
+ * provider call returns — narrow, and not only reachable by a process death,
1802
+ * which is why the caller logs its failure rather than treating it as
1803
+ * impossible.
1804
+ */
1805
+ async markPendingCheckPosted(orgId, heldRunIds) {
1806
+ if (heldRunIds.length === 0) return;
1807
+ await this.db.updateTable("held_runs").set({ posted_pending_check: true }).where("id", "in", [...heldRunIds]).where("org_id", "=", orgId).execute();
1808
+ }
1678
1809
  /** INSERT one decision row using the given executor (root or transaction). */
1679
1810
  insertDecisionRow(exec, heldRunId, data) {
1680
1811
  const clausesSatisfied = data.clausesSatisfied != null ? JSON.stringify(data.clausesSatisfied) : null;
@@ -1839,33 +1970,63 @@ var HeldRunStore = class {
1839
1970
  return this.db.selectFrom("held_runs").selectAll().where("status", "=", "pending").where("expires_at", "<", sql`now()`).execute();
1840
1971
  }
1841
1972
  /**
1842
- * Release overdue workflow timer holds. The install-gate wait action pauses
1843
- * the workflow as a held run; on timer expiry it must RESUME (not fail like
1844
- * a reviewer-hold expiry). Flips each overdue pending `hold_type` timer,
1845
- * `hold_scope='workflow'` row to `released` and returns a `ReleaseSignal`
1846
- * per row so the caller can resume the workflow. Runs BEFORE
1847
- * `expireOverdue()` so these rows leave the pending pool before the
1848
- * expire-and-fail sweep sees them.
1973
+ * Release overdue timer holds at ANY scope. A wait action pauses its element
1974
+ * as a held run; on timer expiry it must RESUME (not fail like a reviewer-hold
1975
+ * expiry). Flips each overdue pending timer row to `released` and returns a
1976
+ * `ReleaseSignal` per row, carrying the row's own scope so the caller can
1977
+ * route it `routeRelease` sends a workflow-scoped one to the install-gate
1978
+ * rebuild and a job-scoped one to the job re-dispatch path.
1849
1979
  *
1850
- * The filter matches every persisted spelling of the timer hold type, so a
1851
- * row an un-upgraded orchestrator wrote as `wait_timer` still resumes rather
1852
- * than falling through to the expire-and-fail sweep. `hold_scope` is what
1853
- * keeps job-scoped dispatch-gate timer holds out of this sweep.
1980
+ * Runs BEFORE `expireOverdue()` so these rows leave the pending pool before
1981
+ * the expire-and-fail sweep sees them. That ordering is load-bearing, not
1982
+ * incidental: `expireOverdue` is not scope-filtered, so a released-but-not-yet-
1983
+ * resumed row would otherwise be expired out from under its resume.
1984
+ *
1985
+ * The filter matches every persisted spelling of the timer hold type, so a row
1986
+ * an un-upgraded orchestrator wrote as `wait_timer` still resumes rather than
1987
+ * falling through to the expire-and-fail sweep. It deliberately does NOT
1988
+ * filter on `hold_scope`: a job-scoped timer hold used to be excluded here,
1989
+ * which left it with no release path at all — created, never released,
1990
+ * eventually expired, its job never dispatched.
1854
1991
  */
1855
1992
  async releaseDueWaitHolds() {
1856
1993
  return (await this.db.updateTable("held_runs").set({
1857
1994
  status: "released",
1858
1995
  resolved_at: sql`now()`
1859
- }).where("status", "=", "pending").where("hold_type", "in", persistedHoldTypeSpellings(HoldType.enum.timer)).where("hold_scope", "=", HoldScope.enum.workflow).where("expires_at", "<", sql`now()`).returningAll().execute()).map((row) => ({
1996
+ }).where("status", "=", "pending").where("hold_type", "in", persistedHoldTypeSpellings(HoldType.enum.timer)).where("expires_at", "<", sql`now()`).returningAll().execute()).map((row) => ({
1860
1997
  holdId: row.id,
1861
1998
  runId: row.run_id,
1862
1999
  jobId: row.job_id,
1863
- scope: row.hold_scope ?? HoldScope.enum.workflow,
2000
+ scope: row.hold_scope,
1864
2001
  stepIndex: row.step_index,
1865
2002
  triggerSource: row.trigger_source ?? TriggerSource.enum.context
1866
2003
  }));
1867
2004
  }
1868
2005
  /**
2006
+ * Every pending queued (concurrency) hold, with the org and concurrency group
2007
+ * it belongs to — the input to the periodic release sweep, which needs to know
2008
+ * WHICH groups have someone waiting before it looks up any limits.
2009
+ *
2010
+ * Returns the pair rather than full rows: the sweep only groups by it, and
2011
+ * `listQueuedHoldsForContext` fetches the rows it actually releases.
2012
+ */
2013
+ async listAllQueuedHolds() {
2014
+ return await this.db.selectFrom("held_runs").innerJoin("execution_runs", "execution_runs.run_id", "held_runs.run_id").select(["held_runs.org_id as orgId", "execution_runs.context as concurrencyGroup"]).where("held_runs.status", "=", "pending").where("held_runs.hold_type", "in", persistedHoldTypeSpellings(HoldType.enum.concurrency)).execute();
2015
+ }
2016
+ /**
2017
+ * List the pending queued (concurrency) holds for one context's concurrency
2018
+ * group, oldest first.
2019
+ *
2020
+ * Joined to `execution_runs` and filtered by `customer_id` as well as
2021
+ * `context`: a context NAME is not unique across tenants, so without the org
2022
+ * predicate one org's completing job could release another org's queued hold.
2023
+ * Oldest-first is the release order — a queue that released newest-first would
2024
+ * starve whoever waited longest.
2025
+ */
2026
+ async listQueuedHoldsForContext(orgId, concurrencyGroup) {
2027
+ return this.db.selectFrom("held_runs").innerJoin("execution_runs", "execution_runs.run_id", "held_runs.run_id").selectAll("held_runs").where("held_runs.org_id", "=", orgId).where("held_runs.status", "=", "pending").where("held_runs.hold_type", "in", persistedHoldTypeSpellings(HoldType.enum.concurrency)).where("execution_runs.context", "=", concurrencyGroup).where("execution_runs.customer_id", "=", orgId).orderBy("held_runs.created_at", "asc").execute();
2028
+ }
2029
+ /**
1869
2030
  * Expire overdue pending runs. Called by the stale detector.
1870
2031
  * Sets status to 'expired' and resolved_at to now() for all
1871
2032
  * pending runs past their expires_at.
@@ -1887,10 +2048,11 @@ var HeldRunStore = class {
1887
2048
  * PR-scoped hold selection against the real deployed orchestrator DB.
1888
2049
  */
1889
2050
  function createHeldRunStoreFromUrl(databaseUrl, opts) {
1890
- const db = new Kysely({ dialect: new PostgresDialect({ pool: new pg.Pool({
2051
+ const pool = new pg.Pool({
1891
2052
  connectionString: databaseUrl,
1892
2053
  max: opts?.maxConnections ?? 3
1893
- }) }) });
2054
+ });
2055
+ const db = new Kysely({ dialect: new PostgresDialect({ pool }) });
1894
2056
  return {
1895
2057
  store: new HeldRunStore(db),
1896
2058
  dispose: async () => {
@@ -1898,6 +2060,23 @@ function createHeldRunStoreFromUrl(databaseUrl, opts) {
1898
2060
  }
1899
2061
  };
1900
2062
  }
2063
+ z.enum(["policy", "legacy"]);
2064
+ /** The fork switch applied when no policy row is stored. */
2065
+ const DEFAULT_FORK_POLICY = ForkPolicy.enum.ignore;
2066
+ Object.freeze({
2067
+ forkPolicy: DEFAULT_FORK_POLICY,
2068
+ unknownContributorPolicy: "hold",
2069
+ workflowChangePolicy: "hold",
2070
+ approvalExpiryHours: DEFAULT_APPROVAL_EXPIRY_HOURS,
2071
+ approvalExpirySeconds: DEFAULT_APPROVAL_EXPIRY_SECONDS
2072
+ });
2073
+ Object.freeze({
2074
+ forkPolicy: ForkPolicy.enum.hold,
2075
+ unknownContributorPolicy: "hold",
2076
+ workflowChangePolicy: "hold",
2077
+ approvalExpiryHours: DEFAULT_APPROVAL_EXPIRY_HOURS,
2078
+ approvalExpirySeconds: DEFAULT_APPROVAL_EXPIRY_SECONDS
2079
+ });
1901
2080
  //#endregion
1902
2081
  //#region src/security/trust-policy-store.ts
1903
2082
  /**
@@ -1914,14 +2093,42 @@ const TrustPolicySource = z.enum(["platform", "local"]);
1914
2093
  /**
1915
2094
  * The strictest documented defaults. Used to fill gaps on a local merge; the
1916
2095
  * fail-closed policy an unknown org resolves to lives in `trust-policy-gate.ts`.
2096
+ *
2097
+ * `forkPolicy` reads that module's `DEFAULT_FORK_POLICY` so a partial local
2098
+ * merge and an absent row settle on the same fork switch.
1917
2099
  */
1918
2100
  const DEFAULT_TRUST_POLICY = {
1919
- forkPolicy: "hold",
2101
+ forkPolicy: DEFAULT_FORK_POLICY,
1920
2102
  unknownContributorPolicy: "hold",
1921
2103
  workflowChangePolicy: "hold",
1922
- approvalExpiryHours: DEFAULT_APPROVAL_EXPIRY_HOURS
2104
+ approvalExpiryHours: DEFAULT_APPROVAL_EXPIRY_HOURS,
2105
+ approvalExpirySeconds: DEFAULT_APPROVAL_EXPIRY_SECONDS
1923
2106
  };
1924
2107
  /**
2108
+ * Resolve a local patch's approval-expiry window against what is already stored.
2109
+ *
2110
+ * One value is being set, spelled two ways, so the pair is merged as a unit and
2111
+ * always re-derived from the winning seconds value:
2112
+ *
2113
+ * - a patch naming `approvalExpirySeconds` sets the window, and the hours field
2114
+ * is recomputed from it — a stale hours value left behind would be the coarse
2115
+ * view of a window that is no longer stored;
2116
+ * - a patch naming only `approvalExpiryHours` sets the window too, and the
2117
+ * seconds field is recomputed from it — keeping the old seconds value would
2118
+ * let it out-rank the operator's explicit hours and silently discard the
2119
+ * write;
2120
+ * - a patch naming both takes the seconds value, because it is the more
2121
+ * specific of the two (`approvalExpirySecondsOf`);
2122
+ * - a patch naming neither leaves the stored window untouched.
2123
+ */
2124
+ function mergedExpiry(patch, existing) {
2125
+ const seconds = (patch.approvalExpirySeconds != null || patch.approvalExpiryHours != null ? approvalExpirySecondsOf(patch) : null) ?? existing?.approvalExpirySeconds ?? DEFAULT_TRUST_POLICY.approvalExpirySeconds ?? DEFAULT_APPROVAL_EXPIRY_SECONDS;
2126
+ return {
2127
+ approvalExpiryHours: approvalExpiryHoursOf(seconds),
2128
+ approvalExpirySeconds: seconds
2129
+ };
2130
+ }
2131
+ /**
1925
2132
  * Namespace for the per-org advisory lock taken by `upsertLocal`, so the key
1926
2133
  * cannot collide with another feature's advisory lock in the same database.
1927
2134
  */
@@ -1940,6 +2147,10 @@ var TrustPolicyStore = class {
1940
2147
  unknownContributorPolicy: row.unknown_contributor_policy,
1941
2148
  workflowChangePolicy: row.workflow_change_policy,
1942
2149
  approvalExpiryHours: Number(row.approval_expiry_hours),
2150
+ approvalExpirySeconds: approvalExpirySecondsOf({
2151
+ approvalExpiryHours: Number(row.approval_expiry_hours),
2152
+ approvalExpirySeconds: row.approval_expiry_seconds == null ? null : Number(row.approval_expiry_seconds)
2153
+ }),
1943
2154
  source: row.source,
1944
2155
  updatedAt: row.updated_at
1945
2156
  };
@@ -1982,7 +2193,7 @@ var TrustPolicyStore = class {
1982
2193
  forkPolicy: patch.forkPolicy ?? existing?.forkPolicy ?? DEFAULT_TRUST_POLICY.forkPolicy,
1983
2194
  unknownContributorPolicy: patch.unknownContributorPolicy ?? existing?.unknownContributorPolicy ?? DEFAULT_TRUST_POLICY.unknownContributorPolicy,
1984
2195
  workflowChangePolicy: patch.workflowChangePolicy ?? existing?.workflowChangePolicy ?? DEFAULT_TRUST_POLICY.workflowChangePolicy,
1985
- approvalExpiryHours: patch.approvalExpiryHours ?? existing?.approvalExpiryHours ?? DEFAULT_TRUST_POLICY.approvalExpiryHours
2196
+ ...mergedExpiry(patch, existing)
1986
2197
  };
1987
2198
  await this.write(orgId, merged, TrustPolicySource.enum.local, trx);
1988
2199
  await onWrite?.(trx, merged);
@@ -1990,11 +2201,13 @@ var TrustPolicyStore = class {
1990
2201
  });
1991
2202
  }
1992
2203
  async write(orgId, policy, source, executor = this.db) {
2204
+ const seconds = approvalExpirySecondsOf(policy);
1993
2205
  const columns = {
1994
2206
  fork_policy: policy.forkPolicy,
1995
2207
  unknown_contributor_policy: policy.unknownContributorPolicy,
1996
2208
  workflow_change_policy: policy.workflowChangePolicy,
1997
- approval_expiry_hours: policy.approvalExpiryHours,
2209
+ approval_expiry_hours: approvalExpiryHoursOf(seconds),
2210
+ approval_expiry_seconds: seconds,
1998
2211
  source,
1999
2212
  updated_at: sql`now()`
2000
2213
  };
@@ -2011,10 +2224,11 @@ var TrustPolicyStore = class {
2011
2224
  * orchestrator DB rather than held in process memory.
2012
2225
  */
2013
2226
  function createTrustPolicyStoreFromUrl(databaseUrl, opts) {
2014
- const db = new Kysely({ dialect: new PostgresDialect({ pool: new pg.Pool({
2227
+ const pool = new pg.Pool({
2015
2228
  connectionString: databaseUrl,
2016
2229
  max: opts?.maxConnections ?? 3
2017
- }) }) });
2230
+ });
2231
+ const db = new Kysely({ dialect: new PostgresDialect({ pool }) });
2018
2232
  return {
2019
2233
  store: new TrustPolicyStore(db),
2020
2234
  dispose: async () => {
@@ -2062,10 +2276,34 @@ function firstCauseMessage(err) {
2062
2276
  }
2063
2277
  return "";
2064
2278
  }
2279
+ /**
2280
+ * True when a rejected `fetch` failed at the transport layer, as opposed to
2281
+ * failing while the request was still being built.
2282
+ *
2283
+ * `fetch` uses a `TypeError` for both, so they are easy to conflate — but only
2284
+ * a transport failure means the orchestrator was actually dialled. Measured
2285
+ * against Node 24's undici:
2286
+ *
2287
+ * closed port → TypeError('fetch failed') cause: Error(ECONNREFUSED)
2288
+ * invalid header → TypeError('Headers.append: … invalid header …') cause: undefined
2289
+ * invalid URL → TypeError('Failed to parse URL from …') cause: Error(ERR_INVALID_URL)
2290
+ *
2291
+ * The discriminator is therefore the message, NOT the presence of a `cause`:
2292
+ * the invalid-URL case carries one. undici uses exactly `fetch failed` for a
2293
+ * transport failure and a descriptive message for everything it rejects before
2294
+ * opening a connection.
2295
+ */
2296
+ function isTransportFailure(err) {
2297
+ return err instanceof TypeError && err.message === "fetch failed";
2298
+ }
2065
2299
  async function fetchAdminApi(url, init, baseUrl) {
2066
2300
  try {
2067
2301
  return await fetch(url, init);
2068
2302
  } catch (err) {
2303
+ if (!isTransportFailure(err)) {
2304
+ const detail = (err instanceof Error ? err.message : String(err)).replace(/\.$/, "");
2305
+ throw new Error(`the admin API request to ${baseUrl} could not be built: ${detail}. The orchestrator was NOT contacted — this is a malformed request, not a connectivity problem. A token or URL carrying a newline or stray whitespace is the usual cause; check KICI_ADMIN_TOKEN / --token and KICI_ADMIN_URL / --url.`, { cause: err });
2306
+ }
2069
2307
  const detail = firstCauseMessage(err);
2070
2308
  const cause = detail ? ` (${detail})` : "";
2071
2309
  throw new Error(`cannot reach the orchestrator admin API at ${baseUrl}${cause}. Set KICI_ADMIN_URL to the orchestrator's HTTP address, or pass --base-url where the subcommand accepts it. Note KICI_ORCHESTRATOR_URL is NOT read by this CLI; if other subcommands appear to work, they are using the --database-url / KICI_DATABASE_URL direct-DB path rather than HTTP.`, { cause: err });
@@ -2765,7 +3003,8 @@ function openBrowserBestEffort(url) {
2765
3003
  exec(override.replace("{url}", url), () => {});
2766
3004
  return;
2767
3005
  }
2768
- exec(platform() === "darwin" ? `open ${JSON.stringify(url)}` : platform() === "win32" ? `start "" ${JSON.stringify(url)}` : `xdg-open ${JSON.stringify(url)}`, () => {});
3006
+ const cmd = platform() === "darwin" ? `open ${JSON.stringify(url)}` : platform() === "win32" ? `start "" ${JSON.stringify(url)}` : `xdg-open ${JSON.stringify(url)}`;
3007
+ exec(cmd, () => {});
2769
3008
  }
2770
3009
  //#endregion
2771
3010
  //#region src/cli/commands/source-manifest.ts
@@ -2784,9 +3023,9 @@ function openBrowserBestEffort(url) {
2784
3023
  /** Static marketing-site page the CLI points at in headless paste-code mode. */
2785
3024
  const STATIC_CALLBACK_URL = "https://kici.dev/gh-manifest-callback";
2786
3025
  /** How long to wait for the operator to click "Create" on GitHub. */
2787
- const CREATE_TIMEOUT_MS = 5 * 6e4;
3026
+ const CREATE_TIMEOUT_MS = 3e5;
2788
3027
  /** How long to wait for the operator to install the App on at least one repo. */
2789
- const INSTALL_TIMEOUT_MS = 5 * 6e4;
3028
+ const INSTALL_TIMEOUT_MS = 3e5;
2790
3029
  const INSTALL_POLL_MS = 3e3;
2791
3030
  function defaultReadLine(prompt) {
2792
3031
  const rl = createInterface({
@@ -2801,7 +3040,8 @@ function defaultReadLine(prompt) {
2801
3040
  });
2802
3041
  }
2803
3042
  function defaultWriteRecoveryFile(appId, pem) {
2804
- const file = join(mkdtempSync(join(tmpdir(), "kici-gh-app-")), `app-${appId}.private-key.pem`);
3043
+ const dir = mkdtempSync(join(tmpdir(), "kici-gh-app-"));
3044
+ const file = join(dir, `app-${appId}.private-key.pem`);
2805
3045
  writeFileSync(file, pem, { mode: 384 });
2806
3046
  return file;
2807
3047
  }