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