@kici-dev/agent 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/execution/dep-installer.d.ts +12 -3
  2. package/dist/execution/dep-packer.d.ts +9 -1
  3. package/dist/execution/env-init/presets/directives.d.ts +20 -0
  4. package/dist/execution/env-init/presets/expand.d.ts +16 -0
  5. package/dist/execution/env-init/presets/mise/cache-key.d.ts +7 -0
  6. package/dist/execution/env-init/presets/mise/expander.d.ts +16 -0
  7. package/dist/execution/env-init/presets/mise/templates.d.ts +16 -0
  8. package/dist/execution/env-init/presets/mise/windows-install.d.ts +18 -0
  9. package/dist/execution/env-init/presets/registry.d.ts +31 -0
  10. package/dist/execution/init-runner.d.ts +7 -0
  11. package/dist/execution/job-runner.d.ts +9 -1
  12. package/dist/execution/sandbox/env-delta.d.ts +2 -0
  13. package/dist/execution/sandbox/index.d.ts +1 -1
  14. package/dist/execution/sandbox/ipc-protocol.d.ts +81 -3
  15. package/dist/execution/sandbox/types.d.ts +9 -1
  16. package/dist/execution/sandbox/workflow-runner.d.ts +12 -7
  17. package/dist/execution/validate-kici-deps.d.ts +10 -2
  18. package/dist/execution/workflow-loader.d.ts +5 -1
  19. package/dist/execution/workspace-siblings.d.ts +33 -0
  20. package/dist/execution/yarnrc-berry-config.d.ts +23 -0
  21. package/dist/index.js +423 -17
  22. package/dist/provenance/attest.d.ts +30 -0
  23. package/dist/provenance/sign.d.ts +21 -0
  24. package/dist/provenance/statement-builder.d.ts +38 -0
  25. package/dist/server.js +703 -160
  26. package/dist/version.d.ts +2 -0
  27. package/dist/workflow-runner.js +955 -59
  28. package/dist/ws/orchestrator-client.d.ts +16 -1
  29. package/package.json +14 -12
  30. package/sbom.spdx.json +2526 -5994
package/dist/index.js CHANGED
@@ -5,12 +5,13 @@ import { z } from "zod";
5
5
  import { LOGGER_ENV_VARS, defineEnv, validateUnknownKiciVars } from "@kici-dev/shared/env";
6
6
  import { KNOWN_ROLES, validateNoReservedLabels } from "@kici-dev/engine";
7
7
  import { execFile } from "node:child_process";
8
- import { access, cp, mkdir, mkdtemp, readFile, readdir, rename, rm, unlink, writeFile } from "node:fs/promises";
8
+ import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
9
9
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
10
10
  import { promisify } from "node:util";
11
11
  import { createLogger, sha256, toErrorMessage } from "@kici-dev/shared";
12
- import { PNPM_IGNORE_BUILD_GATE_ARG, PackageManager, detectPackageManagerFromManifests } from "@kici-dev/shared/package-manager";
12
+ import { PNPM_IGNORE_BUILD_GATE_ARG, PackageManager, YarnFlavor, detectPackageManagerFromManifests, detectYarnFlavor } from "@kici-dev/shared/package-manager";
13
13
  import { existsSync } from "node:fs";
14
+ import { parse, stringify } from "yaml";
14
15
  import { Readable, Transform } from "node:stream";
15
16
  import { pipeline } from "node:stream/promises";
16
17
  import { createGunzip } from "node:zlib";
@@ -214,7 +215,7 @@ function noopResult() {
214
215
  };
215
216
  }
216
217
  /** Build the synthesized env-var name for registry index `i`. */
217
- function tokenEnvName(jobIdShort, index) {
218
+ function tokenEnvName$1(jobIdShort, index) {
218
219
  return `KICI_NPM_TOKEN_${jobIdShort}_${index}`;
219
220
  }
220
221
  /** Render the agent-managed block of `.npmrc` lines. */
@@ -223,7 +224,7 @@ function renderAgentLines(registries, jobIdShort) {
223
224
  const lines = [];
224
225
  for (let i = 0; i < registries.length; i++) {
225
226
  const reg = registries[i];
226
- const envVar = tokenEnvName(jobIdShort, i);
227
+ const envVar = tokenEnvName$1(jobIdShort, i);
227
228
  const authKey = reg.url.replace(/^https?:/, "");
228
229
  if (reg.scope) lines.push(`${reg.scope}:registry=${reg.url}`);
229
230
  else lines.push(`registry=${reg.url}`);
@@ -255,7 +256,7 @@ async function applyNpmRegistryConfig(args) {
255
256
  const tokenEnv = {};
256
257
  const tokensForRedaction = [];
257
258
  for (let i = 0; i < registries.length; i++) {
258
- tokenEnv[tokenEnvName(args.jobIdShort, i)] = registries[i].token;
259
+ tokenEnv[tokenEnvName$1(args.jobIdShort, i)] = registries[i].token;
259
260
  tokensForRedaction.push(registries[i].token);
260
261
  }
261
262
  for (const value of Object.values(installEnvSecrets)) if (value) tokensForRedaction.push(value);
@@ -292,6 +293,112 @@ function redactNpmOutput(input, tokens) {
292
293
  return out;
293
294
  }
294
295
  //#endregion
296
+ //#region src/execution/yarnrc-berry-config.ts
297
+ /**
298
+ * Apply yarn-berry registry auth + a forced `nodeLinker: node-modules` to a
299
+ * workflow's `.kici/.yarnrc.yml` for the lifetime of one `yarn install`, then
300
+ * restore the file on cleanup. The berry analog of `npm-registry-config.ts`:
301
+ * berry reads `.yarnrc.yml` (not `.npmrc`), so the auth block uses berry's
302
+ * `npmRegistryServer` / `npmScopes` / `npmAuthToken` keys with `${VAR}`
303
+ * env-var interpolation. Token bytes never reach disk — each registry token is
304
+ * exposed as a job-scoped env var and the on-disk value is the `${VAR}`
305
+ * reference.
306
+ *
307
+ * `nodeLinker: node-modules` makes berry lay down a real `node_modules` tree
308
+ * (no PnP `.pnp.cjs`), so the agent's packer / restore / sibling-walk /
309
+ * workflow-loader work unchanged. `enableScripts: false` (when a private
310
+ * registry is configured) keeps dependency lifecycle scripts from seeing the
311
+ * synthesized token env vars — the same security model as npm/pnpm/classic
312
+ * `--ignore-scripts`.
313
+ *
314
+ * Reuses the same `ApplyNpmRegistryConfigArgs` / `ApplyNpmRegistryConfigResult`
315
+ * shapes as the npm overlay so `dep-installer` can pick either by flavor.
316
+ */
317
+ /** Build the synthesized env-var name for registry index `i`. */
318
+ function tokenEnvName(jobIdShort, index) {
319
+ return `KICI_NPM_TOKEN_${jobIdShort}_${index}`;
320
+ }
321
+ /** Read + parse an existing `.yarnrc.yml`, or `{}` when absent/empty. */
322
+ async function readOriginalYarnrc(path) {
323
+ try {
324
+ const raw = await readFile(path, "utf8");
325
+ return {
326
+ raw,
327
+ doc: parse(raw) ?? {}
328
+ };
329
+ } catch (err) {
330
+ if (err.code === "ENOENT") return {
331
+ raw: null,
332
+ doc: {}
333
+ };
334
+ throw err;
335
+ }
336
+ }
337
+ function buildRegistryBlock(envVar, url, alwaysAuth) {
338
+ return {
339
+ npmRegistryServer: url,
340
+ npmAuthToken: `\${${envVar}}`,
341
+ ...alwaysAuth ? { npmAlwaysAuth: true } : {}
342
+ };
343
+ }
344
+ async function applyYarnrcBerryConfig(args) {
345
+ const registries = args.npmRegistries ?? [];
346
+ const installEnvSecrets = args.installEnvSecrets ?? {};
347
+ const hasPrivateRegistry = registries.length > 0 || Object.keys(installEnvSecrets).length > 0;
348
+ const yarnrcPath = join(args.kiciDir, ".yarnrc.yml");
349
+ const { raw: original, doc } = await readOriginalYarnrc(yarnrcPath);
350
+ const cacheFolder = await mkdtemp(join(tmpdir(), "kici-yarn-berry-cache-"));
351
+ const merged = {
352
+ ...doc,
353
+ nodeLinker: "node-modules",
354
+ enableGlobalCache: false,
355
+ cacheFolder
356
+ };
357
+ const tokenEnv = {};
358
+ const tokensForRedaction = [];
359
+ if (hasPrivateRegistry) {
360
+ merged.enableScripts = false;
361
+ const npmScopes = { ...doc.npmScopes ?? {} };
362
+ for (let i = 0; i < registries.length; i++) {
363
+ const reg = registries[i];
364
+ const envVar = tokenEnvName(args.jobIdShort, i);
365
+ tokenEnv[envVar] = reg.token;
366
+ tokensForRedaction.push(reg.token);
367
+ const block = buildRegistryBlock(envVar, reg.url, reg.alwaysAuth);
368
+ if (reg.scope) npmScopes[reg.scope] = block;
369
+ else {
370
+ merged.npmRegistryServer = reg.url;
371
+ merged.npmAuthToken = block.npmAuthToken;
372
+ if (reg.alwaysAuth) merged.npmAlwaysAuth = true;
373
+ }
374
+ }
375
+ if (Object.keys(npmScopes).length > 0) merged.npmScopes = npmScopes;
376
+ for (const value of Object.values(installEnvSecrets)) if (value) tokensForRedaction.push(value);
377
+ }
378
+ await writeFile(yarnrcPath, stringify(merged), {
379
+ encoding: "utf8",
380
+ mode: 384
381
+ });
382
+ const cleanup = async () => {
383
+ try {
384
+ if (original === null) await unlink(yarnrcPath).catch(() => {});
385
+ else await writeFile(yarnrcPath, original, { encoding: "utf8" });
386
+ } catch {}
387
+ await rm(cacheFolder, {
388
+ recursive: true,
389
+ force: true
390
+ }).catch(() => {});
391
+ };
392
+ return {
393
+ extraEnv: {
394
+ ...installEnvSecrets,
395
+ ...tokenEnv
396
+ },
397
+ tokensForRedaction,
398
+ cleanup
399
+ };
400
+ }
401
+ //#endregion
295
402
  //#region src/execution/validate-kici-deps.ts
296
403
  /**
297
404
  * Pre-install validation for `.kici/` dependency specifiers.
@@ -309,6 +416,13 @@ function redactNpmOutput(input, tokens) {
309
416
  * clones the whole repo, so an in-repo sibling is present), and resolves
310
417
  * `file:`/`link:`/`portal:` against a path — allowed when that path stays
311
418
  * inside the cloned repo, rejected when it escapes the clone.
419
+ * - yarn classic (v1) has no `workspace:` protocol and no `portal:` — it links
420
+ * in-repo siblings by version range, not by a local specifier — so both are
421
+ * rejected with guidance; `file:`/`link:` are allowed when the path stays
422
+ * inside the clone, rejected when it escapes.
423
+ * - yarn berry (v2+) resolves `workspace:` against the repo-root package.json
424
+ * `workspaces` field and `portal:`/`file:`/`link:` against inside-repo paths,
425
+ * so those are allowed when present/inside the clone and rejected otherwise.
312
426
  *
313
427
  * This module performs that classification so unresolvable specifiers fail
314
428
  * fast with guidance rather than a cryptic install error.
@@ -389,6 +503,17 @@ async function fileExists(target) {
389
503
  return false;
390
504
  }
391
505
  }
506
+ /** Whether the repo-root package.json declares a non-empty `workspaces` array. */
507
+ async function rootHasWorkspaces(repoRoot) {
508
+ try {
509
+ const ws = JSON.parse(await readFile(join(repoRoot, "package.json"), "utf-8")).workspaces;
510
+ if (Array.isArray(ws)) return ws.length > 0;
511
+ if (ws && typeof ws === "object" && Array.isArray(ws.packages)) return ws.packages.length > 0;
512
+ return false;
513
+ } catch {
514
+ return false;
515
+ }
516
+ }
392
517
  /** Resolve a `file:`/`link:`/`portal:` spec to an absolute path under kiciDir. */
393
518
  function resolveLocalPath(kiciDir, dep) {
394
519
  const rawPath = dep.spec.slice(dep.protocol.length);
@@ -403,8 +528,31 @@ function isInsideRepo(repoRoot, target) {
403
528
  * Classify each local-protocol dependency for the detected package manager and
404
529
  * return the ones that are unresolvable in the agent's single-clone model.
405
530
  */
406
- async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
531
+ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot, yarnFlavor) {
407
532
  if (packageManager === PackageManager.Npm) return [...deps];
533
+ if (packageManager === PackageManager.Yarn && yarnFlavor === YarnFlavor.Berry) {
534
+ const hasWorkspaces = await rootHasWorkspaces(repoRoot);
535
+ const unresolvable = [];
536
+ for (const dep of deps) {
537
+ if (dep.protocol === "workspace:") {
538
+ if (!hasWorkspaces) unresolvable.push(dep);
539
+ continue;
540
+ }
541
+ if (!isInsideRepo(repoRoot, resolveLocalPath(kiciDir, dep))) unresolvable.push(dep);
542
+ }
543
+ return unresolvable;
544
+ }
545
+ if (packageManager === PackageManager.Yarn) {
546
+ const unresolvable = [];
547
+ for (const dep of deps) {
548
+ if (dep.protocol === "workspace:" || dep.protocol === "portal:") {
549
+ unresolvable.push(dep);
550
+ continue;
551
+ }
552
+ if (!isInsideRepo(repoRoot, resolveLocalPath(kiciDir, dep))) unresolvable.push(dep);
553
+ }
554
+ return unresolvable;
555
+ }
408
556
  const hasWorkspaceFile = await fileExists(join(repoRoot, "pnpm-workspace.yaml"));
409
557
  const unresolvable = [];
410
558
  for (const dep of deps) {
@@ -417,9 +565,11 @@ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
417
565
  return unresolvable;
418
566
  }
419
567
  /** Build the actionable error for unresolvable local-protocol dependencies. */
420
- function formatUnresolvableDepError(offenders, packageManager) {
568
+ function formatUnresolvableDepError(offenders, packageManager, yarnFlavor) {
421
569
  const list = offenders.map((o) => `${o.name}: ${o.spec}`).join(", ");
422
570
  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.`;
571
+ 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.`;
572
+ 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.)`;
423
573
  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.`;
424
574
  }
425
575
  /**
@@ -433,9 +583,105 @@ async function assertResolvableDeps(args) {
433
583
  if (!pkg) return;
434
584
  const localDeps = findLocalProtocolDeps(pkg);
435
585
  if (localDeps.length === 0) return;
436
- const offenders = await findUnresolvableDeps(localDeps, args.packageManager, args.kiciDir, args.repoRoot);
586
+ const flavor = args.yarnFlavor ?? YarnFlavor.Classic;
587
+ const offenders = await findUnresolvableDeps(localDeps, args.packageManager, args.kiciDir, args.repoRoot, flavor);
437
588
  if (offenders.length === 0) return;
438
- throw new Error(formatUnresolvableDepError(offenders, args.packageManager));
589
+ throw new Error(formatUnresolvableDepError(offenders, args.packageManager, flavor));
590
+ }
591
+ //#endregion
592
+ //#region src/execution/workspace-siblings.ts
593
+ /**
594
+ * In-repo workspace-sibling discovery for the agent's dependency handling.
595
+ *
596
+ * A pnpm or yarn-classic workspace lays out a `.kici/` member's `workspace:`
597
+ * (pnpm) or version-range (yarn) siblings as symlinks pointing at package
598
+ * directories that live inside the clone but outside `.kici/` and outside the
599
+ * `node_modules` store. The dep-cache packer must travel those sibling dirs with
600
+ * the closure (their symlinks would dangle otherwise), and the yarn install path
601
+ * must build them (the install links a sibling but does not build it).
602
+ *
603
+ * `collectInRepoSiblings` walks a starting `node_modules` (and transitively each
604
+ * discovered sibling's `node_modules`), returning each in-repo sibling directory
605
+ * once, repo-root-relative, in breadth-first discovery order. The starting
606
+ * `node_modules` is a parameter so it serves pnpm + yarn-standalone (seeded at
607
+ * `.kici/node_modules`) and yarn-workspace-member (seeded at the hoisted root
608
+ * `node_modules`).
609
+ */
610
+ /**
611
+ * The directory yarn lays `.kici`'s dependencies into. A standalone `.kici`
612
+ * (own lockfile, no parent workspace) gets `.kici/node_modules`; a workspace
613
+ * member hoists everything to the repo-root `node_modules`, leaving no
614
+ * `.kici/node_modules`.
615
+ */
616
+ function resolveYarnNodeModulesRoot(repoRoot, kiciDir) {
617
+ const kiciNm = join(kiciDir, "node_modules");
618
+ return existsSync(kiciNm) ? kiciNm : join(repoRoot, "node_modules");
619
+ }
620
+ /**
621
+ * Walk `seedNodeModules` (and transitively each in-repo sibling's
622
+ * `node_modules`) collecting the repo-root-relative directories of workspace
623
+ * siblings — package dirs that live inside the clone but outside `.kici/` and
624
+ * outside the repo-root `node_modules/` store. Returns each dir once, in
625
+ * discovery (BFS) order.
626
+ */
627
+ async function collectInRepoSiblings(workDir, kiciDir, seedNodeModules = join(kiciDir, "node_modules")) {
628
+ const repoRoot = resolve(workDir);
629
+ const kiciResolved = resolve(kiciDir);
630
+ const rootNodeModules = resolve(join(workDir, "node_modules"));
631
+ const found = /* @__PURE__ */ new Set();
632
+ const visited = /* @__PURE__ */ new Set();
633
+ const queue = [seedNodeModules];
634
+ while (queue.length > 0) {
635
+ const nmDir = queue.shift();
636
+ const real = await realpath(nmDir).catch(() => null);
637
+ if (!real || visited.has(real)) continue;
638
+ visited.add(real);
639
+ for (const target of await resolveNodeModulesLinks(nmDir)) {
640
+ if (!isInside(repoRoot, target)) continue;
641
+ if (isInside(kiciResolved, target) || isInside(rootNodeModules, target)) continue;
642
+ const rel = relative(workDir, target);
643
+ if (!found.has(rel)) {
644
+ found.add(rel);
645
+ queue.push(join(target, "node_modules"));
646
+ }
647
+ }
648
+ }
649
+ return [...found];
650
+ }
651
+ /** Resolve every package symlink target under a `node_modules` dir (descending one level into `@scope` dirs). */
652
+ async function resolveNodeModulesLinks(nmDir) {
653
+ const targets = [];
654
+ for (const entry of await readdir(nmDir).catch(() => [])) {
655
+ if (entry.startsWith(".")) continue;
656
+ const entryPath = join(nmDir, entry);
657
+ if (entry.startsWith("@")) {
658
+ for (const scoped of await readdir(entryPath).catch(() => [])) {
659
+ const target = await resolveIfSymlink(join(entryPath, scoped));
660
+ if (target) targets.push(target);
661
+ }
662
+ continue;
663
+ }
664
+ const target = await resolveIfSymlink(entryPath);
665
+ if (target) targets.push(target);
666
+ }
667
+ return targets;
668
+ }
669
+ /** Return the real path of `p` if it is a symlink, else null. */
670
+ async function resolveIfSymlink(p) {
671
+ try {
672
+ if (!(await lstat(p)).isSymbolicLink()) return null;
673
+ return await realpath(p);
674
+ } catch {
675
+ return null;
676
+ }
677
+ }
678
+ /** Whether `target` is `root` itself or a path inside it. */
679
+ function isInside(root, target) {
680
+ const rel = relative(root, target);
681
+ return rel === "" || !rel.startsWith("..") && !rel.startsWith(`..${sep}`) && !isAbsoluteRel(rel);
682
+ }
683
+ function isAbsoluteRel(rel) {
684
+ return rel.length > 1 && rel[1] === ":";
439
685
  }
440
686
  //#endregion
441
687
  //#region src/execution/dep-installer.ts
@@ -445,12 +691,17 @@ async function assertResolvableDeps(args) {
445
691
  * When the dep cache is unavailable or a download fails, the agent installs
446
692
  * `.kici/` dependencies directly with the repository's package manager.
447
693
  *
448
- * The package manager is detected from the cloned repo (npm / pnpm); the
694
+ * The package manager is detected from the cloned repo (npm / pnpm / yarn); the
449
695
  * presence of `.kici/package.json` signals that deps should be installed. npm
450
696
  * is the default and ships with every Node.js install; pnpm is used when the
451
697
  * repo is a pnpm workspace so a `.kici/` member can resolve in-repo
452
- * `workspace:` siblings. yarn is detected but not yet supported and is
453
- * rejected with an actionable error.
698
+ * `workspace:` siblings. yarn is supported in both flavors: classic (v1) reads
699
+ * `.kici/.npmrc` for registry auth and links version-range workspace siblings;
700
+ * berry (v2+) reads a synthesized `.kici/.yarnrc.yml` for auth, runs with a
701
+ * forced `nodeLinker: node-modules` (so the resulting tree matches classic/npm
702
+ * and the runner's plain node resolution holds), and resolves
703
+ * `workspace:`/`portal:` siblings. Either flavor links the sibling but does not
704
+ * build it, so the agent builds the in-repo closure after install.
454
705
  *
455
706
  * Security: the install runs with an isolated per-invocation cache/store
456
707
  * directory to prevent cache poisoning across build jobs — a malicious
@@ -476,6 +727,15 @@ async function detectKiciPackageManager(repoRoot, kiciDir) {
476
727
  return await detectPackageManagerFromManifests(repoRoot) ?? await detectPackageManagerFromManifests(kiciDir) ?? PackageManager.Npm;
477
728
  }
478
729
  /**
730
+ * Detect the yarn flavor (classic vs berry) for the cloned repo. Mirrors
731
+ * `detectKiciPackageManager`: probe the repo root first, then `.kici/` for a
732
+ * standalone project. Only called when the detected manager is `Yarn`.
733
+ */
734
+ async function detectKiciYarnFlavor(repoRoot, kiciDir) {
735
+ if (await detectYarnFlavor(repoRoot) === YarnFlavor.Berry) return YarnFlavor.Berry;
736
+ return detectYarnFlavor(kiciDir);
737
+ }
738
+ /**
479
739
  * Install `.kici/` dependencies inline with the repo's package manager.
480
740
  *
481
741
  * Falls back to this when the dep cache is unavailable or a download fails.
@@ -494,20 +754,28 @@ async function detectKiciPackageManager(repoRoot, kiciDir) {
494
754
  async function installDeps(kiciDir, opts = {}) {
495
755
  const repoRoot = opts.repoRoot ?? dirname(kiciDir);
496
756
  const packageManager = await detectKiciPackageManager(repoRoot, kiciDir);
757
+ const yarnFlavor = packageManager === PackageManager.Yarn ? await detectKiciYarnFlavor(repoRoot, kiciDir) : YarnFlavor.Classic;
497
758
  logger$2.info("Installing deps inline", {
498
759
  packageManager,
760
+ yarnFlavor,
499
761
  dir: kiciDir
500
762
  });
501
- process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, cwd=${kiciDir}\n`);
502
- if (packageManager === PackageManager.Yarn) throw new Error("This repository uses yarn, which the KiCI agent does not yet support for .kici/ dependency installation. Use npm or pnpm for the .kici/ project, or open a feature request for yarn support.");
763
+ process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, flavor=${yarnFlavor}, cwd=${kiciDir}\n`);
503
764
  await assertResolvableDeps({
504
765
  kiciDir,
505
766
  repoRoot,
506
- packageManager
767
+ packageManager,
768
+ yarnFlavor
507
769
  });
508
770
  const startTime = Date.now();
509
771
  const hasPrivateRegistry = (opts.npmRegistries?.length ?? 0) > 0 || (opts.installEnvSecrets ? Object.keys(opts.installEnvSecrets).length > 0 : false);
510
- const registryConfig = await applyNpmRegistryConfig({
772
+ const isBerry = packageManager === PackageManager.Yarn && yarnFlavor === YarnFlavor.Berry;
773
+ const registryConfig = isBerry ? await applyYarnrcBerryConfig({
774
+ kiciDir,
775
+ npmRegistries: opts.npmRegistries,
776
+ installEnvSecrets: opts.installEnvSecrets,
777
+ jobIdShort: opts.jobIdShort ?? "00000000"
778
+ }) : await applyNpmRegistryConfig({
511
779
  kiciDir,
512
780
  npmRegistries: opts.npmRegistries,
513
781
  installEnvSecrets: opts.installEnvSecrets,
@@ -519,6 +787,15 @@ async function installDeps(kiciDir, opts = {}) {
519
787
  hasPrivateRegistry,
520
788
  registryConfig
521
789
  });
790
+ else if (isBerry) await runYarnBerryInstall({
791
+ kiciDir,
792
+ registryConfig
793
+ });
794
+ else if (packageManager === PackageManager.Yarn) await runYarnInstall({
795
+ kiciDir,
796
+ hasPrivateRegistry,
797
+ registryConfig
798
+ });
522
799
  else await runNpmInstall({
523
800
  kiciDir,
524
801
  hasPrivateRegistry,
@@ -533,6 +810,7 @@ async function installDeps(kiciDir, opts = {}) {
533
810
  await registryConfig.cleanup();
534
811
  }
535
812
  if (packageManager === PackageManager.Pnpm && await kiciHasLocalProtocolDeps(kiciDir)) await buildWorkspaceClosure(repoRoot);
813
+ if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir, yarnFlavor);
536
814
  const durationMs = Date.now() - startTime;
537
815
  process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
538
816
  logger$2.info("Deps installed inline", {
@@ -620,6 +898,131 @@ async function runPnpmInstall(args) {
620
898
  }).catch(() => {});
621
899
  }
622
900
  }
901
+ /** Pure: argv for `yarn install` with an isolated cache folder. */
902
+ function buildYarnInstallArgs(cacheDir, hasPrivateRegistry) {
903
+ const a = [
904
+ "install",
905
+ "--cache-folder",
906
+ cacheDir,
907
+ "--non-interactive",
908
+ "--no-progress"
909
+ ];
910
+ if (hasPrivateRegistry) a.push("--ignore-scripts");
911
+ return a;
912
+ }
913
+ /**
914
+ * Run `yarn install` from `.kici/` with an isolated cache folder. yarn classic
915
+ * reads the synthesized `.kici/.npmrc` (registry + `${VAR}` token expansion) for
916
+ * private-registry auth. A workspace member hoists deps to the repo-root
917
+ * node_modules; a standalone `.kici` gets `.kici/node_modules`. Not
918
+ * `--frozen-lockfile` (resolved URLs in the lockfile may point at a different
919
+ * registry than the synthesized `.npmrc`, e.g. localhost tunnel vs direct IP).
920
+ */
921
+ async function runYarnInstall(args) {
922
+ await assertYarnAvailable();
923
+ const { nodeDir } = resolveNpm();
924
+ const cacheDir = await mkdtemp(join(tmpdir(), "kici-yarn-cache-"));
925
+ const env = envWithNodeOnPath(args.registryConfig.extraEnv, nodeDir);
926
+ const argv = buildYarnInstallArgs(cacheDir, args.hasPrivateRegistry);
927
+ try {
928
+ process.stderr.write(`[dep-installer:trace] running: yarn ${argv.join(" ")}\n`);
929
+ await execFileAsync("yarn", argv, {
930
+ cwd: args.kiciDir,
931
+ env,
932
+ timeout: INSTALL_TIMEOUT_MS,
933
+ maxBuffer: INSTALL_MAX_BUFFER
934
+ });
935
+ } finally {
936
+ await rm(cacheDir, {
937
+ recursive: true,
938
+ force: true
939
+ }).catch(() => {});
940
+ }
941
+ }
942
+ /** Pure: argv for a berry `yarn install`. Cache + linker live in .yarnrc.yml. */
943
+ function buildYarnBerryInstallArgs() {
944
+ return ["install"];
945
+ }
946
+ /**
947
+ * Run a berry `yarn install` from `.kici/`. The synthesized `.kici/.yarnrc.yml`
948
+ * (applied by `applyYarnrcBerryConfig`) forces `nodeLinker: node-modules`, an
949
+ * isolated `cacheFolder`, and — when a private registry is configured —
950
+ * `enableScripts: false` + `npmScopes`/`npmRegistryServer` auth. corepack
951
+ * provisions the repo-pinned berry version; `COREPACK_ENABLE_DOWNLOAD_PROMPT=0`
952
+ * makes that non-interactive. Not `--immutable` (resolved URLs in the lockfile
953
+ * may point at a different registry than the synthesized config).
954
+ */
955
+ async function runYarnBerryInstall(args) {
956
+ await assertYarnAvailable();
957
+ const { nodeDir } = resolveNpm();
958
+ const env = {
959
+ ...envWithNodeOnPath(args.registryConfig.extraEnv, nodeDir),
960
+ COREPACK_ENABLE_DOWNLOAD_PROMPT: "0"
961
+ };
962
+ const argv = buildYarnBerryInstallArgs();
963
+ process.stderr.write(`[dep-installer:trace] running: yarn ${argv.join(" ")} (berry)\n`);
964
+ await execFileAsync("yarn", argv, {
965
+ cwd: args.kiciDir,
966
+ env,
967
+ timeout: INSTALL_TIMEOUT_MS,
968
+ maxBuffer: INSTALL_MAX_BUFFER
969
+ });
970
+ }
971
+ /** Throw an actionable error when the repo needs yarn but it is not installed. */
972
+ async function assertYarnAvailable() {
973
+ try {
974
+ await execFileAsync("yarn", ["--version"], {
975
+ timeout: 3e4,
976
+ cwd: tmpdir()
977
+ });
978
+ } catch (e) {
979
+ throw new Error(`This repository uses yarn, but yarn is not available on this agent. Install yarn (e.g. \`corepack enable\`) or run on a container/Firecracker agent that bundles it. (${toErrorMessage(e)})`);
980
+ }
981
+ }
982
+ /**
983
+ * Build the in-repo workspace siblings `.kici` depends on (yarn links them on
984
+ * install but does not build them). Walks siblings from the resolved
985
+ * node_modules root and runs each sibling's `build` script in leaf-first
986
+ * (reverse-discovery) order with a clean env (no synthesized registry tokens).
987
+ * Deep cross-sibling build chains may build out of strict topological order —
988
+ * real `.kici` closures are shallow.
989
+ */
990
+ async function buildYarnWorkspaceClosure(repoRoot, kiciDir, yarnFlavor) {
991
+ const siblings = await collectInRepoSiblings(repoRoot, kiciDir, resolveYarnNodeModulesRoot(repoRoot, kiciDir));
992
+ if (siblings.length === 0) return;
993
+ const { nodeDir } = resolveNpm();
994
+ const env = envWithNodeOnPath({}, nodeDir);
995
+ for (const rel of [...siblings].reverse()) {
996
+ const sibDir = join(repoRoot, rel);
997
+ if (!await siblingHasBuildScript(sibDir)) continue;
998
+ const [argv, cwd] = yarnFlavor === YarnFlavor.Berry ? [["run", "build"], sibDir] : [[
999
+ "--cwd",
1000
+ sibDir,
1001
+ "run",
1002
+ "build"
1003
+ ], repoRoot];
1004
+ process.stderr.write(`[dep-installer:trace] building yarn sibling (${yarnFlavor}): yarn ${argv.join(" ")} @ ${cwd}\n`);
1005
+ try {
1006
+ await execFileAsync("yarn", argv, {
1007
+ cwd,
1008
+ env,
1009
+ timeout: INSTALL_TIMEOUT_MS,
1010
+ maxBuffer: INSTALL_MAX_BUFFER
1011
+ });
1012
+ } catch (e) {
1013
+ logSubprocessStreams(e, []);
1014
+ throw new Error(`Failed to build .kici yarn workspace sibling ${rel}: ${describeExecError(e)}`);
1015
+ }
1016
+ }
1017
+ }
1018
+ /** Whether a sibling package.json declares a `build` script. */
1019
+ async function siblingHasBuildScript(sibDir) {
1020
+ try {
1021
+ return typeof JSON.parse(await readFile(join(sibDir, "package.json"), "utf-8")).scripts?.build === "string";
1022
+ } catch {
1023
+ return false;
1024
+ }
1025
+ }
623
1026
  /**
624
1027
  * Build the in-repo dependency closure of the `.kici/` package so a
625
1028
  * `workspace:` sibling's build output exists before the workflow that imports
@@ -663,7 +1066,10 @@ function describeExecError(e) {
663
1066
  /** Throw an actionable error when the repo needs pnpm but it is not installed. */
664
1067
  async function assertPnpmAvailable() {
665
1068
  try {
666
- await execFileAsync("pnpm", ["--version"], { timeout: 3e4 });
1069
+ await execFileAsync("pnpm", ["--version"], {
1070
+ timeout: 3e4,
1071
+ cwd: tmpdir()
1072
+ });
667
1073
  } catch (e) {
668
1074
  throw new Error(`This repository is a pnpm workspace, but pnpm is not available on this agent. Install pnpm (e.g. \`corepack enable\`) or run on a container/ Firecracker agent that bundles it. (${toErrorMessage(e)})`);
669
1075
  }
@@ -0,0 +1,30 @@
1
+ import { type KiciBundle } from '@kici-dev/engine/provenance/bundle';
2
+ import type { OidcTokenResult } from '@kici-dev/engine/protocol/messages/oidc-token-relay';
3
+ import { type ProvenanceSubject } from './statement-builder.js';
4
+ export interface AttestDeps {
5
+ /** P1.4 relay: returns a KiCI ID token bound to the current job. */
6
+ getIdToken: (opts: {
7
+ audience: string;
8
+ }) => Promise<OidcTokenResult>;
9
+ /** Upload the serialized bundle; returns the storage key it was written to. */
10
+ persist: (bundle: KiciBundle, subjectDigest: string) => Promise<string>;
11
+ builderVersions: {
12
+ 'kici-agent': string;
13
+ 'kici-orchestrator': string;
14
+ };
15
+ /** Injectable clock for deterministic tests; defaults to wall time. */
16
+ now?: () => string;
17
+ }
18
+ export interface AttestInput {
19
+ subject: ProvenanceSubject;
20
+ audience?: string;
21
+ }
22
+ export interface AttestResult {
23
+ storageKey: string;
24
+ bundle: KiciBundle;
25
+ subjectDigest: string;
26
+ }
27
+ export declare function attestProvenance(deps: AttestDeps, input: AttestInput): Promise<AttestResult>;
28
+ /** Pick the primary digest (`sha256` preferred) as the storage-key discriminator. */
29
+ export declare function subjectDigestString(subject: ProvenanceSubject): string;
30
+ //# sourceMappingURL=attest.d.ts.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Ephemeral-key DSSE signer for KiCI provenance (Mode A).
3
+ *
4
+ * Generates a fresh in-process ES256 keypair (never persisted), DSSE-signs the
5
+ * PAE of the statement bytes with the private half, and returns the envelope
6
+ * plus the public JWK. The public key travels in the bundle so the verifier can
7
+ * check the signature; the key needs no separate trust root because the bundle's
8
+ * identity JWT (verified against the Platform JWKS) anchors the whole package.
9
+ */
10
+ import { type JWK } from 'jose';
11
+ import { type DsseEnvelope } from '@kici-dev/engine/provenance/dsse';
12
+ export interface SignedStatement {
13
+ envelope: DsseEnvelope;
14
+ /** Ephemeral public key as a JWK, with `kid` = its RFC 7638 thumbprint. */
15
+ publicJwk: JWK & {
16
+ kid: string;
17
+ };
18
+ }
19
+ /** DSSE-sign `statementBytes` with a fresh in-process ephemeral ES256 key. */
20
+ export declare function signStatementDsse(payloadType: string, statementBytes: Uint8Array): Promise<SignedStatement>;
21
+ //# sourceMappingURL=sign.d.ts.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Build a SLSA v1.0 in-toto provenance statement from the server-truth identity
3
+ * token claims plus the caller-supplied subject. The build context comes
4
+ * entirely from the JWT claims (Platform-minted, unforgeable), so the
5
+ * statement's identity equals the token's identity by construction.
6
+ */
7
+ import { type KiciProvenanceStatement } from '@kici-dev/engine/provenance/schema';
8
+ /** The KiCI identity-token claims the builder reads (Platform server-truth). */
9
+ export interface ProvenanceTokenClaims {
10
+ iss: string;
11
+ repository?: string | null;
12
+ ref?: string | null;
13
+ sha?: string | null;
14
+ workflow_ref?: string | null;
15
+ kici_run_id: string;
16
+ kici_job_id: string;
17
+ orchestrator_id?: string | null;
18
+ }
19
+ /** Caller-supplied artifact subject: a name plus a lowercase-hex digest map. */
20
+ export interface ProvenanceSubject {
21
+ name: string;
22
+ digest: Record<string, string>;
23
+ }
24
+ export interface BuildStatementInput {
25
+ tokenClaims: ProvenanceTokenClaims;
26
+ subject: ProvenanceSubject;
27
+ builderVersions: {
28
+ 'kici-agent': string;
29
+ 'kici-orchestrator': string;
30
+ };
31
+ /** ISO-8601 timestamp with offset. */
32
+ startedOn: string;
33
+ /** ISO-8601 timestamp with offset. */
34
+ finishedOn: string;
35
+ }
36
+ /** Build a KiCI SLSA v1.0 provenance statement (validates against the P1.1 schema). */
37
+ export declare function buildProvenanceStatement(input: BuildStatementInput): KiciProvenanceStatement;
38
+ //# sourceMappingURL=statement-builder.d.ts.map