@kici-dev/agent 0.1.15 → 0.1.17

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 (30) hide show
  1. package/dist/diagnostics/mini-bundle.d.ts +9 -0
  2. package/dist/execution/dep-installer.d.ts +7 -3
  3. package/dist/execution/dep-packer.d.ts +6 -1
  4. package/dist/execution/env-init/presets/directives.d.ts +20 -0
  5. package/dist/execution/env-init/presets/expand.d.ts +16 -0
  6. package/dist/execution/env-init/presets/mise/cache-key.d.ts +7 -0
  7. package/dist/execution/env-init/presets/mise/expander.d.ts +16 -0
  8. package/dist/execution/env-init/presets/mise/templates.d.ts +16 -0
  9. package/dist/execution/env-init/presets/mise/windows-install.d.ts +18 -0
  10. package/dist/execution/env-init/presets/registry.d.ts +31 -0
  11. package/dist/execution/init-runner.d.ts +7 -0
  12. package/dist/execution/job-runner.d.ts +18 -1
  13. package/dist/execution/sandbox/env-delta.d.ts +2 -0
  14. package/dist/execution/sandbox/index.d.ts +1 -1
  15. package/dist/execution/sandbox/ipc-protocol.d.ts +101 -3
  16. package/dist/execution/sandbox/step-loop.d.ts +24 -1
  17. package/dist/execution/sandbox/types.d.ts +18 -1
  18. package/dist/execution/sandbox/workflow-runner.d.ts +12 -7
  19. package/dist/execution/validate-kici-deps.d.ts +5 -0
  20. package/dist/execution/workspace-siblings.d.ts +33 -0
  21. package/dist/index.js +223 -6
  22. package/dist/provenance/attest.d.ts +30 -0
  23. package/dist/provenance/sign.d.ts +21 -0
  24. package/dist/provenance/statement-builder.d.ts +38 -0
  25. package/dist/server.js +698 -146
  26. package/dist/version.d.ts +2 -0
  27. package/dist/workflow-runner.js +877 -45
  28. package/dist/ws/orchestrator-client.d.ts +53 -4
  29. package/package.json +8 -5
  30. package/sbom.spdx.json +1957 -152
package/dist/server.js CHANGED
@@ -3,24 +3,26 @@ import { dirname as __cjs_dirname } from "node:path";
3
3
  __cjs_dirname(__cjs_fileURLToPath(import.meta.url));
4
4
  import { register } from "node:module";
5
5
  import crypto$1, { createCipheriv, createHash, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync, randomBytes, randomUUID } from "node:crypto";
6
+ import * as os$1 from "node:os";
6
7
  import os, { hostname, tmpdir } from "node:os";
7
8
  import { PassThrough, Readable, Transform, Writable } from "node:stream";
8
9
  import { serve } from "@hono/node-server";
9
10
  import { Hono } from "hono";
10
11
  import winston from "winston";
11
- import { RingBuffer, createHealthRoutes, createLogger, createMeter, createMetricsRoutes, deriveSharedSecret, getPrometheusExporter, getReconnectDelay, getRequestContext, guardStartup, initTelemetry, logger, normalizeLineEndings, requestContext, setServiceName, setupGracefulShutdown, sha256, sha256File, toErrorMessage, validateRequiredTools } from "@kici-dev/shared";
12
+ import { RingBuffer, addLogsToArchive, chunkBuffer, createHealthRoutes, createLogger, createMeter, createMetricsRoutes, deriveSharedSecret, getPrometheusExporter, getReconnectDelay, getRequestContext, guardStartup, initTelemetry, logger, normalizeLineEndings, redactConfig, requestContext, setServiceName, setupGracefulShutdown, sha256, sha256File, toErrorMessage, validateRequiredTools } from "@kici-dev/shared";
12
13
  import { z } from "zod";
13
14
  import { LOGGER_ENV_VARS, defineEnv, validateUnknownKiciVars } from "@kici-dev/shared/env";
14
- import { ALLOWED_SYSTEM_VARS, CacheRunEventType, CacheStepType, ExecutionJobStatus, ExecutionStepStatus, InitFailureCategory, KNOWN_ROLES, PROTOCOL_VERSION, SANDBOX_DEFAULT_VARS, WS_CLOSE_AGENT_AUTH_FAILED, WS_MAX_PAYLOAD_BYTES, deriveOsArchLabels, heartbeatSchema, hostLabel, mergeAutoLabels, orchestratorToAgentMessageSchema, resolveRoleLabels, validateNoReservedLabels } from "@kici-dev/engine";
15
+ import { ALLOWED_SYSTEM_VARS, CacheRunEventType, CacheStepType, ExecutionJobStatus, ExecutionStepStatus, InitFailureCategory, KNOWN_ROLES, PROTOCOL_VERSION, SANDBOX_DEFAULT_VARS, WS_CLOSE_AGENT_AUTH_FAILED, WS_MAX_PAYLOAD_BYTES, applyIncludeExclude, deriveOsArchLabels, expandMatrix, heartbeatSchema, hostLabel, mergeAutoLabels, orchestratorToAgentMessageSchema, resolveRoleLabels, validateNoReservedLabels } from "@kici-dev/engine";
15
16
  import { execFile, execFileSync, execSync, fork, spawn } from "node:child_process";
16
17
  import WebSocket from "ws";
18
+ import archiver from "archiver";
19
+ import fs, { existsSync } from "node:fs";
20
+ import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
21
+ import { fileURLToPath, pathToFileURL } from "node:url";
17
22
  import { AsyncLocalStorage } from "node:async_hooks";
18
23
  import { format, promisify } from "node:util";
19
- import { existsSync } from "node:fs";
20
- import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
21
24
  import { gcStaleTmpDirs } from "@kici-dev/core/tmp-gc";
22
- import { fileURLToPath, pathToFileURL } from "node:url";
23
- import fs, { access, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, unlink, writeFile } from "node:fs/promises";
25
+ import fs$1, { access, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, unlink, writeFile } from "node:fs/promises";
24
26
  import Docker from "dockerode";
25
27
  import { buildKiciApi, isDynamicGroupRef, isDynamicJobFn, isStaticArray, isStaticObject } from "@kici-dev/sdk";
26
28
  import { c, x } from "tar";
@@ -177,6 +179,75 @@ function agentClientConnectionOptions(config) {
177
179
  };
178
180
  }
179
181
  //#endregion
182
+ //#region src/diagnostics/mini-bundle.ts
183
+ /**
184
+ * Agent fleet mini-bundle assembler.
185
+ *
186
+ * Builds an in-memory ZIP of the agent's recent logs, system info, redacted
187
+ * config, and current Prometheus metrics text, streamed to the orchestrator on
188
+ * a fleet.logs.request. No diagnostics runner exists agent-side, so this is a
189
+ * lean subset of the orchestrator's createDebugBundle.
190
+ */
191
+ async function buildAgentMiniBundle(opts) {
192
+ const archive = archiver("zip", { zlib: { level: 6 } });
193
+ const chunks = [];
194
+ archive.on("data", (d) => chunks.push(d));
195
+ const done = new Promise((resolve, reject) => {
196
+ archive.on("end", resolve);
197
+ archive.on("error", reject);
198
+ });
199
+ archive.append(JSON.stringify({
200
+ kind: "agent",
201
+ agentId: opts.agentId,
202
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString()
203
+ }, null, 2), { name: "manifest.json" });
204
+ archive.append(JSON.stringify(redactConfig(opts.config), null, 2), { name: "config/config.json" });
205
+ archive.append(JSON.stringify({
206
+ hostname: os$1.hostname(),
207
+ platform: process.platform,
208
+ arch: process.arch,
209
+ nodeVersion: process.version,
210
+ cpus: os$1.cpus().length,
211
+ totalmem: os$1.totalmem(),
212
+ freemem: os$1.freemem(),
213
+ uptime: os$1.uptime()
214
+ }, null, 2), { name: "system/info.json" });
215
+ if (opts.metricsText) archive.append(opts.metricsText, { name: "system/metrics.txt" });
216
+ if (opts.logDir) await addLogsToArchive(archive, opts.logDir, opts.logWindowHours);
217
+ await archive.finalize();
218
+ await done;
219
+ return Buffer.concat(chunks);
220
+ }
221
+ //#endregion
222
+ //#region src/version.ts
223
+ /**
224
+ * Read the agent's own package version from disk.
225
+ *
226
+ * Walks up from this module's location until it finds a package.json whose
227
+ * name is '@kici-dev/agent'. This is robust to the agent's bundled dist layout
228
+ * (build-service.mjs emits dist/index.js / dist/server.js, so a fixed relative
229
+ * depth would be wrong). Returns null when it can't be resolved so the
230
+ * agent.register message simply omits the field rather than failing.
231
+ */
232
+ function readAgentVersion() {
233
+ try {
234
+ let dir = path.dirname(fileURLToPath(import.meta.url));
235
+ for (let i = 0; i < 8; i++) {
236
+ const candidate = path.join(dir, "package.json");
237
+ if (fs.existsSync(candidate)) {
238
+ const pkg = JSON.parse(fs.readFileSync(candidate, "utf-8"));
239
+ if (pkg.name === "@kici-dev/agent" && typeof pkg.version === "string") return pkg.version;
240
+ }
241
+ const parent = path.dirname(dir);
242
+ if (parent === dir) break;
243
+ dir = parent;
244
+ }
245
+ return null;
246
+ } catch {
247
+ return null;
248
+ }
249
+ }
250
+ //#endregion
180
251
  //#region src/ws/event-buffer.ts
181
252
  /**
182
253
  * In-memory buffer for agent-to-orchestrator messages during disconnection.
@@ -243,6 +314,13 @@ var OrchestratorClient = class OrchestratorClient {
243
314
  pendingApiRequests = /* @__PURE__ */ new Map();
244
315
  /** Pending user-cache restore/save requests awaiting orchestrator response. */
245
316
  pendingUserCacheRequests = /* @__PURE__ */ new Map();
317
+ /**
318
+ * Pending step-approval requests awaiting the orchestrator's resolution.
319
+ * No client-side timeout: the orchestrator owns the (org-/SDK-configured)
320
+ * expiry and sends `step.approval-resolved: expired` when it lapses. The
321
+ * workflow-runner carries an outer safety-net timeout.
322
+ */
323
+ pendingStepApprovals = /* @__PURE__ */ new Map();
246
324
  /** Pending concurrency report requests awaiting orchestrator ack. */
247
325
  pendingConcurrencyRequests = /* @__PURE__ */ new Map();
248
326
  url;
@@ -256,6 +334,7 @@ var OrchestratorClient = class OrchestratorClient {
256
334
  getInFlightJobs;
257
335
  roles;
258
336
  scalerManaged;
337
+ getFleetBundleInputs;
259
338
  /** Timestamp when the connection was lost, used for gap marker outage duration. */
260
339
  disconnectedAt = null;
261
340
  /** Set to true when auth.failure is received. Prevents retrying with a bad token. */
@@ -282,6 +361,7 @@ var OrchestratorClient = class OrchestratorClient {
282
361
  this.getInFlightJobs = options.getInFlightJobs;
283
362
  this.roles = options.roles;
284
363
  this.scalerManaged = options.scalerManaged ?? false;
364
+ this.getFleetBundleInputs = options.getFleetBundleInputs;
285
365
  this.eventBuffer = new EventBuffer({ maxSize: options.maxBufferSize ?? 5e3 });
286
366
  this.logBuffer = new LogBuffer({ maxLines: options.maxLogBufferLines ?? 1e4 });
287
367
  }
@@ -416,6 +496,47 @@ var OrchestratorClient = class OrchestratorClient {
416
496
  });
417
497
  }
418
498
  /**
499
+ * Request a presigned PUT URL for a provenance bundle. Sends a
500
+ * `provenance.upload.request` and waits for a `provenance.upload.response`
501
+ * (resolved via the shared upload-request pending map). Times out after 30s.
502
+ */
503
+ async requestProvenanceUploadUrl(jobId, subjectDigest) {
504
+ const messageId = randomUUID();
505
+ return new Promise((resolve, reject) => {
506
+ const timer = setTimeout(() => {
507
+ this.pendingUploadRequests.delete(messageId);
508
+ reject(/* @__PURE__ */ new Error("Provenance upload URL request timed out (30s)"));
509
+ }, 3e4);
510
+ this.pendingUploadRequests.set(messageId, {
511
+ resolve: (url) => {
512
+ clearTimeout(timer);
513
+ resolve(url);
514
+ },
515
+ reject: (err) => {
516
+ clearTimeout(timer);
517
+ reject(err);
518
+ }
519
+ });
520
+ this.sendDirect({
521
+ type: "provenance.upload.request",
522
+ messageId,
523
+ jobId,
524
+ subjectDigest
525
+ });
526
+ });
527
+ }
528
+ /** Notify the orchestrator a provenance bundle upload completed (records an attestations row). */
529
+ sendProvenanceUploadComplete(jobId, subjectName, subjectDigest, mediaType) {
530
+ this.sendDirect({
531
+ type: "provenance.upload.complete",
532
+ messageId: randomUUID(),
533
+ jobId,
534
+ subjectName,
535
+ subjectDigest,
536
+ mediaType
537
+ });
538
+ }
539
+ /**
419
540
  * Send an event.emit WS message to the orchestrator and await the response.
420
541
  *
421
542
  * Used by the job runner to relay custom event emissions from the sandbox
@@ -453,6 +574,36 @@ var OrchestratorClient = class OrchestratorClient {
453
574
  });
454
575
  }
455
576
  /**
577
+ * Build this agent's fleet mini-bundle and stream it back to the orchestrator
578
+ * as ordered fleet.bundle.chunk frames (the WS frame cap forbids one frame).
579
+ * On failure, sends a single fleet.bundle.error. Public for unit testing.
580
+ */
581
+ async streamFleetBundle(req) {
582
+ try {
583
+ const inputs = await this.getFleetBundleInputs?.() ?? { config: {} };
584
+ const buf = await buildAgentMiniBundle({
585
+ agentId: this.agentId,
586
+ logDir: inputs.logDir,
587
+ logWindowHours: req.logWindowHours,
588
+ config: inputs.config,
589
+ metricsText: inputs.metricsText
590
+ });
591
+ for (const f of chunkBuffer(buf)) this.sendDirect({
592
+ type: "fleet.bundle.chunk",
593
+ requestId: req.requestId,
594
+ seq: f.seq,
595
+ isLast: f.isLast,
596
+ dataB64: f.dataB64
597
+ });
598
+ } catch (err) {
599
+ this.sendDirect({
600
+ type: "fleet.bundle.error",
601
+ requestId: req.requestId,
602
+ message: toErrorMessage(err)
603
+ });
604
+ }
605
+ }
606
+ /**
456
607
  * Send a typed API request to the orchestrator and await the response.
457
608
  *
458
609
  * This is the transport layer for the agent private API. The SDK's typed
@@ -547,6 +698,58 @@ var OrchestratorClient = class OrchestratorClient {
547
698
  });
548
699
  }
549
700
  /**
701
+ * Relay a provenance bundle upload operation to the orchestrator. Maps the
702
+ * IPC `provenance.request` onto `requestProvenanceUploadUrl` (returns the
703
+ * presigned URL) or `sendProvenanceUploadComplete` (fire-and-forget) and
704
+ * returns the result on the IPC response shape.
705
+ */
706
+ async relayProvenance(jobId, request) {
707
+ if (request.op === "complete") {
708
+ this.sendProvenanceUploadComplete(jobId, request.subjectName, request.subjectDigest, request.mediaType);
709
+ return {
710
+ type: "provenance.response",
711
+ requestId: request.requestId
712
+ };
713
+ }
714
+ const uploadUrl = await this.requestProvenanceUploadUrl(jobId, request.subjectDigest);
715
+ return {
716
+ type: "provenance.response",
717
+ requestId: request.requestId,
718
+ uploadUrl
719
+ };
720
+ }
721
+ /**
722
+ * Relay a step-level approval request to the orchestrator. Sends a
723
+ * `step.approval-request` WS message and resolves with the orchestrator's
724
+ * `step.approval-resolved` mapped onto the IPC response shape. No client-side
725
+ * timeout — the orchestrator owns the approval expiry and replies with an
726
+ * `expired` outcome when it lapses. Rejects only on disconnect (the relay
727
+ * caller treats a rejection as a fail-closed reject).
728
+ */
729
+ async sendStepApproval(runId, jobId, request) {
730
+ const messageId = randomUUID();
731
+ return new Promise((resolve, reject) => {
732
+ this.pendingStepApprovals.set(messageId, {
733
+ resolve: (response) => resolve({
734
+ ...response,
735
+ requestId: request.requestId
736
+ }),
737
+ reject
738
+ });
739
+ this.sendDirect({
740
+ type: "step.approval-request",
741
+ messageId,
742
+ runId,
743
+ jobId,
744
+ stepIndex: request.stepIndex,
745
+ stepName: request.stepName,
746
+ clauses: request.clauses,
747
+ reason: request.reason,
748
+ ...request.timeoutSeconds !== void 0 && { timeoutSeconds: request.timeoutSeconds }
749
+ });
750
+ });
751
+ }
752
+ /**
550
753
  * Send a job.context message to the orchestrator.
551
754
  *
552
755
  * Conveys execution environment details (runtime, sandbox type, env vars)
@@ -684,6 +887,8 @@ var OrchestratorClient = class OrchestratorClient {
684
887
  this.pendingUserCacheRequests.clear();
685
888
  for (const [_id, pending] of this.pendingConcurrencyRequests) pending.reject(/* @__PURE__ */ new Error("WebSocket disconnected"));
686
889
  this.pendingConcurrencyRequests.clear();
890
+ for (const [_id, pending] of this.pendingStepApprovals) pending.reject(/* @__PURE__ */ new Error("WebSocket disconnected"));
891
+ this.pendingStepApprovals.clear();
687
892
  if (!this.intentionalDisconnect) this.scheduleReconnect();
688
893
  });
689
894
  this.ws.on("error", (err) => {
@@ -700,7 +905,7 @@ var OrchestratorClient = class OrchestratorClient {
700
905
  return;
701
906
  }
702
907
  const rawMsg = raw;
703
- if (rawMsg.type === "cache.upload.response") {
908
+ if (rawMsg.type === "cache.upload.response" || rawMsg.type === "provenance.upload.response") {
704
909
  const pending = this.pendingUploadRequests.get(rawMsg.requestId);
705
910
  if (pending) {
706
911
  this.pendingUploadRequests.delete(rawMsg.requestId);
@@ -814,6 +1019,33 @@ var OrchestratorClient = class OrchestratorClient {
814
1019
  }
815
1020
  break;
816
1021
  }
1022
+ case "step.approval-resolved": {
1023
+ logger$11.info("Step approval resolved", {
1024
+ requestId: msg.requestId,
1025
+ runId: msg.runId,
1026
+ jobId: msg.jobId,
1027
+ stepIndex: msg.stepIndex,
1028
+ outcome: msg.outcome
1029
+ });
1030
+ const pending = this.pendingStepApprovals.get(msg.requestId);
1031
+ if (pending) {
1032
+ this.pendingStepApprovals.delete(msg.requestId);
1033
+ pending.resolve({
1034
+ type: "approval.resolved",
1035
+ requestId: msg.requestId,
1036
+ outcome: msg.outcome,
1037
+ ...msg.reason !== void 0 && { reason: msg.reason }
1038
+ });
1039
+ }
1040
+ break;
1041
+ }
1042
+ case "fleet.logs.request":
1043
+ logger$11.info("Fleet log collection requested", {
1044
+ requestId: msg.requestId,
1045
+ logWindowHours: msg.logWindowHours
1046
+ });
1047
+ this.streamFleetBundle(msg);
1048
+ break;
817
1049
  }
818
1050
  return;
819
1051
  }
@@ -953,6 +1185,10 @@ var OrchestratorClient = class OrchestratorClient {
953
1185
  totalMemoryMb: Math.round(os.totalmem() / (1024 * 1024)),
954
1186
  cpuCount: os.cpus().length,
955
1187
  nodeVersion: process.versions.node,
1188
+ ...(() => {
1189
+ const v = readAgentVersion();
1190
+ return v ? { version: v } : {};
1191
+ })(),
956
1192
  ...(() => {
957
1193
  try {
958
1194
  const info = os.userInfo();
@@ -1061,14 +1297,14 @@ var init_console_capture = __esmMin((() => {
1061
1297
  init_console_capture();
1062
1298
  function safe(name, fallback = "unknown") {
1063
1299
  switch (name) {
1064
- case "version": return "0.1.15";
1065
- case "buildCommit": return "831f6a763";
1066
- case "sdkVersion": return "0.1.15";
1067
- case "sdkBundleHash": return "cee24b67bbb4a1d2a770e4063dd37a9af33409faeb5ce32f936703b4431316dd";
1068
- case "sharedVersion": return "0.1.15";
1069
- case "sharedBundleHash": return "e82a8b68a7d72698674352158c5de4c0d10824c61bfa7166cc8f846044b8b5e4";
1070
- case "engineVersion": return "0.1.15";
1071
- case "engineBundleHash": return "032d6b28b3d32bcbea80ba80a52934b017ee2c86a97fcaf8fe8db0d86d72a7f7";
1300
+ case "version": return "0.1.17";
1301
+ case "buildCommit": return "5596f8a3c";
1302
+ case "sdkVersion": return "0.1.17";
1303
+ case "sdkBundleHash": return "df47ed5db86eaaa2de8394c0db08335f368e8d620a898cc409765f4545eb3972";
1304
+ case "sharedVersion": return "0.1.17";
1305
+ case "sharedBundleHash": return "9e8a753da73b26fb87d08817f70b4d836f67f599385fa268b2c1f68b97996f54";
1306
+ case "engineVersion": return "0.1.17";
1307
+ case "engineBundleHash": return "706d94fa54a47aea0f69bde105d40213befff401f717604aded2c62a1291cb51";
1072
1308
  default: return fallback;
1073
1309
  }
1074
1310
  }
@@ -1625,7 +1861,7 @@ async function buildAssetDigestFromResolvedPaths(workDir, resolvedPaths) {
1625
1861
  for (const rel of resolvedPaths) {
1626
1862
  const abs = path.join(workDir, rel);
1627
1863
  try {
1628
- const content = await fs.readFile(abs, "utf-8");
1864
+ const content = await fs$1.readFile(abs, "utf-8");
1629
1865
  parts.push(`${rel}\n${content}`);
1630
1866
  } catch {
1631
1867
  parts.push(`${rel}\n`);
@@ -1650,7 +1886,7 @@ async function loadWorkflowSource(workDir, sourceFile, expectedContentHash, reso
1650
1886
  ensureLoaderHookRegistered();
1651
1887
  const filePath = path.join(workDir, sourceFile);
1652
1888
  if (expectedContentHash) {
1653
- const rawSource = await fs.readFile(filePath, "utf-8");
1889
+ const rawSource = await fs$1.readFile(filePath, "utf-8");
1654
1890
  let assetDigest;
1655
1891
  if (resolvedHashFiles?.length) assetDigest = await buildAssetDigestFromResolvedPaths(workDir, resolvedHashFiles);
1656
1892
  const actualHash = computeContentHash(rawSource, assetDigest);
@@ -1752,8 +1988,8 @@ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event
1752
1988
  }
1753
1989
  var AGENT_SDK_VERSION, AGENT_SDK_BUNDLE_HASH, hookRegistered;
1754
1990
  var init_workflow_loader = __esmMin((() => {
1755
- AGENT_SDK_VERSION = "0.1.15";
1756
- AGENT_SDK_BUNDLE_HASH = "cee24b67bbb4a1d2a770e4063dd37a9af33409faeb5ce32f936703b4431316dd";
1991
+ AGENT_SDK_VERSION = "0.1.17";
1992
+ AGENT_SDK_BUNDLE_HASH = "df47ed5db86eaaa2de8394c0db08335f368e8d620a898cc409765f4545eb3972";
1757
1993
  hookRegistered = false;
1758
1994
  }));
1759
1995
  //#endregion
@@ -1919,24 +2155,24 @@ function resolveOrchestratorUrl(url) {
1919
2155
  * have nothing to race; the defensive `rm` covers re-runs.
1920
2156
  */
1921
2157
  async function moveScratchIntoRepo(scratchDir, workDir) {
1922
- for (const child of await fs.readdir(scratchDir)) if (child === ".kici") {
2158
+ for (const child of await fs$1.readdir(scratchDir)) if (child === ".kici") {
1923
2159
  const kiciScratch = join(scratchDir, ".kici");
1924
- for (const sub of await fs.readdir(kiciScratch)) await moveInto(join(kiciScratch, sub), join(workDir, ".kici", sub));
2160
+ for (const sub of await fs$1.readdir(kiciScratch)) await moveInto(join(kiciScratch, sub), join(workDir, ".kici", sub));
1925
2161
  } else await moveInto(join(scratchDir, child), join(workDir, child));
1926
2162
  }
1927
2163
  /** Move `src` to `dest`, creating the parent and clearing any stale dest. */
1928
2164
  async function moveInto(src, dest) {
1929
2165
  await mkdir(dirname(dest), { recursive: true });
1930
- await fs.rm(dest, {
2166
+ await fs$1.rm(dest, {
1931
2167
  recursive: true,
1932
2168
  force: true
1933
2169
  });
1934
- await fs.rename(src, dest);
2170
+ await fs$1.rename(src, dest);
1935
2171
  }
1936
2172
  /** Best-effort cleanup of a settled scratch dir; logs and continues on failure. */
1937
2173
  async function cleanupScratch(scratchDir) {
1938
2174
  try {
1939
- await fs.rm(scratchDir, {
2175
+ await fs$1.rm(scratchDir, {
1940
2176
  recursive: true,
1941
2177
  force: true
1942
2178
  });
@@ -1970,7 +2206,7 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
1970
2206
  const kiciDir = join(workDir, ".kici");
1971
2207
  if (depsUrl.startsWith("file://")) {
1972
2208
  const localPath = fileURLToPath(depsUrl);
1973
- const data = await fs.readFile(localPath);
2209
+ const data = await fs$1.readFile(localPath);
1974
2210
  if (depsHash) {
1975
2211
  const actualHash = computeHash(data);
1976
2212
  if (actualHash !== depsHash) throw new Error(`Dep tarball hash mismatch: expected ${depsHash}, got ${actualHash}`);
@@ -2129,7 +2365,7 @@ async function restoreSource(workDir, sourceTarUrl) {
2129
2365
  let data;
2130
2366
  if (sourceTarUrl.startsWith("file://")) {
2131
2367
  const localPath = fileURLToPath(sourceTarUrl);
2132
- data = await fs.readFile(localPath);
2368
+ data = await fs$1.readFile(localPath);
2133
2369
  } else if (sourceTarUrl.startsWith("http://") || sourceTarUrl.startsWith("https://")) data = await downloadUrl(sourceTarUrl);
2134
2370
  else throw new Error(`Unsupported source tarball URL scheme: ${sourceTarUrl}`);
2135
2371
  await extractSourceTarball(data, workDir);
@@ -2198,6 +2434,28 @@ function findJobByName(workflow, jobName) {
2198
2434
  async function evaluateDynamicFields(workflow, jobName, event, flags, timeoutMs = 6e4) {
2199
2435
  const job = findJobByName(workflow, jobName);
2200
2436
  const result = {};
2437
+ if (flags.dynamicMatrix && typeof job.matrix === "function") {
2438
+ const matrixContext = {
2439
+ $: (await import("zx")).$,
2440
+ ctx: {
2441
+ workflow: { name: workflow.name },
2442
+ job: {
2443
+ name: jobName,
2444
+ runsOn: job.runsOn
2445
+ }
2446
+ },
2447
+ log: {
2448
+ info: () => {},
2449
+ warn: () => {},
2450
+ error: () => {},
2451
+ debug: () => {}
2452
+ },
2453
+ env: { ...process.env }
2454
+ };
2455
+ let combos = expandMatrix(await withTimeout(() => job.matrix(matrixContext), timeoutMs, `dynamicMatrix for job '${jobName}'`));
2456
+ if (job.include || job.exclude) combos = applyIncludeExclude(combos, job.include, job.exclude);
2457
+ result.matrixValues = combos;
2458
+ }
2201
2459
  if (flags.dynamicEnvironment && typeof job.environment === "function") {
2202
2460
  const value = await withTimeout(() => job.environment(event), timeoutMs, `dynamicEnvironment for job '${jobName}'`);
2203
2461
  if (value !== void 0 && value !== null) result.environmentName = value;
@@ -2400,6 +2658,7 @@ var MatrixExpansionError, DYNAMIC_FIELD_TIMEOUT_MS;
2400
2658
  var init_dynamic_job_serializer = __esmMin((() => {
2401
2659
  init_timeout_util();
2402
2660
  MatrixExpansionError = class MatrixExpansionError extends Error {
2661
+ jobName;
2403
2662
  name = "MatrixExpansionError";
2404
2663
  constructor(jobName, message) {
2405
2664
  super(message);
@@ -2673,7 +2932,7 @@ function decryptBuffer(encrypted, aesKey) {
2673
2932
  */
2674
2933
  async function applyOverlay(config) {
2675
2934
  const { tarballUrl, cliPublicKey, orchestratorPrivateKey, repoDir } = config;
2676
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "kici-overlay-"));
2935
+ const tmpDir = await fs$1.mkdtemp(path.join(os.tmpdir(), "kici-overlay-"));
2677
2936
  try {
2678
2937
  logger$7.info("Downloading overlay tarball", { url: tarballUrl.replace(/\?.*$/, "?[redacted]") });
2679
2938
  let encryptedData;
@@ -2687,7 +2946,7 @@ async function applyOverlay(config) {
2687
2946
  const decryptedData = decryptBuffer(encryptedData, aesKey);
2688
2947
  logger$7.info("Extracting overlay tarball", { size: decryptedData.length });
2689
2948
  const extractDir = path.join(tmpDir, "extracted");
2690
- await fs.mkdir(extractDir, { recursive: true });
2949
+ await fs$1.mkdir(extractDir, { recursive: true });
2691
2950
  try {
2692
2951
  const readable = Readable.from(decryptedData);
2693
2952
  await new Promise((resolve, reject) => {
@@ -2702,7 +2961,7 @@ async function applyOverlay(config) {
2702
2961
  const manifestPath = path.join(extractDir, ".kici-overlay-tmp", "manifest.json");
2703
2962
  let manifestContent;
2704
2963
  try {
2705
- manifestContent = await fs.readFile(manifestPath, "utf-8");
2964
+ manifestContent = await fs$1.readFile(manifestPath, "utf-8");
2706
2965
  } catch {
2707
2966
  throw new Error("Overlay manifest not found: expected .kici-overlay-tmp/manifest.json in tarball");
2708
2967
  }
@@ -2723,15 +2982,15 @@ async function applyOverlay(config) {
2723
2982
  for (const file of checksumFiles) {
2724
2983
  const srcPath = path.join(extractDir, file);
2725
2984
  const destPath = path.join(repoDir, file);
2726
- await fs.mkdir(path.dirname(destPath), { recursive: true });
2727
- await fs.copyFile(srcPath, destPath);
2985
+ await fs$1.mkdir(path.dirname(destPath), { recursive: true });
2986
+ await fs$1.copyFile(srcPath, destPath);
2728
2987
  filesApplied++;
2729
2988
  }
2730
2989
  let filesDeleted = 0;
2731
2990
  for (const file of manifest.deletions) {
2732
2991
  const targetPath = path.join(repoDir, file);
2733
2992
  try {
2734
- await fs.unlink(targetPath);
2993
+ await fs$1.unlink(targetPath);
2735
2994
  filesDeleted++;
2736
2995
  } catch {
2737
2996
  logger$7.debug("Deletion target not found, skipping", { file });
@@ -2747,7 +3006,7 @@ async function applyOverlay(config) {
2747
3006
  verified: true
2748
3007
  };
2749
3008
  } finally {
2750
- await fs.rm(tmpDir, {
3009
+ await fs$1.rm(tmpDir, {
2751
3010
  recursive: true,
2752
3011
  force: true
2753
3012
  }).catch(() => {});
@@ -2896,6 +3155,11 @@ var init_npm_registry_config = __esmMin((() => {}));
2896
3155
  * clones the whole repo, so an in-repo sibling is present), and resolves
2897
3156
  * `file:`/`link:`/`portal:` against a path — allowed when that path stays
2898
3157
  * inside the cloned repo, rejected when it escapes the clone.
3158
+ * - yarn classic (v1) has no `workspace:` protocol and no `portal:` — it links
3159
+ * in-repo siblings by version range, not by a local specifier — so both are
3160
+ * rejected with guidance; `file:`/`link:` are allowed when the path stays
3161
+ * inside the clone, rejected when it escapes. (yarn berry is not yet
3162
+ * supported.)
2899
3163
  *
2900
3164
  * This module performs that classification so unresolvable specifiers fail
2901
3165
  * fast with guidance rather than a cryptic install error.
@@ -2971,6 +3235,17 @@ function isInsideRepo(repoRoot, target) {
2971
3235
  */
2972
3236
  async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
2973
3237
  if (packageManager === PackageManager.Npm) return [...deps];
3238
+ if (packageManager === PackageManager.Yarn) {
3239
+ const unresolvable = [];
3240
+ for (const dep of deps) {
3241
+ if (dep.protocol === "workspace:" || dep.protocol === "portal:") {
3242
+ unresolvable.push(dep);
3243
+ continue;
3244
+ }
3245
+ if (!isInsideRepo(repoRoot, resolveLocalPath(kiciDir, dep))) unresolvable.push(dep);
3246
+ }
3247
+ return unresolvable;
3248
+ }
2974
3249
  const hasWorkspaceFile = await fileExists$1(join(repoRoot, "pnpm-workspace.yaml"));
2975
3250
  const unresolvable = [];
2976
3251
  for (const dep of deps) {
@@ -2986,6 +3261,7 @@ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
2986
3261
  function formatUnresolvableDepError(offenders, packageManager) {
2987
3262
  const list = offenders.map((o) => `${o.name}: ${o.spec}`).join(", ");
2988
3263
  if (packageManager === PackageManager.Npm) return `These .kici/ dependencies use local-protocol specifiers npm cannot resolve from a registry: ${list}. npm has no workspace protocol — pin a published version, publish the package to your registry, or use pnpm so an in-repo workspace sibling can be resolved.`;
3264
+ if (packageManager === PackageManager.Yarn) return `These .kici/ dependencies use specifiers yarn classic cannot resolve: ${list}. yarn classic has no workspace: or portal: protocol — reference an in-repo sibling by a version range (yarn links matching workspace members), use pnpm, or keep file:/link: paths inside this repository. (yarn berry support is planned.)`;
2989
3265
  return `These .kici/ dependencies point outside the cloned repository, which the agent never has: ${list}. A workspace: dependency requires a pnpm-workspace.yaml at the repo root, and file:/link:/portal: paths must stay inside this repository.`;
2990
3266
  }
2991
3267
  /**
@@ -3019,6 +3295,102 @@ var init_validate_kici_deps = __esmMin((() => {
3019
3295
  ];
3020
3296
  }));
3021
3297
  //#endregion
3298
+ //#region src/execution/workspace-siblings.ts
3299
+ /**
3300
+ * In-repo workspace-sibling discovery for the agent's dependency handling.
3301
+ *
3302
+ * A pnpm or yarn-classic workspace lays out a `.kici/` member's `workspace:`
3303
+ * (pnpm) or version-range (yarn) siblings as symlinks pointing at package
3304
+ * directories that live inside the clone but outside `.kici/` and outside the
3305
+ * `node_modules` store. The dep-cache packer must travel those sibling dirs with
3306
+ * the closure (their symlinks would dangle otherwise), and the yarn install path
3307
+ * must build them (the install links a sibling but does not build it).
3308
+ *
3309
+ * `collectInRepoSiblings` walks a starting `node_modules` (and transitively each
3310
+ * discovered sibling's `node_modules`), returning each in-repo sibling directory
3311
+ * once, repo-root-relative, in breadth-first discovery order. The starting
3312
+ * `node_modules` is a parameter so it serves pnpm + yarn-standalone (seeded at
3313
+ * `.kici/node_modules`) and yarn-workspace-member (seeded at the hoisted root
3314
+ * `node_modules`).
3315
+ */
3316
+ /**
3317
+ * The directory yarn lays `.kici`'s dependencies into. A standalone `.kici`
3318
+ * (own lockfile, no parent workspace) gets `.kici/node_modules`; a workspace
3319
+ * member hoists everything to the repo-root `node_modules`, leaving no
3320
+ * `.kici/node_modules`.
3321
+ */
3322
+ function resolveYarnNodeModulesRoot(repoRoot, kiciDir) {
3323
+ const kiciNm = join(kiciDir, "node_modules");
3324
+ return existsSync(kiciNm) ? kiciNm : join(repoRoot, "node_modules");
3325
+ }
3326
+ /**
3327
+ * Walk `seedNodeModules` (and transitively each in-repo sibling's
3328
+ * `node_modules`) collecting the repo-root-relative directories of workspace
3329
+ * siblings — package dirs that live inside the clone but outside `.kici/` and
3330
+ * outside the repo-root `node_modules/` store. Returns each dir once, in
3331
+ * discovery (BFS) order.
3332
+ */
3333
+ async function collectInRepoSiblings(workDir, kiciDir, seedNodeModules = join(kiciDir, "node_modules")) {
3334
+ const repoRoot = resolve(workDir);
3335
+ const kiciResolved = resolve(kiciDir);
3336
+ const rootNodeModules = resolve(join(workDir, "node_modules"));
3337
+ const found = /* @__PURE__ */ new Set();
3338
+ const visited = /* @__PURE__ */ new Set();
3339
+ const queue = [seedNodeModules];
3340
+ while (queue.length > 0) {
3341
+ const nmDir = queue.shift();
3342
+ const real = await realpath(nmDir).catch(() => null);
3343
+ if (!real || visited.has(real)) continue;
3344
+ visited.add(real);
3345
+ for (const target of await resolveNodeModulesLinks(nmDir)) {
3346
+ if (!isInside(repoRoot, target)) continue;
3347
+ if (isInside(kiciResolved, target) || isInside(rootNodeModules, target)) continue;
3348
+ const rel = relative(workDir, target);
3349
+ if (!found.has(rel)) {
3350
+ found.add(rel);
3351
+ queue.push(join(target, "node_modules"));
3352
+ }
3353
+ }
3354
+ }
3355
+ return [...found];
3356
+ }
3357
+ /** Resolve every package symlink target under a `node_modules` dir (descending one level into `@scope` dirs). */
3358
+ async function resolveNodeModulesLinks(nmDir) {
3359
+ const targets = [];
3360
+ for (const entry of await readdir(nmDir).catch(() => [])) {
3361
+ if (entry.startsWith(".")) continue;
3362
+ const entryPath = join(nmDir, entry);
3363
+ if (entry.startsWith("@")) {
3364
+ for (const scoped of await readdir(entryPath).catch(() => [])) {
3365
+ const target = await resolveIfSymlink(join(entryPath, scoped));
3366
+ if (target) targets.push(target);
3367
+ }
3368
+ continue;
3369
+ }
3370
+ const target = await resolveIfSymlink(entryPath);
3371
+ if (target) targets.push(target);
3372
+ }
3373
+ return targets;
3374
+ }
3375
+ /** Return the real path of `p` if it is a symlink, else null. */
3376
+ async function resolveIfSymlink(p) {
3377
+ try {
3378
+ if (!(await lstat(p)).isSymbolicLink()) return null;
3379
+ return await realpath(p);
3380
+ } catch {
3381
+ return null;
3382
+ }
3383
+ }
3384
+ /** Whether `target` is `root` itself or a path inside it. */
3385
+ function isInside(root, target) {
3386
+ const rel = relative(root, target);
3387
+ return rel === "" || !rel.startsWith("..") && !rel.startsWith(`..${sep}`) && !isAbsoluteRel(rel);
3388
+ }
3389
+ function isAbsoluteRel(rel) {
3390
+ return rel.length > 1 && rel[1] === ":";
3391
+ }
3392
+ var init_workspace_siblings = __esmMin((() => {}));
3393
+ //#endregion
3022
3394
  //#region src/execution/dep-installer.ts
3023
3395
  /**
3024
3396
  * Inline dependency installation for graceful degradation.
@@ -3026,12 +3398,14 @@ var init_validate_kici_deps = __esmMin((() => {
3026
3398
  * When the dep cache is unavailable or a download fails, the agent installs
3027
3399
  * `.kici/` dependencies directly with the repository's package manager.
3028
3400
  *
3029
- * The package manager is detected from the cloned repo (npm / pnpm); the
3401
+ * The package manager is detected from the cloned repo (npm / pnpm / yarn); the
3030
3402
  * presence of `.kici/package.json` signals that deps should be installed. npm
3031
3403
  * is the default and ships with every Node.js install; pnpm is used when the
3032
3404
  * repo is a pnpm workspace so a `.kici/` member can resolve in-repo
3033
- * `workspace:` siblings. yarn is detected but not yet supported and is
3034
- * rejected with an actionable error.
3405
+ * `workspace:` siblings. yarn classic (v1) is supported for registry
3406
+ * dependencies and version-range workspace siblings (which it links but does
3407
+ * not build, so the agent builds the in-repo closure after install). yarn
3408
+ * berry (v2+) is not yet supported.
3035
3409
  *
3036
3410
  * Security: the install runs with an isolated per-invocation cache/store
3037
3411
  * directory to prevent cache poisoning across build jobs — a malicious
@@ -3075,7 +3449,6 @@ async function installDeps(kiciDir, opts = {}) {
3075
3449
  dir: kiciDir
3076
3450
  });
3077
3451
  process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, cwd=${kiciDir}\n`);
3078
- if (packageManager === PackageManager.Yarn) throw new Error("This repository uses yarn, which the KiCI agent does not yet support for .kici/ dependency installation. Use npm or pnpm for the .kici/ project, or open a feature request for yarn support.");
3079
3452
  await assertResolvableDeps({
3080
3453
  kiciDir,
3081
3454
  repoRoot,
@@ -3095,6 +3468,11 @@ async function installDeps(kiciDir, opts = {}) {
3095
3468
  hasPrivateRegistry,
3096
3469
  registryConfig
3097
3470
  });
3471
+ else if (packageManager === PackageManager.Yarn) await runYarnInstall({
3472
+ kiciDir,
3473
+ hasPrivateRegistry,
3474
+ registryConfig
3475
+ });
3098
3476
  else await runNpmInstall({
3099
3477
  kiciDir,
3100
3478
  hasPrivateRegistry,
@@ -3109,6 +3487,7 @@ async function installDeps(kiciDir, opts = {}) {
3109
3487
  await registryConfig.cleanup();
3110
3488
  }
3111
3489
  if (packageManager === PackageManager.Pnpm && await kiciHasLocalProtocolDeps(kiciDir)) await buildWorkspaceClosure(repoRoot);
3490
+ if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir);
3112
3491
  const durationMs = Date.now() - startTime;
3113
3492
  process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
3114
3493
  logger$6.info("Deps installed inline", {
@@ -3196,6 +3575,101 @@ async function runPnpmInstall(args) {
3196
3575
  }).catch(() => {});
3197
3576
  }
3198
3577
  }
3578
+ /** Pure: argv for `yarn install` with an isolated cache folder. */
3579
+ function buildYarnInstallArgs(cacheDir, hasPrivateRegistry) {
3580
+ const a = [
3581
+ "install",
3582
+ "--cache-folder",
3583
+ cacheDir,
3584
+ "--non-interactive",
3585
+ "--no-progress"
3586
+ ];
3587
+ if (hasPrivateRegistry) a.push("--ignore-scripts");
3588
+ return a;
3589
+ }
3590
+ /**
3591
+ * Run `yarn install` from `.kici/` with an isolated cache folder. yarn classic
3592
+ * reads the synthesized `.kici/.npmrc` (registry + `${VAR}` token expansion) for
3593
+ * private-registry auth. A workspace member hoists deps to the repo-root
3594
+ * node_modules; a standalone `.kici` gets `.kici/node_modules`. Not
3595
+ * `--frozen-lockfile` (resolved URLs in the lockfile may point at a different
3596
+ * registry than the synthesized `.npmrc`, e.g. localhost tunnel vs direct IP).
3597
+ */
3598
+ async function runYarnInstall(args) {
3599
+ await assertYarnAvailable();
3600
+ const { nodeDir } = resolveNpm();
3601
+ const cacheDir = await mkdtemp(join(tmpdir(), "kici-yarn-cache-"));
3602
+ const env = envWithNodeOnPath(args.registryConfig.extraEnv, nodeDir);
3603
+ const argv = buildYarnInstallArgs(cacheDir, args.hasPrivateRegistry);
3604
+ try {
3605
+ process.stderr.write(`[dep-installer:trace] running: yarn ${argv.join(" ")}\n`);
3606
+ await execFileAsync("yarn", argv, {
3607
+ cwd: args.kiciDir,
3608
+ env,
3609
+ timeout: INSTALL_TIMEOUT_MS,
3610
+ maxBuffer: INSTALL_MAX_BUFFER
3611
+ });
3612
+ } finally {
3613
+ await rm(cacheDir, {
3614
+ recursive: true,
3615
+ force: true
3616
+ }).catch(() => {});
3617
+ }
3618
+ }
3619
+ /** Throw an actionable error when the repo needs yarn but it is not installed. */
3620
+ async function assertYarnAvailable() {
3621
+ try {
3622
+ await execFileAsync("yarn", ["--version"], {
3623
+ timeout: 3e4,
3624
+ cwd: tmpdir()
3625
+ });
3626
+ } catch (e) {
3627
+ throw new Error(`This repository uses yarn, but yarn is not available on this agent. Install yarn (e.g. \`corepack enable\`) or run on a container/Firecracker agent that bundles it. (${toErrorMessage(e)})`);
3628
+ }
3629
+ }
3630
+ /**
3631
+ * Build the in-repo workspace siblings `.kici` depends on (yarn links them on
3632
+ * install but does not build them). Walks siblings from the resolved
3633
+ * node_modules root and runs each sibling's `build` script in leaf-first
3634
+ * (reverse-discovery) order with a clean env (no synthesized registry tokens).
3635
+ * Deep cross-sibling build chains may build out of strict topological order —
3636
+ * real `.kici` closures are shallow.
3637
+ */
3638
+ async function buildYarnWorkspaceClosure(repoRoot, kiciDir) {
3639
+ const siblings = await collectInRepoSiblings(repoRoot, kiciDir, resolveYarnNodeModulesRoot(repoRoot, kiciDir));
3640
+ if (siblings.length === 0) return;
3641
+ const { nodeDir } = resolveNpm();
3642
+ const env = envWithNodeOnPath({}, nodeDir);
3643
+ for (const rel of [...siblings].reverse()) {
3644
+ const sibDir = join(repoRoot, rel);
3645
+ if (!await siblingHasBuildScript(sibDir)) continue;
3646
+ process.stderr.write(`[dep-installer:trace] building yarn sibling: yarn --cwd ${sibDir} run build\n`);
3647
+ try {
3648
+ await execFileAsync("yarn", [
3649
+ "--cwd",
3650
+ sibDir,
3651
+ "run",
3652
+ "build"
3653
+ ], {
3654
+ cwd: repoRoot,
3655
+ env,
3656
+ timeout: INSTALL_TIMEOUT_MS,
3657
+ maxBuffer: INSTALL_MAX_BUFFER
3658
+ });
3659
+ } catch (e) {
3660
+ logSubprocessStreams(e, []);
3661
+ throw new Error(`Failed to build .kici yarn workspace sibling ${rel}: ${describeExecError(e)}`);
3662
+ }
3663
+ }
3664
+ }
3665
+ /** Whether a sibling package.json declares a `build` script. */
3666
+ async function siblingHasBuildScript(sibDir) {
3667
+ try {
3668
+ return typeof JSON.parse(await readFile(join(sibDir, "package.json"), "utf-8")).scripts?.build === "string";
3669
+ } catch {
3670
+ return false;
3671
+ }
3672
+ }
3199
3673
  /**
3200
3674
  * Build the in-repo dependency closure of the `.kici/` package so a
3201
3675
  * `workspace:` sibling's build output exists before the workflow that imports
@@ -3239,7 +3713,10 @@ function describeExecError(e) {
3239
3713
  /** Throw an actionable error when the repo needs pnpm but it is not installed. */
3240
3714
  async function assertPnpmAvailable() {
3241
3715
  try {
3242
- await execFileAsync("pnpm", ["--version"], { timeout: 3e4 });
3716
+ await execFileAsync("pnpm", ["--version"], {
3717
+ timeout: 3e4,
3718
+ cwd: tmpdir()
3719
+ });
3243
3720
  } catch (e) {
3244
3721
  throw new Error(`This repository is a pnpm workspace, but pnpm is not available on this agent. Install pnpm (e.g. \`corepack enable\`) or run on a container/ Firecracker agent that bundles it. (${toErrorMessage(e)})`);
3245
3722
  }
@@ -3253,6 +3730,7 @@ var logger$6, execFileAsync, INSTALL_TIMEOUT_MS, INSTALL_MAX_BUFFER;
3253
3730
  var init_dep_installer = __esmMin((() => {
3254
3731
  init_npm_registry_config();
3255
3732
  init_validate_kici_deps();
3733
+ init_workspace_siblings();
3256
3734
  logger$6 = createLogger({ prefix: "dep-installer" });
3257
3735
  execFileAsync = promisify(execFile);
3258
3736
  INSTALL_TIMEOUT_MS = 6e5;
@@ -3268,13 +3746,18 @@ var init_dep_installer = __esmMin((() => {
3268
3746
  * **repo-root-relative** (cwd = the clone root) so restore is a single layout
3269
3747
  * regardless of package manager:
3270
3748
  *
3271
- * - npm / yarn: just `.kici/node_modules`.
3749
+ * - npm: just `.kici/node_modules`.
3272
3750
  * - pnpm: `.kici/node_modules` plus the repo-root `node_modules/.pnpm` virtual
3273
3751
  * store and the in-repo `workspace:` sibling package directories `.kici`
3274
3752
  * depends on (with their built output). pnpm lays `.kici/node_modules` out as
3275
3753
  * symlinks into the root store and into sibling dirs that live outside
3276
3754
  * `.kici/`, so packing `.kici/node_modules` alone would capture dangling
3277
3755
  * links — the store and siblings must travel together.
3756
+ * - yarn classic: the resolved node_modules root (standalone `.kici` →
3757
+ * `.kici/node_modules`; hoisted workspace member → the repo-root
3758
+ * `node_modules`) plus the in-repo version-range sibling package directories
3759
+ * `.kici` depends on (with their built output), whose symlinks would dangle
3760
+ * otherwise.
3278
3761
  *
3279
3762
  * Uses tar.gz (Node.js built-in zlib, no external binary) in portable mode to
3280
3763
  * strip user/group info for cross-machine consistency; symlinks are preserved
@@ -3288,9 +3771,10 @@ var init_dep_installer = __esmMin((() => {
3288
3771
  * @throws Error if `.kici/node_modules` does not exist.
3289
3772
  */
3290
3773
  async function packNodeModules(kiciDir) {
3291
- if (!existsSync(join(kiciDir, "node_modules"))) throw new Error(`node_modules not found at ${join(kiciDir, "node_modules")}`);
3292
3774
  const workDir = dirname(kiciDir);
3293
3775
  const packageManager = await detectPackageManagerFromManifests(workDir) ?? await detectPackageManagerFromManifests(kiciDir) ?? PackageManager.Npm;
3776
+ const nmRoot = packageManager === PackageManager.Yarn ? resolveYarnNodeModulesRoot(workDir, kiciDir) : join(kiciDir, "node_modules");
3777
+ if (!existsSync(nmRoot)) throw new Error(`node_modules not found at ${nmRoot}`);
3294
3778
  const entries = await closureEntries(workDir, kiciDir, packageManager);
3295
3779
  logger$5.info("Packing dependency closure into tarball", {
3296
3780
  dir: workDir,
@@ -3319,85 +3803,31 @@ async function packNodeModules(kiciDir) {
3319
3803
  };
3320
3804
  }
3321
3805
  /**
3322
- * Compute the repo-root-relative tar entries for the dependency closure. npm /
3323
- * yarn need only `.kici/node_modules`; pnpm additionally needs the root store
3324
- * and the in-repo workspace siblings `.kici` resolves.
3806
+ * Compute the repo-root-relative tar entries for the dependency closure. npm
3807
+ * needs only `.kici/node_modules`. pnpm additionally needs the root store and
3808
+ * the in-repo workspace siblings `.kici` resolves. yarn classic packs the
3809
+ * resolved node_modules root (standalone → `.kici/node_modules`; hoisted
3810
+ * workspace member → root `node_modules`) plus the in-repo version-range
3811
+ * siblings, which it links but does not place in the store.
3325
3812
  */
3326
3813
  async function closureEntries(workDir, kiciDir, packageManager) {
3327
- const kiciNodeModules = relative(workDir, join(kiciDir, "node_modules"));
3328
- if (packageManager !== PackageManager.Pnpm) return [kiciNodeModules];
3329
- const entries = [kiciNodeModules];
3330
- if (existsSync(join(workDir, "node_modules", ".pnpm"))) entries.push(join("node_modules", ".pnpm"));
3331
- for (const sibling of await collectInRepoSiblings(workDir, kiciDir)) entries.push(sibling);
3332
- return entries;
3333
- }
3334
- /**
3335
- * Walk `.kici`'s `node_modules` (and transitively each in-repo sibling's
3336
- * `node_modules`) collecting the repo-relative directories of `workspace:`
3337
- * siblings — package dirs that live inside the clone but outside `.kici/` and
3338
- * outside the root `node_modules/` store. Returns each dir once.
3339
- */
3340
- async function collectInRepoSiblings(workDir, kiciDir) {
3341
- const repoRoot = resolve(workDir);
3342
- const kiciResolved = resolve(kiciDir);
3343
- const rootNodeModules = resolve(join(workDir, "node_modules"));
3344
- const found = /* @__PURE__ */ new Set();
3345
- const visited = /* @__PURE__ */ new Set();
3346
- const queue = [join(kiciDir, "node_modules")];
3347
- while (queue.length > 0) {
3348
- const nmDir = queue.shift();
3349
- const real = await realpath(nmDir).catch(() => null);
3350
- if (!real || visited.has(real)) continue;
3351
- visited.add(real);
3352
- for (const target of await resolveNodeModulesLinks(nmDir)) {
3353
- if (!isInside(repoRoot, target)) continue;
3354
- if (isInside(kiciResolved, target) || isInside(rootNodeModules, target)) continue;
3355
- const rel = relative(workDir, target);
3356
- if (!found.has(rel)) {
3357
- found.add(rel);
3358
- queue.push(join(target, "node_modules"));
3359
- }
3360
- }
3814
+ if (packageManager === PackageManager.Pnpm) {
3815
+ const entries = [relative(workDir, join(kiciDir, "node_modules"))];
3816
+ if (existsSync(join(workDir, "node_modules", ".pnpm"))) entries.push(join("node_modules", ".pnpm"));
3817
+ for (const sibling of await collectInRepoSiblings(workDir, kiciDir)) entries.push(sibling);
3818
+ return entries;
3361
3819
  }
3362
- return [...found];
3363
- }
3364
- /** Resolve every package symlink target under a `node_modules` dir (descending one level into `@scope` dirs). */
3365
- async function resolveNodeModulesLinks(nmDir) {
3366
- const targets = [];
3367
- for (const entry of await readdir(nmDir).catch(() => [])) {
3368
- if (entry.startsWith(".")) continue;
3369
- const entryPath = join(nmDir, entry);
3370
- if (entry.startsWith("@")) {
3371
- for (const scoped of await readdir(entryPath).catch(() => [])) {
3372
- const target = await resolveIfSymlink(join(entryPath, scoped));
3373
- if (target) targets.push(target);
3374
- }
3375
- continue;
3376
- }
3377
- const target = await resolveIfSymlink(entryPath);
3378
- if (target) targets.push(target);
3379
- }
3380
- return targets;
3381
- }
3382
- /** Return the real path of `p` if it is a symlink, else null. */
3383
- async function resolveIfSymlink(p) {
3384
- try {
3385
- if (!(await lstat(p)).isSymbolicLink()) return null;
3386
- return await realpath(p);
3387
- } catch {
3388
- return null;
3820
+ if (packageManager === PackageManager.Yarn) {
3821
+ const nmRoot = resolveYarnNodeModulesRoot(workDir, kiciDir);
3822
+ const entries = [relative(workDir, nmRoot)];
3823
+ for (const sibling of await collectInRepoSiblings(workDir, kiciDir, nmRoot)) entries.push(sibling);
3824
+ return entries;
3389
3825
  }
3390
- }
3391
- /** Whether `target` is `root` itself or a path inside it. */
3392
- function isInside(root, target) {
3393
- const rel = relative(root, target);
3394
- return rel === "" || !rel.startsWith("..") && !rel.startsWith(`..${sep}`) && !isAbsoluteRel(rel);
3395
- }
3396
- function isAbsoluteRel(rel) {
3397
- return rel.length > 1 && rel[1] === ":";
3826
+ return [relative(workDir, join(kiciDir, "node_modules"))];
3398
3827
  }
3399
3828
  var logger$5;
3400
3829
  var init_dep_packer = __esmMin((() => {
3830
+ init_workspace_siblings();
3401
3831
  logger$5 = createLogger({ prefix: "dep-packer" });
3402
3832
  }));
3403
3833
  //#endregion
@@ -3541,6 +3971,8 @@ var init_secret_encryption = __esmMin((() => {
3541
3971
  function buildRequest(dispatch, workDir) {
3542
3972
  const jobConfig = dispatch.jobConfig;
3543
3973
  return {
3974
+ runId: dispatch.runId,
3975
+ jobId: dispatch.jobId,
3544
3976
  workDir,
3545
3977
  repoUrl: dispatch.repoUrl,
3546
3978
  ref: dispatch.ref,
@@ -3553,8 +3985,9 @@ function buildRequest(dispatch, workDir) {
3553
3985
  depsUrl: dispatch.depsUrl,
3554
3986
  depsHash: dispatch.depsHash,
3555
3987
  workflowName: jobConfig.workflowName ?? "",
3556
- jobName: jobConfig.name ?? "",
3988
+ jobName: jobConfig.baseJobName ?? jobConfig.name ?? "",
3557
3989
  runsOn: jobConfig.runsOn ?? "",
3990
+ matrixValues: jobConfig.matrixValues,
3558
3991
  secrets: dispatch.secrets,
3559
3992
  namespacedSecrets: dispatch.namespacedSecrets,
3560
3993
  sourceFile: jobConfig.source?.file,
@@ -3821,6 +4254,42 @@ function relayCacheRequest$1(msg, ctx) {
3821
4254
  error: toErrorMessage(err)
3822
4255
  }));
3823
4256
  }
4257
+ /** Relay `provenance.request` and pipe the orchestrator response (or an error
4258
+ * response, or a "not configured" response when the callback isn't wired) back
4259
+ * into the sandbox runner. */
4260
+ function relayProvenanceRequest$1(msg, ctx) {
4261
+ if (!ctx.execOptions.onProvenanceRequest) {
4262
+ safeSendToChild(ctx.child, {
4263
+ type: "provenance.response",
4264
+ requestId: msg.requestId,
4265
+ error: "Provenance not available in this agent configuration"
4266
+ });
4267
+ return;
4268
+ }
4269
+ ctx.execOptions.onProvenanceRequest(msg).then((response) => safeSendToChild(ctx.child, response), (err) => safeSendToChild(ctx.child, {
4270
+ type: "provenance.response",
4271
+ requestId: msg.requestId,
4272
+ error: toErrorMessage(err)
4273
+ }));
4274
+ }
4275
+ /** Relay `approval.request` and pipe the orchestrator's resolution (or a
4276
+ * fail-closed reject when the callback isn't wired or the relay throws) back
4277
+ * into the sandbox runner. */
4278
+ function relayApprovalRequest$1(msg, ctx) {
4279
+ if (!ctx.execOptions.onApprovalRequest) {
4280
+ safeSendToChild(ctx.child, {
4281
+ type: "approval.resolved",
4282
+ requestId: msg.requestId,
4283
+ error: "Approvals not available in this agent configuration"
4284
+ });
4285
+ return;
4286
+ }
4287
+ ctx.execOptions.onApprovalRequest(msg).then((response) => safeSendToChild(ctx.child, response), (err) => safeSendToChild(ctx.child, {
4288
+ type: "approval.resolved",
4289
+ requestId: msg.requestId,
4290
+ error: toErrorMessage(err)
4291
+ }));
4292
+ }
3824
4293
  /** Resolve the result promise for `job.complete` IPC messages. Encrypts
3825
4294
  * secret outputs (when a runPublicKey is available) and overrides status to
3826
4295
  * `cancelled` if a cancel was already in flight. */
@@ -3893,6 +4362,12 @@ function relayChildIpcMessage(msg, dispatch, ctx) {
3893
4362
  case "cache.request":
3894
4363
  relayCacheRequest$1(msg, ctx);
3895
4364
  return;
4365
+ case "provenance.request":
4366
+ relayProvenanceRequest$1(msg, ctx);
4367
+ return;
4368
+ case "approval.request":
4369
+ relayApprovalRequest$1(msg, ctx);
4370
+ return;
3896
4371
  case "job.complete":
3897
4372
  handleJobComplete(msg, dispatch, ctx);
3898
4373
  return;
@@ -4314,6 +4789,58 @@ function relayCacheRequest(stream, options, cacheMsg) {
4314
4789
  }));
4315
4790
  }
4316
4791
  /**
4792
+ * Relay provenance.request from the container runner to the orchestrator via
4793
+ * options.onProvenanceRequest, then write the response back through `stream`.
4794
+ * If the agent doesn't expose a provenance relay, write a structured error so
4795
+ * the runner doesn't hang.
4796
+ */
4797
+ function relayProvenanceRequest(stream, options, provMsg) {
4798
+ const writeResponse = (response) => {
4799
+ try {
4800
+ stream.write(JSON.stringify(response) + "\n");
4801
+ } catch {}
4802
+ };
4803
+ if (!options.onProvenanceRequest) {
4804
+ writeResponse({
4805
+ type: "provenance.response",
4806
+ requestId: provMsg.requestId,
4807
+ error: "Provenance not available in this agent configuration"
4808
+ });
4809
+ return;
4810
+ }
4811
+ options.onProvenanceRequest(provMsg).then((response) => writeResponse(response), (err) => writeResponse({
4812
+ type: "provenance.response",
4813
+ requestId: provMsg.requestId,
4814
+ error: toErrorMessage(err)
4815
+ }));
4816
+ }
4817
+ /**
4818
+ * Relay approval.request from the container runner to the orchestrator via
4819
+ * options.onApprovalRequest, then write the resolution back through `stream`.
4820
+ * If the agent doesn't expose an approval relay (or it throws), write a
4821
+ * fail-closed reject so the runner doesn't hang.
4822
+ */
4823
+ function relayApprovalRequest(stream, options, approvalMsg) {
4824
+ const writeResponse = (response) => {
4825
+ try {
4826
+ stream.write(JSON.stringify(response) + "\n");
4827
+ } catch {}
4828
+ };
4829
+ if (!options.onApprovalRequest) {
4830
+ writeResponse({
4831
+ type: "approval.resolved",
4832
+ requestId: approvalMsg.requestId,
4833
+ error: "Approvals not available in this agent configuration"
4834
+ });
4835
+ return;
4836
+ }
4837
+ options.onApprovalRequest(approvalMsg).then((response) => writeResponse(response), (err) => writeResponse({
4838
+ type: "approval.resolved",
4839
+ requestId: approvalMsg.requestId,
4840
+ error: toErrorMessage(err)
4841
+ }));
4842
+ }
4843
+ /**
4317
4844
  * Apply a job.complete message to the mutable runner state: capture status,
4318
4845
  * merge any bulk-reported step results, propagate plain outputs, and encrypt
4319
4846
  * secret outputs if a run public key is available.
@@ -4570,6 +5097,12 @@ var init_container_sandbox = __esmMin((() => {
4570
5097
  case "cache.request":
4571
5098
  relayCacheRequest(stream, options, msg);
4572
5099
  return false;
5100
+ case "provenance.request":
5101
+ relayProvenanceRequest(stream, options, msg);
5102
+ return false;
5103
+ case "approval.request":
5104
+ relayApprovalRequest(stream, options, msg);
5105
+ return false;
4573
5106
  case "job.complete":
4574
5107
  applyJobComplete(msg, stepResults, state, options);
4575
5108
  return true;
@@ -4666,7 +5199,7 @@ var job_runner_exports = /* @__PURE__ */ __exportAll({ JobRunner: () => JobRunne
4666
5199
  */
4667
5200
  async function fileExists(p) {
4668
5201
  try {
4669
- await fs.access(p);
5202
+ await fs$1.access(p);
4670
5203
  return true;
4671
5204
  } catch {
4672
5205
  return false;
@@ -4740,6 +5273,8 @@ var init_job_runner = __esmMin((() => {
4740
5273
  _sendConcurrencyReport;
4741
5274
  _sendApiRequest;
4742
5275
  _requestUserCache;
5276
+ _relayProvenance;
5277
+ _sendStepApproval;
4743
5278
  /** Tracks running jobs for concurrency and cancellation */
4744
5279
  activeJobs = /* @__PURE__ */ new Map();
4745
5280
  /** Active sandbox for the current job (used for abort). */
@@ -4758,6 +5293,8 @@ var init_job_runner = __esmMin((() => {
4758
5293
  this._sendConcurrencyReport = deps.sendConcurrencyReport;
4759
5294
  this._sendApiRequest = deps.sendApiRequest;
4760
5295
  this._requestUserCache = deps.requestUserCache;
5296
+ this._relayProvenance = deps.relayProvenance;
5297
+ this._sendStepApproval = deps.sendStepApproval;
4761
5298
  }
4762
5299
  /**
4763
5300
  * Execute a dispatched job through its full lifecycle.
@@ -4768,11 +5305,11 @@ var init_job_runner = __esmMin((() => {
4768
5305
  async execute(dispatch) {
4769
5306
  const { runId: _runId, jobId, jobConfig: _jobConfig } = dispatch;
4770
5307
  const abortController = new AbortController();
4771
- const workDir = await fs.mkdtemp(join(tmpdir(), "kici-"));
5308
+ const workDir = await fs$1.mkdtemp(join(tmpdir(), "kici-"));
4772
5309
  const completionPromise = this.runJob(dispatch, workDir, abortController).finally(async () => {
4773
5310
  this.activeJobs.delete(jobId);
4774
5311
  this.activeSandbox = null;
4775
- await fs.rm(workDir, {
5312
+ await fs$1.rm(workDir, {
4776
5313
  recursive: true,
4777
5314
  force: true
4778
5315
  }).catch(() => {});
@@ -4997,6 +5534,8 @@ var init_job_runner = __esmMin((() => {
4997
5534
  },
4998
5535
  onApiRequest: this._sendApiRequest ? async (method, params) => this._sendApiRequest(method, params) : void 0,
4999
5536
  onCacheRequest: this._requestUserCache ? async (request) => this._requestUserCache(jobId, request) : void 0,
5537
+ onProvenanceRequest: this._relayProvenance ? async (request) => this._relayProvenance(jobId, request) : void 0,
5538
+ onApprovalRequest: this._sendStepApproval ? async (request) => this._sendStepApproval(dispatch.runId, dispatch.jobId, request) : void 0,
5000
5539
  onSecretMount: (event) => {
5001
5540
  this.emitRunEvent(runId, "step.secret_mount", {
5002
5541
  jobId,
@@ -5315,11 +5854,12 @@ var init_job_runner = __esmMin((() => {
5315
5854
  const initResult = await runCaptured(initSink, async () => {
5316
5855
  const { module } = await loadWorkflowSource(workDir, config.source, config.contentHash, config.resolvedHashFiles);
5317
5856
  const workflow = extractWorkflow(module, config.workflowName);
5318
- initLog(`Evaluating dynamic fields for job '${config.targetJobName}' (env=${config.dynamicEnv} environment=${config.dynamicEnvironment} concurrencyGroup=${config.dynamicConcurrencyGroup})`);
5857
+ initLog(`Evaluating dynamic fields for job '${config.targetJobName}' (env=${config.dynamicEnv} environment=${config.dynamicEnvironment} concurrencyGroup=${config.dynamicConcurrencyGroup} matrix=${config.dynamicMatrix ?? false})`);
5319
5858
  return evaluateDynamicFields(workflow, config.targetJobName, config.event, {
5320
5859
  dynamicEnvironment: config.dynamicEnvironment,
5321
5860
  dynamicEnv: config.dynamicEnv,
5322
- dynamicConcurrencyGroup: config.dynamicConcurrencyGroup
5861
+ dynamicConcurrencyGroup: config.dynamicConcurrencyGroup,
5862
+ dynamicMatrix: config.dynamicMatrix ?? false
5323
5863
  }, config.timeoutMs);
5324
5864
  });
5325
5865
  logger$2.info("Init job completed successfully", {
@@ -5669,14 +6209,14 @@ var init_job_runner = __esmMin((() => {
5669
6209
  */
5670
6210
  init_console_capture();
5671
6211
  init_npm_resolver();
5672
- const AGENT_VERSION = "0.1.15";
5673
- const BUILD_COMMIT = "831f6a763";
5674
- const SDK_VERSION = "0.1.15";
5675
- const SDK_BUNDLE_HASH = "cee24b67bbb4a1d2a770e4063dd37a9af33409faeb5ce32f936703b4431316dd";
5676
- const SHARED_VERSION = "0.1.15";
5677
- const SHARED_BUNDLE_HASH = "e82a8b68a7d72698674352158c5de4c0d10824c61bfa7166cc8f846044b8b5e4";
5678
- const ENGINE_VERSION = "0.1.15";
5679
- const ENGINE_BUNDLE_HASH = "032d6b28b3d32bcbea80ba80a52934b017ee2c86a97fcaf8fe8db0d86d72a7f7";
6212
+ const AGENT_VERSION = "0.1.17";
6213
+ const BUILD_COMMIT = "5596f8a3c";
6214
+ const SDK_VERSION = "0.1.17";
6215
+ const SDK_BUNDLE_HASH = "df47ed5db86eaaa2de8394c0db08335f368e8d620a898cc409765f4545eb3972";
6216
+ const SHARED_VERSION = "0.1.17";
6217
+ const SHARED_BUNDLE_HASH = "9e8a753da73b26fb87d08817f70b4d836f67f599385fa268b2c1f68b97996f54";
6218
+ const ENGINE_VERSION = "0.1.17";
6219
+ const ENGINE_BUNDLE_HASH = "706d94fa54a47aea0f69bde105d40213befff401f717604aded2c62a1291cb51";
5680
6220
  initTelemetry({
5681
6221
  serviceName: "kici-agent",
5682
6222
  otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
@@ -5685,6 +6225,26 @@ const { connectionStatus, jobsActive, jobsTotal } = await Promise.resolve().then
5685
6225
  const { JobRunner } = await Promise.resolve().then(() => (init_job_runner(), job_runner_exports));
5686
6226
  setServiceName("agent");
5687
6227
  const logger$1 = createLogger({ prefix: "agent" });
6228
+ /**
6229
+ * Serialize the agent's current Prometheus metrics to text. The OTel
6230
+ * PrometheusExporter exposes no direct serialize method, so the metrics are
6231
+ * piped through its request handler with a mock ServerResponse. Returns an
6232
+ * empty string when no exporter is configured. Shared by the /metrics health
6233
+ * route and the fleet mini-bundle.
6234
+ */
6235
+ async function serializeAgentMetrics() {
6236
+ const exporter = getPrometheusExporter();
6237
+ if (!exporter) return "";
6238
+ return new Promise((resolve) => {
6239
+ exporter.getMetricsRequestHandler({}, {
6240
+ statusCode: 200,
6241
+ setHeader: () => {},
6242
+ end: (data) => {
6243
+ resolve(typeof data === "string" ? data : data ? data.toString() : "");
6244
+ }
6245
+ });
6246
+ });
6247
+ }
5688
6248
  installConsoleCapture();
5689
6249
  await guardStartup(logger$1, async () => {
5690
6250
  const config = loadConfig();
@@ -5736,7 +6296,9 @@ await guardStartup(logger$1, async () => {
5736
6296
  sendRunEvent: (runId, eventType, opts) => client.sendRunEvent(runId, eventType, opts),
5737
6297
  sendConcurrencyReport: (runId, jobId, group) => client.sendConcurrencyReport(runId, jobId, group),
5738
6298
  sendApiRequest: (method, params) => client.sendApiRequest(method, params ?? {}),
5739
- requestUserCache: (jobId, request) => client.requestUserCache(jobId, request)
6299
+ requestUserCache: (jobId, request) => client.requestUserCache(jobId, request),
6300
+ relayProvenance: (jobId, request) => client.relayProvenance(jobId, request),
6301
+ sendStepApproval: (runId, jobId, request) => client.sendStepApproval(runId, jobId, request)
5740
6302
  });
5741
6303
  /** Build and send an agent.status message with dynamic OS metadata. */
5742
6304
  function sendAgentStatus() {
@@ -5752,6 +6314,11 @@ await guardStartup(logger$1, async () => {
5752
6314
  }
5753
6315
  client = new OrchestratorClient({
5754
6316
  ...agentClientConnectionOptions(config),
6317
+ getFleetBundleInputs: async () => ({
6318
+ config,
6319
+ logDir: process.env.KICI_LOG_DIR,
6320
+ metricsText: await serializeAgentMetrics()
6321
+ }),
5755
6322
  onJobDispatch: (dispatch) => {
5756
6323
  const reqId = dispatch.requestId ?? randomUUID();
5757
6324
  requestContext.run({
@@ -5899,25 +6466,10 @@ await guardStartup(logger$1, async () => {
5899
6466
  metricsReporter.start();
5900
6467
  const app = new Hono();
5901
6468
  const healthRoutes = createHealthRoutes$1({
5902
- getMetrics: async () => {
5903
- const exporter = getPrometheusExporter();
5904
- if (!exporter) return {
5905
- contentType: "text/plain",
5906
- body: ""
5907
- };
5908
- return new Promise((resolve) => {
5909
- exporter.getMetricsRequestHandler({}, {
5910
- statusCode: 200,
5911
- setHeader: () => {},
5912
- end: (data) => {
5913
- resolve({
5914
- contentType: "text/plain",
5915
- body: typeof data === "string" ? data : data ? data.toString() : ""
5916
- });
5917
- }
5918
- });
5919
- });
5920
- },
6469
+ getMetrics: async () => ({
6470
+ contentType: "text/plain",
6471
+ body: await serializeAgentMetrics()
6472
+ }),
5921
6473
  getStatus: () => ({
5922
6474
  agentId: config.agentId,
5923
6475
  connected: client.state === "registered",