@kici-dev/orchestrator 0.1.26 → 0.2.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 (310) hide show
  1. package/dist/__test-helpers__/mock-db.d.ts +2 -0
  2. package/dist/agent/dispatcher.d.ts +52 -2
  3. package/dist/agent/host-roster-reaper.d.ts +1 -2
  4. package/dist/agent/host-roster.d.ts +34 -0
  5. package/dist/agent/ownership-refusal.d.ts +12 -0
  6. package/dist/agent/ownership-tracker.d.ts +64 -16
  7. package/dist/agent/token-store.d.ts +13 -0
  8. package/dist/agent-packaging/availability.d.ts +27 -0
  9. package/dist/agent-packaging/build-package.d.ts +16 -0
  10. package/dist/agent-packaging/download.d.ts +30 -0
  11. package/dist/agent-packaging/index.d.ts +9 -0
  12. package/dist/agent-packaging/install-closure.d.ts +30 -0
  13. package/dist/agent-packaging/node-binary.d.ts +26 -0
  14. package/dist/agent-packaging/refresh.d.ts +50 -0
  15. package/dist/agent-packaging/store.d.ts +15 -0
  16. package/dist/agent-packaging/upload.d.ts +24 -0
  17. package/dist/app-on-error.d.ts +29 -0
  18. package/dist/app.d.ts +69 -8
  19. package/dist/approvals/apply-decision.d.ts +1 -1
  20. package/dist/approvals/step-approval-bridge.d.ts +1 -1
  21. package/dist/artifacts/artifact-store.d.ts +233 -0
  22. package/dist/artifacts/failure-messages.d.ts +46 -0
  23. package/dist/audit/access-log.d.ts +18 -1
  24. package/dist/cache/dep-cache.d.ts +15 -0
  25. package/dist/cache/pending-inits.d.ts +4 -4
  26. package/dist/cache/user-cache.d.ts +37 -0
  27. package/dist/cancel/cancel-run.d.ts +10 -3
  28. package/dist/cancel/dashboard-cancel-handler.d.ts +19 -0
  29. package/dist/cli/api-client.d.ts +51 -5
  30. package/dist/cli/commands/agent-package.d.ts +34 -0
  31. package/dist/cli/commands/check-run.d.ts +26 -0
  32. package/dist/cli/commands/cluster-settings.d.ts +55 -0
  33. package/dist/cli/commands/context.d.ts +25 -0
  34. package/dist/cli/commands/dashboard-encryption-key.d.ts +3 -0
  35. package/dist/cli/commands/db-backup.d.ts +71 -0
  36. package/dist/cli/commands/debug-bundle.d.ts +1 -1
  37. package/dist/cli/commands/orchestrator-service/drain.d.ts +31 -0
  38. package/dist/cli/commands/orchestrator-service/index.d.ts +7 -4
  39. package/dist/cli/commands/orchestrator-service/install-env.d.ts +58 -0
  40. package/dist/cli/commands/secret.d.ts +45 -1
  41. package/dist/cli/commands/shared/admin-cli-access-log.d.ts +46 -0
  42. package/dist/cli/commands/shared/versioned-upgrade.d.ts +65 -2
  43. package/dist/cli/commands/signing-key.d.ts +3 -0
  44. package/dist/cli/commands/source.d.ts +12 -0
  45. package/dist/cli/commands/token.d.ts +9 -0
  46. package/dist/cli/commands/trust-policy.d.ts +48 -0
  47. package/dist/cli/commands/variable.d.ts +6 -6
  48. package/dist/cli/join.d.ts +1 -1
  49. package/dist/cli/service/index.d.ts +2 -2
  50. package/dist/cli/service/instance/manifest.d.ts +17 -0
  51. package/dist/cli/service/instance/resolve.d.ts +11 -0
  52. package/dist/cli/service/windows.d.ts +6 -0
  53. package/dist/cli/wizard/orchestrator-wizard.d.ts +39 -11
  54. package/dist/cli.js +10263 -3667
  55. package/dist/cluster/cluster-settings-reader.d.ts +78 -0
  56. package/dist/cluster/coordinator.d.ts +81 -9
  57. package/dist/cluster/join-client.d.ts +1 -1
  58. package/dist/cluster/leader-gated-scheduler.d.ts +50 -0
  59. package/dist/cluster/orphan-recovery.d.ts +14 -0
  60. package/dist/cluster/peer-client.d.ts +12 -2
  61. package/dist/cluster/peer-handler.d.ts +15 -4
  62. package/dist/cluster/peer-registry.d.ts +12 -0
  63. package/dist/cluster/verified-issuer-poller.d.ts +11 -0
  64. package/dist/cluster/verified-issuer.d.ts +63 -0
  65. package/dist/cold-store/tables/access-log.d.ts +1 -1
  66. package/dist/cold-store/tables/event-log.d.ts +1 -1
  67. package/dist/cold-store/tables/execution-jobs.d.ts +1 -1
  68. package/dist/cold-store/tables/execution-runs.d.ts +1 -1
  69. package/dist/cold-store/tables/execution-steps.d.ts +1 -1
  70. package/dist/cold-store/tables/secret-audit-log.d.ts +1 -1
  71. package/dist/config/loader.d.ts +1 -1
  72. package/dist/config/resolver.d.ts +2 -1
  73. package/dist/config/schema.d.ts +8 -6
  74. package/dist/config/types.d.ts +3 -2
  75. package/dist/config.d.ts +372 -46
  76. package/dist/contexts/binding-store.d.ts +36 -0
  77. package/dist/contexts/context-store.d.ts +100 -0
  78. package/dist/contexts/glob-specificity.d.ts +30 -0
  79. package/dist/{environments → contexts}/held-runs.d.ts +64 -32
  80. package/dist/{environments → contexts}/index.d.ts +1 -1
  81. package/dist/contexts/protection/aggregate.d.ts +43 -0
  82. package/dist/contexts/protection/branch-gate.d.ts +5 -0
  83. package/dist/contexts/protection/concurrency-gate.d.ts +4 -0
  84. package/dist/{environments → contexts}/protection/pipeline.d.ts +3 -3
  85. package/dist/contexts/protection/reviewer-gate.d.ts +4 -0
  86. package/dist/{environments → contexts}/protection/satisfiability.d.ts +22 -22
  87. package/dist/contexts/protection/trust-gate.d.ts +4 -0
  88. package/dist/contexts/protection/wait-timer-gate.d.ts +4 -0
  89. package/dist/contexts/variable-store.d.ts +39 -0
  90. package/dist/cron/cron-scheduler.d.ts +24 -4
  91. package/dist/cron/cron-store.d.ts +21 -7
  92. package/dist/dashboard/attestation-filters.d.ts +94 -74
  93. package/dist/dashboard/handler.d.ts +63 -3
  94. package/dist/db/migration-test-harness.d.ts +41 -0
  95. package/dist/db/migrations/067_environments_to_contexts.d.ts +36 -0
  96. package/dist/db/migrations/068_request_idempotency.d.ts +17 -0
  97. package/dist/db/migrations/069_reroute_tunables.d.ts +21 -0
  98. package/dist/db/migrations/070_execution_runs_failure_class.d.ts +4 -0
  99. package/dist/db/migrations/071_batch_accumulation.d.ts +26 -0
  100. package/dist/db/migrations/072_dispatch_queue_run_id_index.d.ts +15 -0
  101. package/dist/db/migrations/073_org_settings_ingest_concurrency.d.ts +13 -0
  102. package/dist/db/migrations/074_normalize_zero_concurrency_limit.d.ts +13 -0
  103. package/dist/db/migrations/075_ingest_overflow_buffer.d.ts +16 -0
  104. package/dist/db/migrations/076_artifacts.d.ts +19 -0
  105. package/dist/db/migrations/077_backup_runs.d.ts +13 -0
  106. package/dist/db/migrations/078_org_settings_backup_staleness.d.ts +12 -0
  107. package/dist/db/migrations/079_org_settings_scaler_spawn_timeout.d.ts +14 -0
  108. package/dist/db/migrations/080_cluster_settings.d.ts +17 -0
  109. package/dist/db/migrations/081_org_settings_queue_timeout.d.ts +19 -0
  110. package/dist/db/migrations/082_host_s3_reachable.d.ts +16 -0
  111. package/dist/db/migrations/083_org_settings_artifact_caps.d.ts +18 -0
  112. package/dist/db/migrations/084_orchestrator_signing_keys.d.ts +20 -0
  113. package/dist/db/migrations/085_cluster_settings_reroute_flap_grace_ms.d.ts +12 -0
  114. package/dist/db/migrations/086_cluster_settings_max_fanout_hosts.d.ts +12 -0
  115. package/dist/db/migrations/087_cluster_settings_event_router_rate_limit.d.ts +12 -0
  116. package/dist/db/migrations/088_cluster_settings_cache_max_tarball_bytes.d.ts +12 -0
  117. package/dist/db/migrations/089_cluster_settings_cache_ttl_days.d.ts +12 -0
  118. package/dist/db/migrations/090_cluster_settings_concurrency_wait_timeout_ms.d.ts +12 -0
  119. package/dist/db/migrations/091_cluster_settings_agent_token_ttl_ms.d.ts +12 -0
  120. package/dist/db/migrations/092_cluster_settings_version.d.ts +16 -0
  121. package/dist/db/migrations/093_org_settings_sandbox_allowlist.d.ts +4 -0
  122. package/dist/db/migrations/094_dashboard_encryption_keys.d.ts +21 -0
  123. package/dist/db/migrations/095_dashboard_write_policy_tristate.d.ts +20 -0
  124. package/dist/db/migrations/096_multi_schedule_cron_last_fired.d.ts +4 -0
  125. package/dist/db/migrations/097_execution_runs_pr_number.d.ts +4 -0
  126. package/dist/db/migrations/098_execution_runs_customer_id.d.ts +4 -0
  127. package/dist/db/migrations/099_cluster_settings_dashboard_verified_issuer.d.ts +21 -0
  128. package/dist/db/migrations/100_held_runs_hold_type_vocabulary.d.ts +31 -0
  129. package/dist/db/migrations/101_contexts_hold_expiry_drop_default.d.ts +31 -0
  130. package/dist/db/migrations/102_dispatch_queue_agent_id.d.ts +20 -0
  131. package/dist/db/migrations/103_cluster_settings_ownership_db_check_timeout_ms.d.ts +20 -0
  132. package/dist/db/migrations/104_check_run_terminal_sent.d.ts +22 -0
  133. package/dist/db/migrations/105_org_trust_policy.d.ts +19 -0
  134. package/dist/db/migrations/106_cluster_settings_check_run_tracking_ttl_days.d.ts +18 -0
  135. package/dist/db/migrations/107_check_run_tracking_updated_at_index.d.ts +23 -0
  136. package/dist/db/repos/dashboard-encryption-keys-repo.d.ts +48 -0
  137. package/dist/db/repos/signing-keys-repo.d.ts +34 -0
  138. package/dist/db/types.d.ts +499 -66
  139. package/dist/deployment/deployment-identity.d.ts +87 -2
  140. package/dist/diagnostics/checks/backup.d.ts +18 -0
  141. package/dist/diagnostics/checks/index.d.ts +2 -1
  142. package/dist/diagnostics/fleet-collector.d.ts +1 -1
  143. package/dist/drain/drain-controller.d.ts +52 -0
  144. package/dist/events/batch-accumulator.d.ts +63 -0
  145. package/dist/events/circuit-breaker.d.ts +9 -3
  146. package/dist/events/event-emitter.d.ts +31 -0
  147. package/dist/events/event-retry-scanner.d.ts +12 -2
  148. package/dist/events/event-router.d.ts +29 -0
  149. package/dist/firecracker/host-network.d.ts +2 -0
  150. package/dist/firecracker/net-interfaces.d.ts +14 -0
  151. package/dist/github-app-name-refresher/github-app-name-refresher.d.ts +19 -9
  152. package/dist/index.d.ts +2 -0
  153. package/dist/index.js +625 -22
  154. package/dist/lockfile-cache.d.ts +4 -0
  155. package/dist/lockfile-validate.d.ts +16 -9
  156. package/dist/metrics/prometheus.d.ts +114 -15
  157. package/dist/oidc/aws-kms-signer.d.ts +37 -0
  158. package/dist/oidc/command-signer.d.ts +23 -0
  159. package/dist/oidc/db-signer.d.ts +48 -0
  160. package/dist/oidc/id-token-claims.d.ts +76 -0
  161. package/dist/oidc/jwt.d.ts +18 -0
  162. package/dist/oidc/local-dev-signer.d.ts +31 -0
  163. package/dist/oidc/local-mint.d.ts +77 -0
  164. package/dist/oidc/oidc-mint-registration.d.ts +65 -0
  165. package/dist/oidc/orchestrator-mint.d.ts +97 -0
  166. package/dist/oidc/orchestrator-signer-factory.d.ts +33 -0
  167. package/dist/oidc/reconcile-signing-key.d.ts +21 -0
  168. package/dist/oidc/signer.d.ts +25 -0
  169. package/dist/oidc/signing-key-status.d.ts +23 -0
  170. package/dist/orchestrator-core.d.ts +42 -2
  171. package/dist/pipeline/decorating-secret-resolver.d.ts +5 -4
  172. package/dist/pipeline/dispatch-matched-workflow.d.ts +44 -3
  173. package/dist/pipeline/inline-eval.d.ts +2 -2
  174. package/dist/pipeline/install-secrets-resolver.d.ts +25 -14
  175. package/dist/pipeline/{job-environments.d.ts → job-contexts.d.ts} +23 -23
  176. package/dist/pipeline/manual-schedule.d.ts +28 -14
  177. package/dist/pipeline/pending-workflow-context.d.ts +1 -1
  178. package/dist/pipeline/process-webhook.d.ts +57 -2
  179. package/dist/pipeline/processor.d.ts +49 -13
  180. package/dist/pipeline/request-idempotency.d.ts +28 -0
  181. package/dist/pipeline/rerun.d.ts +20 -13
  182. package/dist/pipeline/resolve-sandbox-grant.d.ts +28 -0
  183. package/dist/pipeline/resume-workflow.d.ts +1 -1
  184. package/dist/pipeline/route-or-dispatch-jobs.d.ts +71 -0
  185. package/dist/pipeline/sandbox-allowlist-reader.d.ts +33 -0
  186. package/dist/pipeline/test-pipeline.d.ts +2 -2
  187. package/dist/policy/dashboard-write-policy.d.ts +9 -4
  188. package/dist/provenance/backfill-run.d.ts +1 -0
  189. package/dist/provenance/trust-root.d.ts +20 -0
  190. package/dist/provider-registry.d.ts +8 -0
  191. package/dist/providers/generic/normalizer.d.ts +1 -1
  192. package/dist/providers/github/auth.d.ts +1 -1
  193. package/dist/providers/github/changed-files.d.ts +14 -6
  194. package/dist/providers/github/check-status-poster.d.ts +19 -3
  195. package/dist/providers/github/contributor-resolver.d.ts +1 -1
  196. package/dist/providers/github/lock-file.d.ts +1 -1
  197. package/dist/providers/github/normalizer.d.ts +1 -1
  198. package/dist/providers/github/repo-url.d.ts +1 -1
  199. package/dist/providers/local/index.d.ts +1 -0
  200. package/dist/providers/local/local-source-config.d.ts +1 -0
  201. package/dist/providers/local/lock-file-fetcher.d.ts +1 -1
  202. package/dist/providers/local/normalizer.d.ts +1 -1
  203. package/dist/providers/local/repo-url-builder.d.ts +1 -1
  204. package/dist/providers/universal-git/changed-files.d.ts +8 -7
  205. package/dist/providers/universal-git/clone-token.d.ts +1 -1
  206. package/dist/providers/universal-git/config.d.ts +4 -4
  207. package/dist/providers/universal-git/index.d.ts +5 -1
  208. package/dist/providers/universal-git/lock-file.d.ts +6 -1
  209. package/dist/providers/universal-git/normalizer.d.ts +1 -1
  210. package/dist/providers/universal-git/repo-url.d.ts +1 -1
  211. package/dist/queue/cleanup.d.ts +69 -8
  212. package/dist/queue/job-queue.d.ts +94 -14
  213. package/dist/queue/scheduled-job.d.ts +2 -2
  214. package/dist/registration/registration-index.d.ts +1 -1
  215. package/dist/reporting/agent-failure-category.d.ts +4 -1
  216. package/dist/reporting/check-run-reporter.d.ts +34 -14
  217. package/dist/reporting/check-run-tracking-store.d.ts +70 -8
  218. package/dist/reporting/execution-tracker.d.ts +173 -17
  219. package/dist/reporting/fs-log-storage.d.ts +31 -2
  220. package/dist/reporting/job-check-run-completion.d.ts +50 -0
  221. package/dist/reporting/log-chunk-sink.d.ts +47 -0
  222. package/dist/reporting/log-retention.d.ts +16 -0
  223. package/dist/reporting/log-storage.d.ts +64 -4
  224. package/dist/reporting/log-writer.d.ts +22 -7
  225. package/dist/reporting/peer-log-normalize.d.ts +14 -0
  226. package/dist/reporting/run-aggregator.d.ts +36 -36
  227. package/dist/reporting/s3-log-storage.d.ts +102 -8
  228. package/dist/routes/admin-auth.d.ts +90 -0
  229. package/dist/routes/admin-cluster-settings.d.ts +31 -0
  230. package/dist/routes/admin-contexts.d.ts +47 -0
  231. package/dist/routes/admin-events.d.ts +3 -0
  232. package/dist/routes/admin-orchestrator-drain.d.ts +35 -0
  233. package/dist/routes/admin-registrations.d.ts +2 -2
  234. package/dist/routes/admin-runs.d.ts +9 -0
  235. package/dist/routes/admin-sources.d.ts +7 -0
  236. package/dist/routes/admin-trust-policy.d.ts +49 -0
  237. package/dist/routes/admin.d.ts +15 -1
  238. package/dist/routes/github-webhook.d.ts +9 -3
  239. package/dist/routes/provenance-oidc.d.ts +38 -0
  240. package/dist/routes/verify-attestation.d.ts +21 -0
  241. package/dist/scaler/bare-metal-backend.d.ts +9 -2
  242. package/dist/scaler/config.d.ts +25 -4
  243. package/dist/scaler/container-backend.d.ts +9 -1
  244. package/dist/scaler/firecracker-backend.d.ts +29 -2
  245. package/dist/scaler/index.d.ts +1 -1
  246. package/dist/scaler/manager.d.ts +166 -8
  247. package/dist/scaler/pending-scale-sweeper.d.ts +35 -0
  248. package/dist/scaler/types.d.ts +75 -6
  249. package/dist/secrets/backend-registry.d.ts +32 -1
  250. package/dist/secrets/context-secret-resolver.d.ts +30 -0
  251. package/dist/secrets/dashboard-encryption-key.d.ts +48 -0
  252. package/dist/secrets/ephemeral-keys.d.ts +16 -0
  253. package/dist/secrets/index.d.ts +1 -1
  254. package/dist/secrets/pg-secret-store.d.ts +13 -3
  255. package/dist/secrets/rbac.d.ts +5 -1
  256. package/dist/secrets/routing-key-scope.d.ts +6 -0
  257. package/dist/secrets/scope-routing.d.ts +126 -0
  258. package/dist/secrets/secret-resolver.d.ts +19 -32
  259. package/dist/secrets/token-manager.d.ts +5 -1
  260. package/dist/security/comment-handler.d.ts +1 -1
  261. package/dist/security/contributor-cache.d.ts +9 -1
  262. package/dist/security/trust-policy-gate.d.ts +117 -0
  263. package/dist/security/trust-policy-store.d.ts +78 -0
  264. package/dist/server.js +44012 -32721
  265. package/dist/sources/source-manager.d.ts +24 -0
  266. package/dist/stale-detector/stale-run-detector.d.ts +8 -1
  267. package/dist/standalone.js +44106 -33690
  268. package/dist/storage/blob-routes.d.ts +16 -0
  269. package/dist/storage/filesystem.d.ts +17 -0
  270. package/dist/storage/key-discriminator.d.ts +20 -0
  271. package/dist/storage/s3.d.ts +2 -0
  272. package/dist/storage/types.d.ts +16 -0
  273. package/dist/webhook/dedup.d.ts +9 -1
  274. package/dist/webhook/event-log.d.ts +13 -2
  275. package/dist/webhook/generic-sources-listener.d.ts +4 -0
  276. package/dist/webhook/ingest-admission.d.ts +140 -0
  277. package/dist/webhook/ingest-overflow-buffer.d.ts +27 -0
  278. package/dist/webhook/ingest-overflow-replayer.d.ts +49 -0
  279. package/dist/webhook/ingest-overflow-types.d.ts +73 -0
  280. package/dist/webhook/loop-lag-source.d.ts +40 -0
  281. package/dist/webhook/observed-relay-guard.d.ts +40 -0
  282. package/dist/webhook/org-ingest-cap-reader.d.ts +24 -0
  283. package/dist/webhook/register-source-bundle.d.ts +3 -0
  284. package/dist/webhook/relay-reinject.d.ts +43 -0
  285. package/dist/worker/in-memory-job-queue.d.ts +8 -3
  286. package/dist/worker-core.d.ts +10 -0
  287. package/dist/ws/agent-api-registry.d.ts +20 -0
  288. package/dist/ws/agent-handler.d.ts +63 -7
  289. package/dist/ws/bringup-api.d.ts +99 -0
  290. package/dist/ws/{dashboard-env-handler.d.ts → dashboard-context-handler.d.ts} +43 -25
  291. package/dist/ws/dashboard-run-state-handler.d.ts +31 -0
  292. package/dist/ws/failure-messages.d.ts +29 -0
  293. package/dist/ws/oidc-token-relay.d.ts +15 -0
  294. package/dist/ws/ownership-gate.d.ts +22 -0
  295. package/dist/ws/platform-client.d.ts +96 -1
  296. package/dist/ws/replay-chunker.d.ts +37 -0
  297. package/installer-image-digests.json +3 -3
  298. package/package.json +22 -21
  299. package/sbom.spdx.json +1245 -1793
  300. package/dist/cli/commands/environment.d.ts +0 -25
  301. package/dist/environments/binding-store.d.ts +0 -36
  302. package/dist/environments/environment-store.d.ts +0 -87
  303. package/dist/environments/protection/aggregate.d.ts +0 -43
  304. package/dist/environments/protection/branch-gate.d.ts +0 -5
  305. package/dist/environments/protection/concurrency-gate.d.ts +0 -7
  306. package/dist/environments/protection/reviewer-gate.d.ts +0 -7
  307. package/dist/environments/protection/trust-gate.d.ts +0 -7
  308. package/dist/environments/protection/wait-timer-gate.d.ts +0 -7
  309. package/dist/environments/variable-store.d.ts +0 -39
  310. package/dist/routes/admin-environments.d.ts +0 -47
package/dist/index.js CHANGED
@@ -6,10 +6,12 @@ import { createLogger, createS3Client, sha256 } from "@kici-dev/shared";
6
6
  import * as fs from "node:fs";
7
7
  import { mkdtempSync, promises, writeFileSync } from "node:fs";
8
8
  import { dirname, join } from "node:path";
9
- import { createHmac, hkdfSync, randomBytes, randomUUID } from "node:crypto";
9
+ import { createHash, createHmac, hkdfSync, randomBytes, randomUUID } from "node:crypto";
10
10
  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
+ import { z } from "zod";
14
+ import { DEFAULT_APPROVAL_EXPIRY_HOURS, HoldScope, HoldType, TriggerSource, persistedHoldTypeSpellings } from "@kici-dev/engine";
13
15
  import { platform, tmpdir } from "node:os";
14
16
  import { createInterface } from "node:readline";
15
17
  import { Octokit } from "@octokit/rest";
@@ -189,6 +191,9 @@ var S3CacheStorage = class {
189
191
  Key: this.objectKey(key)
190
192
  }), { expiresIn: PRESIGNED_URL_EXPIRY_SECONDS });
191
193
  }
194
+ presignedGetTtlSeconds() {
195
+ return PRESIGNED_URL_EXPIRY_SECONDS;
196
+ }
192
197
  async getUploadUrl(key) {
193
198
  const objectKey = this.objectKey(key);
194
199
  return getSignedUrl(this.presignClient, new PutObjectCommand({
@@ -242,6 +247,17 @@ var S3CacheStorage = class {
242
247
  async getMetadata(key) {
243
248
  return this.readMeta(key);
244
249
  }
250
+ async getObjectSize(key) {
251
+ try {
252
+ return (await this.client.send(new HeadObjectCommand({
253
+ Bucket: this.bucket,
254
+ Key: this.objectKey(key)
255
+ }))).ContentLength ?? null;
256
+ } catch (err) {
257
+ if (this.isNotFoundError(err)) return null;
258
+ throw err;
259
+ }
260
+ }
245
261
  async copy(srcKey, destKey) {
246
262
  const srcObj = this.objectKey(srcKey);
247
263
  const destObj = this.objectKey(destKey);
@@ -429,6 +445,9 @@ var FilesystemCacheStorage = class {
429
445
  }
430
446
  return this.signedUrl("GET", key);
431
447
  }
448
+ presignedGetTtlSeconds() {
449
+ return Math.floor(DEFAULT_SIGN_URL_TTL_MS / 1e3);
450
+ }
432
451
  async getUploadUrl(key) {
433
452
  return this.signedUrl("PUT", key);
434
453
  }
@@ -442,21 +461,48 @@ var FilesystemCacheStorage = class {
442
461
  lastAccessedAt: now
443
462
  });
444
463
  }
464
+ /**
465
+ * List cache keys under a prefix.
466
+ *
467
+ * The prefix is a **key prefix**, not necessarily a directory — `UserCache`'s
468
+ * `restoreKeys` fallback lists on `${prefix}${seg(restoreKey)}`, a partial
469
+ * object name. Two shapes, and both must work the way the S3 backend does:
470
+ *
471
+ * - trailing `/` — a directory. Walk it directly; this is the hot path.
472
+ * - no trailing `/` — a partial name. Walk the parent directory and filter by
473
+ * the prefix, since the prefix itself may name a file (or nothing at all).
474
+ *
475
+ * Reading the prefix as a directory in the second case is what made the
476
+ * `restoreKeys` fallback silently never hit here (`ENOENT` → `[]`) or throw
477
+ * (`ENOTDIR`, when a real object carried that exact name).
478
+ */
445
479
  async list(subPrefix) {
446
- const root = this.resolvePath(subPrefix.replace(/\/$/, ""));
480
+ const isDirPrefix = subPrefix.endsWith("/");
481
+ const trimmed = subPrefix.replace(/\/$/, "");
482
+ const walkKey = isDirPrefix ? trimmed : trimmed.slice(0, trimmed.lastIndexOf("/") + 1);
483
+ const walkRoot = walkKey === "" ? this.basePath : this.resolvePath(walkKey.replace(/\/$/, ""));
447
484
  let entries;
448
485
  try {
449
- entries = await this.walkDataFiles(root, subPrefix.replace(/\/$/, ""));
486
+ entries = await this.walkDataFiles(walkRoot, walkKey.replace(/\/$/, ""));
450
487
  } catch (err) {
451
488
  if (isNotFoundFsError(err)) return [];
452
489
  throw err;
453
490
  }
491
+ if (!isDirPrefix) entries = entries.filter((e) => e.key.startsWith(trimmed));
454
492
  entries.sort((a, b) => b.mtime - a.mtime);
455
493
  return entries.map((e) => e.key);
456
494
  }
457
495
  async getMetadata(key) {
458
496
  return this.readMeta(key);
459
497
  }
498
+ async getObjectSize(key) {
499
+ try {
500
+ return (await promises.stat(this.resolvePath(key))).size;
501
+ } catch (err) {
502
+ if (isNotFoundFsError(err)) return null;
503
+ throw err;
504
+ }
505
+ }
460
506
  async copy(srcKey, destKey) {
461
507
  const srcPath = this.resolvePath(srcKey);
462
508
  const destPath = this.resolvePath(destKey);
@@ -504,7 +550,9 @@ var FilesystemCacheStorage = class {
504
550
  }
505
551
  };
506
552
  function isNotFoundFsError(err) {
507
- return typeof err === "object" && err !== null && err.code === "ENOENT";
553
+ if (typeof err !== "object" || err === null) return false;
554
+ const code = err.code;
555
+ return code === "ENOENT" || code === "ENOTDIR";
508
556
  }
509
557
  /**
510
558
  * Write a file atomically: write to a temp sibling, fsync, rename. Avoids
@@ -629,6 +677,9 @@ var SourceCache = class {
629
677
  const logger$2 = createLogger({ prefix: "dep-cache" });
630
678
  /** Default max tarball size: 500MB */
631
679
  const DEFAULT_MAX_TARBALL_BYTES = 524288e3;
680
+ /** Default dependency-cache entry TTL: 30 days. */
681
+ const DEFAULT_CACHE_TTL_DAYS = 30;
682
+ const MS_PER_DAY = 864e5;
632
683
  /** Build cache key for dependency tarball: deps/{platform}-{arch}/{lockfileHash}.tar.gz */
633
684
  function depKey(lockfileHash, platform, arch) {
634
685
  return `deps/${platform}-${arch}/${lockfileHash}.tar.gz`;
@@ -636,14 +687,29 @@ function depKey(lockfileHash, platform, arch) {
636
687
  var DepCache = class {
637
688
  storage;
638
689
  maxTarballBytes;
690
+ cacheTtlDaysFallback;
691
+ clusterSettings;
639
692
  constructor(options) {
640
693
  this.storage = options.storage;
641
694
  this.maxTarballBytes = options.maxTarballBytes ?? DEFAULT_MAX_TARBALL_BYTES;
695
+ this.cacheTtlDaysFallback = options.cacheTtlDaysFallback ?? DEFAULT_CACHE_TTL_DAYS;
696
+ this.clusterSettings = options.clusterSettings;
697
+ }
698
+ /**
699
+ * Resolve the live dependency-cache entry TTL as a per-operation override
700
+ * (ms) from `cluster_settings.cache_ttl_days`, falling back to the cluster
701
+ * default. `undefined` when no reader is wired, so the storage backend uses
702
+ * its own configured TTL. Passed to read/expiry operations so an operator's
703
+ * `cache_ttl_days` change takes effect on the next lookup.
704
+ */
705
+ async resolveTtlMsOverride() {
706
+ if (!this.clusterSettings) return void 0;
707
+ return await this.clusterSettings.getNumber("cache_ttl_days", this.cacheTtlDaysFallback) * MS_PER_DAY;
642
708
  }
643
709
  /** Check if a dep tarball exists in cache. */
644
710
  async has(lockfileHash, platform, arch) {
645
711
  const key = depKey(lockfileHash, platform, arch);
646
- const exists = await this.storage.has(key);
712
+ const exists = await this.storage.has(key, await this.resolveTtlMsOverride());
647
713
  logger$2.debug(`has(${lockfileHash}): ${exists}`, {
648
714
  platform,
649
715
  arch
@@ -656,7 +722,7 @@ var DepCache = class {
656
722
  */
657
723
  async getUrl(lockfileHash, platform, arch) {
658
724
  const key = depKey(lockfileHash, platform, arch);
659
- const url = await this.storage.getUrl(key);
725
+ const url = await this.storage.getUrl(key, await this.resolveTtlMsOverride());
660
726
  if (url) {
661
727
  await this.storage.touch(key);
662
728
  logger$2.debug(`getUrl(${lockfileHash}): hit`, {
@@ -676,7 +742,7 @@ var DepCache = class {
676
742
  */
677
743
  async getUrlAndHash(lockfileHash, platform, arch) {
678
744
  const key = depKey(lockfileHash, platform, arch);
679
- const url = await this.storage.getUrl(key);
745
+ const url = await this.storage.getUrl(key, await this.resolveTtlMsOverride());
680
746
  if (!url) {
681
747
  logger$2.debug(`getUrlAndHash(${lockfileHash}): miss`, {
682
748
  platform,
@@ -709,7 +775,8 @@ var DepCache = class {
709
775
  * Throws if tarball exceeds max size (per user decision).
710
776
  */
711
777
  async store(lockfileHash, platform, arch, tarballData) {
712
- if (tarballData.length > this.maxTarballBytes) throw new Error(`Dep tarball exceeds max size: ${tarballData.length} bytes > ${this.maxTarballBytes} bytes limit`);
778
+ const maxTarballBytes = this.clusterSettings ? await this.clusterSettings.getNumber("cache_max_tarball_bytes", this.maxTarballBytes) : this.maxTarballBytes;
779
+ if (tarballData.length > maxTarballBytes) throw new Error(`Dep tarball exceeds max size: ${tarballData.length} bytes > ${maxTarballBytes} bytes limit`);
713
780
  const key = depKey(lockfileHash, platform, arch);
714
781
  await this.storage.put(key, tarballData);
715
782
  logger$2.info(`store: ${tarballData.length} bytes`, {
@@ -736,13 +803,26 @@ var DepCache = class {
736
803
  return removed;
737
804
  }
738
805
  };
806
+ /**
807
+ * A collision-resistant discriminator for a user-supplied object-key segment.
808
+ *
809
+ * Hashes the **exact** name, before any sanitization. Sanitizing is many-to-one
810
+ * — `seg()` maps both `/` and `_` to `_`, so `a/b` and `a_b` collapse to one
811
+ * segment — and a case-insensitive backend folds `build` onto `Build`. Hashing
812
+ * the pre-sanitization name is what keeps each of those pairs two objects.
813
+ */
814
+ function keyDiscriminator(exact) {
815
+ return createHash("sha256").update(exact, "utf8").digest("hex").slice(0, 32);
816
+ }
739
817
  //#endregion
740
818
  //#region src/cache/user-cache.ts
741
819
  /**
742
820
  * User-facing cache layer wrapping CacheStorage.
743
821
  *
744
- * Namespacing: `cache/<orgId>/<repoId>/<refScope>/<key>.tar.gz`, where
745
- * `refScope` is `shared` for trusted refs (org-shared, default-branch cache)
822
+ * Namespacing: `cache/<orgId>/<repoId>/<refScope>/<key>-<discriminator>.tar.gz`.
823
+ * The discriminator is a hash of the exact cache key, so two keys differing
824
+ * only by case stay two objects even on a case-insensitive store. `refScope`
825
+ * is `shared` for trusted refs (org-shared, default-branch cache)
746
826
  * or `iso/<runId>` for untrusted refs (per-run isolated scope). Restores from
747
827
  * an untrusted ref read the shared scope as a fallback but writes can NEVER
748
828
  * land in the shared scope — the GitHub Actions cache-isolation model. Keyed
@@ -775,6 +855,42 @@ const DEFAULT_USER_CACHE_QUOTA_BYTES = 5 * 1024 * 1024 * 1024;
775
855
  const DEFAULT_USER_CACHE_TTL_MS = 10080 * 60 * 1e3;
776
856
  /** Tarball suffix for committed cache entries. */
777
857
  const TAR_SUFFIX = ".tar.gz";
858
+ /** Matches a stem ending in the fixed-width discriminator this module appends. */
859
+ const KEY_DISCRIMINATOR_TAIL = new RegExp(`^(.*)-([0-9a-f]{32})$`);
860
+ /**
861
+ * Recover the reported cache key from a stored object's stem.
862
+ *
863
+ * The value on this path is already the sanitized, lossy form, so this only
864
+ * needs to undo the discriminator the key format appends. A stem that itself
865
+ * ends in a discriminator-shaped tail loses it — that affects only the string
866
+ * reported back as `matchedKey`, never which object is served.
867
+ */
868
+ function stripKeyDiscriminator(stem) {
869
+ return KEY_DISCRIMINATOR_TAIL.exec(stem)?.[1] ?? stem;
870
+ }
871
+ /** Device names NTFS reserves; a file cannot actually be created under one. */
872
+ const WINDOWS_RESERVED_NAME = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(\.|$)/;
873
+ /** Keys that cannot have been part of a case collision in the old key format. */
874
+ const UNAMBIGUOUS_LEGACY_KEY = /^[a-z0-9_][a-z0-9._-]*$/;
875
+ /**
876
+ * Whether a cache key is safe to look up in the pre-discriminator key format.
877
+ *
878
+ * Every clause rules out a way the old format could have folded two distinct
879
+ * keys onto one object:
880
+ *
881
+ * - the character class is a subset of what `seg()` preserves, so a match
882
+ * implies the key survives sanitization unchanged (no many-to-one rewrite)
883
+ * and is pure ASCII, which rules out HFS+ Unicode normalisation;
884
+ * - requiring lowercase rules out case folding, the collision this change fixes;
885
+ * - a trailing `.` is stripped by NTFS, and a reserved device name cannot be
886
+ * created there at all.
887
+ *
888
+ * A key failing any clause simply gets no legacy fallback — it is re-fetched
889
+ * and re-saved under the discriminated key, which is always correct.
890
+ */
891
+ function unambiguousLegacyKey(key) {
892
+ return UNAMBIGUOUS_LEGACY_KEY.test(key) && !key.endsWith(".") && !WINDOWS_RESERVED_NAME.test(key);
893
+ }
778
894
  var UserCache = class {
779
895
  storage;
780
896
  /** Cluster-wide default quota (the `KICI_USER_CACHE_QUOTA_BYTES` value). */
@@ -854,9 +970,61 @@ var UserCache = class {
854
970
  }
855
971
  return [`${base}/shared/`];
856
972
  }
973
+ /**
974
+ * Object key for a committed cache entry.
975
+ *
976
+ * The trailing discriminator is a hash of the EXACT cache key, and it is what
977
+ * keeps `build` and `Build` apart on a case-insensitive namespace — the
978
+ * filesystem backend on a macOS or Windows host, where the two would
979
+ * otherwise resolve to one object and a restore could return the other key's
980
+ * tarball.
981
+ *
982
+ * It is deliberately a SUFFIX: `restoreByPrefix` matches `restoreKeys` by
983
+ * string prefix, so appending leaves those semantics untouched. The only
984
+ * parse that has to know about it is the `matchedKey` slice.
985
+ */
857
986
  finalKey(prefix, key) {
987
+ return `${prefix}${this.seg(key)}-${keyDiscriminator(key)}${TAR_SUFFIX}`;
988
+ }
989
+ /**
990
+ * The pre-discriminator object key.
991
+ *
992
+ * @deprecated Read-only compatibility path for entries written before the key
993
+ * carried a discriminator. Nothing writes this format; it is removed at the
994
+ * next major, by which point every such entry has aged out of the cache TTL.
995
+ */
996
+ legacyFinalKey(prefix, key) {
858
997
  return `${prefix}${this.seg(key)}${TAR_SUFFIX}`;
859
998
  }
999
+ /**
1000
+ * Restore an entry still stored in the pre-discriminator key format.
1001
+ *
1002
+ * Two gates, and BOTH are load-bearing — dropping either re-creates the
1003
+ * wrong-cache-hit this change fixes:
1004
+ *
1005
+ * 1. `unambiguousLegacyKey` statically rules out any key that could have
1006
+ * collided with a case variant in the old format.
1007
+ * 2. The listed name must match byte-exactly. A case-insensitive backend
1008
+ * resolves `getUrl('build')` to an object created as `Build`, but its
1009
+ * *listing* reports the real created name — so comparing against the
1010
+ * listing is what detects that the object is not really ours.
1011
+ *
1012
+ * @deprecated Removed at the next major; see `legacyFinalKey`.
1013
+ */
1014
+ async restoreLegacyExact(ref, prefix, ttlMs) {
1015
+ if (!unambiguousLegacyKey(ref.key)) return null;
1016
+ const legacyKey = this.legacyFinalKey(prefix, ref.key);
1017
+ if (!(await this.storage.list(prefix)).includes(legacyKey)) return null;
1018
+ const url = await this.storage.getUrl(legacyKey, ttlMs);
1019
+ if (!url) return null;
1020
+ await this.storage.touch(legacyKey);
1021
+ return {
1022
+ hit: true,
1023
+ matchedKey: ref.key,
1024
+ downloadUrl: url,
1025
+ tarHash: await this.readHash(legacyKey)
1026
+ };
1027
+ }
860
1028
  /** Restore: try the exact key across read prefixes, then restoreKeys prefix scan (newest wins). */
861
1029
  async restore(ref) {
862
1030
  const { ttlMs } = await this.resolveLimits(ref.org);
@@ -879,6 +1047,8 @@ var UserCache = class {
879
1047
  tarHash: await this.readHash(key)
880
1048
  };
881
1049
  }
1050
+ const legacy = await this.restoreLegacyExact(ref, prefix, ttlMs);
1051
+ if (legacy) return legacy;
882
1052
  }
883
1053
  return null;
884
1054
  }
@@ -893,7 +1063,7 @@ var UserCache = class {
893
1063
  await this.storage.touch(winner);
894
1064
  return {
895
1065
  hit: true,
896
- matchedKey: winner.slice(prefix.length, -7),
1066
+ matchedKey: stripKeyDiscriminator(winner.slice(prefix.length, -7)),
897
1067
  downloadUrl: url,
898
1068
  tarHash: await this.readHash(winner)
899
1069
  };
@@ -1328,7 +1498,9 @@ var JoinTokenManager = class {
1328
1498
  role
1329
1499
  };
1330
1500
  const secret = randomBytes(32);
1331
- const token = `${TOKEN_PREFIX}.${Buffer.from(JSON.stringify(routing)).toString("base64url")}.${secret.toString("hex")}`;
1501
+ const routingB64 = Buffer.from(JSON.stringify(routing)).toString("base64url");
1502
+ const secretHex = secret.toString("hex");
1503
+ const token = `${TOKEN_PREFIX}.${routingB64}.${secretHex}`;
1332
1504
  const { validationHash } = deriveKeys(secret);
1333
1505
  await this.deps.db.insertInto("join_tokens").values({
1334
1506
  id: randomUUID(),
@@ -1452,6 +1624,404 @@ function deriveKeys(secret) {
1452
1624
  validationHash
1453
1625
  };
1454
1626
  }
1627
+ /**
1628
+ * Reason a run was held in the security queue. Persisted verbatim in
1629
+ * `held_runs.reason` and switched on by `buildSecurityHoldSummary`.
1630
+ */
1631
+ const SecurityHoldReason = z.enum([
1632
+ "workflow_modification",
1633
+ "unknown_contributor",
1634
+ "fork_pr",
1635
+ "context_trust"
1636
+ ]);
1637
+ /** Manages held run lifecycle (pending -> approved/rejected/expired). */
1638
+ var HeldRunStore = class {
1639
+ db;
1640
+ constructor(db) {
1641
+ this.db = db;
1642
+ }
1643
+ /** Create a new held run with pending status. */
1644
+ async create(orgId, data) {
1645
+ return this.db.insertInto("held_runs").values({
1646
+ org_id: orgId,
1647
+ run_id: data.runId,
1648
+ job_id: data.jobId,
1649
+ context_id: data.contextId,
1650
+ hold_type: data.holdType,
1651
+ queue_type: data.queueType ?? "context",
1652
+ reason: data.reason,
1653
+ expires_at: data.expiresAt
1654
+ }).returningAll().executeTakeFirstOrThrow();
1655
+ }
1656
+ /**
1657
+ * Create a generalized approval hold (workflow/job/step scope, explicit or
1658
+ * context trigger) carrying a normalized `ApprovalRequirement`. Returns
1659
+ * the created row.
1660
+ */
1661
+ async createHold(orgId, data) {
1662
+ return this.db.insertInto("held_runs").values({
1663
+ org_id: orgId,
1664
+ run_id: data.runId,
1665
+ job_id: data.jobId,
1666
+ context_id: data.contextId ?? null,
1667
+ hold_type: data.holdType ?? HoldType.enum.reviewer,
1668
+ queue_type: data.queueType ?? "context",
1669
+ reason: data.requirement.reason,
1670
+ expires_at: new Date(data.requirement.expiresAt),
1671
+ hold_scope: data.scope,
1672
+ step_index: data.stepIndex ?? null,
1673
+ trigger_source: data.triggerSource,
1674
+ approval_requirement: data.requirement,
1675
+ ...data.payload !== void 0 && { payload: JSON.stringify(data.payload) }
1676
+ }).returningAll().executeTakeFirstOrThrow();
1677
+ }
1678
+ /** INSERT one decision row using the given executor (root or transaction). */
1679
+ insertDecisionRow(exec, heldRunId, data) {
1680
+ const clausesSatisfied = data.clausesSatisfied != null ? JSON.stringify(data.clausesSatisfied) : null;
1681
+ return exec.insertInto("held_run_approvals").values({
1682
+ held_run_id: heldRunId,
1683
+ approver_user_id: data.approverSub,
1684
+ decision: data.decision,
1685
+ clauses_satisfied: clausesSatisfied
1686
+ }).returningAll().executeTakeFirstOrThrow();
1687
+ }
1688
+ /** Flip a pending hold to 'approved' using the given executor. Undefined if not pending. */
1689
+ flipToApproved(exec, orgId, heldRunId) {
1690
+ return exec.updateTable("held_runs").set({
1691
+ status: "approved",
1692
+ resolved_at: sql`now()`
1693
+ }).where("id", "=", heldRunId).where("org_id", "=", orgId).where("status", "=", "pending").returningAll().executeTakeFirst();
1694
+ }
1695
+ /** Flip a pending hold to 'rejected' using the given executor. Undefined if not pending. */
1696
+ flipToRejected(exec, orgId, heldRunId, reason) {
1697
+ const set = {
1698
+ status: "rejected",
1699
+ resolved_at: sql`now()`
1700
+ };
1701
+ if (reason !== void 0) set.reason = reason;
1702
+ return exec.updateTable("held_runs").set(set).where("id", "=", heldRunId).where("org_id", "=", orgId).where("status", "=", "pending").returningAll().executeTakeFirst();
1703
+ }
1704
+ /** Map a released held_runs row to the resume ReleaseSignal. */
1705
+ toReleaseSignal(row) {
1706
+ return {
1707
+ holdId: row.id,
1708
+ runId: row.run_id,
1709
+ jobId: row.job_id,
1710
+ scope: row.hold_scope ?? HoldScope.enum.job,
1711
+ stepIndex: row.step_index,
1712
+ triggerSource: row.trigger_source ?? TriggerSource.enum.context
1713
+ };
1714
+ }
1715
+ /** Record one approve/reject decision against a hold. */
1716
+ async recordDecision(heldRunId, data) {
1717
+ return this.insertDecisionRow(this.db, heldRunId, data);
1718
+ }
1719
+ /**
1720
+ * Atomically record an approve decision and release the (now-satisfied) hold.
1721
+ * The INSERT into `held_run_approvals` and the `held_runs` → approved UPDATE
1722
+ * run in a single transaction, so a crash between them cannot strand the hold
1723
+ * `pending` with a recorded approve. Throws if the hold is not found or no
1724
+ * longer pending (the whole transaction rolls back).
1725
+ */
1726
+ async recordAndRelease(orgId, heldRunId, data) {
1727
+ return this.db.transaction().execute(async (tx) => {
1728
+ await this.insertDecisionRow(tx, heldRunId, data);
1729
+ const row = await this.flipToApproved(tx, orgId, heldRunId);
1730
+ if (!row) throw new Error(`Held run '${heldRunId}' not found or not pending`);
1731
+ return this.toReleaseSignal(row);
1732
+ });
1733
+ }
1734
+ /**
1735
+ * Atomically record a reject decision and reject the hold. The INSERT and the
1736
+ * `held_runs` → rejected UPDATE run in a single transaction, so a crash
1737
+ * between them cannot strand the hold `pending` with a recorded reject (which
1738
+ * would poison `evaluate()` forever). Throws if the hold is not found or no
1739
+ * longer pending (the whole transaction rolls back).
1740
+ */
1741
+ async recordAndReject(orgId, heldRunId, data, reason) {
1742
+ return this.db.transaction().execute(async (tx) => {
1743
+ await this.insertDecisionRow(tx, heldRunId, data);
1744
+ const row = await this.flipToRejected(tx, orgId, heldRunId, reason);
1745
+ if (!row) throw new Error(`Held run '${heldRunId}' not found or not pending`);
1746
+ return row;
1747
+ });
1748
+ }
1749
+ /** List the recorded decisions for a hold, oldest first. */
1750
+ async listDecisions(heldRunId) {
1751
+ return this.db.selectFrom("held_run_approvals").selectAll().where("held_run_id", "=", heldRunId).orderBy("created_at", "asc").execute();
1752
+ }
1753
+ /** Get a single held run by id (org-scoped). Returns null if absent. */
1754
+ async getById(orgId, heldRunId) {
1755
+ return await this.db.selectFrom("held_runs").selectAll().where("id", "=", heldRunId).where("org_id", "=", orgId).executeTakeFirst() ?? null;
1756
+ }
1757
+ /**
1758
+ * Release a hold whose approval requirement is satisfied. Flips the row to
1759
+ * 'approved' and returns a `ReleaseSignal` describing how the caller must
1760
+ * resume the element (re-dispatch for job/workflow, agent notification for
1761
+ * step). Throws if the hold is not found or not pending. Approver attribution
1762
+ * lives in `held_run_approvals`, not on the row.
1763
+ */
1764
+ async release(orgId, heldRunId) {
1765
+ const row = await this.flipToApproved(this.db, orgId, heldRunId);
1766
+ if (!row) throw new Error(`Held run '${heldRunId}' not found or not pending`);
1767
+ return this.toReleaseSignal(row);
1768
+ }
1769
+ /** Approve a pending held run. Throws if not found or not pending. */
1770
+ async approve(orgId, heldRunId, approvedBy) {
1771
+ const row = await this.db.updateTable("held_runs").set({
1772
+ status: "approved",
1773
+ approved_by: approvedBy,
1774
+ resolved_at: sql`now()`
1775
+ }).where("id", "=", heldRunId).where("org_id", "=", orgId).where("status", "=", "pending").returningAll().executeTakeFirst();
1776
+ if (!row) throw new Error(`Held run '${heldRunId}' not found or not pending`);
1777
+ return row;
1778
+ }
1779
+ /** Reject a pending held run. Throws if not found or not pending. */
1780
+ async reject(orgId, heldRunId, reason) {
1781
+ const row = await this.flipToRejected(this.db, orgId, heldRunId, reason);
1782
+ if (!row) throw new Error(`Held run '${heldRunId}' not found or not pending`);
1783
+ return row;
1784
+ }
1785
+ /** List pending held runs for an org, ordered by creation time. */
1786
+ async listPending(orgId) {
1787
+ return this.db.selectFrom("held_runs").selectAll().where("org_id", "=", orgId).where("status", "=", "pending").orderBy("created_at", "asc").execute();
1788
+ }
1789
+ /** List held runs for an org with optional filters. */
1790
+ async listAll(orgId, options) {
1791
+ let query = this.db.selectFrom("held_runs").selectAll().where("org_id", "=", orgId).orderBy("created_at", "desc");
1792
+ if (options?.status) query = query.where("status", "=", options.status);
1793
+ if (options?.limit) query = query.limit(options.limit);
1794
+ return query.execute();
1795
+ }
1796
+ /** List held runs for an org filtered by queue type with optional filters. */
1797
+ async listByQueueType(orgId, queueType, options) {
1798
+ let query = this.db.selectFrom("held_runs").selectAll().where("org_id", "=", orgId).where("queue_type", "=", queueType).orderBy("created_at", "desc");
1799
+ if (options?.status) query = query.where("status", "=", options.status);
1800
+ if (options?.limit) query = query.limit(options.limit);
1801
+ return query.execute();
1802
+ }
1803
+ /**
1804
+ * List pending `security` holds whose run belongs to a specific PR (and repo).
1805
+ *
1806
+ * Joins `execution_runs` on `run_id` and filters by `repo_identifier` +
1807
+ * `pr_number` so `/kici approve|reject` only affects the commented PR's holds
1808
+ * rather than every pending security hold in the org. A run with a NULL
1809
+ * `pr_number` (non-PR run, or a legacy run predating the column) never matches
1810
+ * the equality predicate, so such holds are never released here — fail-closed.
1811
+ */
1812
+ async listPendingSecurityHoldsForPr(orgId, repoIdentifier, prNumber) {
1813
+ 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.queue_type", "=", "security").where("held_runs.status", "=", "pending").where("execution_runs.repo_identifier", "=", repoIdentifier).where("execution_runs.pr_number", "=", prNumber).orderBy("held_runs.created_at", "desc").execute();
1814
+ }
1815
+ /**
1816
+ * Approve a pending held run, enforcing queue_type boundary.
1817
+ * Prevents context approvals from approving security holds and vice versa.
1818
+ * Throws if not found, not pending, or queue_type mismatch.
1819
+ */
1820
+ async approveByQueueType(orgId, heldRunId, approvedBy, queueType) {
1821
+ const row = await this.db.updateTable("held_runs").set({
1822
+ status: "approved",
1823
+ approved_by: approvedBy,
1824
+ resolved_at: sql`now()`
1825
+ }).where("id", "=", heldRunId).where("org_id", "=", orgId).where("status", "=", "pending").where("queue_type", "=", queueType).returningAll().executeTakeFirst();
1826
+ if (!row) throw new Error(`Held run '${heldRunId}' not found, not pending, or queue type mismatch (expected '${queueType}')`);
1827
+ return row;
1828
+ }
1829
+ /** Get a held run by run ID and job ID. Returns null if not found. */
1830
+ async getByRunAndJob(orgId, runId, jobId) {
1831
+ return await this.db.selectFrom("held_runs").selectAll().where("org_id", "=", orgId).where("run_id", "=", runId).where("job_id", "=", jobId).executeTakeFirst() ?? null;
1832
+ }
1833
+ /**
1834
+ * List pending holds past their `expires_at`. Called by the stale detector
1835
+ * BEFORE `expireOverdue()` so it can route each overdue hold by scope (step
1836
+ * holds notify the waiting agent; job/workflow holds fail the run).
1837
+ */
1838
+ async listOverdue() {
1839
+ return this.db.selectFrom("held_runs").selectAll().where("status", "=", "pending").where("expires_at", "<", sql`now()`).execute();
1840
+ }
1841
+ /**
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.
1849
+ *
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.
1854
+ */
1855
+ async releaseDueWaitHolds() {
1856
+ return (await this.db.updateTable("held_runs").set({
1857
+ status: "released",
1858
+ 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) => ({
1860
+ holdId: row.id,
1861
+ runId: row.run_id,
1862
+ jobId: row.job_id,
1863
+ scope: row.hold_scope ?? HoldScope.enum.workflow,
1864
+ stepIndex: row.step_index,
1865
+ triggerSource: row.trigger_source ?? TriggerSource.enum.context
1866
+ }));
1867
+ }
1868
+ /**
1869
+ * Expire overdue pending runs. Called by the stale detector.
1870
+ * Sets status to 'expired' and resolved_at to now() for all
1871
+ * pending runs past their expires_at.
1872
+ * Returns the number of expired runs.
1873
+ */
1874
+ async expireOverdue() {
1875
+ const result = await this.db.updateTable("held_runs").set({
1876
+ status: "expired",
1877
+ resolved_at: sql`now()`
1878
+ }).where("status", "=", "pending").where("expires_at", "<", sql`now()`).execute();
1879
+ const updateResult = Array.isArray(result) ? result[0] : result;
1880
+ return Number(updateResult?.numUpdatedRows ?? 0n);
1881
+ }
1882
+ };
1883
+ /**
1884
+ * Build a HeldRunStore backed by its own connection pool to the given
1885
+ * orchestrator database URL. Mirrors `createPeerCredentialStoreFromUrl` /
1886
+ * `createJoinTokenManagerFromUrl`; consumed by E2E tests that exercise the
1887
+ * PR-scoped hold selection against the real deployed orchestrator DB.
1888
+ */
1889
+ function createHeldRunStoreFromUrl(databaseUrl, opts) {
1890
+ const db = new Kysely({ dialect: new PostgresDialect({ pool: new pg.Pool({
1891
+ connectionString: databaseUrl,
1892
+ max: opts?.maxConnections ?? 3
1893
+ }) }) });
1894
+ return {
1895
+ store: new HeldRunStore(db),
1896
+ dispose: async () => {
1897
+ await db.destroy();
1898
+ }
1899
+ };
1900
+ }
1901
+ //#endregion
1902
+ //#region src/security/trust-policy-store.ts
1903
+ /**
1904
+ * Cache of the Platform-owned org trust policy.
1905
+ *
1906
+ * The Platform is the authority for a Platform-attached org and pushes the
1907
+ * policy on `trust_policy.update`; this store persists it so the policy survives
1908
+ * a restart instead of being unknown until the next push. In independent mode
1909
+ * there is no Platform, so the operator writes the same row via
1910
+ * `kici-admin trust-policy set` and `source` records which wrote it.
1911
+ */
1912
+ /** Who wrote the cached policy row. */
1913
+ const TrustPolicySource = z.enum(["platform", "local"]);
1914
+ /**
1915
+ * The strictest documented defaults. Used to fill gaps on a local merge; the
1916
+ * fail-closed policy an unknown org resolves to lives in `trust-policy-gate.ts`.
1917
+ */
1918
+ const DEFAULT_TRUST_POLICY = {
1919
+ forkPolicy: "hold",
1920
+ unknownContributorPolicy: "hold",
1921
+ workflowChangePolicy: "hold",
1922
+ approvalExpiryHours: DEFAULT_APPROVAL_EXPIRY_HOURS
1923
+ };
1924
+ /**
1925
+ * Namespace for the per-org advisory lock taken by `upsertLocal`, so the key
1926
+ * cannot collide with another feature's advisory lock in the same database.
1927
+ */
1928
+ const UPSERT_LOCK_NAMESPACE = "org-trust-policy-upsert";
1929
+ var TrustPolicyStore = class {
1930
+ db;
1931
+ constructor(db) {
1932
+ this.db = db;
1933
+ }
1934
+ /** `executor` lets a caller read inside its own transaction. */
1935
+ async get(orgId, executor = this.db) {
1936
+ const row = await executor.selectFrom("org_trust_policy").selectAll().where("customer_id", "=", orgId).executeTakeFirst();
1937
+ if (!row) return null;
1938
+ return {
1939
+ forkPolicy: row.fork_policy,
1940
+ unknownContributorPolicy: row.unknown_contributor_policy,
1941
+ workflowChangePolicy: row.workflow_change_policy,
1942
+ approvalExpiryHours: Number(row.approval_expiry_hours),
1943
+ source: row.source,
1944
+ updatedAt: row.updated_at
1945
+ };
1946
+ }
1947
+ /**
1948
+ * Needs no lock, unlike `upsertLocal`: a push carries every field, so there
1949
+ * is no read-merge to lose and concurrent pushes correctly settle on the
1950
+ * last one. The two writers also never overlap — `upsertLocal` is reachable
1951
+ * only in independent mode, where no Platform pushes.
1952
+ */
1953
+ async upsertFromPlatform(orgId, policy) {
1954
+ await this.write(orgId, policy, TrustPolicySource.enum.platform);
1955
+ }
1956
+ /**
1957
+ * Write an operator-supplied policy. Only reachable in independent mode — the
1958
+ * admin route refuses on a Platform-attached orchestrator, because the next
1959
+ * push would clobber the write.
1960
+ *
1961
+ * The read-merge-write is serialised per org by a transaction-scoped
1962
+ * advisory lock. The transaction alone would NOT give that: under READ
1963
+ * COMMITTED two concurrent PATCHes both read the pre-existing row, and the
1964
+ * second `ON CONFLICT DO UPDATE` then overwrites every column from its own
1965
+ * stale merge — silently dropping the first operator's change (a tightened
1966
+ * `unknownContributorPolicy` reverting to whatever the second caller last
1967
+ * saw). The lock releases on commit or rollback, so there is no unlock to
1968
+ * leak.
1969
+ *
1970
+ * `onWrite` receives the same transaction and the merged result, so an audit
1971
+ * row written there commits or rolls back with the policy itself — a
1972
+ * loosened `forkPolicy` can never land unattributed.
1973
+ *
1974
+ * Returns the merged policy so the caller does not need a second read (which
1975
+ * would be outside the transaction and could observe a later write).
1976
+ */
1977
+ async upsertLocal(orgId, patch, onWrite) {
1978
+ return await this.db.transaction().execute(async (trx) => {
1979
+ await sql`SELECT pg_advisory_xact_lock(hashtext(${`${UPSERT_LOCK_NAMESPACE}|${orgId}`}))`.execute(trx);
1980
+ const existing = await this.get(orgId, trx);
1981
+ const merged = {
1982
+ forkPolicy: patch.forkPolicy ?? existing?.forkPolicy ?? DEFAULT_TRUST_POLICY.forkPolicy,
1983
+ unknownContributorPolicy: patch.unknownContributorPolicy ?? existing?.unknownContributorPolicy ?? DEFAULT_TRUST_POLICY.unknownContributorPolicy,
1984
+ workflowChangePolicy: patch.workflowChangePolicy ?? existing?.workflowChangePolicy ?? DEFAULT_TRUST_POLICY.workflowChangePolicy,
1985
+ approvalExpiryHours: patch.approvalExpiryHours ?? existing?.approvalExpiryHours ?? DEFAULT_TRUST_POLICY.approvalExpiryHours
1986
+ };
1987
+ await this.write(orgId, merged, TrustPolicySource.enum.local, trx);
1988
+ await onWrite?.(trx, merged);
1989
+ return merged;
1990
+ });
1991
+ }
1992
+ async write(orgId, policy, source, executor = this.db) {
1993
+ const columns = {
1994
+ fork_policy: policy.forkPolicy,
1995
+ unknown_contributor_policy: policy.unknownContributorPolicy,
1996
+ workflow_change_policy: policy.workflowChangePolicy,
1997
+ approval_expiry_hours: policy.approvalExpiryHours,
1998
+ source,
1999
+ updated_at: sql`now()`
2000
+ };
2001
+ await executor.insertInto("org_trust_policy").values({
2002
+ customer_id: orgId,
2003
+ ...columns
2004
+ }).onConflict((oc) => oc.column("customer_id").doUpdateSet(columns)).execute();
2005
+ }
2006
+ };
2007
+ /**
2008
+ * Build a TrustPolicyStore backed by its own connection pool to the given
2009
+ * orchestrator database URL. Mirrors `createHeldRunStoreFromUrl`; consumed by
2010
+ * E2E tests that assert the pushed policy is persisted in the real deployed
2011
+ * orchestrator DB rather than held in process memory.
2012
+ */
2013
+ function createTrustPolicyStoreFromUrl(databaseUrl, opts) {
2014
+ const db = new Kysely({ dialect: new PostgresDialect({ pool: new pg.Pool({
2015
+ connectionString: databaseUrl,
2016
+ max: opts?.maxConnections ?? 3
2017
+ }) }) });
2018
+ return {
2019
+ store: new TrustPolicyStore(db),
2020
+ dispose: async () => {
2021
+ await db.destroy();
2022
+ }
2023
+ };
2024
+ }
1455
2025
  //#endregion
1456
2026
  //#region src/cli/api-client.ts
1457
2027
  /**
@@ -1558,8 +2128,17 @@ var AdminApiClient = class {
1558
2128
  const buf = Buffer.from(await res.arrayBuffer());
1559
2129
  fs.writeFileSync(outPath, buf);
1560
2130
  }
1561
- async listScopes(orgId) {
1562
- return this.request("GET", `/api/v1/admin/secrets/scopes?orgId=${encodeURIComponent(orgId)}`);
2131
+ /**
2132
+ * List secret scopes for an org.
2133
+ *
2134
+ * @param allBackends - Aggregate every registered backend and return scopes
2135
+ * in qualified `<backend>:<path>` form. Default (false) returns the bare,
2136
+ * pg-only listing; that default flips at v1.0.0.
2137
+ */
2138
+ async listScopes(orgId, allBackends = false) {
2139
+ const params = new URLSearchParams({ orgId });
2140
+ if (allBackends) params.set("allBackends", "true");
2141
+ return this.request("GET", `/api/v1/admin/secrets/scopes?${params}`);
1563
2142
  }
1564
2143
  async listKeys(orgId, scope) {
1565
2144
  const params = new URLSearchParams({
@@ -1574,20 +2153,35 @@ var AdminApiClient = class {
1574
2153
  async deleteSecret(orgId, scope, key) {
1575
2154
  return this.request("DELETE", `/api/v1/admin/secrets/${encodeURIComponent(orgId)}/${encodeURIComponent(scope)}/${encodeURIComponent(key)}`);
1576
2155
  }
1577
- async listVariables(orgId, environment) {
2156
+ /**
2157
+ * Create (or upsert) a deployment context. `allowLocalExecution` lets
2158
+ * CLI-initiated / local runs resolve secrets through this context.
2159
+ */
2160
+ async createContext(data) {
2161
+ return this.request("POST", `/api/v1/admin/contexts`, data);
2162
+ }
2163
+ /**
2164
+ * Bind a scope pattern to a context so its scoped secrets resolve at dispatch.
2165
+ * `hostPattern` defaults to `**` (every host) server-side.
2166
+ */
2167
+ async bindContext(data) {
2168
+ const { name, ...body } = data;
2169
+ return this.request("POST", `/api/v1/admin/contexts/${encodeURIComponent(name)}/bind`, body);
2170
+ }
2171
+ async listVariables(orgId, context) {
1578
2172
  const params = new URLSearchParams({ orgId });
1579
- return this.request("GET", `/api/v1/admin/environments/${encodeURIComponent(environment)}/variables?${params}`);
2173
+ return this.request("GET", `/api/v1/admin/contexts/${encodeURIComponent(context)}/variables?${params}`);
1580
2174
  }
1581
- async setVariable(orgId, environment, key, value, locked) {
2175
+ async setVariable(orgId, context, key, value, locked) {
1582
2176
  const params = new URLSearchParams({ orgId });
1583
- return this.request("PUT", `/api/v1/admin/environments/${encodeURIComponent(environment)}/variables/${encodeURIComponent(key)}?${params}`, {
2177
+ return this.request("PUT", `/api/v1/admin/contexts/${encodeURIComponent(context)}/variables/${encodeURIComponent(key)}?${params}`, {
1584
2178
  value,
1585
2179
  locked
1586
2180
  });
1587
2181
  }
1588
- async deleteVariable(orgId, environment, key) {
2182
+ async deleteVariable(orgId, context, key) {
1589
2183
  const params = new URLSearchParams({ orgId });
1590
- return this.request("DELETE", `/api/v1/admin/environments/${encodeURIComponent(environment)}/variables/${encodeURIComponent(key)}?${params}`);
2184
+ return this.request("DELETE", `/api/v1/admin/contexts/${encodeURIComponent(context)}/variables/${encodeURIComponent(key)}?${params}`);
1591
2185
  }
1592
2186
  async rotateKey() {
1593
2187
  return this.request("POST", "/api/v1/admin/rotate-key");
@@ -1702,6 +2296,14 @@ var AdminApiClient = class {
1702
2296
  async configReload(opts) {
1703
2297
  return this.request("POST", "/admin/config/reload", opts);
1704
2298
  }
2299
+ /** Flip the coordinator drain flag on ('drain') or off ('resume'). */
2300
+ async drain(action) {
2301
+ return this.request("POST", "/api/v1/admin/orchestrator/drain", { action });
2302
+ }
2303
+ /** Report the coordinator's current drain state without changing it. */
2304
+ async drainStatus() {
2305
+ return this.request("GET", "/api/v1/admin/orchestrator/drain");
2306
+ }
1705
2307
  async createApiKey(opts) {
1706
2308
  return this.request("POST", "/api/v1/api-keys", opts);
1707
2309
  }
@@ -2187,7 +2789,8 @@ async function resolveWebhookUrl(client) {
2187
2789
  */
2188
2790
  async function captureCode(opts, deps, manifestJson, state, redirectUrl, createUrl, loopback) {
2189
2791
  if (opts.noBrowser) {
2190
- const url = `${STATIC_CALLBACK_URL}#m=${Buffer.from(manifestJson, "utf-8").toString("base64url")}&state=${encodeURIComponent(state)}&createUrl=${encodeURIComponent(createUrl)}`;
2792
+ const m = Buffer.from(manifestJson, "utf-8").toString("base64url");
2793
+ const url = `${STATIC_CALLBACK_URL}#m=${m}&state=${encodeURIComponent(state)}&createUrl=${encodeURIComponent(createUrl)}`;
2191
2794
  console.log("Open this URL in a browser to create your App:");
2192
2795
  console.log(` ${url}`);
2193
2796
  console.log("After clicking \"Create\", copy the setup code shown on the page.");
@@ -2272,6 +2875,6 @@ async function runGithubManifestSetup(opts, client, deps = realManifestSetupDeps
2272
2875
  }
2273
2876
  }
2274
2877
  //#endregion
2275
- export { AdminApiClient, DEFAULT_USER_CACHE_QUOTA_BYTES, DEFAULT_USER_CACHE_TTL_MS, DepCache, JoinTokenManager, PeerAuthCoordinator, PeerCredentialStore, S3CacheStorage, SourceCache, UserCache, createCacheStorage, createJoinTokenManagerFromUrl, createPeerCredentialStoreFromUrl, runGithubManifestSetup };
2878
+ export { AdminApiClient, DEFAULT_TRUST_POLICY, DEFAULT_USER_CACHE_QUOTA_BYTES, DEFAULT_USER_CACHE_TTL_MS, DepCache, HeldRunStore, JoinTokenManager, PeerAuthCoordinator, PeerCredentialStore, S3CacheStorage, SecurityHoldReason, SourceCache, TrustPolicySource, TrustPolicyStore, UserCache, createCacheStorage, createHeldRunStoreFromUrl, createJoinTokenManagerFromUrl, createPeerCredentialStoreFromUrl, createTrustPolicyStoreFromUrl, runGithubManifestSetup };
2276
2879
 
2277
2880
  //# sourceMappingURL=index.js.map