@kici-dev/agent 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/checkout/clone-job-repos.d.ts +53 -0
  2. package/dist/checkout/credential-helper-bin.d.ts +28 -0
  3. package/dist/checkout/credential-helper-host.d.ts +48 -0
  4. package/dist/checkout/credential-helper.d.ts +44 -0
  5. package/dist/checkout/git-clone.d.ts +26 -0
  6. package/dist/checkout/grant-table.d.ts +30 -0
  7. package/dist/checkout/job-git-credentials.d.ts +49 -0
  8. package/dist/checkout/write-elevation.d.ts +40 -0
  9. package/dist/config.d.ts +38 -0
  10. package/dist/container-ts-loader-hook.js +2047 -1814
  11. package/dist/execution/between-jobs-controller.d.ts +50 -0
  12. package/dist/execution/between-jobs-reset.d.ts +25 -0
  13. package/dist/execution/cleanup-rerun.d.ts +21 -0
  14. package/dist/execution/dynamic-job-serializer.d.ts +6 -2
  15. package/dist/execution/image-build/build-engine.d.ts +75 -0
  16. package/dist/execution/image-build/build-step.d.ts +57 -0
  17. package/dist/execution/image-build/resolve-build-spec.d.ts +41 -0
  18. package/dist/execution/image-build/runtime-facts.d.ts +31 -0
  19. package/dist/execution/job-runner.d.ts +48 -0
  20. package/dist/execution/sandbox/bare-metal-sandbox.d.ts +25 -1
  21. package/dist/execution/sandbox/container-sandbox.d.ts +97 -2
  22. package/dist/execution/sandbox/fork-runner.d.ts +55 -1
  23. package/dist/execution/sandbox/image-preflight.d.ts +38 -0
  24. package/dist/execution/sandbox/ipc-protocol.d.ts +96 -2
  25. package/dist/execution/sandbox/kici-runtime.d.ts +48 -0
  26. package/dist/execution/sandbox/step-loop.d.ts +7 -0
  27. package/dist/execution/sandbox/types.d.ts +55 -1
  28. package/dist/execution/sandbox/workflow-runner.d.ts +23 -3
  29. package/dist/idle-shutdown.d.ts +24 -0
  30. package/dist/index.js +133 -62
  31. package/dist/metrics/prometheus.d.ts +26 -0
  32. package/dist/server.js +2050 -312
  33. package/dist/workflow-runner-bundle.js +41971 -41318
  34. package/dist/workflow-runner.js +332 -136
  35. package/dist/ws/orchestrator-client.d.ts +95 -3
  36. package/package.json +10 -10
  37. package/sbom.spdx.json +460 -455
package/dist/index.js CHANGED
@@ -6,12 +6,12 @@ import { LOGGER_ENV_VARS, defineEnv, validateUnknownKiciVars } from "@kici-dev/s
6
6
  import { KNOWN_ROLES, parseHostPropertyAssignments, validateNoReservedLabels } from "@kici-dev/engine";
7
7
  import { execFile } from "node:child_process";
8
8
  import { access, cp, lstat, mkdir, readFile, readdir, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
9
- import { makeTempDir } from "@kici-dev/core/tmp";
9
+ import { existsSync } from "node:fs";
10
10
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
11
+ import { makeTempDir } from "@kici-dev/core/tmp";
11
12
  import { promisify } from "node:util";
12
13
  import { createLogger, sha256, toErrorMessage } from "@kici-dev/shared";
13
14
  import { PNPM_IGNORE_BUILD_GATE_ARG, PackageManager, YarnFlavor, detectPackageManagerFromManifests, detectYarnFlavor } from "@kici-dev/shared/package-manager";
14
- import { existsSync } from "node:fs";
15
15
  import { parse, stringify } from "yaml";
16
16
  import { Readable, Transform } from "node:stream";
17
17
  import { pipeline } from "node:stream/promises";
@@ -40,6 +40,20 @@ var __exportAll = (all, no_symbols) => {
40
40
  return target;
41
41
  };
42
42
  //#endregion
43
+ //#region src/execution/image-build/build-engine.ts
44
+ /**
45
+ * Build a job's container image with the host's build CLI.
46
+ *
47
+ * The CLI is REQUIRED — there is deliberately no socket-API fallback. One build
48
+ * path means one set of Dockerfile semantics: `.dockerignore`, BuildKit and
49
+ * every directive behave as they do on the author's own machine, instead of
50
+ * depending on which agent happened to pick the job up. A host without a CLI
51
+ * cannot run a Dockerfile job, and says so in as many words.
52
+ */
53
+ /** The build CLIs an agent host may provide. */
54
+ const ContainerBuildCli = z.enum(["docker", "podman"]);
55
+ ContainerBuildCli.enum.docker, ContainerBuildCli.enum.podman;
56
+ //#endregion
43
57
  //#region src/config.ts
44
58
  /** Execution mode for the agent's sandbox backend. Mirrors the runtime enum. */
45
59
  const ExecutionMode = z.enum([
@@ -47,6 +61,71 @@ const ExecutionMode = z.enum([
47
61
  "bare-metal",
48
62
  "firecracker"
49
63
  ]);
64
+ /** When the operator between-jobs reset command runs. */
65
+ const BetweenJobsRunOn = z.enum(["always", "on-failure"]);
66
+ const configSchema = z.object({
67
+ orchestratorUrl: z.string().url().min(1, "KICI_ORCHESTRATOR_URL is required"),
68
+ agentId: z.string().optional(),
69
+ labels: z.string().default("").transform((s) => s.split(",").filter(Boolean)),
70
+ properties: z.string().default("").transform((s) => parseHostPropertyAssignments(s.split(",").filter(Boolean))),
71
+ roles: z.string().optional().transform((s) => {
72
+ if (s === void 0) return void 0;
73
+ if (s === "") return [];
74
+ return s.split(",").filter(Boolean);
75
+ }).refine((roles) => {
76
+ if (roles === void 0) return true;
77
+ const validValues = [...KNOWN_ROLES, "all"];
78
+ return roles.every((r) => validValues.includes(r));
79
+ }, { message: `KICI_ROLES must contain only: ${[...KNOWN_ROLES, "all"].join(", ")}` }).transform((roles) => {
80
+ if (roles === void 0) return void 0;
81
+ if (roles.includes("all")) return void 0;
82
+ if (roles.length === 0) return [];
83
+ return roles.filter((r) => r !== "all");
84
+ }),
85
+ port: z.coerce.number().default(8080),
86
+ logLevel: z.enum([
87
+ "debug",
88
+ "info",
89
+ "warn",
90
+ "error"
91
+ ]).default("info"),
92
+ agentToken: z.string().optional(),
93
+ githubToken: z.string().optional(),
94
+ maxLogSizeBytes: z.coerce.number().default(10485760),
95
+ defaultStepTimeoutMs: z.coerce.number().default(18e5),
96
+ dockerKeepFailed: z.string().default("false").transform((s) => s === "true"),
97
+ jobHeartbeatIntervalMs: z.coerce.number().default(6e4),
98
+ backpressureMode: z.enum(["pause", "drop"]).default("pause"),
99
+ agentPayloadDir: z.string().optional(),
100
+ agentCommand: z.string().optional(),
101
+ sandbox: z.string().default("false").transform((s) => s === "true"),
102
+ trustedEnv: z.string().default("false").transform((s) => s === "true"),
103
+ inPlace: z.string().default("false").transform((s) => s === "true"),
104
+ sandboxNetwork: z.enum(["isolated", "host"]).default("isolated"),
105
+ sandboxHardened: z.string().default("true").transform((s) => s !== "false"),
106
+ sandboxReadonlyRootfs: z.string().default("false").transform((s) => s === "true"),
107
+ sandboxUser: z.string().optional(),
108
+ sandboxPidsLimit: z.coerce.number().int().positive().default(512),
109
+ sandboxMemoryBytes: z.coerce.number().int().positive().default(2147483648),
110
+ sandboxNanoCpus: z.coerce.number().int().positive().default(2e9),
111
+ scalerManaged: z.string().optional().transform((s) => s === "1"),
112
+ jobImageAgent: z.string().optional().transform((s) => s === "1"),
113
+ runtimeImage: z.string().optional(),
114
+ runtimeNodeSource: z.string().optional(),
115
+ containerBuildCli: ContainerBuildCli.optional(),
116
+ scalerClaimCode: z.string().optional(),
117
+ scalerIdleTimeoutMs: z.coerce.number().default(5e3),
118
+ scalerPendingDispatchTimeoutMs: z.coerce.number().default(6e4),
119
+ executionMode: ExecutionMode.optional(),
120
+ otelExporterOtlpEndpoint: z.string().optional(),
121
+ concurrencyWaitTimeoutMs: z.coerce.number().int().min(1e3).default(36e5),
122
+ isOrchestratorHost: z.string().optional().transform((v) => v === "true"),
123
+ betweenJobsResetCommand: z.string().optional(),
124
+ betweenJobsResetTimeoutMs: z.coerce.number().int().positive().default(6e4),
125
+ betweenJobsResetRunOn: BetweenJobsRunOn.default("always"),
126
+ orphanCleanup: z.string().default("true").transform((s) => s !== "false"),
127
+ drainOnResetFailure: z.string().default("false").transform((s) => s === "true")
128
+ });
50
129
  /**
51
130
  * Env-var definition for the agent. Exported so the docs generator and the
52
131
  * deploy-stg pre-validator can inspect / re-parse without round-tripping
@@ -54,59 +133,7 @@ const ExecutionMode = z.enum([
54
133
  */
55
134
  const envDef = defineEnv({
56
135
  service: "agent",
57
- schema: z.object({
58
- orchestratorUrl: z.string().url().min(1, "KICI_ORCHESTRATOR_URL is required"),
59
- agentId: z.string().optional(),
60
- labels: z.string().default("").transform((s) => s.split(",").filter(Boolean)),
61
- properties: z.string().default("").transform((s) => parseHostPropertyAssignments(s.split(",").filter(Boolean))),
62
- roles: z.string().optional().transform((s) => {
63
- if (s === void 0) return void 0;
64
- if (s === "") return [];
65
- return s.split(",").filter(Boolean);
66
- }).refine((roles) => {
67
- if (roles === void 0) return true;
68
- const validValues = [...KNOWN_ROLES, "all"];
69
- return roles.every((r) => validValues.includes(r));
70
- }, { message: `KICI_ROLES must contain only: ${[...KNOWN_ROLES, "all"].join(", ")}` }).transform((roles) => {
71
- if (roles === void 0) return void 0;
72
- if (roles.includes("all")) return void 0;
73
- if (roles.length === 0) return [];
74
- return roles.filter((r) => r !== "all");
75
- }),
76
- port: z.coerce.number().default(8080),
77
- logLevel: z.enum([
78
- "debug",
79
- "info",
80
- "warn",
81
- "error"
82
- ]).default("info"),
83
- agentToken: z.string().optional(),
84
- githubToken: z.string().optional(),
85
- maxLogSizeBytes: z.coerce.number().default(10 * 1024 * 1024),
86
- defaultStepTimeoutMs: z.coerce.number().default(1800 * 1e3),
87
- dockerKeepFailed: z.string().default("false").transform((s) => s === "true"),
88
- jobHeartbeatIntervalMs: z.coerce.number().default(6e4),
89
- backpressureMode: z.enum(["pause", "drop"]).default("pause"),
90
- agentPayloadDir: z.string().optional(),
91
- agentCommand: z.string().optional(),
92
- sandbox: z.string().default("false").transform((s) => s === "true"),
93
- trustedEnv: z.string().default("false").transform((s) => s === "true"),
94
- inPlace: z.string().default("false").transform((s) => s === "true"),
95
- sandboxNetwork: z.enum(["isolated", "host"]).default("isolated"),
96
- sandboxHardened: z.string().default("true").transform((s) => s !== "false"),
97
- sandboxReadonlyRootfs: z.string().default("false").transform((s) => s === "true"),
98
- sandboxUser: z.string().optional(),
99
- sandboxPidsLimit: z.coerce.number().int().positive().default(512),
100
- sandboxMemoryBytes: z.coerce.number().int().positive().default(2 * 1024 * 1024 * 1024),
101
- sandboxNanoCpus: z.coerce.number().int().positive().default(2 * 1e9),
102
- scalerManaged: z.string().optional().transform((s) => s === "1"),
103
- scalerIdleTimeoutMs: z.coerce.number().default(5e3),
104
- scalerPendingDispatchTimeoutMs: z.coerce.number().default(6e4),
105
- executionMode: ExecutionMode.optional(),
106
- otelExporterOtlpEndpoint: z.string().optional(),
107
- concurrencyWaitTimeoutMs: z.coerce.number().int().min(1e3).default(36e5),
108
- isOrchestratorHost: z.string().optional().transform((v) => v === "true")
109
- }),
136
+ schema: configSchema,
110
137
  envMap: {
111
138
  orchestratorUrl: "KICI_ORCHESTRATOR_URL",
112
139
  agentId: "KICI_AGENT_ID",
@@ -136,11 +163,21 @@ const envDef = defineEnv({
136
163
  sandboxMemoryBytes: "KICI_SANDBOX_MEMORY_BYTES",
137
164
  sandboxNanoCpus: "KICI_SANDBOX_NANO_CPUS",
138
165
  scalerManaged: "KICI_SCALER_MANAGED",
166
+ jobImageAgent: "KICI_JOB_IMAGE_AGENT",
167
+ runtimeImage: "KICI_RUNTIME_IMAGE",
168
+ runtimeNodeSource: "KICI_RUNTIME_NODE_SOURCE",
169
+ containerBuildCli: "KICI_CONTAINER_BUILD_CLI",
170
+ scalerClaimCode: "KICI_SCALER_CLAIM_CODE",
139
171
  scalerIdleTimeoutMs: "KICI_SCALER_IDLE_TIMEOUT",
140
172
  scalerPendingDispatchTimeoutMs: "KICI_SCALER_PENDING_DISPATCH_TIMEOUT",
141
173
  executionMode: "KICI_EXECUTION_MODE",
142
174
  otelExporterOtlpEndpoint: "OTEL_EXPORTER_OTLP_ENDPOINT",
143
- concurrencyWaitTimeoutMs: "KICI_CONCURRENCY_WAIT_TIMEOUT_MS"
175
+ concurrencyWaitTimeoutMs: "KICI_CONCURRENCY_WAIT_TIMEOUT_MS",
176
+ betweenJobsResetCommand: "KICI_AGENT_BETWEEN_JOBS_RESET_COMMAND",
177
+ betweenJobsResetTimeoutMs: "KICI_AGENT_BETWEEN_JOBS_RESET_TIMEOUT_MS",
178
+ betweenJobsResetRunOn: "KICI_AGENT_BETWEEN_JOBS_RESET_RUN_ON",
179
+ orphanCleanup: "KICI_AGENT_ORPHAN_CLEANUP",
180
+ drainOnResetFailure: "KICI_AGENT_DRAIN_ON_RESET_FAILURE"
144
181
  }
145
182
  });
146
183
  /**
@@ -172,10 +209,16 @@ const envDef = defineEnv({
172
209
  * - KICI_SANDBOX_MEMORY_BYTES (default: 2 GiB) — memory cap in bytes for the job container cgroup
173
210
  * - KICI_SANDBOX_NANO_CPUS (default: 2 CPUs) — CPU cap in nano-CPUs for the job container cgroup
174
211
  * - KICI_SCALER_MANAGED (set to "1" by the orchestrator's auto-scaler — agent self-shuts down on idle)
212
+ * - KICI_SCALER_CLAIM_CODE (optional single-use claim code the agent exchanges for its own ephemeral credentials before registering; ignored when KICI_AGENT_TOKEN is set)
175
213
  * - KICI_SCALER_IDLE_TIMEOUT (ms, default 5000) — how long a scaler-managed agent waits before shutdown after going idle
176
214
  * - KICI_SCALER_PENDING_DISPATCH_TIMEOUT (ms, default 60000) — extended idle window when register.ack signals a queued bound job
177
215
  * - KICI_EXECUTION_MODE (optional, options: container | bare-metal | firecracker) — override the runner's mode-pick logic
178
216
  * - KICI_CONCURRENCY_WAIT_TIMEOUT_MS (default: 3_600_000) — workflow-runner timeout when long-polling for a slot-release follow-up `concurrency.ack`
217
+ * - KICI_AGENT_BETWEEN_JOBS_RESET_COMMAND (optional) — host-reset command run between jobs on a reused agent (fail-open)
218
+ * - KICI_AGENT_BETWEEN_JOBS_RESET_TIMEOUT_MS (default: 60000) — reset command timeout
219
+ * - KICI_AGENT_BETWEEN_JOBS_RESET_RUN_ON (default: always, options: always | on-failure) — when the reset command runs
220
+ * - KICI_AGENT_ORPHAN_CLEANUP (default: true) — reap a finished job's leaked process tree (bare-metal); false = only signal the runner child
221
+ * - KICI_AGENT_DRAIN_ON_RESET_FAILURE (default: false) — drain the agent after repeated consecutive reset failures
179
222
  */
180
223
  function loadConfig() {
181
224
  const data = envDef.parse();
@@ -752,7 +795,7 @@ const logger$3 = createLogger({ prefix: "dep-installer" });
752
795
  const execFileAsync = promisify(execFile);
753
796
  /** Install subprocess timeout (10 min) and stdout/stderr buffer (128 MiB). */
754
797
  const INSTALL_TIMEOUT_MS = 6e5;
755
- const INSTALL_MAX_BUFFER = 128 * 1024 * 1024;
798
+ const INSTALL_MAX_BUFFER = 134217728;
756
799
  /**
757
800
  * Detect the package manager for the cloned repo from its committed manifests.
758
801
  * A pnpm workspace's `packageManager` field + `pnpm-lock.yaml` live at the repo
@@ -1268,8 +1311,8 @@ var logger$1, DOWNLOAD_TIMEOUT_MS$1, UPLOAD_TIMEOUT_MS, UPLOAD_RETRY_BASE_DELAY_
1268
1311
  var init_download = __esmMin((() => {
1269
1312
  init_dep_restore();
1270
1313
  logger$1 = createLogger({ prefix: "agent:download" });
1271
- DOWNLOAD_TIMEOUT_MS$1 = 300 * 1e3;
1272
- UPLOAD_TIMEOUT_MS = 300 * 1e3;
1314
+ DOWNLOAD_TIMEOUT_MS$1 = 3e5;
1315
+ UPLOAD_TIMEOUT_MS = 3e5;
1273
1316
  UPLOAD_RETRY_BASE_DELAY_MS = 500;
1274
1317
  PresignedUploadHttpError = class extends Error {
1275
1318
  statusCode;
@@ -1301,11 +1344,13 @@ var init_download = __esmMin((() => {
1301
1344
  * right destination (repo entries under `workDir`, home entries under the
1302
1345
  * homedir). Extraction lands in a scratch dir first, then moves each group
1303
1346
  * into place so a partial restore never leaves half-written paths in the live
1304
- * tree (mirrors dep-restore).
1347
+ * tree (mirrors dep-restore). The scratch dir honors `KICI_TMPDIR`, which may
1348
+ * sit on a different filesystem from the workspace, so the move falls back to
1349
+ * copy-then-remove on `EXDEV` rather than assuming a same-filesystem rename.
1305
1350
  */
1306
1351
  const logger = createLogger({ prefix: "cache-engine" });
1307
1352
  /** Download timeout for a presigned cache GET: 5 minutes. */
1308
- const DOWNLOAD_TIMEOUT_MS = 300 * 1e3;
1353
+ const DOWNLOAD_TIMEOUT_MS = 3e5;
1309
1354
  /**
1310
1355
  * Resolve a cache path. `~`-prefixed -> home root; otherwise repo-root-relative.
1311
1356
  * Rejects absolute paths and `..` escapes so a workflow cannot read or clobber
@@ -1380,6 +1425,32 @@ async function packCachePaths(workDir, paths, roots) {
1380
1425
  await cleanup();
1381
1426
  }
1382
1427
  }
1428
+ /**
1429
+ * Move a tree from `src` to `dest`. Attempts a rename (same-filesystem, cheap)
1430
+ * and falls back to copy-then-remove only on `EXDEV` — the errno `rename(2)`
1431
+ * reports when the scratch dir (which honors `KICI_TMPDIR`) is on a different
1432
+ * filesystem from the destination workspace. `verbatimSymlinks` keeps a cached
1433
+ * symlink graph intact (mirroring packCachePaths), and `preserveTimestamps`
1434
+ * keeps mtimes stable so the fallback is byte-for-byte equivalent to the
1435
+ * rename. Any non-`EXDEV` error propagates so a real permission or corruption
1436
+ * failure is never silently turned into a copy.
1437
+ */
1438
+ async function moveOrCopy(src, dest) {
1439
+ try {
1440
+ await rename(src, dest);
1441
+ } catch (err) {
1442
+ if (err.code !== "EXDEV") throw err;
1443
+ await cp(src, dest, {
1444
+ recursive: true,
1445
+ verbatimSymlinks: true,
1446
+ preserveTimestamps: true
1447
+ });
1448
+ await rm(src, {
1449
+ recursive: true,
1450
+ force: true
1451
+ });
1452
+ }
1453
+ }
1383
1454
  /** Move the extracted `__repo__` / `__home__` groups from a scratch dir into place. */
1384
1455
  async function moveAnchoredGroups(scratchDir, workDir, home) {
1385
1456
  for (const anchor of await readdir(scratchDir)) {
@@ -1393,7 +1464,7 @@ async function moveAnchoredGroups(scratchDir, workDir, home) {
1393
1464
  recursive: true,
1394
1465
  force: true
1395
1466
  });
1396
- await rename(join(anchorDir, child), dest);
1467
+ await moveOrCopy(join(anchorDir, child), dest);
1397
1468
  }
1398
1469
  }
1399
1470
  }
@@ -92,4 +92,30 @@ export declare const logBackpressureActive: import("@opentelemetry/api").UpDownC
92
92
  * - scaler: injected by orch-side AgentMetricsAggregator; `stateful` for static agents, backend type (`container` / `firecracker` / `bare-metal`) for scaler-managed
93
93
  */
94
94
  export declare const connectionStatus: import("@opentelemetry/api").UpDownCounter<import("@opentelemetry/api").Attributes>;
95
+ /**
96
+ * Total operator between-jobs reset command invocations.
97
+ * Labels:
98
+ * - status: success | failed | timeout
99
+ * - scaler: injected orch-side; `stateful` for static agents
100
+ */
101
+ export declare const betweenJobsResetTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
102
+ /**
103
+ * Between-jobs reset command duration in seconds.
104
+ * Labels:
105
+ * - scaler: injected orch-side
106
+ */
107
+ export declare const betweenJobsResetDurationSeconds: import("@opentelemetry/api").Histogram<import("@opentelemetry/api").Attributes>;
108
+ /**
109
+ * Processes killed by the between-jobs process-group reap.
110
+ * Labels:
111
+ * - scaler: injected orch-side
112
+ */
113
+ export declare const orphansReapedTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
114
+ /**
115
+ * Out-of-band declared-cleanup re-runs after a hard-killed runner.
116
+ * Labels:
117
+ * - status: success | failed | timeout | skipped
118
+ * - scaler: injected orch-side
119
+ */
120
+ export declare const orphanCleanupTotal: import("@opentelemetry/api").Counter<import("@opentelemetry/api").Attributes>;
95
121
  //# sourceMappingURL=prometheus.d.ts.map