@kici-dev/agent 0.1.17 → 0.1.19

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
@@ -15,8 +15,9 @@ import { LOGGER_ENV_VARS, defineEnv, validateUnknownKiciVars } from "@kici-dev/s
15
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 * as fs$2 from "node:fs";
19
19
  import fs, { existsSync } from "node:fs";
20
+ import { ZipArchive } from "archiver";
20
21
  import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
21
22
  import { fileURLToPath, pathToFileURL } from "node:url";
22
23
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -24,13 +25,15 @@ import { format, promisify } from "node:util";
24
25
  import { gcStaleTmpDirs } from "@kici-dev/core/tmp-gc";
25
26
  import fs$1, { access, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, unlink, writeFile } from "node:fs/promises";
26
27
  import Docker from "dockerode";
27
- import { buildKiciApi, isDynamicGroupRef, isDynamicJobFn, isStaticArray, isStaticObject } from "@kici-dev/sdk";
28
+ import { buildKiciApi, buildNeedsContext, isDynamicGroupRef, isDynamicJobFn, isStaticArray, isStaticObject } from "@kici-dev/sdk";
28
29
  import { c, x } from "tar";
29
30
  import https from "node:https";
30
31
  import http from "node:http";
31
32
  import { pipeline } from "node:stream/promises";
32
33
  import { createGunzip } from "node:zlib";
33
- import { PNPM_IGNORE_BUILD_GATE_ARG, PackageManager, detectPackageManagerFromManifests } from "@kici-dev/shared/package-manager";
34
+ import { normalizeRunsOnToMatchers } from "@kici-dev/engine/labels/compile";
35
+ import { PNPM_IGNORE_BUILD_GATE_ARG, PackageManager, YarnFlavor, detectPackageManagerFromManifests, detectYarnFlavor } from "@kici-dev/shared/package-manager";
36
+ import { parse, stringify } from "yaml";
34
37
  import { createInterface } from "node:readline";
35
38
  var __defProp = Object.defineProperty;
36
39
  var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -189,7 +192,7 @@ function agentClientConnectionOptions(config) {
189
192
  * lean subset of the orchestrator's createDebugBundle.
190
193
  */
191
194
  async function buildAgentMiniBundle(opts) {
192
- const archive = archiver("zip", { zlib: { level: 6 } });
195
+ const archive = new ZipArchive({ zlib: { level: 6 } });
193
196
  const chunks = [];
194
197
  archive.on("data", (d) => chunks.push(d));
195
198
  const done = new Promise((resolve, reject) => {
@@ -213,7 +216,7 @@ async function buildAgentMiniBundle(opts) {
213
216
  uptime: os$1.uptime()
214
217
  }, null, 2), { name: "system/info.json" });
215
218
  if (opts.metricsText) archive.append(opts.metricsText, { name: "system/metrics.txt" });
216
- if (opts.logDir) await addLogsToArchive(archive, opts.logDir, opts.logWindowHours);
219
+ if (opts.logDir && fs$2.existsSync(opts.logDir)) await addLogsToArchive(archive, opts.logDir, opts.logWindowHours);
217
220
  await archive.finalize();
218
221
  await done;
219
222
  return Buffer.concat(chunks);
@@ -1297,14 +1300,14 @@ var init_console_capture = __esmMin((() => {
1297
1300
  init_console_capture();
1298
1301
  function safe(name, fallback = "unknown") {
1299
1302
  switch (name) {
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";
1303
+ case "version": return "0.1.19";
1304
+ case "buildCommit": return "1590f5e99";
1305
+ case "sdkVersion": return "0.1.19";
1306
+ case "sdkBundleHash": return "8308089347c304e41b457d3867b17bbff11d6b5cd9706b6823e7abdbd849f33f";
1307
+ case "sharedVersion": return "0.1.19";
1308
+ case "sharedBundleHash": return "5f2c220f24d166b0f13d0620a2e80d19689ac8b683a12154daef44e938fd46a7";
1309
+ case "engineVersion": return "0.1.19";
1310
+ case "engineBundleHash": return "79ce14640d1798eaaa7cb3aa6c4bc325da3bff1e9f4716936e19614eabb1d858";
1308
1311
  default: return fallback;
1309
1312
  }
1310
1313
  }
@@ -1949,18 +1952,20 @@ function extractSteps(workflow, jobName) {
1949
1952
  * A sibling mismatch logs a warning; a missing target job throws a clear
1950
1953
  * determinism error.
1951
1954
  */
1952
- async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event, env, apiTransport, expectedJobNames) {
1955
+ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event, env, apiTransport, expectedJobNames, upstreamSnapshot, declaredNeeds) {
1953
1956
  const dynamicFn = extractDynamicJobFn(workflow, dynamicIndex);
1954
1957
  const { $ } = await import("zx");
1955
1958
  const { createLogger } = await import("@kici-dev/shared");
1956
- const { buildKiciApi } = await import("@kici-dev/sdk");
1959
+ const { buildKiciApi, buildNeedsContext } = await import("@kici-dev/sdk");
1957
1960
  const log = createLogger({ prefix: `dynamic-job-fn:${workflow.name}` });
1958
1961
  const kici = buildKiciApi(apiTransport ?? (() => Promise.reject(/* @__PURE__ */ new Error("Agent API not available during re-evaluation"))));
1962
+ const needs = upstreamSnapshot ? buildNeedsContext(upstreamSnapshot, declaredNeeds ?? []) : void 0;
1959
1963
  const generatedJobs = await dynamicFn({
1960
1964
  $,
1961
1965
  ctx: {
1962
1966
  workflow: { name: workflow.name },
1963
- event
1967
+ event,
1968
+ ...needs && { needs }
1964
1969
  },
1965
1970
  log,
1966
1971
  env,
@@ -1988,8 +1993,8 @@ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event
1988
1993
  }
1989
1994
  var AGENT_SDK_VERSION, AGENT_SDK_BUNDLE_HASH, hookRegistered;
1990
1995
  var init_workflow_loader = __esmMin((() => {
1991
- AGENT_SDK_VERSION = "0.1.17";
1992
- AGENT_SDK_BUNDLE_HASH = "df47ed5db86eaaa2de8394c0db08335f368e8d620a898cc409765f4545eb3972";
1996
+ AGENT_SDK_VERSION = "0.1.19";
1997
+ AGENT_SDK_BUNDLE_HASH = "8308089347c304e41b457d3867b17bbff11d6b5cd9706b6823e7abdbd849f33f";
1993
1998
  hookRegistered = false;
1994
1999
  }));
1995
2000
  //#endregion
@@ -2496,7 +2501,7 @@ async function serializeJobsToLock(jobs, ctx, staticNames, allowedGroups) {
2496
2501
  return result;
2497
2502
  }
2498
2503
  async function serializeJob(job, generatedNames, ctx, staticNames, allowedGroups) {
2499
- const { runsOn, excludeLabels } = normalizeRunsOn(job.runsOn);
2504
+ const { include: runsOn, exclude: excludeLabels } = normalizeRunsOnToMatchers(job.runsOn, `generated job '${job.name}' runsOn`);
2500
2505
  let resolvedEnvironment;
2501
2506
  if (typeof job.environment === "function") {
2502
2507
  const value = await withTimeout(() => job.environment(ctx.event), DYNAMIC_FIELD_TIMEOUT_MS, `dynamic environment for generated job '${job.name}'`);
@@ -2519,7 +2524,7 @@ async function serializeJob(job, generatedNames, ctx, staticNames, allowedGroups
2519
2524
  _type: "static",
2520
2525
  name: job.name,
2521
2526
  runsOn,
2522
- ...excludeLabels ? { excludeLabels } : {},
2527
+ ...excludeLabels.length > 0 ? { excludeLabels } : {},
2523
2528
  needs: resolvedNeeds,
2524
2529
  ...dependsOnGroups.length > 0 ? { dependsOnGroups } : {},
2525
2530
  steps: serializeSteps(job.steps),
@@ -2533,20 +2538,6 @@ async function serializeJob(job, generatedNames, ctx, staticNames, allowedGroups
2533
2538
  };
2534
2539
  }
2535
2540
  /**
2536
- * Normalize the polymorphic RunsOn type into the lock file format.
2537
- */
2538
- function normalizeRunsOn(runsOn) {
2539
- if (typeof runsOn === "string") return { runsOn };
2540
- if (Array.isArray(runsOn)) return { runsOn };
2541
- const selector = runsOn;
2542
- const labels = typeof selector.labels === "string" ? [selector.labels] : selector.labels;
2543
- const exclude = selector.exclude ? typeof selector.exclude === "string" ? [selector.exclude] : selector.exclude : void 0;
2544
- return {
2545
- runsOn: labels,
2546
- ...exclude ? { excludeLabels: exclude } : {}
2547
- };
2548
- }
2549
- /**
2550
2541
  * Resolve needs references. Jobs can reference other jobs by name (string),
2551
2542
  * Job object reference, DynamicGroupRef, or NeedsEntry/NeedsGroupEntry objects.
2552
2543
  * Validates against generatedNames union staticNames union allowedGroups.
@@ -2632,7 +2623,7 @@ async function serializeMatrix(matrix, jobName, runsOn, ctx) {
2632
2623
  workflow: { name: ctx.workflowName },
2633
2624
  job: {
2634
2625
  name: jobName,
2635
- runsOn: typeof runsOn === "string" ? runsOn : [...runsOn]
2626
+ runsOn: runsOn.map((m) => m.kind === "exact" ? m.value : `/${m.source}/${m.flags}`)
2636
2627
  }
2637
2628
  },
2638
2629
  log: ctx.log,
@@ -3059,7 +3050,7 @@ function noopResult() {
3059
3050
  };
3060
3051
  }
3061
3052
  /** Build the synthesized env-var name for registry index `i`. */
3062
- function tokenEnvName(jobIdShort, index) {
3053
+ function tokenEnvName$1(jobIdShort, index) {
3063
3054
  return `KICI_NPM_TOKEN_${jobIdShort}_${index}`;
3064
3055
  }
3065
3056
  /** Render the agent-managed block of `.npmrc` lines. */
@@ -3068,7 +3059,7 @@ function renderAgentLines(registries, jobIdShort) {
3068
3059
  const lines = [];
3069
3060
  for (let i = 0; i < registries.length; i++) {
3070
3061
  const reg = registries[i];
3071
- const envVar = tokenEnvName(jobIdShort, i);
3062
+ const envVar = tokenEnvName$1(jobIdShort, i);
3072
3063
  const authKey = reg.url.replace(/^https?:/, "");
3073
3064
  if (reg.scope) lines.push(`${reg.scope}:registry=${reg.url}`);
3074
3065
  else lines.push(`registry=${reg.url}`);
@@ -3100,7 +3091,7 @@ async function applyNpmRegistryConfig(args) {
3100
3091
  const tokenEnv = {};
3101
3092
  const tokensForRedaction = [];
3102
3093
  for (let i = 0; i < registries.length; i++) {
3103
- tokenEnv[tokenEnvName(args.jobIdShort, i)] = registries[i].token;
3094
+ tokenEnv[tokenEnvName$1(args.jobIdShort, i)] = registries[i].token;
3104
3095
  tokensForRedaction.push(registries[i].token);
3105
3096
  }
3106
3097
  for (const value of Object.values(installEnvSecrets)) if (value) tokensForRedaction.push(value);
@@ -3138,6 +3129,113 @@ function redactNpmOutput(input, tokens) {
3138
3129
  }
3139
3130
  var init_npm_registry_config = __esmMin((() => {}));
3140
3131
  //#endregion
3132
+ //#region src/execution/yarnrc-berry-config.ts
3133
+ /**
3134
+ * Apply yarn-berry registry auth + a forced `nodeLinker: node-modules` to a
3135
+ * workflow's `.kici/.yarnrc.yml` for the lifetime of one `yarn install`, then
3136
+ * restore the file on cleanup. The berry analog of `npm-registry-config.ts`:
3137
+ * berry reads `.yarnrc.yml` (not `.npmrc`), so the auth block uses berry's
3138
+ * `npmRegistryServer` / `npmScopes` / `npmAuthToken` keys with `${VAR}`
3139
+ * env-var interpolation. Token bytes never reach disk — each registry token is
3140
+ * exposed as a job-scoped env var and the on-disk value is the `${VAR}`
3141
+ * reference.
3142
+ *
3143
+ * `nodeLinker: node-modules` makes berry lay down a real `node_modules` tree
3144
+ * (no PnP `.pnp.cjs`), so the agent's packer / restore / sibling-walk /
3145
+ * workflow-loader work unchanged. `enableScripts: false` (when a private
3146
+ * registry is configured) keeps dependency lifecycle scripts from seeing the
3147
+ * synthesized token env vars — the same security model as npm/pnpm/classic
3148
+ * `--ignore-scripts`.
3149
+ *
3150
+ * Reuses the same `ApplyNpmRegistryConfigArgs` / `ApplyNpmRegistryConfigResult`
3151
+ * shapes as the npm overlay so `dep-installer` can pick either by flavor.
3152
+ */
3153
+ /** Build the synthesized env-var name for registry index `i`. */
3154
+ function tokenEnvName(jobIdShort, index) {
3155
+ return `KICI_NPM_TOKEN_${jobIdShort}_${index}`;
3156
+ }
3157
+ /** Read + parse an existing `.yarnrc.yml`, or `{}` when absent/empty. */
3158
+ async function readOriginalYarnrc(path) {
3159
+ try {
3160
+ const raw = await readFile(path, "utf8");
3161
+ return {
3162
+ raw,
3163
+ doc: parse(raw) ?? {}
3164
+ };
3165
+ } catch (err) {
3166
+ if (err.code === "ENOENT") return {
3167
+ raw: null,
3168
+ doc: {}
3169
+ };
3170
+ throw err;
3171
+ }
3172
+ }
3173
+ function buildRegistryBlock(envVar, url, alwaysAuth) {
3174
+ return {
3175
+ npmRegistryServer: url,
3176
+ npmAuthToken: `\${${envVar}}`,
3177
+ ...alwaysAuth ? { npmAlwaysAuth: true } : {}
3178
+ };
3179
+ }
3180
+ async function applyYarnrcBerryConfig(args) {
3181
+ const registries = args.npmRegistries ?? [];
3182
+ const installEnvSecrets = args.installEnvSecrets ?? {};
3183
+ const hasPrivateRegistry = registries.length > 0 || Object.keys(installEnvSecrets).length > 0;
3184
+ const yarnrcPath = join(args.kiciDir, ".yarnrc.yml");
3185
+ const { raw: original, doc } = await readOriginalYarnrc(yarnrcPath);
3186
+ const cacheFolder = await mkdtemp(join(tmpdir(), "kici-yarn-berry-cache-"));
3187
+ const merged = {
3188
+ ...doc,
3189
+ nodeLinker: "node-modules",
3190
+ enableGlobalCache: false,
3191
+ cacheFolder
3192
+ };
3193
+ const tokenEnv = {};
3194
+ const tokensForRedaction = [];
3195
+ if (hasPrivateRegistry) {
3196
+ merged.enableScripts = false;
3197
+ const npmScopes = { ...doc.npmScopes ?? {} };
3198
+ for (let i = 0; i < registries.length; i++) {
3199
+ const reg = registries[i];
3200
+ const envVar = tokenEnvName(args.jobIdShort, i);
3201
+ tokenEnv[envVar] = reg.token;
3202
+ tokensForRedaction.push(reg.token);
3203
+ const block = buildRegistryBlock(envVar, reg.url, reg.alwaysAuth);
3204
+ if (reg.scope) npmScopes[reg.scope] = block;
3205
+ else {
3206
+ merged.npmRegistryServer = reg.url;
3207
+ merged.npmAuthToken = block.npmAuthToken;
3208
+ if (reg.alwaysAuth) merged.npmAlwaysAuth = true;
3209
+ }
3210
+ }
3211
+ if (Object.keys(npmScopes).length > 0) merged.npmScopes = npmScopes;
3212
+ for (const value of Object.values(installEnvSecrets)) if (value) tokensForRedaction.push(value);
3213
+ }
3214
+ await writeFile(yarnrcPath, stringify(merged), {
3215
+ encoding: "utf8",
3216
+ mode: 384
3217
+ });
3218
+ const cleanup = async () => {
3219
+ try {
3220
+ if (original === null) await unlink(yarnrcPath).catch(() => {});
3221
+ else await writeFile(yarnrcPath, original, { encoding: "utf8" });
3222
+ } catch {}
3223
+ await rm(cacheFolder, {
3224
+ recursive: true,
3225
+ force: true
3226
+ }).catch(() => {});
3227
+ };
3228
+ return {
3229
+ extraEnv: {
3230
+ ...installEnvSecrets,
3231
+ ...tokenEnv
3232
+ },
3233
+ tokensForRedaction,
3234
+ cleanup
3235
+ };
3236
+ }
3237
+ var init_yarnrc_berry_config = __esmMin((() => {}));
3238
+ //#endregion
3141
3239
  //#region src/execution/validate-kici-deps.ts
3142
3240
  /**
3143
3241
  * Pre-install validation for `.kici/` dependency specifiers.
@@ -3158,8 +3256,10 @@ var init_npm_registry_config = __esmMin((() => {}));
3158
3256
  * - yarn classic (v1) has no `workspace:` protocol and no `portal:` — it links
3159
3257
  * in-repo siblings by version range, not by a local specifier — so both are
3160
3258
  * 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.)
3259
+ * inside the clone, rejected when it escapes.
3260
+ * - yarn berry (v2+) resolves `workspace:` against the repo-root package.json
3261
+ * `workspaces` field and `portal:`/`file:`/`link:` against inside-repo paths,
3262
+ * so those are allowed when present/inside the clone and rejected otherwise.
3163
3263
  *
3164
3264
  * This module performs that classification so unresolvable specifiers fail
3165
3265
  * fast with guidance rather than a cryptic install error.
@@ -3219,6 +3319,17 @@ async function fileExists$1(target) {
3219
3319
  return false;
3220
3320
  }
3221
3321
  }
3322
+ /** Whether the repo-root package.json declares a non-empty `workspaces` array. */
3323
+ async function rootHasWorkspaces(repoRoot) {
3324
+ try {
3325
+ const ws = JSON.parse(await readFile(join(repoRoot, "package.json"), "utf-8")).workspaces;
3326
+ if (Array.isArray(ws)) return ws.length > 0;
3327
+ if (ws && typeof ws === "object" && Array.isArray(ws.packages)) return ws.packages.length > 0;
3328
+ return false;
3329
+ } catch {
3330
+ return false;
3331
+ }
3332
+ }
3222
3333
  /** Resolve a `file:`/`link:`/`portal:` spec to an absolute path under kiciDir. */
3223
3334
  function resolveLocalPath(kiciDir, dep) {
3224
3335
  const rawPath = dep.spec.slice(dep.protocol.length);
@@ -3233,8 +3344,20 @@ function isInsideRepo(repoRoot, target) {
3233
3344
  * Classify each local-protocol dependency for the detected package manager and
3234
3345
  * return the ones that are unresolvable in the agent's single-clone model.
3235
3346
  */
3236
- async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
3347
+ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot, yarnFlavor) {
3237
3348
  if (packageManager === PackageManager.Npm) return [...deps];
3349
+ if (packageManager === PackageManager.Yarn && yarnFlavor === YarnFlavor.Berry) {
3350
+ const hasWorkspaces = await rootHasWorkspaces(repoRoot);
3351
+ const unresolvable = [];
3352
+ for (const dep of deps) {
3353
+ if (dep.protocol === "workspace:") {
3354
+ if (!hasWorkspaces) unresolvable.push(dep);
3355
+ continue;
3356
+ }
3357
+ if (!isInsideRepo(repoRoot, resolveLocalPath(kiciDir, dep))) unresolvable.push(dep);
3358
+ }
3359
+ return unresolvable;
3360
+ }
3238
3361
  if (packageManager === PackageManager.Yarn) {
3239
3362
  const unresolvable = [];
3240
3363
  for (const dep of deps) {
@@ -3258,10 +3381,11 @@ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
3258
3381
  return unresolvable;
3259
3382
  }
3260
3383
  /** Build the actionable error for unresolvable local-protocol dependencies. */
3261
- function formatUnresolvableDepError(offenders, packageManager) {
3384
+ function formatUnresolvableDepError(offenders, packageManager, yarnFlavor) {
3262
3385
  const list = offenders.map((o) => `${o.name}: ${o.spec}`).join(", ");
3263
3386
  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.)`;
3387
+ 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.`;
3388
+ 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.)`;
3265
3389
  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.`;
3266
3390
  }
3267
3391
  /**
@@ -3275,9 +3399,10 @@ async function assertResolvableDeps(args) {
3275
3399
  if (!pkg) return;
3276
3400
  const localDeps = findLocalProtocolDeps(pkg);
3277
3401
  if (localDeps.length === 0) return;
3278
- const offenders = await findUnresolvableDeps(localDeps, args.packageManager, args.kiciDir, args.repoRoot);
3402
+ const flavor = args.yarnFlavor ?? YarnFlavor.Classic;
3403
+ const offenders = await findUnresolvableDeps(localDeps, args.packageManager, args.kiciDir, args.repoRoot, flavor);
3279
3404
  if (offenders.length === 0) return;
3280
- throw new Error(formatUnresolvableDepError(offenders, args.packageManager));
3405
+ throw new Error(formatUnresolvableDepError(offenders, args.packageManager, flavor));
3281
3406
  }
3282
3407
  var LOCAL_PROTOCOLS, DEP_FIELDS;
3283
3408
  var init_validate_kici_deps = __esmMin((() => {
@@ -3402,10 +3527,13 @@ var init_workspace_siblings = __esmMin((() => {}));
3402
3527
  * presence of `.kici/package.json` signals that deps should be installed. npm
3403
3528
  * is the default and ships with every Node.js install; pnpm is used when the
3404
3529
  * repo is a pnpm workspace so a `.kici/` member can resolve in-repo
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.
3530
+ * `workspace:` siblings. yarn is supported in both flavors: classic (v1) reads
3531
+ * `.kici/.npmrc` for registry auth and links version-range workspace siblings;
3532
+ * berry (v2+) reads a synthesized `.kici/.yarnrc.yml` for auth, runs with a
3533
+ * forced `nodeLinker: node-modules` (so the resulting tree matches classic/npm
3534
+ * and the runner's plain node resolution holds), and resolves
3535
+ * `workspace:`/`portal:` siblings. Either flavor links the sibling but does not
3536
+ * build it, so the agent builds the in-repo closure after install.
3409
3537
  *
3410
3538
  * Security: the install runs with an isolated per-invocation cache/store
3411
3539
  * directory to prevent cache poisoning across build jobs — a malicious
@@ -3426,6 +3554,15 @@ async function detectKiciPackageManager(repoRoot, kiciDir) {
3426
3554
  return await detectPackageManagerFromManifests(repoRoot) ?? await detectPackageManagerFromManifests(kiciDir) ?? PackageManager.Npm;
3427
3555
  }
3428
3556
  /**
3557
+ * Detect the yarn flavor (classic vs berry) for the cloned repo. Mirrors
3558
+ * `detectKiciPackageManager`: probe the repo root first, then `.kici/` for a
3559
+ * standalone project. Only called when the detected manager is `Yarn`.
3560
+ */
3561
+ async function detectKiciYarnFlavor(repoRoot, kiciDir) {
3562
+ if (await detectYarnFlavor(repoRoot) === YarnFlavor.Berry) return YarnFlavor.Berry;
3563
+ return detectYarnFlavor(kiciDir);
3564
+ }
3565
+ /**
3429
3566
  * Install `.kici/` dependencies inline with the repo's package manager.
3430
3567
  *
3431
3568
  * Falls back to this when the dep cache is unavailable or a download fails.
@@ -3444,19 +3581,28 @@ async function detectKiciPackageManager(repoRoot, kiciDir) {
3444
3581
  async function installDeps(kiciDir, opts = {}) {
3445
3582
  const repoRoot = opts.repoRoot ?? dirname(kiciDir);
3446
3583
  const packageManager = await detectKiciPackageManager(repoRoot, kiciDir);
3584
+ const yarnFlavor = packageManager === PackageManager.Yarn ? await detectKiciYarnFlavor(repoRoot, kiciDir) : YarnFlavor.Classic;
3447
3585
  logger$6.info("Installing deps inline", {
3448
3586
  packageManager,
3587
+ yarnFlavor,
3449
3588
  dir: kiciDir
3450
3589
  });
3451
- process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, cwd=${kiciDir}\n`);
3590
+ process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, flavor=${yarnFlavor}, cwd=${kiciDir}\n`);
3452
3591
  await assertResolvableDeps({
3453
3592
  kiciDir,
3454
3593
  repoRoot,
3455
- packageManager
3594
+ packageManager,
3595
+ yarnFlavor
3456
3596
  });
3457
3597
  const startTime = Date.now();
3458
3598
  const hasPrivateRegistry = (opts.npmRegistries?.length ?? 0) > 0 || (opts.installEnvSecrets ? Object.keys(opts.installEnvSecrets).length > 0 : false);
3459
- const registryConfig = await applyNpmRegistryConfig({
3599
+ const isBerry = packageManager === PackageManager.Yarn && yarnFlavor === YarnFlavor.Berry;
3600
+ const registryConfig = isBerry ? await applyYarnrcBerryConfig({
3601
+ kiciDir,
3602
+ npmRegistries: opts.npmRegistries,
3603
+ installEnvSecrets: opts.installEnvSecrets,
3604
+ jobIdShort: opts.jobIdShort ?? "00000000"
3605
+ }) : await applyNpmRegistryConfig({
3460
3606
  kiciDir,
3461
3607
  npmRegistries: opts.npmRegistries,
3462
3608
  installEnvSecrets: opts.installEnvSecrets,
@@ -3468,6 +3614,10 @@ async function installDeps(kiciDir, opts = {}) {
3468
3614
  hasPrivateRegistry,
3469
3615
  registryConfig
3470
3616
  });
3617
+ else if (isBerry) await runYarnBerryInstall({
3618
+ kiciDir,
3619
+ registryConfig
3620
+ });
3471
3621
  else if (packageManager === PackageManager.Yarn) await runYarnInstall({
3472
3622
  kiciDir,
3473
3623
  hasPrivateRegistry,
@@ -3487,7 +3637,7 @@ async function installDeps(kiciDir, opts = {}) {
3487
3637
  await registryConfig.cleanup();
3488
3638
  }
3489
3639
  if (packageManager === PackageManager.Pnpm && await kiciHasLocalProtocolDeps(kiciDir)) await buildWorkspaceClosure(repoRoot);
3490
- if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir);
3640
+ if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir, yarnFlavor);
3491
3641
  const durationMs = Date.now() - startTime;
3492
3642
  process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
3493
3643
  logger$6.info("Deps installed inline", {
@@ -3616,6 +3766,35 @@ async function runYarnInstall(args) {
3616
3766
  }).catch(() => {});
3617
3767
  }
3618
3768
  }
3769
+ /** Pure: argv for a berry `yarn install`. Cache + linker live in .yarnrc.yml. */
3770
+ function buildYarnBerryInstallArgs() {
3771
+ return ["install"];
3772
+ }
3773
+ /**
3774
+ * Run a berry `yarn install` from `.kici/`. The synthesized `.kici/.yarnrc.yml`
3775
+ * (applied by `applyYarnrcBerryConfig`) forces `nodeLinker: node-modules`, an
3776
+ * isolated `cacheFolder`, and — when a private registry is configured —
3777
+ * `enableScripts: false` + `npmScopes`/`npmRegistryServer` auth. corepack
3778
+ * provisions the repo-pinned berry version; `COREPACK_ENABLE_DOWNLOAD_PROMPT=0`
3779
+ * makes that non-interactive. Not `--immutable` (resolved URLs in the lockfile
3780
+ * may point at a different registry than the synthesized config).
3781
+ */
3782
+ async function runYarnBerryInstall(args) {
3783
+ await assertYarnAvailable();
3784
+ const { nodeDir } = resolveNpm();
3785
+ const env = {
3786
+ ...envWithNodeOnPath(args.registryConfig.extraEnv, nodeDir),
3787
+ COREPACK_ENABLE_DOWNLOAD_PROMPT: "0"
3788
+ };
3789
+ const argv = buildYarnBerryInstallArgs();
3790
+ process.stderr.write(`[dep-installer:trace] running: yarn ${argv.join(" ")} (berry)\n`);
3791
+ await execFileAsync("yarn", argv, {
3792
+ cwd: args.kiciDir,
3793
+ env,
3794
+ timeout: INSTALL_TIMEOUT_MS,
3795
+ maxBuffer: INSTALL_MAX_BUFFER
3796
+ });
3797
+ }
3619
3798
  /** Throw an actionable error when the repo needs yarn but it is not installed. */
3620
3799
  async function assertYarnAvailable() {
3621
3800
  try {
@@ -3635,7 +3814,7 @@ async function assertYarnAvailable() {
3635
3814
  * Deep cross-sibling build chains may build out of strict topological order —
3636
3815
  * real `.kici` closures are shallow.
3637
3816
  */
3638
- async function buildYarnWorkspaceClosure(repoRoot, kiciDir) {
3817
+ async function buildYarnWorkspaceClosure(repoRoot, kiciDir, yarnFlavor) {
3639
3818
  const siblings = await collectInRepoSiblings(repoRoot, kiciDir, resolveYarnNodeModulesRoot(repoRoot, kiciDir));
3640
3819
  if (siblings.length === 0) return;
3641
3820
  const { nodeDir } = resolveNpm();
@@ -3643,15 +3822,16 @@ async function buildYarnWorkspaceClosure(repoRoot, kiciDir) {
3643
3822
  for (const rel of [...siblings].reverse()) {
3644
3823
  const sibDir = join(repoRoot, rel);
3645
3824
  if (!await siblingHasBuildScript(sibDir)) continue;
3646
- process.stderr.write(`[dep-installer:trace] building yarn sibling: yarn --cwd ${sibDir} run build\n`);
3825
+ const [argv, cwd] = yarnFlavor === YarnFlavor.Berry ? [["run", "build"], sibDir] : [[
3826
+ "--cwd",
3827
+ sibDir,
3828
+ "run",
3829
+ "build"
3830
+ ], repoRoot];
3831
+ process.stderr.write(`[dep-installer:trace] building yarn sibling (${yarnFlavor}): yarn ${argv.join(" ")} @ ${cwd}\n`);
3647
3832
  try {
3648
- await execFileAsync("yarn", [
3649
- "--cwd",
3650
- sibDir,
3651
- "run",
3652
- "build"
3653
- ], {
3654
- cwd: repoRoot,
3833
+ await execFileAsync("yarn", argv, {
3834
+ cwd,
3655
3835
  env,
3656
3836
  timeout: INSTALL_TIMEOUT_MS,
3657
3837
  maxBuffer: INSTALL_MAX_BUFFER
@@ -3729,6 +3909,7 @@ function logSubprocessStreams(e, tokens) {
3729
3909
  var logger$6, execFileAsync, INSTALL_TIMEOUT_MS, INSTALL_MAX_BUFFER;
3730
3910
  var init_dep_installer = __esmMin((() => {
3731
3911
  init_npm_registry_config();
3912
+ init_yarnrc_berry_config();
3732
3913
  init_validate_kici_deps();
3733
3914
  init_workspace_siblings();
3734
3915
  logger$6 = createLogger({ prefix: "dep-installer" });
@@ -3753,11 +3934,14 @@ var init_dep_installer = __esmMin((() => {
3753
3934
  * symlinks into the root store and into sibling dirs that live outside
3754
3935
  * `.kici/`, so packing `.kici/node_modules` alone would capture dangling
3755
3936
  * 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.
3937
+ * - yarn (classic + berry): the resolved node_modules root (standalone `.kici`
3938
+ * `.kici/node_modules`; hoisted workspace member → the repo-root
3939
+ * `node_modules`) plus the in-repo sibling package directories `.kici` depends
3940
+ * on (with their built output), whose symlinks would dangle otherwise. Berry
3941
+ * runs with a forced `nodeLinker: node-modules`, so its tree has the same
3942
+ * node_modules shape as classic and is packed identically (the flavor only
3943
+ * changes how siblings are referenced — version range vs `workspace:`/`portal:`
3944
+ * — not the packed layout).
3761
3945
  *
3762
3946
  * Uses tar.gz (Node.js built-in zlib, no external binary) in portable mode to
3763
3947
  * strip user/group info for cross-machine consistency; symlinks are preserved
@@ -3805,10 +3989,11 @@ async function packNodeModules(kiciDir) {
3805
3989
  /**
3806
3990
  * Compute the repo-root-relative tar entries for the dependency closure. npm
3807
3991
  * 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.
3992
+ * the in-repo workspace siblings `.kici` resolves. yarn (classic + berry) packs
3993
+ * the resolved node_modules root (standalone → `.kici/node_modules`; hoisted
3994
+ * workspace member → root `node_modules`) plus the in-repo siblings, which it
3995
+ * links but does not place in the store. Berry's forced node-modules linker
3996
+ * gives it the same packed shape as classic.
3812
3997
  */
3813
3998
  async function closureEntries(workDir, kiciDir, packageManager) {
3814
3999
  if (packageManager === PackageManager.Pnpm) {
@@ -3988,6 +4173,8 @@ function buildRequest(dispatch, workDir) {
3988
4173
  jobName: jobConfig.baseJobName ?? jobConfig.name ?? "",
3989
4174
  runsOn: jobConfig.runsOn ?? "",
3990
4175
  matrixValues: jobConfig.matrixValues,
4176
+ host: jobConfig.host,
4177
+ agent: jobConfig.agent,
3991
4178
  secrets: dispatch.secrets,
3992
4179
  namespacedSecrets: dispatch.namespacedSecrets,
3993
4180
  sourceFile: jobConfig.source?.file,
@@ -5241,6 +5428,14 @@ function determineExecutionMode(jobConfig, agentConfig) {
5241
5428
  if (agentConfig.scalerManaged) return "firecracker";
5242
5429
  return "bare-metal";
5243
5430
  }
5431
+ /**
5432
+ * Build the result-aware `ctx.needs` for a dynamic eval from its frozen upstream
5433
+ * snapshot. Returns undefined for an event-only generator (no snapshot).
5434
+ */
5435
+ function buildEvalNeedsContext(config) {
5436
+ if (!config.resultAware || !config.upstreamSnapshot) return void 0;
5437
+ return buildNeedsContext(config.upstreamSnapshot, config.declaredNeeds ?? []);
5438
+ }
5244
5439
  var logger$2, JobRunner$1;
5245
5440
  var init_job_runner = __esmMin((() => {
5246
5441
  init_git_clone();
@@ -5988,11 +6183,13 @@ var init_job_runner = __esmMin((() => {
5988
6183
  const { extractDynamicJobFn } = await Promise.resolve().then(() => (init_workflow_loader(), workflow_loader_exports));
5989
6184
  const dynamicFn = extractDynamicJobFn(extractWorkflow(module, config.workflowName), config.source.index);
5990
6185
  evalLog(`Evaluating DynamicJobFn (index ${config.source.index}, timeout ${timeoutMs}ms)`);
6186
+ const needs = buildEvalNeedsContext(config);
5991
6187
  const context = {
5992
6188
  $: scopedDollar,
5993
6189
  ctx: {
5994
6190
  workflow: { name: config.workflowName },
5995
- event: config.event
6191
+ event: config.event,
6192
+ ...needs && { needs }
5996
6193
  },
5997
6194
  log: dynamicJobLogger,
5998
6195
  env: process.env,
@@ -6209,14 +6406,14 @@ var init_job_runner = __esmMin((() => {
6209
6406
  */
6210
6407
  init_console_capture();
6211
6408
  init_npm_resolver();
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";
6409
+ const AGENT_VERSION = "0.1.19";
6410
+ const BUILD_COMMIT = "1590f5e99";
6411
+ const SDK_VERSION = "0.1.19";
6412
+ const SDK_BUNDLE_HASH = "8308089347c304e41b457d3867b17bbff11d6b5cd9706b6823e7abdbd849f33f";
6413
+ const SHARED_VERSION = "0.1.19";
6414
+ const SHARED_BUNDLE_HASH = "5f2c220f24d166b0f13d0620a2e80d19689ac8b683a12154daef44e938fd46a7";
6415
+ const ENGINE_VERSION = "0.1.19";
6416
+ const ENGINE_BUNDLE_HASH = "79ce14640d1798eaaa7cb3aa6c4bc325da3bff1e9f4716936e19614eabb1d858";
6220
6417
  initTelemetry({
6221
6418
  serviceName: "kici-agent",
6222
6419
  otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT