@kici-dev/agent 0.1.17 → 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.
package/dist/server.js CHANGED
@@ -15,7 +15,7 @@ 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 { ZipArchive } from "archiver";
19
19
  import fs, { existsSync } from "node:fs";
20
20
  import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
21
21
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -24,13 +24,15 @@ import { format, promisify } from "node:util";
24
24
  import { gcStaleTmpDirs } from "@kici-dev/core/tmp-gc";
25
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) => {
@@ -1297,14 +1299,14 @@ var init_console_capture = __esmMin((() => {
1297
1299
  init_console_capture();
1298
1300
  function safe(name, fallback = "unknown") {
1299
1301
  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";
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";
1308
1310
  default: return fallback;
1309
1311
  }
1310
1312
  }
@@ -1949,18 +1951,20 @@ function extractSteps(workflow, jobName) {
1949
1951
  * A sibling mismatch logs a warning; a missing target job throws a clear
1950
1952
  * determinism error.
1951
1953
  */
1952
- async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event, env, apiTransport, expectedJobNames) {
1954
+ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event, env, apiTransport, expectedJobNames, upstreamSnapshot, declaredNeeds) {
1953
1955
  const dynamicFn = extractDynamicJobFn(workflow, dynamicIndex);
1954
1956
  const { $ } = await import("zx");
1955
1957
  const { createLogger } = await import("@kici-dev/shared");
1956
- const { buildKiciApi } = await import("@kici-dev/sdk");
1958
+ const { buildKiciApi, buildNeedsContext } = await import("@kici-dev/sdk");
1957
1959
  const log = createLogger({ prefix: `dynamic-job-fn:${workflow.name}` });
1958
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;
1959
1962
  const generatedJobs = await dynamicFn({
1960
1963
  $,
1961
1964
  ctx: {
1962
1965
  workflow: { name: workflow.name },
1963
- event
1966
+ event,
1967
+ ...needs && { needs }
1964
1968
  },
1965
1969
  log,
1966
1970
  env,
@@ -1988,8 +1992,8 @@ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event
1988
1992
  }
1989
1993
  var AGENT_SDK_VERSION, AGENT_SDK_BUNDLE_HASH, hookRegistered;
1990
1994
  var init_workflow_loader = __esmMin((() => {
1991
- AGENT_SDK_VERSION = "0.1.17";
1992
- AGENT_SDK_BUNDLE_HASH = "df47ed5db86eaaa2de8394c0db08335f368e8d620a898cc409765f4545eb3972";
1995
+ AGENT_SDK_VERSION = "0.1.18";
1996
+ AGENT_SDK_BUNDLE_HASH = "8308089347c304e41b457d3867b17bbff11d6b5cd9706b6823e7abdbd849f33f";
1993
1997
  hookRegistered = false;
1994
1998
  }));
1995
1999
  //#endregion
@@ -2496,7 +2500,7 @@ async function serializeJobsToLock(jobs, ctx, staticNames, allowedGroups) {
2496
2500
  return result;
2497
2501
  }
2498
2502
  async function serializeJob(job, generatedNames, ctx, staticNames, allowedGroups) {
2499
- const { runsOn, excludeLabels } = normalizeRunsOn(job.runsOn);
2503
+ const { include: runsOn, exclude: excludeLabels } = normalizeRunsOnToMatchers(job.runsOn, `generated job '${job.name}' runsOn`);
2500
2504
  let resolvedEnvironment;
2501
2505
  if (typeof job.environment === "function") {
2502
2506
  const value = await withTimeout(() => job.environment(ctx.event), DYNAMIC_FIELD_TIMEOUT_MS, `dynamic environment for generated job '${job.name}'`);
@@ -2519,7 +2523,7 @@ async function serializeJob(job, generatedNames, ctx, staticNames, allowedGroups
2519
2523
  _type: "static",
2520
2524
  name: job.name,
2521
2525
  runsOn,
2522
- ...excludeLabels ? { excludeLabels } : {},
2526
+ ...excludeLabels.length > 0 ? { excludeLabels } : {},
2523
2527
  needs: resolvedNeeds,
2524
2528
  ...dependsOnGroups.length > 0 ? { dependsOnGroups } : {},
2525
2529
  steps: serializeSteps(job.steps),
@@ -2533,20 +2537,6 @@ async function serializeJob(job, generatedNames, ctx, staticNames, allowedGroups
2533
2537
  };
2534
2538
  }
2535
2539
  /**
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
2540
  * Resolve needs references. Jobs can reference other jobs by name (string),
2551
2541
  * Job object reference, DynamicGroupRef, or NeedsEntry/NeedsGroupEntry objects.
2552
2542
  * Validates against generatedNames union staticNames union allowedGroups.
@@ -2632,7 +2622,7 @@ async function serializeMatrix(matrix, jobName, runsOn, ctx) {
2632
2622
  workflow: { name: ctx.workflowName },
2633
2623
  job: {
2634
2624
  name: jobName,
2635
- runsOn: typeof runsOn === "string" ? runsOn : [...runsOn]
2625
+ runsOn: runsOn.map((m) => m.kind === "exact" ? m.value : `/${m.source}/${m.flags}`)
2636
2626
  }
2637
2627
  },
2638
2628
  log: ctx.log,
@@ -3059,7 +3049,7 @@ function noopResult() {
3059
3049
  };
3060
3050
  }
3061
3051
  /** Build the synthesized env-var name for registry index `i`. */
3062
- function tokenEnvName(jobIdShort, index) {
3052
+ function tokenEnvName$1(jobIdShort, index) {
3063
3053
  return `KICI_NPM_TOKEN_${jobIdShort}_${index}`;
3064
3054
  }
3065
3055
  /** Render the agent-managed block of `.npmrc` lines. */
@@ -3068,7 +3058,7 @@ function renderAgentLines(registries, jobIdShort) {
3068
3058
  const lines = [];
3069
3059
  for (let i = 0; i < registries.length; i++) {
3070
3060
  const reg = registries[i];
3071
- const envVar = tokenEnvName(jobIdShort, i);
3061
+ const envVar = tokenEnvName$1(jobIdShort, i);
3072
3062
  const authKey = reg.url.replace(/^https?:/, "");
3073
3063
  if (reg.scope) lines.push(`${reg.scope}:registry=${reg.url}`);
3074
3064
  else lines.push(`registry=${reg.url}`);
@@ -3100,7 +3090,7 @@ async function applyNpmRegistryConfig(args) {
3100
3090
  const tokenEnv = {};
3101
3091
  const tokensForRedaction = [];
3102
3092
  for (let i = 0; i < registries.length; i++) {
3103
- tokenEnv[tokenEnvName(args.jobIdShort, i)] = registries[i].token;
3093
+ tokenEnv[tokenEnvName$1(args.jobIdShort, i)] = registries[i].token;
3104
3094
  tokensForRedaction.push(registries[i].token);
3105
3095
  }
3106
3096
  for (const value of Object.values(installEnvSecrets)) if (value) tokensForRedaction.push(value);
@@ -3138,6 +3128,113 @@ function redactNpmOutput(input, tokens) {
3138
3128
  }
3139
3129
  var init_npm_registry_config = __esmMin((() => {}));
3140
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
3141
3238
  //#region src/execution/validate-kici-deps.ts
3142
3239
  /**
3143
3240
  * Pre-install validation for `.kici/` dependency specifiers.
@@ -3158,8 +3255,10 @@ var init_npm_registry_config = __esmMin((() => {}));
3158
3255
  * - yarn classic (v1) has no `workspace:` protocol and no `portal:` — it links
3159
3256
  * in-repo siblings by version range, not by a local specifier — so both are
3160
3257
  * 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.)
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.
3163
3262
  *
3164
3263
  * This module performs that classification so unresolvable specifiers fail
3165
3264
  * fast with guidance rather than a cryptic install error.
@@ -3219,6 +3318,17 @@ async function fileExists$1(target) {
3219
3318
  return false;
3220
3319
  }
3221
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
+ }
3222
3332
  /** Resolve a `file:`/`link:`/`portal:` spec to an absolute path under kiciDir. */
3223
3333
  function resolveLocalPath(kiciDir, dep) {
3224
3334
  const rawPath = dep.spec.slice(dep.protocol.length);
@@ -3233,8 +3343,20 @@ function isInsideRepo(repoRoot, target) {
3233
3343
  * Classify each local-protocol dependency for the detected package manager and
3234
3344
  * return the ones that are unresolvable in the agent's single-clone model.
3235
3345
  */
3236
- async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
3346
+ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot, yarnFlavor) {
3237
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
+ }
3238
3360
  if (packageManager === PackageManager.Yarn) {
3239
3361
  const unresolvable = [];
3240
3362
  for (const dep of deps) {
@@ -3258,10 +3380,11 @@ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
3258
3380
  return unresolvable;
3259
3381
  }
3260
3382
  /** Build the actionable error for unresolvable local-protocol dependencies. */
3261
- function formatUnresolvableDepError(offenders, packageManager) {
3383
+ function formatUnresolvableDepError(offenders, packageManager, yarnFlavor) {
3262
3384
  const list = offenders.map((o) => `${o.name}: ${o.spec}`).join(", ");
3263
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.`;
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.)`;
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.)`;
3265
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.`;
3266
3389
  }
3267
3390
  /**
@@ -3275,9 +3398,10 @@ async function assertResolvableDeps(args) {
3275
3398
  if (!pkg) return;
3276
3399
  const localDeps = findLocalProtocolDeps(pkg);
3277
3400
  if (localDeps.length === 0) return;
3278
- 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);
3279
3403
  if (offenders.length === 0) return;
3280
- throw new Error(formatUnresolvableDepError(offenders, args.packageManager));
3404
+ throw new Error(formatUnresolvableDepError(offenders, args.packageManager, flavor));
3281
3405
  }
3282
3406
  var LOCAL_PROTOCOLS, DEP_FIELDS;
3283
3407
  var init_validate_kici_deps = __esmMin((() => {
@@ -3402,10 +3526,13 @@ var init_workspace_siblings = __esmMin((() => {}));
3402
3526
  * presence of `.kici/package.json` signals that deps should be installed. npm
3403
3527
  * is the default and ships with every Node.js install; pnpm is used when the
3404
3528
  * 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.
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.
3409
3536
  *
3410
3537
  * Security: the install runs with an isolated per-invocation cache/store
3411
3538
  * directory to prevent cache poisoning across build jobs — a malicious
@@ -3426,6 +3553,15 @@ async function detectKiciPackageManager(repoRoot, kiciDir) {
3426
3553
  return await detectPackageManagerFromManifests(repoRoot) ?? await detectPackageManagerFromManifests(kiciDir) ?? PackageManager.Npm;
3427
3554
  }
3428
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
+ /**
3429
3565
  * Install `.kici/` dependencies inline with the repo's package manager.
3430
3566
  *
3431
3567
  * Falls back to this when the dep cache is unavailable or a download fails.
@@ -3444,19 +3580,28 @@ async function detectKiciPackageManager(repoRoot, kiciDir) {
3444
3580
  async function installDeps(kiciDir, opts = {}) {
3445
3581
  const repoRoot = opts.repoRoot ?? dirname(kiciDir);
3446
3582
  const packageManager = await detectKiciPackageManager(repoRoot, kiciDir);
3583
+ const yarnFlavor = packageManager === PackageManager.Yarn ? await detectKiciYarnFlavor(repoRoot, kiciDir) : YarnFlavor.Classic;
3447
3584
  logger$6.info("Installing deps inline", {
3448
3585
  packageManager,
3586
+ yarnFlavor,
3449
3587
  dir: kiciDir
3450
3588
  });
3451
- process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, cwd=${kiciDir}\n`);
3589
+ process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, flavor=${yarnFlavor}, cwd=${kiciDir}\n`);
3452
3590
  await assertResolvableDeps({
3453
3591
  kiciDir,
3454
3592
  repoRoot,
3455
- packageManager
3593
+ packageManager,
3594
+ yarnFlavor
3456
3595
  });
3457
3596
  const startTime = Date.now();
3458
3597
  const hasPrivateRegistry = (opts.npmRegistries?.length ?? 0) > 0 || (opts.installEnvSecrets ? Object.keys(opts.installEnvSecrets).length > 0 : false);
3459
- 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({
3460
3605
  kiciDir,
3461
3606
  npmRegistries: opts.npmRegistries,
3462
3607
  installEnvSecrets: opts.installEnvSecrets,
@@ -3468,6 +3613,10 @@ async function installDeps(kiciDir, opts = {}) {
3468
3613
  hasPrivateRegistry,
3469
3614
  registryConfig
3470
3615
  });
3616
+ else if (isBerry) await runYarnBerryInstall({
3617
+ kiciDir,
3618
+ registryConfig
3619
+ });
3471
3620
  else if (packageManager === PackageManager.Yarn) await runYarnInstall({
3472
3621
  kiciDir,
3473
3622
  hasPrivateRegistry,
@@ -3487,7 +3636,7 @@ async function installDeps(kiciDir, opts = {}) {
3487
3636
  await registryConfig.cleanup();
3488
3637
  }
3489
3638
  if (packageManager === PackageManager.Pnpm && await kiciHasLocalProtocolDeps(kiciDir)) await buildWorkspaceClosure(repoRoot);
3490
- if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir);
3639
+ if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir, yarnFlavor);
3491
3640
  const durationMs = Date.now() - startTime;
3492
3641
  process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
3493
3642
  logger$6.info("Deps installed inline", {
@@ -3616,6 +3765,35 @@ async function runYarnInstall(args) {
3616
3765
  }).catch(() => {});
3617
3766
  }
3618
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
+ }
3619
3797
  /** Throw an actionable error when the repo needs yarn but it is not installed. */
3620
3798
  async function assertYarnAvailable() {
3621
3799
  try {
@@ -3635,7 +3813,7 @@ async function assertYarnAvailable() {
3635
3813
  * Deep cross-sibling build chains may build out of strict topological order —
3636
3814
  * real `.kici` closures are shallow.
3637
3815
  */
3638
- async function buildYarnWorkspaceClosure(repoRoot, kiciDir) {
3816
+ async function buildYarnWorkspaceClosure(repoRoot, kiciDir, yarnFlavor) {
3639
3817
  const siblings = await collectInRepoSiblings(repoRoot, kiciDir, resolveYarnNodeModulesRoot(repoRoot, kiciDir));
3640
3818
  if (siblings.length === 0) return;
3641
3819
  const { nodeDir } = resolveNpm();
@@ -3643,15 +3821,16 @@ async function buildYarnWorkspaceClosure(repoRoot, kiciDir) {
3643
3821
  for (const rel of [...siblings].reverse()) {
3644
3822
  const sibDir = join(repoRoot, rel);
3645
3823
  if (!await siblingHasBuildScript(sibDir)) continue;
3646
- process.stderr.write(`[dep-installer:trace] building yarn sibling: yarn --cwd ${sibDir} run build\n`);
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`);
3647
3831
  try {
3648
- await execFileAsync("yarn", [
3649
- "--cwd",
3650
- sibDir,
3651
- "run",
3652
- "build"
3653
- ], {
3654
- cwd: repoRoot,
3832
+ await execFileAsync("yarn", argv, {
3833
+ cwd,
3655
3834
  env,
3656
3835
  timeout: INSTALL_TIMEOUT_MS,
3657
3836
  maxBuffer: INSTALL_MAX_BUFFER
@@ -3729,6 +3908,7 @@ function logSubprocessStreams(e, tokens) {
3729
3908
  var logger$6, execFileAsync, INSTALL_TIMEOUT_MS, INSTALL_MAX_BUFFER;
3730
3909
  var init_dep_installer = __esmMin((() => {
3731
3910
  init_npm_registry_config();
3911
+ init_yarnrc_berry_config();
3732
3912
  init_validate_kici_deps();
3733
3913
  init_workspace_siblings();
3734
3914
  logger$6 = createLogger({ prefix: "dep-installer" });
@@ -3753,11 +3933,14 @@ var init_dep_installer = __esmMin((() => {
3753
3933
  * symlinks into the root store and into sibling dirs that live outside
3754
3934
  * `.kici/`, so packing `.kici/node_modules` alone would capture dangling
3755
3935
  * 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.
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).
3761
3944
  *
3762
3945
  * Uses tar.gz (Node.js built-in zlib, no external binary) in portable mode to
3763
3946
  * strip user/group info for cross-machine consistency; symlinks are preserved
@@ -3805,10 +3988,11 @@ async function packNodeModules(kiciDir) {
3805
3988
  /**
3806
3989
  * Compute the repo-root-relative tar entries for the dependency closure. npm
3807
3990
  * 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.
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.
3812
3996
  */
3813
3997
  async function closureEntries(workDir, kiciDir, packageManager) {
3814
3998
  if (packageManager === PackageManager.Pnpm) {
@@ -3988,6 +4172,8 @@ function buildRequest(dispatch, workDir) {
3988
4172
  jobName: jobConfig.baseJobName ?? jobConfig.name ?? "",
3989
4173
  runsOn: jobConfig.runsOn ?? "",
3990
4174
  matrixValues: jobConfig.matrixValues,
4175
+ host: jobConfig.host,
4176
+ agent: jobConfig.agent,
3991
4177
  secrets: dispatch.secrets,
3992
4178
  namespacedSecrets: dispatch.namespacedSecrets,
3993
4179
  sourceFile: jobConfig.source?.file,
@@ -5241,6 +5427,14 @@ function determineExecutionMode(jobConfig, agentConfig) {
5241
5427
  if (agentConfig.scalerManaged) return "firecracker";
5242
5428
  return "bare-metal";
5243
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
+ }
5244
5438
  var logger$2, JobRunner$1;
5245
5439
  var init_job_runner = __esmMin((() => {
5246
5440
  init_git_clone();
@@ -5988,11 +6182,13 @@ var init_job_runner = __esmMin((() => {
5988
6182
  const { extractDynamicJobFn } = await Promise.resolve().then(() => (init_workflow_loader(), workflow_loader_exports));
5989
6183
  const dynamicFn = extractDynamicJobFn(extractWorkflow(module, config.workflowName), config.source.index);
5990
6184
  evalLog(`Evaluating DynamicJobFn (index ${config.source.index}, timeout ${timeoutMs}ms)`);
6185
+ const needs = buildEvalNeedsContext(config);
5991
6186
  const context = {
5992
6187
  $: scopedDollar,
5993
6188
  ctx: {
5994
6189
  workflow: { name: config.workflowName },
5995
- event: config.event
6190
+ event: config.event,
6191
+ ...needs && { needs }
5996
6192
  },
5997
6193
  log: dynamicJobLogger,
5998
6194
  env: process.env,
@@ -6209,14 +6405,14 @@ var init_job_runner = __esmMin((() => {
6209
6405
  */
6210
6406
  init_console_capture();
6211
6407
  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";
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";
6220
6416
  initTelemetry({
6221
6417
  serviceName: "kici-agent",
6222
6418
  otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT