@kici-dev/agent 0.1.16 → 0.1.18

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/execution/dep-installer.d.ts +12 -3
  2. package/dist/execution/dep-packer.d.ts +9 -1
  3. package/dist/execution/env-init/presets/directives.d.ts +20 -0
  4. package/dist/execution/env-init/presets/expand.d.ts +16 -0
  5. package/dist/execution/env-init/presets/mise/cache-key.d.ts +7 -0
  6. package/dist/execution/env-init/presets/mise/expander.d.ts +16 -0
  7. package/dist/execution/env-init/presets/mise/templates.d.ts +16 -0
  8. package/dist/execution/env-init/presets/mise/windows-install.d.ts +18 -0
  9. package/dist/execution/env-init/presets/registry.d.ts +31 -0
  10. package/dist/execution/init-runner.d.ts +7 -0
  11. package/dist/execution/job-runner.d.ts +9 -1
  12. package/dist/execution/sandbox/env-delta.d.ts +2 -0
  13. package/dist/execution/sandbox/index.d.ts +1 -1
  14. package/dist/execution/sandbox/ipc-protocol.d.ts +81 -3
  15. package/dist/execution/sandbox/types.d.ts +9 -1
  16. package/dist/execution/sandbox/workflow-runner.d.ts +12 -7
  17. package/dist/execution/validate-kici-deps.d.ts +10 -2
  18. package/dist/execution/workflow-loader.d.ts +5 -1
  19. package/dist/execution/workspace-siblings.d.ts +33 -0
  20. package/dist/execution/yarnrc-berry-config.d.ts +23 -0
  21. package/dist/index.js +423 -17
  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 +703 -160
  26. package/dist/version.d.ts +2 -0
  27. package/dist/workflow-runner.js +955 -59
  28. package/dist/ws/orchestrator-client.d.ts +16 -1
  29. package/package.json +14 -12
  30. package/sbom.spdx.json +2526 -5994
package/dist/server.js CHANGED
@@ -12,25 +12,27 @@ 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
- import archiver from "archiver";
18
+ import { ZipArchive } 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
- import { buildKiciApi, isDynamicGroupRef, isDynamicJobFn, isStaticArray, isStaticObject } from "@kici-dev/sdk";
27
+ import { buildKiciApi, buildNeedsContext, isDynamicGroupRef, isDynamicJobFn, isStaticArray, isStaticObject } from "@kici-dev/sdk";
28
28
  import { c, x } from "tar";
29
29
  import https from "node:https";
30
30
  import http from "node:http";
31
31
  import { pipeline } from "node:stream/promises";
32
32
  import { createGunzip } from "node:zlib";
33
- import { PNPM_IGNORE_BUILD_GATE_ARG, PackageManager, detectPackageManagerFromManifests } from "@kici-dev/shared/package-manager";
33
+ import { normalizeRunsOnToMatchers } from "@kici-dev/engine/labels/compile";
34
+ import { PNPM_IGNORE_BUILD_GATE_ARG, PackageManager, YarnFlavor, detectPackageManagerFromManifests, detectYarnFlavor } from "@kici-dev/shared/package-manager";
35
+ import { parse, stringify } from "yaml";
34
36
  import { createInterface } from "node:readline";
35
37
  var __defProp = Object.defineProperty;
36
38
  var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -189,7 +191,7 @@ function agentClientConnectionOptions(config) {
189
191
  * lean subset of the orchestrator's createDebugBundle.
190
192
  */
191
193
  async function buildAgentMiniBundle(opts) {
192
- const archive = archiver("zip", { zlib: { level: 6 } });
194
+ const archive = new ZipArchive({ zlib: { level: 6 } });
193
195
  const chunks = [];
194
196
  archive.on("data", (d) => chunks.push(d));
195
197
  const done = new Promise((resolve, reject) => {
@@ -219,6 +221,35 @@ async function buildAgentMiniBundle(opts) {
219
221
  return Buffer.concat(chunks);
220
222
  }
221
223
  //#endregion
224
+ //#region src/version.ts
225
+ /**
226
+ * Read the agent's own package version from disk.
227
+ *
228
+ * Walks up from this module's location until it finds a package.json whose
229
+ * name is '@kici-dev/agent'. This is robust to the agent's bundled dist layout
230
+ * (build-service.mjs emits dist/index.js / dist/server.js, so a fixed relative
231
+ * depth would be wrong). Returns null when it can't be resolved so the
232
+ * agent.register message simply omits the field rather than failing.
233
+ */
234
+ function readAgentVersion() {
235
+ try {
236
+ let dir = path.dirname(fileURLToPath(import.meta.url));
237
+ for (let i = 0; i < 8; i++) {
238
+ const candidate = path.join(dir, "package.json");
239
+ if (fs.existsSync(candidate)) {
240
+ const pkg = JSON.parse(fs.readFileSync(candidate, "utf-8"));
241
+ if (pkg.name === "@kici-dev/agent" && typeof pkg.version === "string") return pkg.version;
242
+ }
243
+ const parent = path.dirname(dir);
244
+ if (parent === dir) break;
245
+ dir = parent;
246
+ }
247
+ return null;
248
+ } catch {
249
+ return null;
250
+ }
251
+ }
252
+ //#endregion
222
253
  //#region src/ws/event-buffer.ts
223
254
  /**
224
255
  * In-memory buffer for agent-to-orchestrator messages during disconnection.
@@ -467,6 +498,47 @@ var OrchestratorClient = class OrchestratorClient {
467
498
  });
468
499
  }
469
500
  /**
501
+ * Request a presigned PUT URL for a provenance bundle. Sends a
502
+ * `provenance.upload.request` and waits for a `provenance.upload.response`
503
+ * (resolved via the shared upload-request pending map). Times out after 30s.
504
+ */
505
+ async requestProvenanceUploadUrl(jobId, subjectDigest) {
506
+ const messageId = randomUUID();
507
+ return new Promise((resolve, reject) => {
508
+ const timer = setTimeout(() => {
509
+ this.pendingUploadRequests.delete(messageId);
510
+ reject(/* @__PURE__ */ new Error("Provenance upload URL request timed out (30s)"));
511
+ }, 3e4);
512
+ this.pendingUploadRequests.set(messageId, {
513
+ resolve: (url) => {
514
+ clearTimeout(timer);
515
+ resolve(url);
516
+ },
517
+ reject: (err) => {
518
+ clearTimeout(timer);
519
+ reject(err);
520
+ }
521
+ });
522
+ this.sendDirect({
523
+ type: "provenance.upload.request",
524
+ messageId,
525
+ jobId,
526
+ subjectDigest
527
+ });
528
+ });
529
+ }
530
+ /** Notify the orchestrator a provenance bundle upload completed (records an attestations row). */
531
+ sendProvenanceUploadComplete(jobId, subjectName, subjectDigest, mediaType) {
532
+ this.sendDirect({
533
+ type: "provenance.upload.complete",
534
+ messageId: randomUUID(),
535
+ jobId,
536
+ subjectName,
537
+ subjectDigest,
538
+ mediaType
539
+ });
540
+ }
541
+ /**
470
542
  * Send an event.emit WS message to the orchestrator and await the response.
471
543
  *
472
544
  * Used by the job runner to relay custom event emissions from the sandbox
@@ -628,6 +700,27 @@ var OrchestratorClient = class OrchestratorClient {
628
700
  });
629
701
  }
630
702
  /**
703
+ * Relay a provenance bundle upload operation to the orchestrator. Maps the
704
+ * IPC `provenance.request` onto `requestProvenanceUploadUrl` (returns the
705
+ * presigned URL) or `sendProvenanceUploadComplete` (fire-and-forget) and
706
+ * returns the result on the IPC response shape.
707
+ */
708
+ async relayProvenance(jobId, request) {
709
+ if (request.op === "complete") {
710
+ this.sendProvenanceUploadComplete(jobId, request.subjectName, request.subjectDigest, request.mediaType);
711
+ return {
712
+ type: "provenance.response",
713
+ requestId: request.requestId
714
+ };
715
+ }
716
+ const uploadUrl = await this.requestProvenanceUploadUrl(jobId, request.subjectDigest);
717
+ return {
718
+ type: "provenance.response",
719
+ requestId: request.requestId,
720
+ uploadUrl
721
+ };
722
+ }
723
+ /**
631
724
  * Relay a step-level approval request to the orchestrator. Sends a
632
725
  * `step.approval-request` WS message and resolves with the orchestrator's
633
726
  * `step.approval-resolved` mapped onto the IPC response shape. No client-side
@@ -814,7 +907,7 @@ var OrchestratorClient = class OrchestratorClient {
814
907
  return;
815
908
  }
816
909
  const rawMsg = raw;
817
- if (rawMsg.type === "cache.upload.response") {
910
+ if (rawMsg.type === "cache.upload.response" || rawMsg.type === "provenance.upload.response") {
818
911
  const pending = this.pendingUploadRequests.get(rawMsg.requestId);
819
912
  if (pending) {
820
913
  this.pendingUploadRequests.delete(rawMsg.requestId);
@@ -1094,6 +1187,10 @@ var OrchestratorClient = class OrchestratorClient {
1094
1187
  totalMemoryMb: Math.round(os.totalmem() / (1024 * 1024)),
1095
1188
  cpuCount: os.cpus().length,
1096
1189
  nodeVersion: process.versions.node,
1190
+ ...(() => {
1191
+ const v = readAgentVersion();
1192
+ return v ? { version: v } : {};
1193
+ })(),
1097
1194
  ...(() => {
1098
1195
  try {
1099
1196
  const info = os.userInfo();
@@ -1202,14 +1299,14 @@ var init_console_capture = __esmMin((() => {
1202
1299
  init_console_capture();
1203
1300
  function safe(name, fallback = "unknown") {
1204
1301
  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";
1302
+ case "version": return "0.1.18";
1303
+ case "buildCommit": return "d8cff38bb";
1304
+ case "sdkVersion": return "0.1.18";
1305
+ case "sdkBundleHash": return "8308089347c304e41b457d3867b17bbff11d6b5cd9706b6823e7abdbd849f33f";
1306
+ case "sharedVersion": return "0.1.18";
1307
+ case "sharedBundleHash": return "5f2c220f24d166b0f13d0620a2e80d19689ac8b683a12154daef44e938fd46a7";
1308
+ case "engineVersion": return "0.1.18";
1309
+ case "engineBundleHash": return "79ce14640d1798eaaa7cb3aa6c4bc325da3bff1e9f4716936e19614eabb1d858";
1213
1310
  default: return fallback;
1214
1311
  }
1215
1312
  }
@@ -1766,7 +1863,7 @@ async function buildAssetDigestFromResolvedPaths(workDir, resolvedPaths) {
1766
1863
  for (const rel of resolvedPaths) {
1767
1864
  const abs = path.join(workDir, rel);
1768
1865
  try {
1769
- const content = await fs.readFile(abs, "utf-8");
1866
+ const content = await fs$1.readFile(abs, "utf-8");
1770
1867
  parts.push(`${rel}\n${content}`);
1771
1868
  } catch {
1772
1869
  parts.push(`${rel}\n`);
@@ -1791,7 +1888,7 @@ async function loadWorkflowSource(workDir, sourceFile, expectedContentHash, reso
1791
1888
  ensureLoaderHookRegistered();
1792
1889
  const filePath = path.join(workDir, sourceFile);
1793
1890
  if (expectedContentHash) {
1794
- const rawSource = await fs.readFile(filePath, "utf-8");
1891
+ const rawSource = await fs$1.readFile(filePath, "utf-8");
1795
1892
  let assetDigest;
1796
1893
  if (resolvedHashFiles?.length) assetDigest = await buildAssetDigestFromResolvedPaths(workDir, resolvedHashFiles);
1797
1894
  const actualHash = computeContentHash(rawSource, assetDigest);
@@ -1854,18 +1951,20 @@ function extractSteps(workflow, jobName) {
1854
1951
  * A sibling mismatch logs a warning; a missing target job throws a clear
1855
1952
  * determinism error.
1856
1953
  */
1857
- async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event, env, apiTransport, expectedJobNames) {
1954
+ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event, env, apiTransport, expectedJobNames, upstreamSnapshot, declaredNeeds) {
1858
1955
  const dynamicFn = extractDynamicJobFn(workflow, dynamicIndex);
1859
1956
  const { $ } = await import("zx");
1860
1957
  const { createLogger } = await import("@kici-dev/shared");
1861
- const { buildKiciApi } = await import("@kici-dev/sdk");
1958
+ const { buildKiciApi, buildNeedsContext } = await import("@kici-dev/sdk");
1862
1959
  const log = createLogger({ prefix: `dynamic-job-fn:${workflow.name}` });
1863
1960
  const kici = buildKiciApi(apiTransport ?? (() => Promise.reject(/* @__PURE__ */ new Error("Agent API not available during re-evaluation"))));
1961
+ const needs = upstreamSnapshot ? buildNeedsContext(upstreamSnapshot, declaredNeeds ?? []) : void 0;
1864
1962
  const generatedJobs = await dynamicFn({
1865
1963
  $,
1866
1964
  ctx: {
1867
1965
  workflow: { name: workflow.name },
1868
- event
1966
+ event,
1967
+ ...needs && { needs }
1869
1968
  },
1870
1969
  log,
1871
1970
  env,
@@ -1893,8 +1992,8 @@ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event
1893
1992
  }
1894
1993
  var AGENT_SDK_VERSION, AGENT_SDK_BUNDLE_HASH, hookRegistered;
1895
1994
  var init_workflow_loader = __esmMin((() => {
1896
- AGENT_SDK_VERSION = "0.1.16";
1897
- AGENT_SDK_BUNDLE_HASH = "cd4b8e0d91efe578c96e7c746f89b44a3df8fcaebaec2185399168ef8eb4ebca";
1995
+ AGENT_SDK_VERSION = "0.1.18";
1996
+ AGENT_SDK_BUNDLE_HASH = "8308089347c304e41b457d3867b17bbff11d6b5cd9706b6823e7abdbd849f33f";
1898
1997
  hookRegistered = false;
1899
1998
  }));
1900
1999
  //#endregion
@@ -2060,24 +2159,24 @@ function resolveOrchestratorUrl(url) {
2060
2159
  * have nothing to race; the defensive `rm` covers re-runs.
2061
2160
  */
2062
2161
  async function moveScratchIntoRepo(scratchDir, workDir) {
2063
- for (const child of await fs.readdir(scratchDir)) if (child === ".kici") {
2162
+ for (const child of await fs$1.readdir(scratchDir)) if (child === ".kici") {
2064
2163
  const kiciScratch = join(scratchDir, ".kici");
2065
- for (const sub of await fs.readdir(kiciScratch)) await moveInto(join(kiciScratch, sub), join(workDir, ".kici", sub));
2164
+ for (const sub of await fs$1.readdir(kiciScratch)) await moveInto(join(kiciScratch, sub), join(workDir, ".kici", sub));
2066
2165
  } else await moveInto(join(scratchDir, child), join(workDir, child));
2067
2166
  }
2068
2167
  /** Move `src` to `dest`, creating the parent and clearing any stale dest. */
2069
2168
  async function moveInto(src, dest) {
2070
2169
  await mkdir(dirname(dest), { recursive: true });
2071
- await fs.rm(dest, {
2170
+ await fs$1.rm(dest, {
2072
2171
  recursive: true,
2073
2172
  force: true
2074
2173
  });
2075
- await fs.rename(src, dest);
2174
+ await fs$1.rename(src, dest);
2076
2175
  }
2077
2176
  /** Best-effort cleanup of a settled scratch dir; logs and continues on failure. */
2078
2177
  async function cleanupScratch(scratchDir) {
2079
2178
  try {
2080
- await fs.rm(scratchDir, {
2179
+ await fs$1.rm(scratchDir, {
2081
2180
  recursive: true,
2082
2181
  force: true
2083
2182
  });
@@ -2111,7 +2210,7 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
2111
2210
  const kiciDir = join(workDir, ".kici");
2112
2211
  if (depsUrl.startsWith("file://")) {
2113
2212
  const localPath = fileURLToPath(depsUrl);
2114
- const data = await fs.readFile(localPath);
2213
+ const data = await fs$1.readFile(localPath);
2115
2214
  if (depsHash) {
2116
2215
  const actualHash = computeHash(data);
2117
2216
  if (actualHash !== depsHash) throw new Error(`Dep tarball hash mismatch: expected ${depsHash}, got ${actualHash}`);
@@ -2270,7 +2369,7 @@ async function restoreSource(workDir, sourceTarUrl) {
2270
2369
  let data;
2271
2370
  if (sourceTarUrl.startsWith("file://")) {
2272
2371
  const localPath = fileURLToPath(sourceTarUrl);
2273
- data = await fs.readFile(localPath);
2372
+ data = await fs$1.readFile(localPath);
2274
2373
  } else if (sourceTarUrl.startsWith("http://") || sourceTarUrl.startsWith("https://")) data = await downloadUrl(sourceTarUrl);
2275
2374
  else throw new Error(`Unsupported source tarball URL scheme: ${sourceTarUrl}`);
2276
2375
  await extractSourceTarball(data, workDir);
@@ -2339,6 +2438,28 @@ function findJobByName(workflow, jobName) {
2339
2438
  async function evaluateDynamicFields(workflow, jobName, event, flags, timeoutMs = 6e4) {
2340
2439
  const job = findJobByName(workflow, jobName);
2341
2440
  const result = {};
2441
+ if (flags.dynamicMatrix && typeof job.matrix === "function") {
2442
+ const matrixContext = {
2443
+ $: (await import("zx")).$,
2444
+ ctx: {
2445
+ workflow: { name: workflow.name },
2446
+ job: {
2447
+ name: jobName,
2448
+ runsOn: job.runsOn
2449
+ }
2450
+ },
2451
+ log: {
2452
+ info: () => {},
2453
+ warn: () => {},
2454
+ error: () => {},
2455
+ debug: () => {}
2456
+ },
2457
+ env: { ...process.env }
2458
+ };
2459
+ let combos = expandMatrix(await withTimeout(() => job.matrix(matrixContext), timeoutMs, `dynamicMatrix for job '${jobName}'`));
2460
+ if (job.include || job.exclude) combos = applyIncludeExclude(combos, job.include, job.exclude);
2461
+ result.matrixValues = combos;
2462
+ }
2342
2463
  if (flags.dynamicEnvironment && typeof job.environment === "function") {
2343
2464
  const value = await withTimeout(() => job.environment(event), timeoutMs, `dynamicEnvironment for job '${jobName}'`);
2344
2465
  if (value !== void 0 && value !== null) result.environmentName = value;
@@ -2379,7 +2500,7 @@ async function serializeJobsToLock(jobs, ctx, staticNames, allowedGroups) {
2379
2500
  return result;
2380
2501
  }
2381
2502
  async function serializeJob(job, generatedNames, ctx, staticNames, allowedGroups) {
2382
- const { runsOn, excludeLabels } = normalizeRunsOn(job.runsOn);
2503
+ const { include: runsOn, exclude: excludeLabels } = normalizeRunsOnToMatchers(job.runsOn, `generated job '${job.name}' runsOn`);
2383
2504
  let resolvedEnvironment;
2384
2505
  if (typeof job.environment === "function") {
2385
2506
  const value = await withTimeout(() => job.environment(ctx.event), DYNAMIC_FIELD_TIMEOUT_MS, `dynamic environment for generated job '${job.name}'`);
@@ -2402,7 +2523,7 @@ async function serializeJob(job, generatedNames, ctx, staticNames, allowedGroups
2402
2523
  _type: "static",
2403
2524
  name: job.name,
2404
2525
  runsOn,
2405
- ...excludeLabels ? { excludeLabels } : {},
2526
+ ...excludeLabels.length > 0 ? { excludeLabels } : {},
2406
2527
  needs: resolvedNeeds,
2407
2528
  ...dependsOnGroups.length > 0 ? { dependsOnGroups } : {},
2408
2529
  steps: serializeSteps(job.steps),
@@ -2416,20 +2537,6 @@ async function serializeJob(job, generatedNames, ctx, staticNames, allowedGroups
2416
2537
  };
2417
2538
  }
2418
2539
  /**
2419
- * Normalize the polymorphic RunsOn type into the lock file format.
2420
- */
2421
- function normalizeRunsOn(runsOn) {
2422
- if (typeof runsOn === "string") return { runsOn };
2423
- if (Array.isArray(runsOn)) return { runsOn };
2424
- const selector = runsOn;
2425
- const labels = typeof selector.labels === "string" ? [selector.labels] : selector.labels;
2426
- const exclude = selector.exclude ? typeof selector.exclude === "string" ? [selector.exclude] : selector.exclude : void 0;
2427
- return {
2428
- runsOn: labels,
2429
- ...exclude ? { excludeLabels: exclude } : {}
2430
- };
2431
- }
2432
- /**
2433
2540
  * Resolve needs references. Jobs can reference other jobs by name (string),
2434
2541
  * Job object reference, DynamicGroupRef, or NeedsEntry/NeedsGroupEntry objects.
2435
2542
  * Validates against generatedNames union staticNames union allowedGroups.
@@ -2515,7 +2622,7 @@ async function serializeMatrix(matrix, jobName, runsOn, ctx) {
2515
2622
  workflow: { name: ctx.workflowName },
2516
2623
  job: {
2517
2624
  name: jobName,
2518
- runsOn: typeof runsOn === "string" ? runsOn : [...runsOn]
2625
+ runsOn: runsOn.map((m) => m.kind === "exact" ? m.value : `/${m.source}/${m.flags}`)
2519
2626
  }
2520
2627
  },
2521
2628
  log: ctx.log,
@@ -2541,6 +2648,7 @@ var MatrixExpansionError, DYNAMIC_FIELD_TIMEOUT_MS;
2541
2648
  var init_dynamic_job_serializer = __esmMin((() => {
2542
2649
  init_timeout_util();
2543
2650
  MatrixExpansionError = class MatrixExpansionError extends Error {
2651
+ jobName;
2544
2652
  name = "MatrixExpansionError";
2545
2653
  constructor(jobName, message) {
2546
2654
  super(message);
@@ -2814,7 +2922,7 @@ function decryptBuffer(encrypted, aesKey) {
2814
2922
  */
2815
2923
  async function applyOverlay(config) {
2816
2924
  const { tarballUrl, cliPublicKey, orchestratorPrivateKey, repoDir } = config;
2817
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "kici-overlay-"));
2925
+ const tmpDir = await fs$1.mkdtemp(path.join(os.tmpdir(), "kici-overlay-"));
2818
2926
  try {
2819
2927
  logger$7.info("Downloading overlay tarball", { url: tarballUrl.replace(/\?.*$/, "?[redacted]") });
2820
2928
  let encryptedData;
@@ -2828,7 +2936,7 @@ async function applyOverlay(config) {
2828
2936
  const decryptedData = decryptBuffer(encryptedData, aesKey);
2829
2937
  logger$7.info("Extracting overlay tarball", { size: decryptedData.length });
2830
2938
  const extractDir = path.join(tmpDir, "extracted");
2831
- await fs.mkdir(extractDir, { recursive: true });
2939
+ await fs$1.mkdir(extractDir, { recursive: true });
2832
2940
  try {
2833
2941
  const readable = Readable.from(decryptedData);
2834
2942
  await new Promise((resolve, reject) => {
@@ -2843,7 +2951,7 @@ async function applyOverlay(config) {
2843
2951
  const manifestPath = path.join(extractDir, ".kici-overlay-tmp", "manifest.json");
2844
2952
  let manifestContent;
2845
2953
  try {
2846
- manifestContent = await fs.readFile(manifestPath, "utf-8");
2954
+ manifestContent = await fs$1.readFile(manifestPath, "utf-8");
2847
2955
  } catch {
2848
2956
  throw new Error("Overlay manifest not found: expected .kici-overlay-tmp/manifest.json in tarball");
2849
2957
  }
@@ -2864,15 +2972,15 @@ async function applyOverlay(config) {
2864
2972
  for (const file of checksumFiles) {
2865
2973
  const srcPath = path.join(extractDir, file);
2866
2974
  const destPath = path.join(repoDir, file);
2867
- await fs.mkdir(path.dirname(destPath), { recursive: true });
2868
- await fs.copyFile(srcPath, destPath);
2975
+ await fs$1.mkdir(path.dirname(destPath), { recursive: true });
2976
+ await fs$1.copyFile(srcPath, destPath);
2869
2977
  filesApplied++;
2870
2978
  }
2871
2979
  let filesDeleted = 0;
2872
2980
  for (const file of manifest.deletions) {
2873
2981
  const targetPath = path.join(repoDir, file);
2874
2982
  try {
2875
- await fs.unlink(targetPath);
2983
+ await fs$1.unlink(targetPath);
2876
2984
  filesDeleted++;
2877
2985
  } catch {
2878
2986
  logger$7.debug("Deletion target not found, skipping", { file });
@@ -2888,7 +2996,7 @@ async function applyOverlay(config) {
2888
2996
  verified: true
2889
2997
  };
2890
2998
  } finally {
2891
- await fs.rm(tmpDir, {
2999
+ await fs$1.rm(tmpDir, {
2892
3000
  recursive: true,
2893
3001
  force: true
2894
3002
  }).catch(() => {});
@@ -2941,7 +3049,7 @@ function noopResult() {
2941
3049
  };
2942
3050
  }
2943
3051
  /** Build the synthesized env-var name for registry index `i`. */
2944
- function tokenEnvName(jobIdShort, index) {
3052
+ function tokenEnvName$1(jobIdShort, index) {
2945
3053
  return `KICI_NPM_TOKEN_${jobIdShort}_${index}`;
2946
3054
  }
2947
3055
  /** Render the agent-managed block of `.npmrc` lines. */
@@ -2950,7 +3058,7 @@ function renderAgentLines(registries, jobIdShort) {
2950
3058
  const lines = [];
2951
3059
  for (let i = 0; i < registries.length; i++) {
2952
3060
  const reg = registries[i];
2953
- const envVar = tokenEnvName(jobIdShort, i);
3061
+ const envVar = tokenEnvName$1(jobIdShort, i);
2954
3062
  const authKey = reg.url.replace(/^https?:/, "");
2955
3063
  if (reg.scope) lines.push(`${reg.scope}:registry=${reg.url}`);
2956
3064
  else lines.push(`registry=${reg.url}`);
@@ -2982,7 +3090,7 @@ async function applyNpmRegistryConfig(args) {
2982
3090
  const tokenEnv = {};
2983
3091
  const tokensForRedaction = [];
2984
3092
  for (let i = 0; i < registries.length; i++) {
2985
- tokenEnv[tokenEnvName(args.jobIdShort, i)] = registries[i].token;
3093
+ tokenEnv[tokenEnvName$1(args.jobIdShort, i)] = registries[i].token;
2986
3094
  tokensForRedaction.push(registries[i].token);
2987
3095
  }
2988
3096
  for (const value of Object.values(installEnvSecrets)) if (value) tokensForRedaction.push(value);
@@ -3020,6 +3128,113 @@ function redactNpmOutput(input, tokens) {
3020
3128
  }
3021
3129
  var init_npm_registry_config = __esmMin((() => {}));
3022
3130
  //#endregion
3131
+ //#region src/execution/yarnrc-berry-config.ts
3132
+ /**
3133
+ * Apply yarn-berry registry auth + a forced `nodeLinker: node-modules` to a
3134
+ * workflow's `.kici/.yarnrc.yml` for the lifetime of one `yarn install`, then
3135
+ * restore the file on cleanup. The berry analog of `npm-registry-config.ts`:
3136
+ * berry reads `.yarnrc.yml` (not `.npmrc`), so the auth block uses berry's
3137
+ * `npmRegistryServer` / `npmScopes` / `npmAuthToken` keys with `${VAR}`
3138
+ * env-var interpolation. Token bytes never reach disk — each registry token is
3139
+ * exposed as a job-scoped env var and the on-disk value is the `${VAR}`
3140
+ * reference.
3141
+ *
3142
+ * `nodeLinker: node-modules` makes berry lay down a real `node_modules` tree
3143
+ * (no PnP `.pnp.cjs`), so the agent's packer / restore / sibling-walk /
3144
+ * workflow-loader work unchanged. `enableScripts: false` (when a private
3145
+ * registry is configured) keeps dependency lifecycle scripts from seeing the
3146
+ * synthesized token env vars — the same security model as npm/pnpm/classic
3147
+ * `--ignore-scripts`.
3148
+ *
3149
+ * Reuses the same `ApplyNpmRegistryConfigArgs` / `ApplyNpmRegistryConfigResult`
3150
+ * shapes as the npm overlay so `dep-installer` can pick either by flavor.
3151
+ */
3152
+ /** Build the synthesized env-var name for registry index `i`. */
3153
+ function tokenEnvName(jobIdShort, index) {
3154
+ return `KICI_NPM_TOKEN_${jobIdShort}_${index}`;
3155
+ }
3156
+ /** Read + parse an existing `.yarnrc.yml`, or `{}` when absent/empty. */
3157
+ async function readOriginalYarnrc(path) {
3158
+ try {
3159
+ const raw = await readFile(path, "utf8");
3160
+ return {
3161
+ raw,
3162
+ doc: parse(raw) ?? {}
3163
+ };
3164
+ } catch (err) {
3165
+ if (err.code === "ENOENT") return {
3166
+ raw: null,
3167
+ doc: {}
3168
+ };
3169
+ throw err;
3170
+ }
3171
+ }
3172
+ function buildRegistryBlock(envVar, url, alwaysAuth) {
3173
+ return {
3174
+ npmRegistryServer: url,
3175
+ npmAuthToken: `\${${envVar}}`,
3176
+ ...alwaysAuth ? { npmAlwaysAuth: true } : {}
3177
+ };
3178
+ }
3179
+ async function applyYarnrcBerryConfig(args) {
3180
+ const registries = args.npmRegistries ?? [];
3181
+ const installEnvSecrets = args.installEnvSecrets ?? {};
3182
+ const hasPrivateRegistry = registries.length > 0 || Object.keys(installEnvSecrets).length > 0;
3183
+ const yarnrcPath = join(args.kiciDir, ".yarnrc.yml");
3184
+ const { raw: original, doc } = await readOriginalYarnrc(yarnrcPath);
3185
+ const cacheFolder = await mkdtemp(join(tmpdir(), "kici-yarn-berry-cache-"));
3186
+ const merged = {
3187
+ ...doc,
3188
+ nodeLinker: "node-modules",
3189
+ enableGlobalCache: false,
3190
+ cacheFolder
3191
+ };
3192
+ const tokenEnv = {};
3193
+ const tokensForRedaction = [];
3194
+ if (hasPrivateRegistry) {
3195
+ merged.enableScripts = false;
3196
+ const npmScopes = { ...doc.npmScopes ?? {} };
3197
+ for (let i = 0; i < registries.length; i++) {
3198
+ const reg = registries[i];
3199
+ const envVar = tokenEnvName(args.jobIdShort, i);
3200
+ tokenEnv[envVar] = reg.token;
3201
+ tokensForRedaction.push(reg.token);
3202
+ const block = buildRegistryBlock(envVar, reg.url, reg.alwaysAuth);
3203
+ if (reg.scope) npmScopes[reg.scope] = block;
3204
+ else {
3205
+ merged.npmRegistryServer = reg.url;
3206
+ merged.npmAuthToken = block.npmAuthToken;
3207
+ if (reg.alwaysAuth) merged.npmAlwaysAuth = true;
3208
+ }
3209
+ }
3210
+ if (Object.keys(npmScopes).length > 0) merged.npmScopes = npmScopes;
3211
+ for (const value of Object.values(installEnvSecrets)) if (value) tokensForRedaction.push(value);
3212
+ }
3213
+ await writeFile(yarnrcPath, stringify(merged), {
3214
+ encoding: "utf8",
3215
+ mode: 384
3216
+ });
3217
+ const cleanup = async () => {
3218
+ try {
3219
+ if (original === null) await unlink(yarnrcPath).catch(() => {});
3220
+ else await writeFile(yarnrcPath, original, { encoding: "utf8" });
3221
+ } catch {}
3222
+ await rm(cacheFolder, {
3223
+ recursive: true,
3224
+ force: true
3225
+ }).catch(() => {});
3226
+ };
3227
+ return {
3228
+ extraEnv: {
3229
+ ...installEnvSecrets,
3230
+ ...tokenEnv
3231
+ },
3232
+ tokensForRedaction,
3233
+ cleanup
3234
+ };
3235
+ }
3236
+ var init_yarnrc_berry_config = __esmMin((() => {}));
3237
+ //#endregion
3023
3238
  //#region src/execution/validate-kici-deps.ts
3024
3239
  /**
3025
3240
  * Pre-install validation for `.kici/` dependency specifiers.
@@ -3037,6 +3252,13 @@ var init_npm_registry_config = __esmMin((() => {}));
3037
3252
  * clones the whole repo, so an in-repo sibling is present), and resolves
3038
3253
  * `file:`/`link:`/`portal:` against a path — allowed when that path stays
3039
3254
  * inside the cloned repo, rejected when it escapes the clone.
3255
+ * - yarn classic (v1) has no `workspace:` protocol and no `portal:` — it links
3256
+ * in-repo siblings by version range, not by a local specifier — so both are
3257
+ * rejected with guidance; `file:`/`link:` are allowed when the path stays
3258
+ * inside the clone, rejected when it escapes.
3259
+ * - yarn berry (v2+) resolves `workspace:` against the repo-root package.json
3260
+ * `workspaces` field and `portal:`/`file:`/`link:` against inside-repo paths,
3261
+ * so those are allowed when present/inside the clone and rejected otherwise.
3040
3262
  *
3041
3263
  * This module performs that classification so unresolvable specifiers fail
3042
3264
  * fast with guidance rather than a cryptic install error.
@@ -3096,6 +3318,17 @@ async function fileExists$1(target) {
3096
3318
  return false;
3097
3319
  }
3098
3320
  }
3321
+ /** Whether the repo-root package.json declares a non-empty `workspaces` array. */
3322
+ async function rootHasWorkspaces(repoRoot) {
3323
+ try {
3324
+ const ws = JSON.parse(await readFile(join(repoRoot, "package.json"), "utf-8")).workspaces;
3325
+ if (Array.isArray(ws)) return ws.length > 0;
3326
+ if (ws && typeof ws === "object" && Array.isArray(ws.packages)) return ws.packages.length > 0;
3327
+ return false;
3328
+ } catch {
3329
+ return false;
3330
+ }
3331
+ }
3099
3332
  /** Resolve a `file:`/`link:`/`portal:` spec to an absolute path under kiciDir. */
3100
3333
  function resolveLocalPath(kiciDir, dep) {
3101
3334
  const rawPath = dep.spec.slice(dep.protocol.length);
@@ -3110,8 +3343,31 @@ function isInsideRepo(repoRoot, target) {
3110
3343
  * Classify each local-protocol dependency for the detected package manager and
3111
3344
  * return the ones that are unresolvable in the agent's single-clone model.
3112
3345
  */
3113
- async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
3346
+ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot, yarnFlavor) {
3114
3347
  if (packageManager === PackageManager.Npm) return [...deps];
3348
+ if (packageManager === PackageManager.Yarn && yarnFlavor === YarnFlavor.Berry) {
3349
+ const hasWorkspaces = await rootHasWorkspaces(repoRoot);
3350
+ const unresolvable = [];
3351
+ for (const dep of deps) {
3352
+ if (dep.protocol === "workspace:") {
3353
+ if (!hasWorkspaces) unresolvable.push(dep);
3354
+ continue;
3355
+ }
3356
+ if (!isInsideRepo(repoRoot, resolveLocalPath(kiciDir, dep))) unresolvable.push(dep);
3357
+ }
3358
+ return unresolvable;
3359
+ }
3360
+ if (packageManager === PackageManager.Yarn) {
3361
+ const unresolvable = [];
3362
+ for (const dep of deps) {
3363
+ if (dep.protocol === "workspace:" || dep.protocol === "portal:") {
3364
+ unresolvable.push(dep);
3365
+ continue;
3366
+ }
3367
+ if (!isInsideRepo(repoRoot, resolveLocalPath(kiciDir, dep))) unresolvable.push(dep);
3368
+ }
3369
+ return unresolvable;
3370
+ }
3115
3371
  const hasWorkspaceFile = await fileExists$1(join(repoRoot, "pnpm-workspace.yaml"));
3116
3372
  const unresolvable = [];
3117
3373
  for (const dep of deps) {
@@ -3124,9 +3380,11 @@ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
3124
3380
  return unresolvable;
3125
3381
  }
3126
3382
  /** Build the actionable error for unresolvable local-protocol dependencies. */
3127
- function formatUnresolvableDepError(offenders, packageManager) {
3383
+ function formatUnresolvableDepError(offenders, packageManager, yarnFlavor) {
3128
3384
  const list = offenders.map((o) => `${o.name}: ${o.spec}`).join(", ");
3129
3385
  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.`;
3386
+ if (packageManager === PackageManager.Yarn && yarnFlavor === YarnFlavor.Berry) return `These .kici/ dependencies cannot be resolved by yarn berry from the cloned repository: ${list}. A workspace: dependency requires a "workspaces" array in the repo-root package.json, and file:/link:/portal: paths must stay inside this repository.`;
3387
+ 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 requires a yarn@2+ packageManager field or a .yarnrc.yml.)`;
3130
3388
  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
3389
  }
3132
3390
  /**
@@ -3140,9 +3398,10 @@ async function assertResolvableDeps(args) {
3140
3398
  if (!pkg) return;
3141
3399
  const localDeps = findLocalProtocolDeps(pkg);
3142
3400
  if (localDeps.length === 0) return;
3143
- const offenders = await findUnresolvableDeps(localDeps, args.packageManager, args.kiciDir, args.repoRoot);
3401
+ const flavor = args.yarnFlavor ?? YarnFlavor.Classic;
3402
+ const offenders = await findUnresolvableDeps(localDeps, args.packageManager, args.kiciDir, args.repoRoot, flavor);
3144
3403
  if (offenders.length === 0) return;
3145
- throw new Error(formatUnresolvableDepError(offenders, args.packageManager));
3404
+ throw new Error(formatUnresolvableDepError(offenders, args.packageManager, flavor));
3146
3405
  }
3147
3406
  var LOCAL_PROTOCOLS, DEP_FIELDS;
3148
3407
  var init_validate_kici_deps = __esmMin((() => {
@@ -3160,6 +3419,102 @@ var init_validate_kici_deps = __esmMin((() => {
3160
3419
  ];
3161
3420
  }));
3162
3421
  //#endregion
3422
+ //#region src/execution/workspace-siblings.ts
3423
+ /**
3424
+ * In-repo workspace-sibling discovery for the agent's dependency handling.
3425
+ *
3426
+ * A pnpm or yarn-classic workspace lays out a `.kici/` member's `workspace:`
3427
+ * (pnpm) or version-range (yarn) siblings as symlinks pointing at package
3428
+ * directories that live inside the clone but outside `.kici/` and outside the
3429
+ * `node_modules` store. The dep-cache packer must travel those sibling dirs with
3430
+ * the closure (their symlinks would dangle otherwise), and the yarn install path
3431
+ * must build them (the install links a sibling but does not build it).
3432
+ *
3433
+ * `collectInRepoSiblings` walks a starting `node_modules` (and transitively each
3434
+ * discovered sibling's `node_modules`), returning each in-repo sibling directory
3435
+ * once, repo-root-relative, in breadth-first discovery order. The starting
3436
+ * `node_modules` is a parameter so it serves pnpm + yarn-standalone (seeded at
3437
+ * `.kici/node_modules`) and yarn-workspace-member (seeded at the hoisted root
3438
+ * `node_modules`).
3439
+ */
3440
+ /**
3441
+ * The directory yarn lays `.kici`'s dependencies into. A standalone `.kici`
3442
+ * (own lockfile, no parent workspace) gets `.kici/node_modules`; a workspace
3443
+ * member hoists everything to the repo-root `node_modules`, leaving no
3444
+ * `.kici/node_modules`.
3445
+ */
3446
+ function resolveYarnNodeModulesRoot(repoRoot, kiciDir) {
3447
+ const kiciNm = join(kiciDir, "node_modules");
3448
+ return existsSync(kiciNm) ? kiciNm : join(repoRoot, "node_modules");
3449
+ }
3450
+ /**
3451
+ * Walk `seedNodeModules` (and transitively each in-repo sibling's
3452
+ * `node_modules`) collecting the repo-root-relative directories of workspace
3453
+ * siblings — package dirs that live inside the clone but outside `.kici/` and
3454
+ * outside the repo-root `node_modules/` store. Returns each dir once, in
3455
+ * discovery (BFS) order.
3456
+ */
3457
+ async function collectInRepoSiblings(workDir, kiciDir, seedNodeModules = join(kiciDir, "node_modules")) {
3458
+ const repoRoot = resolve(workDir);
3459
+ const kiciResolved = resolve(kiciDir);
3460
+ const rootNodeModules = resolve(join(workDir, "node_modules"));
3461
+ const found = /* @__PURE__ */ new Set();
3462
+ const visited = /* @__PURE__ */ new Set();
3463
+ const queue = [seedNodeModules];
3464
+ while (queue.length > 0) {
3465
+ const nmDir = queue.shift();
3466
+ const real = await realpath(nmDir).catch(() => null);
3467
+ if (!real || visited.has(real)) continue;
3468
+ visited.add(real);
3469
+ for (const target of await resolveNodeModulesLinks(nmDir)) {
3470
+ if (!isInside(repoRoot, target)) continue;
3471
+ if (isInside(kiciResolved, target) || isInside(rootNodeModules, target)) continue;
3472
+ const rel = relative(workDir, target);
3473
+ if (!found.has(rel)) {
3474
+ found.add(rel);
3475
+ queue.push(join(target, "node_modules"));
3476
+ }
3477
+ }
3478
+ }
3479
+ return [...found];
3480
+ }
3481
+ /** Resolve every package symlink target under a `node_modules` dir (descending one level into `@scope` dirs). */
3482
+ async function resolveNodeModulesLinks(nmDir) {
3483
+ const targets = [];
3484
+ for (const entry of await readdir(nmDir).catch(() => [])) {
3485
+ if (entry.startsWith(".")) continue;
3486
+ const entryPath = join(nmDir, entry);
3487
+ if (entry.startsWith("@")) {
3488
+ for (const scoped of await readdir(entryPath).catch(() => [])) {
3489
+ const target = await resolveIfSymlink(join(entryPath, scoped));
3490
+ if (target) targets.push(target);
3491
+ }
3492
+ continue;
3493
+ }
3494
+ const target = await resolveIfSymlink(entryPath);
3495
+ if (target) targets.push(target);
3496
+ }
3497
+ return targets;
3498
+ }
3499
+ /** Return the real path of `p` if it is a symlink, else null. */
3500
+ async function resolveIfSymlink(p) {
3501
+ try {
3502
+ if (!(await lstat(p)).isSymbolicLink()) return null;
3503
+ return await realpath(p);
3504
+ } catch {
3505
+ return null;
3506
+ }
3507
+ }
3508
+ /** Whether `target` is `root` itself or a path inside it. */
3509
+ function isInside(root, target) {
3510
+ const rel = relative(root, target);
3511
+ return rel === "" || !rel.startsWith("..") && !rel.startsWith(`..${sep}`) && !isAbsoluteRel(rel);
3512
+ }
3513
+ function isAbsoluteRel(rel) {
3514
+ return rel.length > 1 && rel[1] === ":";
3515
+ }
3516
+ var init_workspace_siblings = __esmMin((() => {}));
3517
+ //#endregion
3163
3518
  //#region src/execution/dep-installer.ts
3164
3519
  /**
3165
3520
  * Inline dependency installation for graceful degradation.
@@ -3167,12 +3522,17 @@ var init_validate_kici_deps = __esmMin((() => {
3167
3522
  * When the dep cache is unavailable or a download fails, the agent installs
3168
3523
  * `.kici/` dependencies directly with the repository's package manager.
3169
3524
  *
3170
- * The package manager is detected from the cloned repo (npm / pnpm); the
3525
+ * The package manager is detected from the cloned repo (npm / pnpm / yarn); the
3171
3526
  * presence of `.kici/package.json` signals that deps should be installed. npm
3172
3527
  * is the default and ships with every Node.js install; pnpm is used when the
3173
3528
  * 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.
3529
+ * `workspace:` siblings. yarn is supported in both flavors: classic (v1) reads
3530
+ * `.kici/.npmrc` for registry auth and links version-range workspace siblings;
3531
+ * berry (v2+) reads a synthesized `.kici/.yarnrc.yml` for auth, runs with a
3532
+ * forced `nodeLinker: node-modules` (so the resulting tree matches classic/npm
3533
+ * and the runner's plain node resolution holds), and resolves
3534
+ * `workspace:`/`portal:` siblings. Either flavor links the sibling but does not
3535
+ * build it, so the agent builds the in-repo closure after install.
3176
3536
  *
3177
3537
  * Security: the install runs with an isolated per-invocation cache/store
3178
3538
  * directory to prevent cache poisoning across build jobs — a malicious
@@ -3193,6 +3553,15 @@ async function detectKiciPackageManager(repoRoot, kiciDir) {
3193
3553
  return await detectPackageManagerFromManifests(repoRoot) ?? await detectPackageManagerFromManifests(kiciDir) ?? PackageManager.Npm;
3194
3554
  }
3195
3555
  /**
3556
+ * Detect the yarn flavor (classic vs berry) for the cloned repo. Mirrors
3557
+ * `detectKiciPackageManager`: probe the repo root first, then `.kici/` for a
3558
+ * standalone project. Only called when the detected manager is `Yarn`.
3559
+ */
3560
+ async function detectKiciYarnFlavor(repoRoot, kiciDir) {
3561
+ if (await detectYarnFlavor(repoRoot) === YarnFlavor.Berry) return YarnFlavor.Berry;
3562
+ return detectYarnFlavor(kiciDir);
3563
+ }
3564
+ /**
3196
3565
  * Install `.kici/` dependencies inline with the repo's package manager.
3197
3566
  *
3198
3567
  * Falls back to this when the dep cache is unavailable or a download fails.
@@ -3211,20 +3580,28 @@ async function detectKiciPackageManager(repoRoot, kiciDir) {
3211
3580
  async function installDeps(kiciDir, opts = {}) {
3212
3581
  const repoRoot = opts.repoRoot ?? dirname(kiciDir);
3213
3582
  const packageManager = await detectKiciPackageManager(repoRoot, kiciDir);
3583
+ const yarnFlavor = packageManager === PackageManager.Yarn ? await detectKiciYarnFlavor(repoRoot, kiciDir) : YarnFlavor.Classic;
3214
3584
  logger$6.info("Installing deps inline", {
3215
3585
  packageManager,
3586
+ yarnFlavor,
3216
3587
  dir: kiciDir
3217
3588
  });
3218
- 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.");
3589
+ process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, flavor=${yarnFlavor}, cwd=${kiciDir}\n`);
3220
3590
  await assertResolvableDeps({
3221
3591
  kiciDir,
3222
3592
  repoRoot,
3223
- packageManager
3593
+ packageManager,
3594
+ yarnFlavor
3224
3595
  });
3225
3596
  const startTime = Date.now();
3226
3597
  const hasPrivateRegistry = (opts.npmRegistries?.length ?? 0) > 0 || (opts.installEnvSecrets ? Object.keys(opts.installEnvSecrets).length > 0 : false);
3227
- const registryConfig = await applyNpmRegistryConfig({
3598
+ const isBerry = packageManager === PackageManager.Yarn && yarnFlavor === YarnFlavor.Berry;
3599
+ const registryConfig = isBerry ? await applyYarnrcBerryConfig({
3600
+ kiciDir,
3601
+ npmRegistries: opts.npmRegistries,
3602
+ installEnvSecrets: opts.installEnvSecrets,
3603
+ jobIdShort: opts.jobIdShort ?? "00000000"
3604
+ }) : await applyNpmRegistryConfig({
3228
3605
  kiciDir,
3229
3606
  npmRegistries: opts.npmRegistries,
3230
3607
  installEnvSecrets: opts.installEnvSecrets,
@@ -3236,6 +3613,15 @@ async function installDeps(kiciDir, opts = {}) {
3236
3613
  hasPrivateRegistry,
3237
3614
  registryConfig
3238
3615
  });
3616
+ else if (isBerry) await runYarnBerryInstall({
3617
+ kiciDir,
3618
+ registryConfig
3619
+ });
3620
+ else if (packageManager === PackageManager.Yarn) await runYarnInstall({
3621
+ kiciDir,
3622
+ hasPrivateRegistry,
3623
+ registryConfig
3624
+ });
3239
3625
  else await runNpmInstall({
3240
3626
  kiciDir,
3241
3627
  hasPrivateRegistry,
@@ -3250,6 +3636,7 @@ async function installDeps(kiciDir, opts = {}) {
3250
3636
  await registryConfig.cleanup();
3251
3637
  }
3252
3638
  if (packageManager === PackageManager.Pnpm && await kiciHasLocalProtocolDeps(kiciDir)) await buildWorkspaceClosure(repoRoot);
3639
+ if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir, yarnFlavor);
3253
3640
  const durationMs = Date.now() - startTime;
3254
3641
  process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
3255
3642
  logger$6.info("Deps installed inline", {
@@ -3337,6 +3724,131 @@ async function runPnpmInstall(args) {
3337
3724
  }).catch(() => {});
3338
3725
  }
3339
3726
  }
3727
+ /** Pure: argv for `yarn install` with an isolated cache folder. */
3728
+ function buildYarnInstallArgs(cacheDir, hasPrivateRegistry) {
3729
+ const a = [
3730
+ "install",
3731
+ "--cache-folder",
3732
+ cacheDir,
3733
+ "--non-interactive",
3734
+ "--no-progress"
3735
+ ];
3736
+ if (hasPrivateRegistry) a.push("--ignore-scripts");
3737
+ return a;
3738
+ }
3739
+ /**
3740
+ * Run `yarn install` from `.kici/` with an isolated cache folder. yarn classic
3741
+ * reads the synthesized `.kici/.npmrc` (registry + `${VAR}` token expansion) for
3742
+ * private-registry auth. A workspace member hoists deps to the repo-root
3743
+ * node_modules; a standalone `.kici` gets `.kici/node_modules`. Not
3744
+ * `--frozen-lockfile` (resolved URLs in the lockfile may point at a different
3745
+ * registry than the synthesized `.npmrc`, e.g. localhost tunnel vs direct IP).
3746
+ */
3747
+ async function runYarnInstall(args) {
3748
+ await assertYarnAvailable();
3749
+ const { nodeDir } = resolveNpm();
3750
+ const cacheDir = await mkdtemp(join(tmpdir(), "kici-yarn-cache-"));
3751
+ const env = envWithNodeOnPath(args.registryConfig.extraEnv, nodeDir);
3752
+ const argv = buildYarnInstallArgs(cacheDir, args.hasPrivateRegistry);
3753
+ try {
3754
+ process.stderr.write(`[dep-installer:trace] running: yarn ${argv.join(" ")}\n`);
3755
+ await execFileAsync("yarn", argv, {
3756
+ cwd: args.kiciDir,
3757
+ env,
3758
+ timeout: INSTALL_TIMEOUT_MS,
3759
+ maxBuffer: INSTALL_MAX_BUFFER
3760
+ });
3761
+ } finally {
3762
+ await rm(cacheDir, {
3763
+ recursive: true,
3764
+ force: true
3765
+ }).catch(() => {});
3766
+ }
3767
+ }
3768
+ /** Pure: argv for a berry `yarn install`. Cache + linker live in .yarnrc.yml. */
3769
+ function buildYarnBerryInstallArgs() {
3770
+ return ["install"];
3771
+ }
3772
+ /**
3773
+ * Run a berry `yarn install` from `.kici/`. The synthesized `.kici/.yarnrc.yml`
3774
+ * (applied by `applyYarnrcBerryConfig`) forces `nodeLinker: node-modules`, an
3775
+ * isolated `cacheFolder`, and — when a private registry is configured —
3776
+ * `enableScripts: false` + `npmScopes`/`npmRegistryServer` auth. corepack
3777
+ * provisions the repo-pinned berry version; `COREPACK_ENABLE_DOWNLOAD_PROMPT=0`
3778
+ * makes that non-interactive. Not `--immutable` (resolved URLs in the lockfile
3779
+ * may point at a different registry than the synthesized config).
3780
+ */
3781
+ async function runYarnBerryInstall(args) {
3782
+ await assertYarnAvailable();
3783
+ const { nodeDir } = resolveNpm();
3784
+ const env = {
3785
+ ...envWithNodeOnPath(args.registryConfig.extraEnv, nodeDir),
3786
+ COREPACK_ENABLE_DOWNLOAD_PROMPT: "0"
3787
+ };
3788
+ const argv = buildYarnBerryInstallArgs();
3789
+ process.stderr.write(`[dep-installer:trace] running: yarn ${argv.join(" ")} (berry)\n`);
3790
+ await execFileAsync("yarn", argv, {
3791
+ cwd: args.kiciDir,
3792
+ env,
3793
+ timeout: INSTALL_TIMEOUT_MS,
3794
+ maxBuffer: INSTALL_MAX_BUFFER
3795
+ });
3796
+ }
3797
+ /** Throw an actionable error when the repo needs yarn but it is not installed. */
3798
+ async function assertYarnAvailable() {
3799
+ try {
3800
+ await execFileAsync("yarn", ["--version"], {
3801
+ timeout: 3e4,
3802
+ cwd: tmpdir()
3803
+ });
3804
+ } catch (e) {
3805
+ 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)})`);
3806
+ }
3807
+ }
3808
+ /**
3809
+ * Build the in-repo workspace siblings `.kici` depends on (yarn links them on
3810
+ * install but does not build them). Walks siblings from the resolved
3811
+ * node_modules root and runs each sibling's `build` script in leaf-first
3812
+ * (reverse-discovery) order with a clean env (no synthesized registry tokens).
3813
+ * Deep cross-sibling build chains may build out of strict topological order —
3814
+ * real `.kici` closures are shallow.
3815
+ */
3816
+ async function buildYarnWorkspaceClosure(repoRoot, kiciDir, yarnFlavor) {
3817
+ const siblings = await collectInRepoSiblings(repoRoot, kiciDir, resolveYarnNodeModulesRoot(repoRoot, kiciDir));
3818
+ if (siblings.length === 0) return;
3819
+ const { nodeDir } = resolveNpm();
3820
+ const env = envWithNodeOnPath({}, nodeDir);
3821
+ for (const rel of [...siblings].reverse()) {
3822
+ const sibDir = join(repoRoot, rel);
3823
+ if (!await siblingHasBuildScript(sibDir)) continue;
3824
+ const [argv, cwd] = yarnFlavor === YarnFlavor.Berry ? [["run", "build"], sibDir] : [[
3825
+ "--cwd",
3826
+ sibDir,
3827
+ "run",
3828
+ "build"
3829
+ ], repoRoot];
3830
+ process.stderr.write(`[dep-installer:trace] building yarn sibling (${yarnFlavor}): yarn ${argv.join(" ")} @ ${cwd}\n`);
3831
+ try {
3832
+ await execFileAsync("yarn", argv, {
3833
+ cwd,
3834
+ env,
3835
+ timeout: INSTALL_TIMEOUT_MS,
3836
+ maxBuffer: INSTALL_MAX_BUFFER
3837
+ });
3838
+ } catch (e) {
3839
+ logSubprocessStreams(e, []);
3840
+ throw new Error(`Failed to build .kici yarn workspace sibling ${rel}: ${describeExecError(e)}`);
3841
+ }
3842
+ }
3843
+ }
3844
+ /** Whether a sibling package.json declares a `build` script. */
3845
+ async function siblingHasBuildScript(sibDir) {
3846
+ try {
3847
+ return typeof JSON.parse(await readFile(join(sibDir, "package.json"), "utf-8")).scripts?.build === "string";
3848
+ } catch {
3849
+ return false;
3850
+ }
3851
+ }
3340
3852
  /**
3341
3853
  * Build the in-repo dependency closure of the `.kici/` package so a
3342
3854
  * `workspace:` sibling's build output exists before the workflow that imports
@@ -3380,7 +3892,10 @@ function describeExecError(e) {
3380
3892
  /** Throw an actionable error when the repo needs pnpm but it is not installed. */
3381
3893
  async function assertPnpmAvailable() {
3382
3894
  try {
3383
- await execFileAsync("pnpm", ["--version"], { timeout: 3e4 });
3895
+ await execFileAsync("pnpm", ["--version"], {
3896
+ timeout: 3e4,
3897
+ cwd: tmpdir()
3898
+ });
3384
3899
  } catch (e) {
3385
3900
  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
3901
  }
@@ -3393,7 +3908,9 @@ function logSubprocessStreams(e, tokens) {
3393
3908
  var logger$6, execFileAsync, INSTALL_TIMEOUT_MS, INSTALL_MAX_BUFFER;
3394
3909
  var init_dep_installer = __esmMin((() => {
3395
3910
  init_npm_registry_config();
3911
+ init_yarnrc_berry_config();
3396
3912
  init_validate_kici_deps();
3913
+ init_workspace_siblings();
3397
3914
  logger$6 = createLogger({ prefix: "dep-installer" });
3398
3915
  execFileAsync = promisify(execFile);
3399
3916
  INSTALL_TIMEOUT_MS = 6e5;
@@ -3409,13 +3926,21 @@ var init_dep_installer = __esmMin((() => {
3409
3926
  * **repo-root-relative** (cwd = the clone root) so restore is a single layout
3410
3927
  * regardless of package manager:
3411
3928
  *
3412
- * - npm / yarn: just `.kici/node_modules`.
3929
+ * - npm: just `.kici/node_modules`.
3413
3930
  * - pnpm: `.kici/node_modules` plus the repo-root `node_modules/.pnpm` virtual
3414
3931
  * store and the in-repo `workspace:` sibling package directories `.kici`
3415
3932
  * depends on (with their built output). pnpm lays `.kici/node_modules` out as
3416
3933
  * symlinks into the root store and into sibling dirs that live outside
3417
3934
  * `.kici/`, so packing `.kici/node_modules` alone would capture dangling
3418
3935
  * links — the store and siblings must travel together.
3936
+ * - yarn (classic + berry): the resolved node_modules root (standalone `.kici`
3937
+ * → `.kici/node_modules`; hoisted workspace member → the repo-root
3938
+ * `node_modules`) plus the in-repo sibling package directories `.kici` depends
3939
+ * on (with their built output), whose symlinks would dangle otherwise. Berry
3940
+ * runs with a forced `nodeLinker: node-modules`, so its tree has the same
3941
+ * node_modules shape as classic and is packed identically (the flavor only
3942
+ * changes how siblings are referenced — version range vs `workspace:`/`portal:`
3943
+ * — not the packed layout).
3419
3944
  *
3420
3945
  * Uses tar.gz (Node.js built-in zlib, no external binary) in portable mode to
3421
3946
  * strip user/group info for cross-machine consistency; symlinks are preserved
@@ -3429,9 +3954,10 @@ var init_dep_installer = __esmMin((() => {
3429
3954
  * @throws Error if `.kici/node_modules` does not exist.
3430
3955
  */
3431
3956
  async function packNodeModules(kiciDir) {
3432
- if (!existsSync(join(kiciDir, "node_modules"))) throw new Error(`node_modules not found at ${join(kiciDir, "node_modules")}`);
3433
3957
  const workDir = dirname(kiciDir);
3434
3958
  const packageManager = await detectPackageManagerFromManifests(workDir) ?? await detectPackageManagerFromManifests(kiciDir) ?? PackageManager.Npm;
3959
+ const nmRoot = packageManager === PackageManager.Yarn ? resolveYarnNodeModulesRoot(workDir, kiciDir) : join(kiciDir, "node_modules");
3960
+ if (!existsSync(nmRoot)) throw new Error(`node_modules not found at ${nmRoot}`);
3435
3961
  const entries = await closureEntries(workDir, kiciDir, packageManager);
3436
3962
  logger$5.info("Packing dependency closure into tarball", {
3437
3963
  dir: workDir,
@@ -3460,85 +3986,32 @@ async function packNodeModules(kiciDir) {
3460
3986
  };
3461
3987
  }
3462
3988
  /**
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.
3989
+ * Compute the repo-root-relative tar entries for the dependency closure. npm
3990
+ * needs only `.kici/node_modules`. pnpm additionally needs the root store and
3991
+ * the in-repo workspace siblings `.kici` resolves. yarn (classic + berry) packs
3992
+ * the resolved node_modules root (standalone → `.kici/node_modules`; hoisted
3993
+ * workspace member → root `node_modules`) plus the in-repo siblings, which it
3994
+ * links but does not place in the store. Berry's forced node-modules linker
3995
+ * gives it the same packed shape as classic.
3466
3996
  */
3467
3997
  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
- }
3998
+ if (packageManager === PackageManager.Pnpm) {
3999
+ const entries = [relative(workDir, join(kiciDir, "node_modules"))];
4000
+ if (existsSync(join(workDir, "node_modules", ".pnpm"))) entries.push(join("node_modules", ".pnpm"));
4001
+ for (const sibling of await collectInRepoSiblings(workDir, kiciDir)) entries.push(sibling);
4002
+ return entries;
3502
4003
  }
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);
4004
+ if (packageManager === PackageManager.Yarn) {
4005
+ const nmRoot = resolveYarnNodeModulesRoot(workDir, kiciDir);
4006
+ const entries = [relative(workDir, nmRoot)];
4007
+ for (const sibling of await collectInRepoSiblings(workDir, kiciDir, nmRoot)) entries.push(sibling);
4008
+ return entries;
3520
4009
  }
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;
3530
- }
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] === ":";
4010
+ return [relative(workDir, join(kiciDir, "node_modules"))];
3539
4011
  }
3540
4012
  var logger$5;
3541
4013
  var init_dep_packer = __esmMin((() => {
4014
+ init_workspace_siblings();
3542
4015
  logger$5 = createLogger({ prefix: "dep-packer" });
3543
4016
  }));
3544
4017
  //#endregion
@@ -3682,6 +4155,8 @@ var init_secret_encryption = __esmMin((() => {
3682
4155
  function buildRequest(dispatch, workDir) {
3683
4156
  const jobConfig = dispatch.jobConfig;
3684
4157
  return {
4158
+ runId: dispatch.runId,
4159
+ jobId: dispatch.jobId,
3685
4160
  workDir,
3686
4161
  repoUrl: dispatch.repoUrl,
3687
4162
  ref: dispatch.ref,
@@ -3694,8 +4169,11 @@ function buildRequest(dispatch, workDir) {
3694
4169
  depsUrl: dispatch.depsUrl,
3695
4170
  depsHash: dispatch.depsHash,
3696
4171
  workflowName: jobConfig.workflowName ?? "",
3697
- jobName: jobConfig.name ?? "",
4172
+ jobName: jobConfig.baseJobName ?? jobConfig.name ?? "",
3698
4173
  runsOn: jobConfig.runsOn ?? "",
4174
+ matrixValues: jobConfig.matrixValues,
4175
+ host: jobConfig.host,
4176
+ agent: jobConfig.agent,
3699
4177
  secrets: dispatch.secrets,
3700
4178
  namespacedSecrets: dispatch.namespacedSecrets,
3701
4179
  sourceFile: jobConfig.source?.file,
@@ -3962,6 +4440,24 @@ function relayCacheRequest$1(msg, ctx) {
3962
4440
  error: toErrorMessage(err)
3963
4441
  }));
3964
4442
  }
4443
+ /** Relay `provenance.request` and pipe the orchestrator response (or an error
4444
+ * response, or a "not configured" response when the callback isn't wired) back
4445
+ * into the sandbox runner. */
4446
+ function relayProvenanceRequest$1(msg, ctx) {
4447
+ if (!ctx.execOptions.onProvenanceRequest) {
4448
+ safeSendToChild(ctx.child, {
4449
+ type: "provenance.response",
4450
+ requestId: msg.requestId,
4451
+ error: "Provenance not available in this agent configuration"
4452
+ });
4453
+ return;
4454
+ }
4455
+ ctx.execOptions.onProvenanceRequest(msg).then((response) => safeSendToChild(ctx.child, response), (err) => safeSendToChild(ctx.child, {
4456
+ type: "provenance.response",
4457
+ requestId: msg.requestId,
4458
+ error: toErrorMessage(err)
4459
+ }));
4460
+ }
3965
4461
  /** Relay `approval.request` and pipe the orchestrator's resolution (or a
3966
4462
  * fail-closed reject when the callback isn't wired or the relay throws) back
3967
4463
  * into the sandbox runner. */
@@ -4052,6 +4548,9 @@ function relayChildIpcMessage(msg, dispatch, ctx) {
4052
4548
  case "cache.request":
4053
4549
  relayCacheRequest$1(msg, ctx);
4054
4550
  return;
4551
+ case "provenance.request":
4552
+ relayProvenanceRequest$1(msg, ctx);
4553
+ return;
4055
4554
  case "approval.request":
4056
4555
  relayApprovalRequest$1(msg, ctx);
4057
4556
  return;
@@ -4476,6 +4975,32 @@ function relayCacheRequest(stream, options, cacheMsg) {
4476
4975
  }));
4477
4976
  }
4478
4977
  /**
4978
+ * Relay provenance.request from the container runner to the orchestrator via
4979
+ * options.onProvenanceRequest, then write the response back through `stream`.
4980
+ * If the agent doesn't expose a provenance relay, write a structured error so
4981
+ * the runner doesn't hang.
4982
+ */
4983
+ function relayProvenanceRequest(stream, options, provMsg) {
4984
+ const writeResponse = (response) => {
4985
+ try {
4986
+ stream.write(JSON.stringify(response) + "\n");
4987
+ } catch {}
4988
+ };
4989
+ if (!options.onProvenanceRequest) {
4990
+ writeResponse({
4991
+ type: "provenance.response",
4992
+ requestId: provMsg.requestId,
4993
+ error: "Provenance not available in this agent configuration"
4994
+ });
4995
+ return;
4996
+ }
4997
+ options.onProvenanceRequest(provMsg).then((response) => writeResponse(response), (err) => writeResponse({
4998
+ type: "provenance.response",
4999
+ requestId: provMsg.requestId,
5000
+ error: toErrorMessage(err)
5001
+ }));
5002
+ }
5003
+ /**
4479
5004
  * Relay approval.request from the container runner to the orchestrator via
4480
5005
  * options.onApprovalRequest, then write the resolution back through `stream`.
4481
5006
  * If the agent doesn't expose an approval relay (or it throws), write a
@@ -4758,6 +5283,9 @@ var init_container_sandbox = __esmMin((() => {
4758
5283
  case "cache.request":
4759
5284
  relayCacheRequest(stream, options, msg);
4760
5285
  return false;
5286
+ case "provenance.request":
5287
+ relayProvenanceRequest(stream, options, msg);
5288
+ return false;
4761
5289
  case "approval.request":
4762
5290
  relayApprovalRequest(stream, options, msg);
4763
5291
  return false;
@@ -4857,7 +5385,7 @@ var job_runner_exports = /* @__PURE__ */ __exportAll({ JobRunner: () => JobRunne
4857
5385
  */
4858
5386
  async function fileExists(p) {
4859
5387
  try {
4860
- await fs.access(p);
5388
+ await fs$1.access(p);
4861
5389
  return true;
4862
5390
  } catch {
4863
5391
  return false;
@@ -4899,6 +5427,14 @@ function determineExecutionMode(jobConfig, agentConfig) {
4899
5427
  if (agentConfig.scalerManaged) return "firecracker";
4900
5428
  return "bare-metal";
4901
5429
  }
5430
+ /**
5431
+ * Build the result-aware `ctx.needs` for a dynamic eval from its frozen upstream
5432
+ * snapshot. Returns undefined for an event-only generator (no snapshot).
5433
+ */
5434
+ function buildEvalNeedsContext(config) {
5435
+ if (!config.resultAware || !config.upstreamSnapshot) return void 0;
5436
+ return buildNeedsContext(config.upstreamSnapshot, config.declaredNeeds ?? []);
5437
+ }
4902
5438
  var logger$2, JobRunner$1;
4903
5439
  var init_job_runner = __esmMin((() => {
4904
5440
  init_git_clone();
@@ -4931,6 +5467,7 @@ var init_job_runner = __esmMin((() => {
4931
5467
  _sendConcurrencyReport;
4932
5468
  _sendApiRequest;
4933
5469
  _requestUserCache;
5470
+ _relayProvenance;
4934
5471
  _sendStepApproval;
4935
5472
  /** Tracks running jobs for concurrency and cancellation */
4936
5473
  activeJobs = /* @__PURE__ */ new Map();
@@ -4950,6 +5487,7 @@ var init_job_runner = __esmMin((() => {
4950
5487
  this._sendConcurrencyReport = deps.sendConcurrencyReport;
4951
5488
  this._sendApiRequest = deps.sendApiRequest;
4952
5489
  this._requestUserCache = deps.requestUserCache;
5490
+ this._relayProvenance = deps.relayProvenance;
4953
5491
  this._sendStepApproval = deps.sendStepApproval;
4954
5492
  }
4955
5493
  /**
@@ -4961,11 +5499,11 @@ var init_job_runner = __esmMin((() => {
4961
5499
  async execute(dispatch) {
4962
5500
  const { runId: _runId, jobId, jobConfig: _jobConfig } = dispatch;
4963
5501
  const abortController = new AbortController();
4964
- const workDir = await fs.mkdtemp(join(tmpdir(), "kici-"));
5502
+ const workDir = await fs$1.mkdtemp(join(tmpdir(), "kici-"));
4965
5503
  const completionPromise = this.runJob(dispatch, workDir, abortController).finally(async () => {
4966
5504
  this.activeJobs.delete(jobId);
4967
5505
  this.activeSandbox = null;
4968
- await fs.rm(workDir, {
5506
+ await fs$1.rm(workDir, {
4969
5507
  recursive: true,
4970
5508
  force: true
4971
5509
  }).catch(() => {});
@@ -5190,6 +5728,7 @@ var init_job_runner = __esmMin((() => {
5190
5728
  },
5191
5729
  onApiRequest: this._sendApiRequest ? async (method, params) => this._sendApiRequest(method, params) : void 0,
5192
5730
  onCacheRequest: this._requestUserCache ? async (request) => this._requestUserCache(jobId, request) : void 0,
5731
+ onProvenanceRequest: this._relayProvenance ? async (request) => this._relayProvenance(jobId, request) : void 0,
5193
5732
  onApprovalRequest: this._sendStepApproval ? async (request) => this._sendStepApproval(dispatch.runId, dispatch.jobId, request) : void 0,
5194
5733
  onSecretMount: (event) => {
5195
5734
  this.emitRunEvent(runId, "step.secret_mount", {
@@ -5509,11 +6048,12 @@ var init_job_runner = __esmMin((() => {
5509
6048
  const initResult = await runCaptured(initSink, async () => {
5510
6049
  const { module } = await loadWorkflowSource(workDir, config.source, config.contentHash, config.resolvedHashFiles);
5511
6050
  const workflow = extractWorkflow(module, config.workflowName);
5512
- initLog(`Evaluating dynamic fields for job '${config.targetJobName}' (env=${config.dynamicEnv} environment=${config.dynamicEnvironment} concurrencyGroup=${config.dynamicConcurrencyGroup})`);
6051
+ initLog(`Evaluating dynamic fields for job '${config.targetJobName}' (env=${config.dynamicEnv} environment=${config.dynamicEnvironment} concurrencyGroup=${config.dynamicConcurrencyGroup} matrix=${config.dynamicMatrix ?? false})`);
5513
6052
  return evaluateDynamicFields(workflow, config.targetJobName, config.event, {
5514
6053
  dynamicEnvironment: config.dynamicEnvironment,
5515
6054
  dynamicEnv: config.dynamicEnv,
5516
- dynamicConcurrencyGroup: config.dynamicConcurrencyGroup
6055
+ dynamicConcurrencyGroup: config.dynamicConcurrencyGroup,
6056
+ dynamicMatrix: config.dynamicMatrix ?? false
5517
6057
  }, config.timeoutMs);
5518
6058
  });
5519
6059
  logger$2.info("Init job completed successfully", {
@@ -5642,11 +6182,13 @@ var init_job_runner = __esmMin((() => {
5642
6182
  const { extractDynamicJobFn } = await Promise.resolve().then(() => (init_workflow_loader(), workflow_loader_exports));
5643
6183
  const dynamicFn = extractDynamicJobFn(extractWorkflow(module, config.workflowName), config.source.index);
5644
6184
  evalLog(`Evaluating DynamicJobFn (index ${config.source.index}, timeout ${timeoutMs}ms)`);
6185
+ const needs = buildEvalNeedsContext(config);
5645
6186
  const context = {
5646
6187
  $: scopedDollar,
5647
6188
  ctx: {
5648
6189
  workflow: { name: config.workflowName },
5649
- event: config.event
6190
+ event: config.event,
6191
+ ...needs && { needs }
5650
6192
  },
5651
6193
  log: dynamicJobLogger,
5652
6194
  env: process.env,
@@ -5863,14 +6405,14 @@ var init_job_runner = __esmMin((() => {
5863
6405
  */
5864
6406
  init_console_capture();
5865
6407
  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";
6408
+ const AGENT_VERSION = "0.1.18";
6409
+ const BUILD_COMMIT = "d8cff38bb";
6410
+ const SDK_VERSION = "0.1.18";
6411
+ const SDK_BUNDLE_HASH = "8308089347c304e41b457d3867b17bbff11d6b5cd9706b6823e7abdbd849f33f";
6412
+ const SHARED_VERSION = "0.1.18";
6413
+ const SHARED_BUNDLE_HASH = "5f2c220f24d166b0f13d0620a2e80d19689ac8b683a12154daef44e938fd46a7";
6414
+ const ENGINE_VERSION = "0.1.18";
6415
+ const ENGINE_BUNDLE_HASH = "79ce14640d1798eaaa7cb3aa6c4bc325da3bff1e9f4716936e19614eabb1d858";
5874
6416
  initTelemetry({
5875
6417
  serviceName: "kici-agent",
5876
6418
  otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
@@ -5951,6 +6493,7 @@ await guardStartup(logger$1, async () => {
5951
6493
  sendConcurrencyReport: (runId, jobId, group) => client.sendConcurrencyReport(runId, jobId, group),
5952
6494
  sendApiRequest: (method, params) => client.sendApiRequest(method, params ?? {}),
5953
6495
  requestUserCache: (jobId, request) => client.requestUserCache(jobId, request),
6496
+ relayProvenance: (jobId, request) => client.relayProvenance(jobId, request),
5954
6497
  sendStepApproval: (runId, jobId, request) => client.sendStepApproval(runId, jobId, request)
5955
6498
  });
5956
6499
  /** Build and send an agent.status message with dynamic OS metadata. */