@kici-dev/agent 0.1.16 → 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.
package/dist/server.js CHANGED
@@ -12,17 +12,17 @@ import winston from "winston";
12
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";
13
13
  import { z } from "zod";
14
14
  import { LOGGER_ENV_VARS, defineEnv, validateUnknownKiciVars } from "@kici-dev/shared/env";
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, 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";
16
16
  import { execFile, execFileSync, execSync, fork, spawn } from "node:child_process";
17
17
  import WebSocket from "ws";
18
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";
19
22
  import { AsyncLocalStorage } from "node:async_hooks";
20
23
  import { format, promisify } from "node:util";
21
- import { existsSync } from "node:fs";
22
- import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
23
24
  import { gcStaleTmpDirs } from "@kici-dev/core/tmp-gc";
24
- import { fileURLToPath, pathToFileURL } from "node:url";
25
- 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";
26
26
  import Docker from "dockerode";
27
27
  import { buildKiciApi, isDynamicGroupRef, isDynamicJobFn, isStaticArray, isStaticObject } from "@kici-dev/sdk";
28
28
  import { c, x } from "tar";
@@ -219,6 +219,35 @@ async function buildAgentMiniBundle(opts) {
219
219
  return Buffer.concat(chunks);
220
220
  }
221
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
222
251
  //#region src/ws/event-buffer.ts
223
252
  /**
224
253
  * In-memory buffer for agent-to-orchestrator messages during disconnection.
@@ -467,6 +496,47 @@ var OrchestratorClient = class OrchestratorClient {
467
496
  });
468
497
  }
469
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
+ /**
470
540
  * Send an event.emit WS message to the orchestrator and await the response.
471
541
  *
472
542
  * Used by the job runner to relay custom event emissions from the sandbox
@@ -628,6 +698,27 @@ var OrchestratorClient = class OrchestratorClient {
628
698
  });
629
699
  }
630
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
+ /**
631
722
  * Relay a step-level approval request to the orchestrator. Sends a
632
723
  * `step.approval-request` WS message and resolves with the orchestrator's
633
724
  * `step.approval-resolved` mapped onto the IPC response shape. No client-side
@@ -814,7 +905,7 @@ var OrchestratorClient = class OrchestratorClient {
814
905
  return;
815
906
  }
816
907
  const rawMsg = raw;
817
- if (rawMsg.type === "cache.upload.response") {
908
+ if (rawMsg.type === "cache.upload.response" || rawMsg.type === "provenance.upload.response") {
818
909
  const pending = this.pendingUploadRequests.get(rawMsg.requestId);
819
910
  if (pending) {
820
911
  this.pendingUploadRequests.delete(rawMsg.requestId);
@@ -1094,6 +1185,10 @@ var OrchestratorClient = class OrchestratorClient {
1094
1185
  totalMemoryMb: Math.round(os.totalmem() / (1024 * 1024)),
1095
1186
  cpuCount: os.cpus().length,
1096
1187
  nodeVersion: process.versions.node,
1188
+ ...(() => {
1189
+ const v = readAgentVersion();
1190
+ return v ? { version: v } : {};
1191
+ })(),
1097
1192
  ...(() => {
1098
1193
  try {
1099
1194
  const info = os.userInfo();
@@ -1202,14 +1297,14 @@ var init_console_capture = __esmMin((() => {
1202
1297
  init_console_capture();
1203
1298
  function safe(name, fallback = "unknown") {
1204
1299
  switch (name) {
1205
- case "version": return "0.1.16";
1206
- case "buildCommit": return "7d97bb32c";
1207
- case "sdkVersion": return "0.1.16";
1208
- case "sdkBundleHash": return "cd4b8e0d91efe578c96e7c746f89b44a3df8fcaebaec2185399168ef8eb4ebca";
1209
- case "sharedVersion": return "0.1.16";
1210
- case "sharedBundleHash": return "c58b1596e92c8423ef83958e86894cb150315d8b91578a0080f1c645000e93e8";
1211
- case "engineVersion": return "0.1.16";
1212
- case "engineBundleHash": return "a611c0017d08faa9c5aa4fd97c3dd6259f53f3cca248bd2b369ad5d30c394847";
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";
1213
1308
  default: return fallback;
1214
1309
  }
1215
1310
  }
@@ -1766,7 +1861,7 @@ async function buildAssetDigestFromResolvedPaths(workDir, resolvedPaths) {
1766
1861
  for (const rel of resolvedPaths) {
1767
1862
  const abs = path.join(workDir, rel);
1768
1863
  try {
1769
- const content = await fs.readFile(abs, "utf-8");
1864
+ const content = await fs$1.readFile(abs, "utf-8");
1770
1865
  parts.push(`${rel}\n${content}`);
1771
1866
  } catch {
1772
1867
  parts.push(`${rel}\n`);
@@ -1791,7 +1886,7 @@ async function loadWorkflowSource(workDir, sourceFile, expectedContentHash, reso
1791
1886
  ensureLoaderHookRegistered();
1792
1887
  const filePath = path.join(workDir, sourceFile);
1793
1888
  if (expectedContentHash) {
1794
- const rawSource = await fs.readFile(filePath, "utf-8");
1889
+ const rawSource = await fs$1.readFile(filePath, "utf-8");
1795
1890
  let assetDigest;
1796
1891
  if (resolvedHashFiles?.length) assetDigest = await buildAssetDigestFromResolvedPaths(workDir, resolvedHashFiles);
1797
1892
  const actualHash = computeContentHash(rawSource, assetDigest);
@@ -1893,8 +1988,8 @@ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event
1893
1988
  }
1894
1989
  var AGENT_SDK_VERSION, AGENT_SDK_BUNDLE_HASH, hookRegistered;
1895
1990
  var init_workflow_loader = __esmMin((() => {
1896
- AGENT_SDK_VERSION = "0.1.16";
1897
- AGENT_SDK_BUNDLE_HASH = "cd4b8e0d91efe578c96e7c746f89b44a3df8fcaebaec2185399168ef8eb4ebca";
1991
+ AGENT_SDK_VERSION = "0.1.17";
1992
+ AGENT_SDK_BUNDLE_HASH = "df47ed5db86eaaa2de8394c0db08335f368e8d620a898cc409765f4545eb3972";
1898
1993
  hookRegistered = false;
1899
1994
  }));
1900
1995
  //#endregion
@@ -2060,24 +2155,24 @@ function resolveOrchestratorUrl(url) {
2060
2155
  * have nothing to race; the defensive `rm` covers re-runs.
2061
2156
  */
2062
2157
  async function moveScratchIntoRepo(scratchDir, workDir) {
2063
- for (const child of await fs.readdir(scratchDir)) if (child === ".kici") {
2158
+ for (const child of await fs$1.readdir(scratchDir)) if (child === ".kici") {
2064
2159
  const kiciScratch = join(scratchDir, ".kici");
2065
- 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));
2066
2161
  } else await moveInto(join(scratchDir, child), join(workDir, child));
2067
2162
  }
2068
2163
  /** Move `src` to `dest`, creating the parent and clearing any stale dest. */
2069
2164
  async function moveInto(src, dest) {
2070
2165
  await mkdir(dirname(dest), { recursive: true });
2071
- await fs.rm(dest, {
2166
+ await fs$1.rm(dest, {
2072
2167
  recursive: true,
2073
2168
  force: true
2074
2169
  });
2075
- await fs.rename(src, dest);
2170
+ await fs$1.rename(src, dest);
2076
2171
  }
2077
2172
  /** Best-effort cleanup of a settled scratch dir; logs and continues on failure. */
2078
2173
  async function cleanupScratch(scratchDir) {
2079
2174
  try {
2080
- await fs.rm(scratchDir, {
2175
+ await fs$1.rm(scratchDir, {
2081
2176
  recursive: true,
2082
2177
  force: true
2083
2178
  });
@@ -2111,7 +2206,7 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
2111
2206
  const kiciDir = join(workDir, ".kici");
2112
2207
  if (depsUrl.startsWith("file://")) {
2113
2208
  const localPath = fileURLToPath(depsUrl);
2114
- const data = await fs.readFile(localPath);
2209
+ const data = await fs$1.readFile(localPath);
2115
2210
  if (depsHash) {
2116
2211
  const actualHash = computeHash(data);
2117
2212
  if (actualHash !== depsHash) throw new Error(`Dep tarball hash mismatch: expected ${depsHash}, got ${actualHash}`);
@@ -2270,7 +2365,7 @@ async function restoreSource(workDir, sourceTarUrl) {
2270
2365
  let data;
2271
2366
  if (sourceTarUrl.startsWith("file://")) {
2272
2367
  const localPath = fileURLToPath(sourceTarUrl);
2273
- data = await fs.readFile(localPath);
2368
+ data = await fs$1.readFile(localPath);
2274
2369
  } else if (sourceTarUrl.startsWith("http://") || sourceTarUrl.startsWith("https://")) data = await downloadUrl(sourceTarUrl);
2275
2370
  else throw new Error(`Unsupported source tarball URL scheme: ${sourceTarUrl}`);
2276
2371
  await extractSourceTarball(data, workDir);
@@ -2339,6 +2434,28 @@ function findJobByName(workflow, jobName) {
2339
2434
  async function evaluateDynamicFields(workflow, jobName, event, flags, timeoutMs = 6e4) {
2340
2435
  const job = findJobByName(workflow, jobName);
2341
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
+ }
2342
2459
  if (flags.dynamicEnvironment && typeof job.environment === "function") {
2343
2460
  const value = await withTimeout(() => job.environment(event), timeoutMs, `dynamicEnvironment for job '${jobName}'`);
2344
2461
  if (value !== void 0 && value !== null) result.environmentName = value;
@@ -2541,6 +2658,7 @@ var MatrixExpansionError, DYNAMIC_FIELD_TIMEOUT_MS;
2541
2658
  var init_dynamic_job_serializer = __esmMin((() => {
2542
2659
  init_timeout_util();
2543
2660
  MatrixExpansionError = class MatrixExpansionError extends Error {
2661
+ jobName;
2544
2662
  name = "MatrixExpansionError";
2545
2663
  constructor(jobName, message) {
2546
2664
  super(message);
@@ -2814,7 +2932,7 @@ function decryptBuffer(encrypted, aesKey) {
2814
2932
  */
2815
2933
  async function applyOverlay(config) {
2816
2934
  const { tarballUrl, cliPublicKey, orchestratorPrivateKey, repoDir } = config;
2817
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "kici-overlay-"));
2935
+ const tmpDir = await fs$1.mkdtemp(path.join(os.tmpdir(), "kici-overlay-"));
2818
2936
  try {
2819
2937
  logger$7.info("Downloading overlay tarball", { url: tarballUrl.replace(/\?.*$/, "?[redacted]") });
2820
2938
  let encryptedData;
@@ -2828,7 +2946,7 @@ async function applyOverlay(config) {
2828
2946
  const decryptedData = decryptBuffer(encryptedData, aesKey);
2829
2947
  logger$7.info("Extracting overlay tarball", { size: decryptedData.length });
2830
2948
  const extractDir = path.join(tmpDir, "extracted");
2831
- await fs.mkdir(extractDir, { recursive: true });
2949
+ await fs$1.mkdir(extractDir, { recursive: true });
2832
2950
  try {
2833
2951
  const readable = Readable.from(decryptedData);
2834
2952
  await new Promise((resolve, reject) => {
@@ -2843,7 +2961,7 @@ async function applyOverlay(config) {
2843
2961
  const manifestPath = path.join(extractDir, ".kici-overlay-tmp", "manifest.json");
2844
2962
  let manifestContent;
2845
2963
  try {
2846
- manifestContent = await fs.readFile(manifestPath, "utf-8");
2964
+ manifestContent = await fs$1.readFile(manifestPath, "utf-8");
2847
2965
  } catch {
2848
2966
  throw new Error("Overlay manifest not found: expected .kici-overlay-tmp/manifest.json in tarball");
2849
2967
  }
@@ -2864,15 +2982,15 @@ async function applyOverlay(config) {
2864
2982
  for (const file of checksumFiles) {
2865
2983
  const srcPath = path.join(extractDir, file);
2866
2984
  const destPath = path.join(repoDir, file);
2867
- await fs.mkdir(path.dirname(destPath), { recursive: true });
2868
- await fs.copyFile(srcPath, destPath);
2985
+ await fs$1.mkdir(path.dirname(destPath), { recursive: true });
2986
+ await fs$1.copyFile(srcPath, destPath);
2869
2987
  filesApplied++;
2870
2988
  }
2871
2989
  let filesDeleted = 0;
2872
2990
  for (const file of manifest.deletions) {
2873
2991
  const targetPath = path.join(repoDir, file);
2874
2992
  try {
2875
- await fs.unlink(targetPath);
2993
+ await fs$1.unlink(targetPath);
2876
2994
  filesDeleted++;
2877
2995
  } catch {
2878
2996
  logger$7.debug("Deletion target not found, skipping", { file });
@@ -2888,7 +3006,7 @@ async function applyOverlay(config) {
2888
3006
  verified: true
2889
3007
  };
2890
3008
  } finally {
2891
- await fs.rm(tmpDir, {
3009
+ await fs$1.rm(tmpDir, {
2892
3010
  recursive: true,
2893
3011
  force: true
2894
3012
  }).catch(() => {});
@@ -3037,6 +3155,11 @@ var init_npm_registry_config = __esmMin((() => {}));
3037
3155
  * clones the whole repo, so an in-repo sibling is present), and resolves
3038
3156
  * `file:`/`link:`/`portal:` against a path — allowed when that path stays
3039
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.)
3040
3163
  *
3041
3164
  * This module performs that classification so unresolvable specifiers fail
3042
3165
  * fast with guidance rather than a cryptic install error.
@@ -3112,6 +3235,17 @@ function isInsideRepo(repoRoot, target) {
3112
3235
  */
3113
3236
  async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
3114
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
+ }
3115
3249
  const hasWorkspaceFile = await fileExists$1(join(repoRoot, "pnpm-workspace.yaml"));
3116
3250
  const unresolvable = [];
3117
3251
  for (const dep of deps) {
@@ -3127,6 +3261,7 @@ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
3127
3261
  function formatUnresolvableDepError(offenders, packageManager) {
3128
3262
  const list = offenders.map((o) => `${o.name}: ${o.spec}`).join(", ");
3129
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.)`;
3130
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.`;
3131
3266
  }
3132
3267
  /**
@@ -3160,6 +3295,102 @@ var init_validate_kici_deps = __esmMin((() => {
3160
3295
  ];
3161
3296
  }));
3162
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
3163
3394
  //#region src/execution/dep-installer.ts
3164
3395
  /**
3165
3396
  * Inline dependency installation for graceful degradation.
@@ -3167,12 +3398,14 @@ var init_validate_kici_deps = __esmMin((() => {
3167
3398
  * When the dep cache is unavailable or a download fails, the agent installs
3168
3399
  * `.kici/` dependencies directly with the repository's package manager.
3169
3400
  *
3170
- * 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
3171
3402
  * presence of `.kici/package.json` signals that deps should be installed. npm
3172
3403
  * is the default and ships with every Node.js install; pnpm is used when the
3173
3404
  * repo is a pnpm workspace so a `.kici/` member can resolve in-repo
3174
- * `workspace:` siblings. yarn is detected but not yet supported and is
3175
- * 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.
3176
3409
  *
3177
3410
  * Security: the install runs with an isolated per-invocation cache/store
3178
3411
  * directory to prevent cache poisoning across build jobs — a malicious
@@ -3216,7 +3449,6 @@ async function installDeps(kiciDir, opts = {}) {
3216
3449
  dir: kiciDir
3217
3450
  });
3218
3451
  process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, cwd=${kiciDir}\n`);
3219
- 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.");
3220
3452
  await assertResolvableDeps({
3221
3453
  kiciDir,
3222
3454
  repoRoot,
@@ -3236,6 +3468,11 @@ async function installDeps(kiciDir, opts = {}) {
3236
3468
  hasPrivateRegistry,
3237
3469
  registryConfig
3238
3470
  });
3471
+ else if (packageManager === PackageManager.Yarn) await runYarnInstall({
3472
+ kiciDir,
3473
+ hasPrivateRegistry,
3474
+ registryConfig
3475
+ });
3239
3476
  else await runNpmInstall({
3240
3477
  kiciDir,
3241
3478
  hasPrivateRegistry,
@@ -3250,6 +3487,7 @@ async function installDeps(kiciDir, opts = {}) {
3250
3487
  await registryConfig.cleanup();
3251
3488
  }
3252
3489
  if (packageManager === PackageManager.Pnpm && await kiciHasLocalProtocolDeps(kiciDir)) await buildWorkspaceClosure(repoRoot);
3490
+ if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir);
3253
3491
  const durationMs = Date.now() - startTime;
3254
3492
  process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
3255
3493
  logger$6.info("Deps installed inline", {
@@ -3337,6 +3575,101 @@ async function runPnpmInstall(args) {
3337
3575
  }).catch(() => {});
3338
3576
  }
3339
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
+ }
3340
3673
  /**
3341
3674
  * Build the in-repo dependency closure of the `.kici/` package so a
3342
3675
  * `workspace:` sibling's build output exists before the workflow that imports
@@ -3380,7 +3713,10 @@ function describeExecError(e) {
3380
3713
  /** Throw an actionable error when the repo needs pnpm but it is not installed. */
3381
3714
  async function assertPnpmAvailable() {
3382
3715
  try {
3383
- await execFileAsync("pnpm", ["--version"], { timeout: 3e4 });
3716
+ await execFileAsync("pnpm", ["--version"], {
3717
+ timeout: 3e4,
3718
+ cwd: tmpdir()
3719
+ });
3384
3720
  } catch (e) {
3385
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)})`);
3386
3722
  }
@@ -3394,6 +3730,7 @@ var logger$6, execFileAsync, INSTALL_TIMEOUT_MS, INSTALL_MAX_BUFFER;
3394
3730
  var init_dep_installer = __esmMin((() => {
3395
3731
  init_npm_registry_config();
3396
3732
  init_validate_kici_deps();
3733
+ init_workspace_siblings();
3397
3734
  logger$6 = createLogger({ prefix: "dep-installer" });
3398
3735
  execFileAsync = promisify(execFile);
3399
3736
  INSTALL_TIMEOUT_MS = 6e5;
@@ -3409,13 +3746,18 @@ var init_dep_installer = __esmMin((() => {
3409
3746
  * **repo-root-relative** (cwd = the clone root) so restore is a single layout
3410
3747
  * regardless of package manager:
3411
3748
  *
3412
- * - npm / yarn: just `.kici/node_modules`.
3749
+ * - npm: just `.kici/node_modules`.
3413
3750
  * - pnpm: `.kici/node_modules` plus the repo-root `node_modules/.pnpm` virtual
3414
3751
  * store and the in-repo `workspace:` sibling package directories `.kici`
3415
3752
  * depends on (with their built output). pnpm lays `.kici/node_modules` out as
3416
3753
  * symlinks into the root store and into sibling dirs that live outside
3417
3754
  * `.kici/`, so packing `.kici/node_modules` alone would capture dangling
3418
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.
3419
3761
  *
3420
3762
  * Uses tar.gz (Node.js built-in zlib, no external binary) in portable mode to
3421
3763
  * strip user/group info for cross-machine consistency; symlinks are preserved
@@ -3429,9 +3771,10 @@ var init_dep_installer = __esmMin((() => {
3429
3771
  * @throws Error if `.kici/node_modules` does not exist.
3430
3772
  */
3431
3773
  async function packNodeModules(kiciDir) {
3432
- if (!existsSync(join(kiciDir, "node_modules"))) throw new Error(`node_modules not found at ${join(kiciDir, "node_modules")}`);
3433
3774
  const workDir = dirname(kiciDir);
3434
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}`);
3435
3778
  const entries = await closureEntries(workDir, kiciDir, packageManager);
3436
3779
  logger$5.info("Packing dependency closure into tarball", {
3437
3780
  dir: workDir,
@@ -3460,85 +3803,31 @@ async function packNodeModules(kiciDir) {
3460
3803
  };
3461
3804
  }
3462
3805
  /**
3463
- * Compute the repo-root-relative tar entries for the dependency closure. npm /
3464
- * yarn need only `.kici/node_modules`; pnpm additionally needs the root store
3465
- * 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.
3466
3812
  */
3467
3813
  async function closureEntries(workDir, kiciDir, packageManager) {
3468
- const kiciNodeModules = relative(workDir, join(kiciDir, "node_modules"));
3469
- if (packageManager !== PackageManager.Pnpm) return [kiciNodeModules];
3470
- const entries = [kiciNodeModules];
3471
- if (existsSync(join(workDir, "node_modules", ".pnpm"))) entries.push(join("node_modules", ".pnpm"));
3472
- for (const sibling of await collectInRepoSiblings(workDir, kiciDir)) entries.push(sibling);
3473
- return entries;
3474
- }
3475
- /**
3476
- * Walk `.kici`'s `node_modules` (and transitively each in-repo sibling's
3477
- * `node_modules`) collecting the repo-relative directories of `workspace:`
3478
- * siblings — package dirs that live inside the clone but outside `.kici/` and
3479
- * outside the root `node_modules/` store. Returns each dir once.
3480
- */
3481
- async function collectInRepoSiblings(workDir, kiciDir) {
3482
- const repoRoot = resolve(workDir);
3483
- const kiciResolved = resolve(kiciDir);
3484
- const rootNodeModules = resolve(join(workDir, "node_modules"));
3485
- const found = /* @__PURE__ */ new Set();
3486
- const visited = /* @__PURE__ */ new Set();
3487
- const queue = [join(kiciDir, "node_modules")];
3488
- while (queue.length > 0) {
3489
- const nmDir = queue.shift();
3490
- const real = await realpath(nmDir).catch(() => null);
3491
- if (!real || visited.has(real)) continue;
3492
- visited.add(real);
3493
- for (const target of await resolveNodeModulesLinks(nmDir)) {
3494
- if (!isInside(repoRoot, target)) continue;
3495
- if (isInside(kiciResolved, target) || isInside(rootNodeModules, target)) continue;
3496
- const rel = relative(workDir, target);
3497
- if (!found.has(rel)) {
3498
- found.add(rel);
3499
- queue.push(join(target, "node_modules"));
3500
- }
3501
- }
3502
- }
3503
- return [...found];
3504
- }
3505
- /** Resolve every package symlink target under a `node_modules` dir (descending one level into `@scope` dirs). */
3506
- async function resolveNodeModulesLinks(nmDir) {
3507
- const targets = [];
3508
- for (const entry of await readdir(nmDir).catch(() => [])) {
3509
- if (entry.startsWith(".")) continue;
3510
- const entryPath = join(nmDir, entry);
3511
- if (entry.startsWith("@")) {
3512
- for (const scoped of await readdir(entryPath).catch(() => [])) {
3513
- const target = await resolveIfSymlink(join(entryPath, scoped));
3514
- if (target) targets.push(target);
3515
- }
3516
- continue;
3517
- }
3518
- const target = await resolveIfSymlink(entryPath);
3519
- if (target) targets.push(target);
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;
3520
3819
  }
3521
- return targets;
3522
- }
3523
- /** Return the real path of `p` if it is a symlink, else null. */
3524
- async function resolveIfSymlink(p) {
3525
- try {
3526
- if (!(await lstat(p)).isSymbolicLink()) return null;
3527
- return await realpath(p);
3528
- } catch {
3529
- 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;
3530
3825
  }
3531
- }
3532
- /** Whether `target` is `root` itself or a path inside it. */
3533
- function isInside(root, target) {
3534
- const rel = relative(root, target);
3535
- return rel === "" || !rel.startsWith("..") && !rel.startsWith(`..${sep}`) && !isAbsoluteRel(rel);
3536
- }
3537
- function isAbsoluteRel(rel) {
3538
- return rel.length > 1 && rel[1] === ":";
3826
+ return [relative(workDir, join(kiciDir, "node_modules"))];
3539
3827
  }
3540
3828
  var logger$5;
3541
3829
  var init_dep_packer = __esmMin((() => {
3830
+ init_workspace_siblings();
3542
3831
  logger$5 = createLogger({ prefix: "dep-packer" });
3543
3832
  }));
3544
3833
  //#endregion
@@ -3682,6 +3971,8 @@ var init_secret_encryption = __esmMin((() => {
3682
3971
  function buildRequest(dispatch, workDir) {
3683
3972
  const jobConfig = dispatch.jobConfig;
3684
3973
  return {
3974
+ runId: dispatch.runId,
3975
+ jobId: dispatch.jobId,
3685
3976
  workDir,
3686
3977
  repoUrl: dispatch.repoUrl,
3687
3978
  ref: dispatch.ref,
@@ -3694,8 +3985,9 @@ function buildRequest(dispatch, workDir) {
3694
3985
  depsUrl: dispatch.depsUrl,
3695
3986
  depsHash: dispatch.depsHash,
3696
3987
  workflowName: jobConfig.workflowName ?? "",
3697
- jobName: jobConfig.name ?? "",
3988
+ jobName: jobConfig.baseJobName ?? jobConfig.name ?? "",
3698
3989
  runsOn: jobConfig.runsOn ?? "",
3990
+ matrixValues: jobConfig.matrixValues,
3699
3991
  secrets: dispatch.secrets,
3700
3992
  namespacedSecrets: dispatch.namespacedSecrets,
3701
3993
  sourceFile: jobConfig.source?.file,
@@ -3962,6 +4254,24 @@ function relayCacheRequest$1(msg, ctx) {
3962
4254
  error: toErrorMessage(err)
3963
4255
  }));
3964
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
+ }
3965
4275
  /** Relay `approval.request` and pipe the orchestrator's resolution (or a
3966
4276
  * fail-closed reject when the callback isn't wired or the relay throws) back
3967
4277
  * into the sandbox runner. */
@@ -4052,6 +4362,9 @@ function relayChildIpcMessage(msg, dispatch, ctx) {
4052
4362
  case "cache.request":
4053
4363
  relayCacheRequest$1(msg, ctx);
4054
4364
  return;
4365
+ case "provenance.request":
4366
+ relayProvenanceRequest$1(msg, ctx);
4367
+ return;
4055
4368
  case "approval.request":
4056
4369
  relayApprovalRequest$1(msg, ctx);
4057
4370
  return;
@@ -4476,6 +4789,32 @@ function relayCacheRequest(stream, options, cacheMsg) {
4476
4789
  }));
4477
4790
  }
4478
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
+ /**
4479
4818
  * Relay approval.request from the container runner to the orchestrator via
4480
4819
  * options.onApprovalRequest, then write the resolution back through `stream`.
4481
4820
  * If the agent doesn't expose an approval relay (or it throws), write a
@@ -4758,6 +5097,9 @@ var init_container_sandbox = __esmMin((() => {
4758
5097
  case "cache.request":
4759
5098
  relayCacheRequest(stream, options, msg);
4760
5099
  return false;
5100
+ case "provenance.request":
5101
+ relayProvenanceRequest(stream, options, msg);
5102
+ return false;
4761
5103
  case "approval.request":
4762
5104
  relayApprovalRequest(stream, options, msg);
4763
5105
  return false;
@@ -4857,7 +5199,7 @@ var job_runner_exports = /* @__PURE__ */ __exportAll({ JobRunner: () => JobRunne
4857
5199
  */
4858
5200
  async function fileExists(p) {
4859
5201
  try {
4860
- await fs.access(p);
5202
+ await fs$1.access(p);
4861
5203
  return true;
4862
5204
  } catch {
4863
5205
  return false;
@@ -4931,6 +5273,7 @@ var init_job_runner = __esmMin((() => {
4931
5273
  _sendConcurrencyReport;
4932
5274
  _sendApiRequest;
4933
5275
  _requestUserCache;
5276
+ _relayProvenance;
4934
5277
  _sendStepApproval;
4935
5278
  /** Tracks running jobs for concurrency and cancellation */
4936
5279
  activeJobs = /* @__PURE__ */ new Map();
@@ -4950,6 +5293,7 @@ var init_job_runner = __esmMin((() => {
4950
5293
  this._sendConcurrencyReport = deps.sendConcurrencyReport;
4951
5294
  this._sendApiRequest = deps.sendApiRequest;
4952
5295
  this._requestUserCache = deps.requestUserCache;
5296
+ this._relayProvenance = deps.relayProvenance;
4953
5297
  this._sendStepApproval = deps.sendStepApproval;
4954
5298
  }
4955
5299
  /**
@@ -4961,11 +5305,11 @@ var init_job_runner = __esmMin((() => {
4961
5305
  async execute(dispatch) {
4962
5306
  const { runId: _runId, jobId, jobConfig: _jobConfig } = dispatch;
4963
5307
  const abortController = new AbortController();
4964
- const workDir = await fs.mkdtemp(join(tmpdir(), "kici-"));
5308
+ const workDir = await fs$1.mkdtemp(join(tmpdir(), "kici-"));
4965
5309
  const completionPromise = this.runJob(dispatch, workDir, abortController).finally(async () => {
4966
5310
  this.activeJobs.delete(jobId);
4967
5311
  this.activeSandbox = null;
4968
- await fs.rm(workDir, {
5312
+ await fs$1.rm(workDir, {
4969
5313
  recursive: true,
4970
5314
  force: true
4971
5315
  }).catch(() => {});
@@ -5190,6 +5534,7 @@ var init_job_runner = __esmMin((() => {
5190
5534
  },
5191
5535
  onApiRequest: this._sendApiRequest ? async (method, params) => this._sendApiRequest(method, params) : void 0,
5192
5536
  onCacheRequest: this._requestUserCache ? async (request) => this._requestUserCache(jobId, request) : void 0,
5537
+ onProvenanceRequest: this._relayProvenance ? async (request) => this._relayProvenance(jobId, request) : void 0,
5193
5538
  onApprovalRequest: this._sendStepApproval ? async (request) => this._sendStepApproval(dispatch.runId, dispatch.jobId, request) : void 0,
5194
5539
  onSecretMount: (event) => {
5195
5540
  this.emitRunEvent(runId, "step.secret_mount", {
@@ -5509,11 +5854,12 @@ var init_job_runner = __esmMin((() => {
5509
5854
  const initResult = await runCaptured(initSink, async () => {
5510
5855
  const { module } = await loadWorkflowSource(workDir, config.source, config.contentHash, config.resolvedHashFiles);
5511
5856
  const workflow = extractWorkflow(module, config.workflowName);
5512
- 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})`);
5513
5858
  return evaluateDynamicFields(workflow, config.targetJobName, config.event, {
5514
5859
  dynamicEnvironment: config.dynamicEnvironment,
5515
5860
  dynamicEnv: config.dynamicEnv,
5516
- dynamicConcurrencyGroup: config.dynamicConcurrencyGroup
5861
+ dynamicConcurrencyGroup: config.dynamicConcurrencyGroup,
5862
+ dynamicMatrix: config.dynamicMatrix ?? false
5517
5863
  }, config.timeoutMs);
5518
5864
  });
5519
5865
  logger$2.info("Init job completed successfully", {
@@ -5863,14 +6209,14 @@ var init_job_runner = __esmMin((() => {
5863
6209
  */
5864
6210
  init_console_capture();
5865
6211
  init_npm_resolver();
5866
- const AGENT_VERSION = "0.1.16";
5867
- const BUILD_COMMIT = "7d97bb32c";
5868
- const SDK_VERSION = "0.1.16";
5869
- const SDK_BUNDLE_HASH = "cd4b8e0d91efe578c96e7c746f89b44a3df8fcaebaec2185399168ef8eb4ebca";
5870
- const SHARED_VERSION = "0.1.16";
5871
- const SHARED_BUNDLE_HASH = "c58b1596e92c8423ef83958e86894cb150315d8b91578a0080f1c645000e93e8";
5872
- const ENGINE_VERSION = "0.1.16";
5873
- const ENGINE_BUNDLE_HASH = "a611c0017d08faa9c5aa4fd97c3dd6259f53f3cca248bd2b369ad5d30c394847";
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";
5874
6220
  initTelemetry({
5875
6221
  serviceName: "kici-agent",
5876
6222
  otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
@@ -5951,6 +6297,7 @@ await guardStartup(logger$1, async () => {
5951
6297
  sendConcurrencyReport: (runId, jobId, group) => client.sendConcurrencyReport(runId, jobId, group),
5952
6298
  sendApiRequest: (method, params) => client.sendApiRequest(method, params ?? {}),
5953
6299
  requestUserCache: (jobId, request) => client.requestUserCache(jobId, request),
6300
+ relayProvenance: (jobId, request) => client.relayProvenance(jobId, request),
5954
6301
  sendStepApproval: (runId, jobId, request) => client.sendStepApproval(runId, jobId, request)
5955
6302
  });
5956
6303
  /** Build and send an agent.status message with dynamic OS metadata. */