@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
package/dist/workflow-runner.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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
|
|
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)
|
|
2491
|
-
*
|
|
2492
|
-
*
|
|
2493
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
2830
|
-
const AGENT_SDK_BUNDLE_HASH = "
|
|
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.
|
|
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.
|
|
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.
|
|
57
|
-
"archiver": "^
|
|
56
|
+
"@hono/node-server": "^2.0.4",
|
|
57
|
+
"archiver": "^8.0.0",
|
|
58
58
|
"dockerode": "^5.0.0",
|
|
59
|
-
"hono": "^4.12.
|
|
59
|
+
"hono": "^4.12.25",
|
|
60
60
|
"jose": "^6.1.0",
|
|
61
|
-
"tar": "^7.5.
|
|
61
|
+
"tar": "^7.5.16",
|
|
62
62
|
"winston": "^3.19.0",
|
|
63
|
-
"ws": "^8.
|
|
64
|
-
"
|
|
63
|
+
"ws": "^8.21.0",
|
|
64
|
+
"yaml": "^2.9.0",
|
|
65
|
+
"zod": "^4.4.3",
|
|
65
66
|
"zx": "^8.8.5",
|
|
66
|
-
"@kici-dev/
|
|
67
|
-
"@kici-dev/sdk": "0.1.
|
|
68
|
-
"@kici-dev/
|
|
69
|
-
"@kici-dev/
|
|
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": "^
|
|
79
|
+
"@types/archiver": "^8.0.0",
|
|
79
80
|
"@types/dockerode": "^4.0.1"
|
|
80
81
|
},
|
|
81
82
|
"scripts": {
|