@kici-dev/agent 0.1.17 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/execution/dep-installer.d.ts +9 -4
- package/dist/execution/dep-packer.d.ts +8 -5
- package/dist/execution/sandbox/ipc-protocol.d.ts +23 -0
- package/dist/execution/validate-kici-deps.d.ts +7 -4
- package/dist/execution/workflow-loader.d.ts +5 -1
- package/dist/execution/yarnrc-berry-config.d.ts +23 -0
- package/dist/index.js +217 -28
- package/dist/provenance/attest.d.ts +9 -1
- package/dist/server.js +277 -80
- package/dist/workflow-runner.js +238 -37
- package/package.json +13 -12
- package/sbom.spdx.json +2551 -6049
|
@@ -8,10 +8,13 @@
|
|
|
8
8
|
* presence of `.kici/package.json` signals that deps should be installed. npm
|
|
9
9
|
* is the default and ships with every Node.js install; pnpm is used when the
|
|
10
10
|
* repo is a pnpm workspace so a `.kici/` member can resolve in-repo
|
|
11
|
-
* `workspace:` siblings. yarn classic (v1)
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* `workspace:` siblings. yarn is supported in both flavors: classic (v1) reads
|
|
12
|
+
* `.kici/.npmrc` for registry auth and links version-range workspace siblings;
|
|
13
|
+
* berry (v2+) reads a synthesized `.kici/.yarnrc.yml` for auth, runs with a
|
|
14
|
+
* forced `nodeLinker: node-modules` (so the resulting tree matches classic/npm
|
|
15
|
+
* and the runner's plain node resolution holds), and resolves
|
|
16
|
+
* `workspace:`/`portal:` siblings. Either flavor links the sibling but does not
|
|
17
|
+
* build it, so the agent builds the in-repo closure after install.
|
|
15
18
|
*
|
|
16
19
|
* Security: the install runs with an isolated per-invocation cache/store
|
|
17
20
|
* directory to prevent cache poisoning across build jobs — a malicious
|
|
@@ -53,4 +56,6 @@ export interface InstallDepsOptions {
|
|
|
53
56
|
export declare function installDeps(kiciDir: string, opts?: InstallDepsOptions): Promise<void>;
|
|
54
57
|
/** Pure: argv for `yarn install` with an isolated cache folder. */
|
|
55
58
|
export declare function buildYarnInstallArgs(cacheDir: string, hasPrivateRegistry: boolean): string[];
|
|
59
|
+
/** Pure: argv for a berry `yarn install`. Cache + linker live in .yarnrc.yml. */
|
|
60
|
+
export declare function buildYarnBerryInstallArgs(): string[];
|
|
56
61
|
//# sourceMappingURL=dep-installer.d.ts.map
|
|
@@ -13,11 +13,14 @@
|
|
|
13
13
|
* symlinks into the root store and into sibling dirs that live outside
|
|
14
14
|
* `.kici/`, so packing `.kici/node_modules` alone would capture dangling
|
|
15
15
|
* links — the store and siblings must travel together.
|
|
16
|
-
* - yarn classic: the resolved node_modules root (standalone `.kici`
|
|
17
|
-
* `.kici/node_modules`; hoisted workspace member → the repo-root
|
|
18
|
-
* `node_modules`) plus the in-repo
|
|
19
|
-
*
|
|
20
|
-
*
|
|
16
|
+
* - yarn (classic + berry): the resolved node_modules root (standalone `.kici`
|
|
17
|
+
* → `.kici/node_modules`; hoisted workspace member → the repo-root
|
|
18
|
+
* `node_modules`) plus the in-repo sibling package directories `.kici` depends
|
|
19
|
+
* on (with their built output), whose symlinks would dangle otherwise. Berry
|
|
20
|
+
* runs with a forced `nodeLinker: node-modules`, so its tree has the same
|
|
21
|
+
* node_modules shape as classic and is packed identically (the flavor only
|
|
22
|
+
* changes how siblings are referenced — version range vs `workspace:`/`portal:`
|
|
23
|
+
* — not the packed layout).
|
|
21
24
|
*
|
|
22
25
|
* Uses tar.gz (Node.js built-in zlib, no external binary) in portable mode to
|
|
23
26
|
* strip user/group info for cross-machine consistency; symlinks are preserved
|
|
@@ -379,6 +379,21 @@ export interface JobExecutionRequest {
|
|
|
379
379
|
* to steps as `ctx.matrix`. Absent for non-matrix jobs.
|
|
380
380
|
*/
|
|
381
381
|
matrixValues?: Record<string, unknown>;
|
|
382
|
+
/**
|
|
383
|
+
* For a `runsOnAll` host-fanout child: the hostname this child runs on,
|
|
384
|
+
* exposed to steps as `ctx.host`. Absent for non-host jobs.
|
|
385
|
+
*/
|
|
386
|
+
host?: string;
|
|
387
|
+
/**
|
|
388
|
+
* For a `runsOnAll` host-fanout child: the resolved agent facts, exposed to
|
|
389
|
+
* steps as `ctx.agent`. Absent for non-host jobs.
|
|
390
|
+
*/
|
|
391
|
+
agent?: {
|
|
392
|
+
host: string;
|
|
393
|
+
labels: string[];
|
|
394
|
+
platform?: string;
|
|
395
|
+
arch?: string;
|
|
396
|
+
};
|
|
382
397
|
/** Secrets to merge into step environment (highest precedence). */
|
|
383
398
|
secrets?: Record<string, string>;
|
|
384
399
|
/** Namespaced secrets by context name for ctx.secrets['context-name'].KEY access. */
|
|
@@ -468,6 +483,14 @@ export interface JobExecutionRequest {
|
|
|
468
483
|
event: Record<string, unknown>;
|
|
469
484
|
/** Expected job names from the original eval (for determinism validation). */
|
|
470
485
|
expectedJobNames?: string[];
|
|
486
|
+
/**
|
|
487
|
+
* Frozen upstream-output snapshot for a result-aware generator. When present
|
|
488
|
+
* the re-eval rebuilds `ctx.needs` from this snapshot (never a live read),
|
|
489
|
+
* so the generator sees the same upstream data as the original eval.
|
|
490
|
+
*/
|
|
491
|
+
upstreamSnapshot?: import('@kici-dev/engine').UpstreamSnapshot;
|
|
492
|
+
/** Declared upstream needs (normalized lock edges) used to shape ctx.needs. */
|
|
493
|
+
declaredNeeds?: readonly unknown[];
|
|
471
494
|
};
|
|
472
495
|
}
|
|
473
496
|
export {};
|
|
@@ -17,13 +17,15 @@
|
|
|
17
17
|
* - yarn classic (v1) has no `workspace:` protocol and no `portal:` — it links
|
|
18
18
|
* in-repo siblings by version range, not by a local specifier — so both are
|
|
19
19
|
* rejected with guidance; `file:`/`link:` are allowed when the path stays
|
|
20
|
-
* inside the clone, rejected when it escapes.
|
|
21
|
-
*
|
|
20
|
+
* inside the clone, rejected when it escapes.
|
|
21
|
+
* - yarn berry (v2+) resolves `workspace:` against the repo-root package.json
|
|
22
|
+
* `workspaces` field and `portal:`/`file:`/`link:` against inside-repo paths,
|
|
23
|
+
* so those are allowed when present/inside the clone and rejected otherwise.
|
|
22
24
|
*
|
|
23
25
|
* This module performs that classification so unresolvable specifiers fail
|
|
24
26
|
* fast with guidance rather than a cryptic install error.
|
|
25
27
|
*/
|
|
26
|
-
import { PackageManager } from '@kici-dev/shared/package-manager';
|
|
28
|
+
import { PackageManager, YarnFlavor } from '@kici-dev/shared/package-manager';
|
|
27
29
|
/** Local-protocol specifier prefixes that resolve against the filesystem. */
|
|
28
30
|
export declare enum LocalDepProtocol {
|
|
29
31
|
Workspace = "workspace:",
|
|
@@ -54,7 +56,7 @@ export declare function findLocalProtocolDeps(pkg: PackageJsonShape): LocalProto
|
|
|
54
56
|
*/
|
|
55
57
|
export declare function kiciHasLocalProtocolDeps(kiciDir: string): Promise<boolean>;
|
|
56
58
|
/** Build the actionable error for unresolvable local-protocol dependencies. */
|
|
57
|
-
export declare function formatUnresolvableDepError(offenders: readonly LocalProtocolDep[], packageManager: PackageManager): string;
|
|
59
|
+
export declare function formatUnresolvableDepError(offenders: readonly LocalProtocolDep[], packageManager: PackageManager, yarnFlavor: YarnFlavor): string;
|
|
58
60
|
/**
|
|
59
61
|
* Throw an actionable error when `.kici/package.json` declares a local-protocol
|
|
60
62
|
* dependency the detected package manager cannot resolve from the single cloned
|
|
@@ -65,6 +67,7 @@ export declare function assertResolvableDeps(args: {
|
|
|
65
67
|
kiciDir: string;
|
|
66
68
|
repoRoot: string;
|
|
67
69
|
packageManager: PackageManager;
|
|
70
|
+
yarnFlavor?: YarnFlavor;
|
|
68
71
|
}): Promise<void>;
|
|
69
72
|
export {};
|
|
70
73
|
//# sourceMappingURL=validate-kici-deps.d.ts.map
|
|
@@ -64,7 +64,11 @@ export declare function extractSteps(workflow: Workflow, jobName: string): reado
|
|
|
64
64
|
* A sibling mismatch logs a warning; a missing target job throws a clear
|
|
65
65
|
* determinism error.
|
|
66
66
|
*/
|
|
67
|
-
export declare function extractStepsFromDynamicJob(workflow: Workflow, dynamicIndex: number, jobName: string, event: Record<string, unknown>, env: Record<string, string | undefined>, apiTransport?: (method: string, params?: Record<string, unknown>) => Promise<unknown>, expectedJobNames?: string[]
|
|
67
|
+
export declare function extractStepsFromDynamicJob(workflow: Workflow, dynamicIndex: number, jobName: string, event: Record<string, unknown>, env: Record<string, string | undefined>, apiTransport?: (method: string, params?: Record<string, unknown>) => Promise<unknown>, expectedJobNames?: string[],
|
|
68
|
+
/** Frozen upstream snapshot for a result-aware generator (rebuilds ctx.needs). */
|
|
69
|
+
upstreamSnapshot?: import('@kici-dev/engine').UpstreamSnapshot,
|
|
70
|
+
/** Declared upstream needs that shape ctx.needs. */
|
|
71
|
+
declaredNeeds?: readonly unknown[]): Promise<{
|
|
68
72
|
steps: readonly StepInput[];
|
|
69
73
|
droppedJobs: string[];
|
|
70
74
|
}>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Apply yarn-berry registry auth + a forced `nodeLinker: node-modules` to a
|
|
3
|
+
* workflow's `.kici/.yarnrc.yml` for the lifetime of one `yarn install`, then
|
|
4
|
+
* restore the file on cleanup. The berry analog of `npm-registry-config.ts`:
|
|
5
|
+
* berry reads `.yarnrc.yml` (not `.npmrc`), so the auth block uses berry's
|
|
6
|
+
* `npmRegistryServer` / `npmScopes` / `npmAuthToken` keys with `${VAR}`
|
|
7
|
+
* env-var interpolation. Token bytes never reach disk — each registry token is
|
|
8
|
+
* exposed as a job-scoped env var and the on-disk value is the `${VAR}`
|
|
9
|
+
* reference.
|
|
10
|
+
*
|
|
11
|
+
* `nodeLinker: node-modules` makes berry lay down a real `node_modules` tree
|
|
12
|
+
* (no PnP `.pnp.cjs`), so the agent's packer / restore / sibling-walk /
|
|
13
|
+
* workflow-loader work unchanged. `enableScripts: false` (when a private
|
|
14
|
+
* registry is configured) keeps dependency lifecycle scripts from seeing the
|
|
15
|
+
* synthesized token env vars — the same security model as npm/pnpm/classic
|
|
16
|
+
* `--ignore-scripts`.
|
|
17
|
+
*
|
|
18
|
+
* Reuses the same `ApplyNpmRegistryConfigArgs` / `ApplyNpmRegistryConfigResult`
|
|
19
|
+
* shapes as the npm overlay so `dep-installer` can pick either by flavor.
|
|
20
|
+
*/
|
|
21
|
+
import type { ApplyNpmRegistryConfigArgs, ApplyNpmRegistryConfigResult } from './npm-registry-config.js';
|
|
22
|
+
export declare function applyYarnrcBerryConfig(args: ApplyNpmRegistryConfigArgs): Promise<ApplyNpmRegistryConfigResult>;
|
|
23
|
+
//# sourceMappingURL=yarnrc-berry-config.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -9,8 +9,9 @@ import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rename,
|
|
|
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.
|
|
@@ -312,8 +419,10 @@ function redactNpmOutput(input, tokens) {
|
|
|
312
419
|
* - yarn classic (v1) has no `workspace:` protocol and no `portal:` — it links
|
|
313
420
|
* in-repo siblings by version range, not by a local specifier — so both are
|
|
314
421
|
* rejected with guidance; `file:`/`link:` are allowed when the path stays
|
|
315
|
-
* inside the clone, rejected when it escapes.
|
|
316
|
-
*
|
|
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.
|
|
317
426
|
*
|
|
318
427
|
* This module performs that classification so unresolvable specifiers fail
|
|
319
428
|
* fast with guidance rather than a cryptic install error.
|
|
@@ -394,6 +503,17 @@ async function fileExists(target) {
|
|
|
394
503
|
return false;
|
|
395
504
|
}
|
|
396
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
|
+
}
|
|
397
517
|
/** Resolve a `file:`/`link:`/`portal:` spec to an absolute path under kiciDir. */
|
|
398
518
|
function resolveLocalPath(kiciDir, dep) {
|
|
399
519
|
const rawPath = dep.spec.slice(dep.protocol.length);
|
|
@@ -408,8 +528,20 @@ function isInsideRepo(repoRoot, target) {
|
|
|
408
528
|
* Classify each local-protocol dependency for the detected package manager and
|
|
409
529
|
* return the ones that are unresolvable in the agent's single-clone model.
|
|
410
530
|
*/
|
|
411
|
-
async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
|
|
531
|
+
async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot, yarnFlavor) {
|
|
412
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
|
+
}
|
|
413
545
|
if (packageManager === PackageManager.Yarn) {
|
|
414
546
|
const unresolvable = [];
|
|
415
547
|
for (const dep of deps) {
|
|
@@ -433,10 +565,11 @@ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
|
|
|
433
565
|
return unresolvable;
|
|
434
566
|
}
|
|
435
567
|
/** Build the actionable error for unresolvable local-protocol dependencies. */
|
|
436
|
-
function formatUnresolvableDepError(offenders, packageManager) {
|
|
568
|
+
function formatUnresolvableDepError(offenders, packageManager, yarnFlavor) {
|
|
437
569
|
const list = offenders.map((o) => `${o.name}: ${o.spec}`).join(", ");
|
|
438
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.`;
|
|
439
|
-
if (packageManager === PackageManager.Yarn) return `These .kici/ dependencies
|
|
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.)`;
|
|
440
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.`;
|
|
441
574
|
}
|
|
442
575
|
/**
|
|
@@ -450,9 +583,10 @@ async function assertResolvableDeps(args) {
|
|
|
450
583
|
if (!pkg) return;
|
|
451
584
|
const localDeps = findLocalProtocolDeps(pkg);
|
|
452
585
|
if (localDeps.length === 0) return;
|
|
453
|
-
const
|
|
586
|
+
const flavor = args.yarnFlavor ?? YarnFlavor.Classic;
|
|
587
|
+
const offenders = await findUnresolvableDeps(localDeps, args.packageManager, args.kiciDir, args.repoRoot, flavor);
|
|
454
588
|
if (offenders.length === 0) return;
|
|
455
|
-
throw new Error(formatUnresolvableDepError(offenders, args.packageManager));
|
|
589
|
+
throw new Error(formatUnresolvableDepError(offenders, args.packageManager, flavor));
|
|
456
590
|
}
|
|
457
591
|
//#endregion
|
|
458
592
|
//#region src/execution/workspace-siblings.ts
|
|
@@ -561,10 +695,13 @@ function isAbsoluteRel(rel) {
|
|
|
561
695
|
* presence of `.kici/package.json` signals that deps should be installed. npm
|
|
562
696
|
* is the default and ships with every Node.js install; pnpm is used when the
|
|
563
697
|
* repo is a pnpm workspace so a `.kici/` member can resolve in-repo
|
|
564
|
-
* `workspace:` siblings. yarn classic (v1)
|
|
565
|
-
*
|
|
566
|
-
*
|
|
567
|
-
*
|
|
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.
|
|
568
705
|
*
|
|
569
706
|
* Security: the install runs with an isolated per-invocation cache/store
|
|
570
707
|
* directory to prevent cache poisoning across build jobs — a malicious
|
|
@@ -590,6 +727,15 @@ async function detectKiciPackageManager(repoRoot, kiciDir) {
|
|
|
590
727
|
return await detectPackageManagerFromManifests(repoRoot) ?? await detectPackageManagerFromManifests(kiciDir) ?? PackageManager.Npm;
|
|
591
728
|
}
|
|
592
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
|
+
/**
|
|
593
739
|
* Install `.kici/` dependencies inline with the repo's package manager.
|
|
594
740
|
*
|
|
595
741
|
* Falls back to this when the dep cache is unavailable or a download fails.
|
|
@@ -608,19 +754,28 @@ async function detectKiciPackageManager(repoRoot, kiciDir) {
|
|
|
608
754
|
async function installDeps(kiciDir, opts = {}) {
|
|
609
755
|
const repoRoot = opts.repoRoot ?? dirname(kiciDir);
|
|
610
756
|
const packageManager = await detectKiciPackageManager(repoRoot, kiciDir);
|
|
757
|
+
const yarnFlavor = packageManager === PackageManager.Yarn ? await detectKiciYarnFlavor(repoRoot, kiciDir) : YarnFlavor.Classic;
|
|
611
758
|
logger$2.info("Installing deps inline", {
|
|
612
759
|
packageManager,
|
|
760
|
+
yarnFlavor,
|
|
613
761
|
dir: kiciDir
|
|
614
762
|
});
|
|
615
|
-
process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, cwd=${kiciDir}\n`);
|
|
763
|
+
process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, flavor=${yarnFlavor}, cwd=${kiciDir}\n`);
|
|
616
764
|
await assertResolvableDeps({
|
|
617
765
|
kiciDir,
|
|
618
766
|
repoRoot,
|
|
619
|
-
packageManager
|
|
767
|
+
packageManager,
|
|
768
|
+
yarnFlavor
|
|
620
769
|
});
|
|
621
770
|
const startTime = Date.now();
|
|
622
771
|
const hasPrivateRegistry = (opts.npmRegistries?.length ?? 0) > 0 || (opts.installEnvSecrets ? Object.keys(opts.installEnvSecrets).length > 0 : false);
|
|
623
|
-
const
|
|
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({
|
|
624
779
|
kiciDir,
|
|
625
780
|
npmRegistries: opts.npmRegistries,
|
|
626
781
|
installEnvSecrets: opts.installEnvSecrets,
|
|
@@ -632,6 +787,10 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
632
787
|
hasPrivateRegistry,
|
|
633
788
|
registryConfig
|
|
634
789
|
});
|
|
790
|
+
else if (isBerry) await runYarnBerryInstall({
|
|
791
|
+
kiciDir,
|
|
792
|
+
registryConfig
|
|
793
|
+
});
|
|
635
794
|
else if (packageManager === PackageManager.Yarn) await runYarnInstall({
|
|
636
795
|
kiciDir,
|
|
637
796
|
hasPrivateRegistry,
|
|
@@ -651,7 +810,7 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
651
810
|
await registryConfig.cleanup();
|
|
652
811
|
}
|
|
653
812
|
if (packageManager === PackageManager.Pnpm && await kiciHasLocalProtocolDeps(kiciDir)) await buildWorkspaceClosure(repoRoot);
|
|
654
|
-
if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir);
|
|
813
|
+
if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir, yarnFlavor);
|
|
655
814
|
const durationMs = Date.now() - startTime;
|
|
656
815
|
process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
|
|
657
816
|
logger$2.info("Deps installed inline", {
|
|
@@ -780,6 +939,35 @@ async function runYarnInstall(args) {
|
|
|
780
939
|
}).catch(() => {});
|
|
781
940
|
}
|
|
782
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
|
+
}
|
|
783
971
|
/** Throw an actionable error when the repo needs yarn but it is not installed. */
|
|
784
972
|
async function assertYarnAvailable() {
|
|
785
973
|
try {
|
|
@@ -799,7 +987,7 @@ async function assertYarnAvailable() {
|
|
|
799
987
|
* Deep cross-sibling build chains may build out of strict topological order —
|
|
800
988
|
* real `.kici` closures are shallow.
|
|
801
989
|
*/
|
|
802
|
-
async function buildYarnWorkspaceClosure(repoRoot, kiciDir) {
|
|
990
|
+
async function buildYarnWorkspaceClosure(repoRoot, kiciDir, yarnFlavor) {
|
|
803
991
|
const siblings = await collectInRepoSiblings(repoRoot, kiciDir, resolveYarnNodeModulesRoot(repoRoot, kiciDir));
|
|
804
992
|
if (siblings.length === 0) return;
|
|
805
993
|
const { nodeDir } = resolveNpm();
|
|
@@ -807,15 +995,16 @@ async function buildYarnWorkspaceClosure(repoRoot, kiciDir) {
|
|
|
807
995
|
for (const rel of [...siblings].reverse()) {
|
|
808
996
|
const sibDir = join(repoRoot, rel);
|
|
809
997
|
if (!await siblingHasBuildScript(sibDir)) continue;
|
|
810
|
-
|
|
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`);
|
|
811
1005
|
try {
|
|
812
|
-
await execFileAsync("yarn",
|
|
813
|
-
|
|
814
|
-
sibDir,
|
|
815
|
-
"run",
|
|
816
|
-
"build"
|
|
817
|
-
], {
|
|
818
|
-
cwd: repoRoot,
|
|
1006
|
+
await execFileAsync("yarn", argv, {
|
|
1007
|
+
cwd,
|
|
819
1008
|
env,
|
|
820
1009
|
timeout: INSTALL_TIMEOUT_MS,
|
|
821
1010
|
maxBuffer: INSTALL_MAX_BUFFER
|
|
@@ -25,6 +25,14 @@ export interface AttestResult {
|
|
|
25
25
|
subjectDigest: string;
|
|
26
26
|
}
|
|
27
27
|
export declare function attestProvenance(deps: AttestDeps, input: AttestInput): Promise<AttestResult>;
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Pick the primary digest (`sha256` preferred) as the storage-key discriminator.
|
|
30
|
+
* Throws when the subject carries no digest: an empty digest set would otherwise
|
|
31
|
+
* yield an `undefined` storage-key segment (`provenance/<run>/<job>/undefined.kici.json`)
|
|
32
|
+
* and an unverifiable statement, since in-toto requires every subject to carry
|
|
33
|
+
* at least one digest. The SDK `ProvenanceSubjectInput` type allows an empty
|
|
34
|
+
* digest map (both `sha256` and `sha512` are optional), so this is the runtime
|
|
35
|
+
* boundary that upholds the engine `digestSetSchema` invariant.
|
|
36
|
+
*/
|
|
29
37
|
export declare function subjectDigestString(subject: ProvenanceSubject): string;
|
|
30
38
|
//# sourceMappingURL=attest.d.ts.map
|