@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.
@@ -24,7 +24,8 @@ import { fileURLToPath, pathToFileURL } from "node:url";
24
24
  import { c, x } from "tar";
25
25
  import { execFile } from "node:child_process";
26
26
  import { promisify } from "node:util";
27
- import { PNPM_IGNORE_BUILD_GATE_ARG, PackageManager, detectPackageManagerFromManifests } from "@kici-dev/shared/package-manager";
27
+ import { PNPM_IGNORE_BUILD_GATE_ARG, PackageManager, YarnFlavor, detectPackageManagerFromManifests, detectYarnFlavor } from "@kici-dev/shared/package-manager";
28
+ import { parse, stringify } from "yaml";
28
29
  var __defProp = Object.defineProperty;
29
30
  var __esmMin = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
30
31
  var __exportAll = (all, no_symbols) => {
@@ -123,6 +124,7 @@ async function signStatementDsse(payloadType, statementBytes) {
123
124
  */
124
125
  async function attestProvenance(deps, input) {
125
126
  const audience = input.audience ?? KICI_PROVENANCE_AUDIENCE;
127
+ const subjectDigest = subjectDigestString(input.subject);
126
128
  const { token } = await deps.getIdToken({ audience });
127
129
  const claims = decodeJwt(token);
128
130
  const now = (deps.now ?? (() => (/* @__PURE__ */ new Date()).toISOString()))();
@@ -142,16 +144,25 @@ async function attestProvenance(deps, input) {
142
144
  identityToken: token
143
145
  }
144
146
  };
145
- const subjectDigest = subjectDigestString(input.subject);
146
147
  return {
147
148
  storageKey: await deps.persist(bundle, subjectDigest),
148
149
  bundle,
149
150
  subjectDigest
150
151
  };
151
152
  }
152
- /** Pick the primary digest (`sha256` preferred) as the storage-key discriminator. */
153
+ /**
154
+ * Pick the primary digest (`sha256` preferred) as the storage-key discriminator.
155
+ * Throws when the subject carries no digest: an empty digest set would otherwise
156
+ * yield an `undefined` storage-key segment (`provenance/<run>/<job>/undefined.kici.json`)
157
+ * and an unverifiable statement, since in-toto requires every subject to carry
158
+ * at least one digest. The SDK `ProvenanceSubjectInput` type allows an empty
159
+ * digest map (both `sha256` and `sha512` are optional), so this is the runtime
160
+ * boundary that upholds the engine `digestSetSchema` invariant.
161
+ */
153
162
  function subjectDigestString(subject) {
154
- return subject.digest.sha256 ?? Object.values(subject.digest)[0];
163
+ const digest = subject.digest.sha256 ?? Object.values(subject.digest)[0];
164
+ if (digest === void 0) throw new Error("provenance subject digest is empty: at least one digest algorithm is required");
165
+ return digest;
155
166
  }
156
167
  //#endregion
157
168
  //#region src/execution/dep-restore.ts
@@ -2175,7 +2186,7 @@ function noopResult() {
2175
2186
  };
2176
2187
  }
2177
2188
  /** Build the synthesized env-var name for registry index `i`. */
2178
- function tokenEnvName(jobIdShort, index) {
2189
+ function tokenEnvName$1(jobIdShort, index) {
2179
2190
  return `KICI_NPM_TOKEN_${jobIdShort}_${index}`;
2180
2191
  }
2181
2192
  /** Render the agent-managed block of `.npmrc` lines. */
@@ -2184,7 +2195,7 @@ function renderAgentLines(registries, jobIdShort) {
2184
2195
  const lines = [];
2185
2196
  for (let i = 0; i < registries.length; i++) {
2186
2197
  const reg = registries[i];
2187
- const envVar = tokenEnvName(jobIdShort, i);
2198
+ const envVar = tokenEnvName$1(jobIdShort, i);
2188
2199
  const authKey = reg.url.replace(/^https?:/, "");
2189
2200
  if (reg.scope) lines.push(`${reg.scope}:registry=${reg.url}`);
2190
2201
  else lines.push(`registry=${reg.url}`);
@@ -2216,7 +2227,7 @@ async function applyNpmRegistryConfig(args) {
2216
2227
  const tokenEnv = {};
2217
2228
  const tokensForRedaction = [];
2218
2229
  for (let i = 0; i < registries.length; i++) {
2219
- tokenEnv[tokenEnvName(args.jobIdShort, i)] = registries[i].token;
2230
+ tokenEnv[tokenEnvName$1(args.jobIdShort, i)] = registries[i].token;
2220
2231
  tokensForRedaction.push(registries[i].token);
2221
2232
  }
2222
2233
  for (const value of Object.values(installEnvSecrets)) if (value) tokensForRedaction.push(value);
@@ -2252,6 +2263,112 @@ function redactNpmOutput(input, tokens) {
2252
2263
  }
2253
2264
  return out;
2254
2265
  }
2266
+ //#endregion
2267
+ //#region src/execution/yarnrc-berry-config.ts
2268
+ /**
2269
+ * Apply yarn-berry registry auth + a forced `nodeLinker: node-modules` to a
2270
+ * workflow's `.kici/.yarnrc.yml` for the lifetime of one `yarn install`, then
2271
+ * restore the file on cleanup. The berry analog of `npm-registry-config.ts`:
2272
+ * berry reads `.yarnrc.yml` (not `.npmrc`), so the auth block uses berry's
2273
+ * `npmRegistryServer` / `npmScopes` / `npmAuthToken` keys with `${VAR}`
2274
+ * env-var interpolation. Token bytes never reach disk — each registry token is
2275
+ * exposed as a job-scoped env var and the on-disk value is the `${VAR}`
2276
+ * reference.
2277
+ *
2278
+ * `nodeLinker: node-modules` makes berry lay down a real `node_modules` tree
2279
+ * (no PnP `.pnp.cjs`), so the agent's packer / restore / sibling-walk /
2280
+ * workflow-loader work unchanged. `enableScripts: false` (when a private
2281
+ * registry is configured) keeps dependency lifecycle scripts from seeing the
2282
+ * synthesized token env vars — the same security model as npm/pnpm/classic
2283
+ * `--ignore-scripts`.
2284
+ *
2285
+ * Reuses the same `ApplyNpmRegistryConfigArgs` / `ApplyNpmRegistryConfigResult`
2286
+ * shapes as the npm overlay so `dep-installer` can pick either by flavor.
2287
+ */
2288
+ /** Build the synthesized env-var name for registry index `i`. */
2289
+ function tokenEnvName(jobIdShort, index) {
2290
+ return `KICI_NPM_TOKEN_${jobIdShort}_${index}`;
2291
+ }
2292
+ /** Read + parse an existing `.yarnrc.yml`, or `{}` when absent/empty. */
2293
+ async function readOriginalYarnrc(path) {
2294
+ try {
2295
+ const raw = await readFile(path, "utf8");
2296
+ return {
2297
+ raw,
2298
+ doc: parse(raw) ?? {}
2299
+ };
2300
+ } catch (err) {
2301
+ if (err.code === "ENOENT") return {
2302
+ raw: null,
2303
+ doc: {}
2304
+ };
2305
+ throw err;
2306
+ }
2307
+ }
2308
+ function buildRegistryBlock(envVar, url, alwaysAuth) {
2309
+ return {
2310
+ npmRegistryServer: url,
2311
+ npmAuthToken: `\${${envVar}}`,
2312
+ ...alwaysAuth ? { npmAlwaysAuth: true } : {}
2313
+ };
2314
+ }
2315
+ async function applyYarnrcBerryConfig(args) {
2316
+ const registries = args.npmRegistries ?? [];
2317
+ const installEnvSecrets = args.installEnvSecrets ?? {};
2318
+ const hasPrivateRegistry = registries.length > 0 || Object.keys(installEnvSecrets).length > 0;
2319
+ const yarnrcPath = join(args.kiciDir, ".yarnrc.yml");
2320
+ const { raw: original, doc } = await readOriginalYarnrc(yarnrcPath);
2321
+ const cacheFolder = await mkdtemp(join(tmpdir(), "kici-yarn-berry-cache-"));
2322
+ const merged = {
2323
+ ...doc,
2324
+ nodeLinker: "node-modules",
2325
+ enableGlobalCache: false,
2326
+ cacheFolder
2327
+ };
2328
+ const tokenEnv = {};
2329
+ const tokensForRedaction = [];
2330
+ if (hasPrivateRegistry) {
2331
+ merged.enableScripts = false;
2332
+ const npmScopes = { ...doc.npmScopes ?? {} };
2333
+ for (let i = 0; i < registries.length; i++) {
2334
+ const reg = registries[i];
2335
+ const envVar = tokenEnvName(args.jobIdShort, i);
2336
+ tokenEnv[envVar] = reg.token;
2337
+ tokensForRedaction.push(reg.token);
2338
+ const block = buildRegistryBlock(envVar, reg.url, reg.alwaysAuth);
2339
+ if (reg.scope) npmScopes[reg.scope] = block;
2340
+ else {
2341
+ merged.npmRegistryServer = reg.url;
2342
+ merged.npmAuthToken = block.npmAuthToken;
2343
+ if (reg.alwaysAuth) merged.npmAlwaysAuth = true;
2344
+ }
2345
+ }
2346
+ if (Object.keys(npmScopes).length > 0) merged.npmScopes = npmScopes;
2347
+ for (const value of Object.values(installEnvSecrets)) if (value) tokensForRedaction.push(value);
2348
+ }
2349
+ await writeFile(yarnrcPath, stringify(merged), {
2350
+ encoding: "utf8",
2351
+ mode: 384
2352
+ });
2353
+ const cleanup = async () => {
2354
+ try {
2355
+ if (original === null) await unlink(yarnrcPath).catch(() => {});
2356
+ else await writeFile(yarnrcPath, original, { encoding: "utf8" });
2357
+ } catch {}
2358
+ await rm(cacheFolder, {
2359
+ recursive: true,
2360
+ force: true
2361
+ }).catch(() => {});
2362
+ };
2363
+ return {
2364
+ extraEnv: {
2365
+ ...installEnvSecrets,
2366
+ ...tokenEnv
2367
+ },
2368
+ tokensForRedaction,
2369
+ cleanup
2370
+ };
2371
+ }
2255
2372
  const LOCAL_PROTOCOLS = [
2256
2373
  "workspace:",
2257
2374
  "file:",
@@ -2320,6 +2437,17 @@ async function fileExists$1(target) {
2320
2437
  return false;
2321
2438
  }
2322
2439
  }
2440
+ /** Whether the repo-root package.json declares a non-empty `workspaces` array. */
2441
+ async function rootHasWorkspaces(repoRoot) {
2442
+ try {
2443
+ const ws = JSON.parse(await readFile(join(repoRoot, "package.json"), "utf-8")).workspaces;
2444
+ if (Array.isArray(ws)) return ws.length > 0;
2445
+ if (ws && typeof ws === "object" && Array.isArray(ws.packages)) return ws.packages.length > 0;
2446
+ return false;
2447
+ } catch {
2448
+ return false;
2449
+ }
2450
+ }
2323
2451
  /** Resolve a `file:`/`link:`/`portal:` spec to an absolute path under kiciDir. */
2324
2452
  function resolveLocalPath(kiciDir, dep) {
2325
2453
  const rawPath = dep.spec.slice(dep.protocol.length);
@@ -2334,8 +2462,20 @@ function isInsideRepo(repoRoot, target) {
2334
2462
  * Classify each local-protocol dependency for the detected package manager and
2335
2463
  * return the ones that are unresolvable in the agent's single-clone model.
2336
2464
  */
2337
- async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
2465
+ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot, yarnFlavor) {
2338
2466
  if (packageManager === PackageManager.Npm) return [...deps];
2467
+ if (packageManager === PackageManager.Yarn && yarnFlavor === YarnFlavor.Berry) {
2468
+ const hasWorkspaces = await rootHasWorkspaces(repoRoot);
2469
+ const unresolvable = [];
2470
+ for (const dep of deps) {
2471
+ if (dep.protocol === "workspace:") {
2472
+ if (!hasWorkspaces) unresolvable.push(dep);
2473
+ continue;
2474
+ }
2475
+ if (!isInsideRepo(repoRoot, resolveLocalPath(kiciDir, dep))) unresolvable.push(dep);
2476
+ }
2477
+ return unresolvable;
2478
+ }
2339
2479
  if (packageManager === PackageManager.Yarn) {
2340
2480
  const unresolvable = [];
2341
2481
  for (const dep of deps) {
@@ -2359,10 +2499,11 @@ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
2359
2499
  return unresolvable;
2360
2500
  }
2361
2501
  /** Build the actionable error for unresolvable local-protocol dependencies. */
2362
- function formatUnresolvableDepError(offenders, packageManager) {
2502
+ function formatUnresolvableDepError(offenders, packageManager, yarnFlavor) {
2363
2503
  const list = offenders.map((o) => `${o.name}: ${o.spec}`).join(", ");
2364
2504
  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.`;
2365
- 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.)`;
2505
+ 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.`;
2506
+ 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.)`;
2366
2507
  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.`;
2367
2508
  }
2368
2509
  /**
@@ -2376,9 +2517,10 @@ async function assertResolvableDeps(args) {
2376
2517
  if (!pkg) return;
2377
2518
  const localDeps = findLocalProtocolDeps(pkg);
2378
2519
  if (localDeps.length === 0) return;
2379
- const offenders = await findUnresolvableDeps(localDeps, args.packageManager, args.kiciDir, args.repoRoot);
2520
+ const flavor = args.yarnFlavor ?? YarnFlavor.Classic;
2521
+ const offenders = await findUnresolvableDeps(localDeps, args.packageManager, args.kiciDir, args.repoRoot, flavor);
2380
2522
  if (offenders.length === 0) return;
2381
- throw new Error(formatUnresolvableDepError(offenders, args.packageManager));
2523
+ throw new Error(formatUnresolvableDepError(offenders, args.packageManager, flavor));
2382
2524
  }
2383
2525
  //#endregion
2384
2526
  //#region src/execution/workspace-siblings.ts
@@ -2487,10 +2629,13 @@ function isAbsoluteRel(rel) {
2487
2629
  * presence of `.kici/package.json` signals that deps should be installed. npm
2488
2630
  * is the default and ships with every Node.js install; pnpm is used when the
2489
2631
  * repo is a pnpm workspace so a `.kici/` member can resolve in-repo
2490
- * `workspace:` siblings. yarn classic (v1) is supported for registry
2491
- * dependencies and version-range workspace siblings (which it links but does
2492
- * not build, so the agent builds the in-repo closure after install). yarn
2493
- * berry (v2+) is not yet supported.
2632
+ * `workspace:` siblings. yarn is supported in both flavors: classic (v1) reads
2633
+ * `.kici/.npmrc` for registry auth and links version-range workspace siblings;
2634
+ * berry (v2+) reads a synthesized `.kici/.yarnrc.yml` for auth, runs with a
2635
+ * forced `nodeLinker: node-modules` (so the resulting tree matches classic/npm
2636
+ * and the runner's plain node resolution holds), and resolves
2637
+ * `workspace:`/`portal:` siblings. Either flavor links the sibling but does not
2638
+ * build it, so the agent builds the in-repo closure after install.
2494
2639
  *
2495
2640
  * Security: the install runs with an isolated per-invocation cache/store
2496
2641
  * directory to prevent cache poisoning across build jobs — a malicious
@@ -2516,6 +2661,15 @@ async function detectKiciPackageManager(repoRoot, kiciDir) {
2516
2661
  return await detectPackageManagerFromManifests(repoRoot) ?? await detectPackageManagerFromManifests(kiciDir) ?? PackageManager.Npm;
2517
2662
  }
2518
2663
  /**
2664
+ * Detect the yarn flavor (classic vs berry) for the cloned repo. Mirrors
2665
+ * `detectKiciPackageManager`: probe the repo root first, then `.kici/` for a
2666
+ * standalone project. Only called when the detected manager is `Yarn`.
2667
+ */
2668
+ async function detectKiciYarnFlavor(repoRoot, kiciDir) {
2669
+ if (await detectYarnFlavor(repoRoot) === YarnFlavor.Berry) return YarnFlavor.Berry;
2670
+ return detectYarnFlavor(kiciDir);
2671
+ }
2672
+ /**
2519
2673
  * Install `.kici/` dependencies inline with the repo's package manager.
2520
2674
  *
2521
2675
  * Falls back to this when the dep cache is unavailable or a download fails.
@@ -2534,19 +2688,28 @@ async function detectKiciPackageManager(repoRoot, kiciDir) {
2534
2688
  async function installDeps(kiciDir, opts = {}) {
2535
2689
  const repoRoot = opts.repoRoot ?? dirname(kiciDir);
2536
2690
  const packageManager = await detectKiciPackageManager(repoRoot, kiciDir);
2691
+ const yarnFlavor = packageManager === PackageManager.Yarn ? await detectKiciYarnFlavor(repoRoot, kiciDir) : YarnFlavor.Classic;
2537
2692
  logger$2.info("Installing deps inline", {
2538
2693
  packageManager,
2694
+ yarnFlavor,
2539
2695
  dir: kiciDir
2540
2696
  });
2541
- process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, cwd=${kiciDir}\n`);
2697
+ process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, flavor=${yarnFlavor}, cwd=${kiciDir}\n`);
2542
2698
  await assertResolvableDeps({
2543
2699
  kiciDir,
2544
2700
  repoRoot,
2545
- packageManager
2701
+ packageManager,
2702
+ yarnFlavor
2546
2703
  });
2547
2704
  const startTime = Date.now();
2548
2705
  const hasPrivateRegistry = (opts.npmRegistries?.length ?? 0) > 0 || (opts.installEnvSecrets ? Object.keys(opts.installEnvSecrets).length > 0 : false);
2549
- const registryConfig = await applyNpmRegistryConfig({
2706
+ const isBerry = packageManager === PackageManager.Yarn && yarnFlavor === YarnFlavor.Berry;
2707
+ const registryConfig = isBerry ? await applyYarnrcBerryConfig({
2708
+ kiciDir,
2709
+ npmRegistries: opts.npmRegistries,
2710
+ installEnvSecrets: opts.installEnvSecrets,
2711
+ jobIdShort: opts.jobIdShort ?? "00000000"
2712
+ }) : await applyNpmRegistryConfig({
2550
2713
  kiciDir,
2551
2714
  npmRegistries: opts.npmRegistries,
2552
2715
  installEnvSecrets: opts.installEnvSecrets,
@@ -2558,6 +2721,10 @@ async function installDeps(kiciDir, opts = {}) {
2558
2721
  hasPrivateRegistry,
2559
2722
  registryConfig
2560
2723
  });
2724
+ else if (isBerry) await runYarnBerryInstall({
2725
+ kiciDir,
2726
+ registryConfig
2727
+ });
2561
2728
  else if (packageManager === PackageManager.Yarn) await runYarnInstall({
2562
2729
  kiciDir,
2563
2730
  hasPrivateRegistry,
@@ -2577,7 +2744,7 @@ async function installDeps(kiciDir, opts = {}) {
2577
2744
  await registryConfig.cleanup();
2578
2745
  }
2579
2746
  if (packageManager === PackageManager.Pnpm && await kiciHasLocalProtocolDeps(kiciDir)) await buildWorkspaceClosure(repoRoot);
2580
- if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir);
2747
+ if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir, yarnFlavor);
2581
2748
  const durationMs = Date.now() - startTime;
2582
2749
  process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
2583
2750
  logger$2.info("Deps installed inline", {
@@ -2706,6 +2873,35 @@ async function runYarnInstall(args) {
2706
2873
  }).catch(() => {});
2707
2874
  }
2708
2875
  }
2876
+ /** Pure: argv for a berry `yarn install`. Cache + linker live in .yarnrc.yml. */
2877
+ function buildYarnBerryInstallArgs() {
2878
+ return ["install"];
2879
+ }
2880
+ /**
2881
+ * Run a berry `yarn install` from `.kici/`. The synthesized `.kici/.yarnrc.yml`
2882
+ * (applied by `applyYarnrcBerryConfig`) forces `nodeLinker: node-modules`, an
2883
+ * isolated `cacheFolder`, and — when a private registry is configured —
2884
+ * `enableScripts: false` + `npmScopes`/`npmRegistryServer` auth. corepack
2885
+ * provisions the repo-pinned berry version; `COREPACK_ENABLE_DOWNLOAD_PROMPT=0`
2886
+ * makes that non-interactive. Not `--immutable` (resolved URLs in the lockfile
2887
+ * may point at a different registry than the synthesized config).
2888
+ */
2889
+ async function runYarnBerryInstall(args) {
2890
+ await assertYarnAvailable();
2891
+ const { nodeDir } = resolveNpm();
2892
+ const env = {
2893
+ ...envWithNodeOnPath(args.registryConfig.extraEnv, nodeDir),
2894
+ COREPACK_ENABLE_DOWNLOAD_PROMPT: "0"
2895
+ };
2896
+ const argv = buildYarnBerryInstallArgs();
2897
+ process.stderr.write(`[dep-installer:trace] running: yarn ${argv.join(" ")} (berry)\n`);
2898
+ await execFileAsync("yarn", argv, {
2899
+ cwd: args.kiciDir,
2900
+ env,
2901
+ timeout: INSTALL_TIMEOUT_MS,
2902
+ maxBuffer: INSTALL_MAX_BUFFER
2903
+ });
2904
+ }
2709
2905
  /** Throw an actionable error when the repo needs yarn but it is not installed. */
2710
2906
  async function assertYarnAvailable() {
2711
2907
  try {
@@ -2725,7 +2921,7 @@ async function assertYarnAvailable() {
2725
2921
  * Deep cross-sibling build chains may build out of strict topological order —
2726
2922
  * real `.kici` closures are shallow.
2727
2923
  */
2728
- async function buildYarnWorkspaceClosure(repoRoot, kiciDir) {
2924
+ async function buildYarnWorkspaceClosure(repoRoot, kiciDir, yarnFlavor) {
2729
2925
  const siblings = await collectInRepoSiblings(repoRoot, kiciDir, resolveYarnNodeModulesRoot(repoRoot, kiciDir));
2730
2926
  if (siblings.length === 0) return;
2731
2927
  const { nodeDir } = resolveNpm();
@@ -2733,15 +2929,16 @@ async function buildYarnWorkspaceClosure(repoRoot, kiciDir) {
2733
2929
  for (const rel of [...siblings].reverse()) {
2734
2930
  const sibDir = join(repoRoot, rel);
2735
2931
  if (!await siblingHasBuildScript(sibDir)) continue;
2736
- process.stderr.write(`[dep-installer:trace] building yarn sibling: yarn --cwd ${sibDir} run build\n`);
2932
+ const [argv, cwd] = yarnFlavor === YarnFlavor.Berry ? [["run", "build"], sibDir] : [[
2933
+ "--cwd",
2934
+ sibDir,
2935
+ "run",
2936
+ "build"
2937
+ ], repoRoot];
2938
+ process.stderr.write(`[dep-installer:trace] building yarn sibling (${yarnFlavor}): yarn ${argv.join(" ")} @ ${cwd}\n`);
2737
2939
  try {
2738
- await execFileAsync("yarn", [
2739
- "--cwd",
2740
- sibDir,
2741
- "run",
2742
- "build"
2743
- ], {
2744
- cwd: repoRoot,
2940
+ await execFileAsync("yarn", argv, {
2941
+ cwd,
2745
2942
  env,
2746
2943
  timeout: INSTALL_TIMEOUT_MS,
2747
2944
  maxBuffer: INSTALL_MAX_BUFFER
@@ -2826,8 +3023,8 @@ function logSubprocessStreams(e, tokens) {
2826
3023
  * no Rolldown step at runtime. `@kici-dev/sdk` and host-repo deps resolve via
2827
3024
  * Node's normal ESM lookup against `.kici/node_modules/`.
2828
3025
  */
2829
- const AGENT_SDK_VERSION = "0.1.17";
2830
- const AGENT_SDK_BUNDLE_HASH = "df47ed5db86eaaa2de8394c0db08335f368e8d620a898cc409765f4545eb3972";
3026
+ const AGENT_SDK_VERSION = "0.1.19";
3027
+ const AGENT_SDK_BUNDLE_HASH = "8308089347c304e41b457d3867b17bbff11d6b5cd9706b6823e7abdbd849f33f";
2831
3028
  /**
2832
3029
  * Register the `@kici-dev/core/ts-loader-hook` oxc-transform ESM loader hook so
2833
3030
  * subsequent dynamic `import()` calls for `.ts` / `.tsx` files transform on the
@@ -2950,18 +3147,20 @@ function extractSteps(workflow, jobName) {
2950
3147
  * A sibling mismatch logs a warning; a missing target job throws a clear
2951
3148
  * determinism error.
2952
3149
  */
2953
- async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event, env, apiTransport, expectedJobNames) {
3150
+ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event, env, apiTransport, expectedJobNames, upstreamSnapshot, declaredNeeds) {
2954
3151
  const dynamicFn = extractDynamicJobFn(workflow, dynamicIndex);
2955
3152
  const { $ } = await import("zx");
2956
3153
  const { createLogger } = await import("@kici-dev/shared");
2957
- const { buildKiciApi } = await import("@kici-dev/sdk");
3154
+ const { buildKiciApi, buildNeedsContext } = await import("@kici-dev/sdk");
2958
3155
  const log = createLogger({ prefix: `dynamic-job-fn:${workflow.name}` });
2959
3156
  const kici = buildKiciApi(apiTransport ?? (() => Promise.reject(/* @__PURE__ */ new Error("Agent API not available during re-evaluation"))));
3157
+ const needs = upstreamSnapshot ? buildNeedsContext(upstreamSnapshot, declaredNeeds ?? []) : void 0;
2960
3158
  const generatedJobs = await dynamicFn({
2961
3159
  $,
2962
3160
  ctx: {
2963
3161
  workflow: { name: workflow.name },
2964
- event
3162
+ event,
3163
+ ...needs && { needs }
2965
3164
  },
2966
3165
  log,
2967
3166
  env,
@@ -3187,7 +3386,7 @@ async function applyOverlay(config) {
3187
3386
  */
3188
3387
  init_download();
3189
3388
  init_dep_restore();
3190
- const AGENT_VERSION = "0.1.17";
3389
+ const AGENT_VERSION = "0.1.19";
3191
3390
  process.on("uncaughtException", (err) => {
3192
3391
  process.stderr.write(`[workflow-runner] UNCAUGHT EXCEPTION: ${err.message}\n`);
3193
3392
  if (err.stack) process.stderr.write(`[workflow-runner] Stack: ${err.stack}\n`);
@@ -4034,7 +4233,9 @@ function createSandboxStepContext(workDir, stepIndex, stepName, request, maskedS
4034
4233
  attestProvenance: buildAttestProvenanceFn(request, workDir, (o) => kici.oidc.token(o)),
4035
4234
  ...rawPayload && { rawPayload },
4036
4235
  ...request.provider && { provider: request.provider },
4037
- ...request.matrixValues && { matrix: request.matrixValues }
4236
+ ...request.matrixValues && { matrix: request.matrixValues },
4237
+ ...request.host && { host: request.host },
4238
+ ...request.agent && { agent: request.agent }
4038
4239
  };
4039
4240
  }
4040
4241
  /** Raw provider webhook body for ctx.rawPayload — nested in the envelope. */
@@ -4585,7 +4786,7 @@ async function extractAndNormalizeSteps(workflow, request, apiTransport) {
4585
4786
  let rawSteps;
4586
4787
  let driftDroppedJobs = [];
4587
4788
  if (request.dynamicSource) {
4588
- const dynamicResult = await extractStepsFromDynamicJob(workflow, request.dynamicSource.index, request.jobName, request.dynamicSource.event, process.env, apiTransport, request.dynamicSource.expectedJobNames);
4789
+ const dynamicResult = await extractStepsFromDynamicJob(workflow, request.dynamicSource.index, request.jobName, request.dynamicSource.event, process.env, apiTransport, request.dynamicSource.expectedJobNames, request.dynamicSource.upstreamSnapshot, request.dynamicSource.declaredNeeds);
4589
4790
  rawSteps = dynamicResult.steps;
4590
4791
  driftDroppedJobs = dynamicResult.droppedJobs;
4591
4792
  if (driftDroppedJobs.length > 0) trace(`Determinism drift: ${driftDroppedJobs.length} job(s) dropped: ${driftDroppedJobs.join(", ")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kici-dev/agent",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "Customer-deployable agent for the KiCI CI/CD stack. Connects to an orchestrator, clones the workflow repo, executes steps, and streams logs back.",
5
5
  "keywords": [
6
6
  "ci",
@@ -53,20 +53,21 @@
53
53
  }
54
54
  },
55
55
  "dependencies": {
56
- "@hono/node-server": "^2.0.0",
57
- "archiver": "^7.0.1",
56
+ "@hono/node-server": "^2.0.4",
57
+ "archiver": "^8.0.0",
58
58
  "dockerode": "^5.0.0",
59
- "hono": "^4.12.18",
59
+ "hono": "^4.12.25",
60
60
  "jose": "^6.1.0",
61
- "tar": "^7.5.13",
61
+ "tar": "^7.5.16",
62
62
  "winston": "^3.19.0",
63
- "ws": "^8.20.0",
64
- "zod": "^4.3.6",
63
+ "ws": "^8.21.0",
64
+ "yaml": "^2.9.0",
65
+ "zod": "^4.4.3",
65
66
  "zx": "^8.8.5",
66
- "@kici-dev/core": "0.1.17",
67
- "@kici-dev/sdk": "0.1.17",
68
- "@kici-dev/shared": "0.1.17",
69
- "@kici-dev/engine": "0.1.17"
67
+ "@kici-dev/engine": "0.1.19",
68
+ "@kici-dev/sdk": "0.1.19",
69
+ "@kici-dev/core": "0.1.19",
70
+ "@kici-dev/shared": "0.1.19"
70
71
  },
71
72
  "kici": {
72
73
  "metrics": {
@@ -75,7 +76,7 @@
75
76
  }
76
77
  },
77
78
  "devDependencies": {
78
- "@types/archiver": "^7.0.0",
79
+ "@types/archiver": "^8.0.0",
79
80
  "@types/dockerode": "^4.0.1"
80
81
  },
81
82
  "scripts": {