@kici-dev/agent 0.1.16 → 0.1.17
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 +7 -3
- package/dist/execution/dep-packer.d.ts +6 -1
- package/dist/execution/env-init/presets/directives.d.ts +20 -0
- package/dist/execution/env-init/presets/expand.d.ts +16 -0
- package/dist/execution/env-init/presets/mise/cache-key.d.ts +7 -0
- package/dist/execution/env-init/presets/mise/expander.d.ts +16 -0
- package/dist/execution/env-init/presets/mise/templates.d.ts +16 -0
- package/dist/execution/env-init/presets/mise/windows-install.d.ts +18 -0
- package/dist/execution/env-init/presets/registry.d.ts +31 -0
- package/dist/execution/init-runner.d.ts +7 -0
- package/dist/execution/job-runner.d.ts +9 -1
- package/dist/execution/sandbox/env-delta.d.ts +2 -0
- package/dist/execution/sandbox/index.d.ts +1 -1
- package/dist/execution/sandbox/ipc-protocol.d.ts +58 -3
- package/dist/execution/sandbox/types.d.ts +9 -1
- package/dist/execution/sandbox/workflow-runner.d.ts +12 -7
- package/dist/execution/validate-kici-deps.d.ts +5 -0
- package/dist/execution/workspace-siblings.d.ts +33 -0
- package/dist/index.js +223 -6
- package/dist/provenance/attest.d.ts +30 -0
- package/dist/provenance/sign.d.ts +21 -0
- package/dist/provenance/statement-builder.d.ts +38 -0
- package/dist/server.js +472 -125
- package/dist/version.d.ts +2 -0
- package/dist/workflow-runner.js +749 -44
- package/dist/ws/orchestrator-client.d.ts +16 -1
- package/package.json +6 -5
- package/sbom.spdx.json +95 -65
package/dist/server.js
CHANGED
|
@@ -12,17 +12,17 @@ import winston from "winston";
|
|
|
12
12
|
import { RingBuffer, addLogsToArchive, chunkBuffer, createHealthRoutes, createLogger, createMeter, createMetricsRoutes, deriveSharedSecret, getPrometheusExporter, getReconnectDelay, getRequestContext, guardStartup, initTelemetry, logger, normalizeLineEndings, redactConfig, requestContext, setServiceName, setupGracefulShutdown, sha256, sha256File, toErrorMessage, validateRequiredTools } from "@kici-dev/shared";
|
|
13
13
|
import { z } from "zod";
|
|
14
14
|
import { LOGGER_ENV_VARS, defineEnv, validateUnknownKiciVars } from "@kici-dev/shared/env";
|
|
15
|
-
import { ALLOWED_SYSTEM_VARS, CacheRunEventType, CacheStepType, ExecutionJobStatus, ExecutionStepStatus, InitFailureCategory, KNOWN_ROLES, PROTOCOL_VERSION, SANDBOX_DEFAULT_VARS, WS_CLOSE_AGENT_AUTH_FAILED, WS_MAX_PAYLOAD_BYTES, deriveOsArchLabels, heartbeatSchema, hostLabel, mergeAutoLabels, orchestratorToAgentMessageSchema, resolveRoleLabels, validateNoReservedLabels } from "@kici-dev/engine";
|
|
15
|
+
import { ALLOWED_SYSTEM_VARS, CacheRunEventType, CacheStepType, ExecutionJobStatus, ExecutionStepStatus, InitFailureCategory, KNOWN_ROLES, PROTOCOL_VERSION, SANDBOX_DEFAULT_VARS, WS_CLOSE_AGENT_AUTH_FAILED, WS_MAX_PAYLOAD_BYTES, applyIncludeExclude, deriveOsArchLabels, expandMatrix, heartbeatSchema, hostLabel, mergeAutoLabels, orchestratorToAgentMessageSchema, resolveRoleLabels, validateNoReservedLabels } from "@kici-dev/engine";
|
|
16
16
|
import { execFile, execFileSync, execSync, fork, spawn } from "node:child_process";
|
|
17
17
|
import WebSocket from "ws";
|
|
18
18
|
import archiver from "archiver";
|
|
19
|
+
import fs, { existsSync } from "node:fs";
|
|
20
|
+
import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
21
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
19
22
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
20
23
|
import { format, promisify } from "node:util";
|
|
21
|
-
import { existsSync } from "node:fs";
|
|
22
|
-
import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
23
24
|
import { gcStaleTmpDirs } from "@kici-dev/core/tmp-gc";
|
|
24
|
-
import {
|
|
25
|
-
import fs, { access, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, unlink, writeFile } from "node:fs/promises";
|
|
25
|
+
import fs$1, { access, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, unlink, writeFile } from "node:fs/promises";
|
|
26
26
|
import Docker from "dockerode";
|
|
27
27
|
import { buildKiciApi, isDynamicGroupRef, isDynamicJobFn, isStaticArray, isStaticObject } from "@kici-dev/sdk";
|
|
28
28
|
import { c, x } from "tar";
|
|
@@ -219,6 +219,35 @@ async function buildAgentMiniBundle(opts) {
|
|
|
219
219
|
return Buffer.concat(chunks);
|
|
220
220
|
}
|
|
221
221
|
//#endregion
|
|
222
|
+
//#region src/version.ts
|
|
223
|
+
/**
|
|
224
|
+
* Read the agent's own package version from disk.
|
|
225
|
+
*
|
|
226
|
+
* Walks up from this module's location until it finds a package.json whose
|
|
227
|
+
* name is '@kici-dev/agent'. This is robust to the agent's bundled dist layout
|
|
228
|
+
* (build-service.mjs emits dist/index.js / dist/server.js, so a fixed relative
|
|
229
|
+
* depth would be wrong). Returns null when it can't be resolved so the
|
|
230
|
+
* agent.register message simply omits the field rather than failing.
|
|
231
|
+
*/
|
|
232
|
+
function readAgentVersion() {
|
|
233
|
+
try {
|
|
234
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
235
|
+
for (let i = 0; i < 8; i++) {
|
|
236
|
+
const candidate = path.join(dir, "package.json");
|
|
237
|
+
if (fs.existsSync(candidate)) {
|
|
238
|
+
const pkg = JSON.parse(fs.readFileSync(candidate, "utf-8"));
|
|
239
|
+
if (pkg.name === "@kici-dev/agent" && typeof pkg.version === "string") return pkg.version;
|
|
240
|
+
}
|
|
241
|
+
const parent = path.dirname(dir);
|
|
242
|
+
if (parent === dir) break;
|
|
243
|
+
dir = parent;
|
|
244
|
+
}
|
|
245
|
+
return null;
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
//#endregion
|
|
222
251
|
//#region src/ws/event-buffer.ts
|
|
223
252
|
/**
|
|
224
253
|
* In-memory buffer for agent-to-orchestrator messages during disconnection.
|
|
@@ -467,6 +496,47 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
467
496
|
});
|
|
468
497
|
}
|
|
469
498
|
/**
|
|
499
|
+
* Request a presigned PUT URL for a provenance bundle. Sends a
|
|
500
|
+
* `provenance.upload.request` and waits for a `provenance.upload.response`
|
|
501
|
+
* (resolved via the shared upload-request pending map). Times out after 30s.
|
|
502
|
+
*/
|
|
503
|
+
async requestProvenanceUploadUrl(jobId, subjectDigest) {
|
|
504
|
+
const messageId = randomUUID();
|
|
505
|
+
return new Promise((resolve, reject) => {
|
|
506
|
+
const timer = setTimeout(() => {
|
|
507
|
+
this.pendingUploadRequests.delete(messageId);
|
|
508
|
+
reject(/* @__PURE__ */ new Error("Provenance upload URL request timed out (30s)"));
|
|
509
|
+
}, 3e4);
|
|
510
|
+
this.pendingUploadRequests.set(messageId, {
|
|
511
|
+
resolve: (url) => {
|
|
512
|
+
clearTimeout(timer);
|
|
513
|
+
resolve(url);
|
|
514
|
+
},
|
|
515
|
+
reject: (err) => {
|
|
516
|
+
clearTimeout(timer);
|
|
517
|
+
reject(err);
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
this.sendDirect({
|
|
521
|
+
type: "provenance.upload.request",
|
|
522
|
+
messageId,
|
|
523
|
+
jobId,
|
|
524
|
+
subjectDigest
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
/** Notify the orchestrator a provenance bundle upload completed (records an attestations row). */
|
|
529
|
+
sendProvenanceUploadComplete(jobId, subjectName, subjectDigest, mediaType) {
|
|
530
|
+
this.sendDirect({
|
|
531
|
+
type: "provenance.upload.complete",
|
|
532
|
+
messageId: randomUUID(),
|
|
533
|
+
jobId,
|
|
534
|
+
subjectName,
|
|
535
|
+
subjectDigest,
|
|
536
|
+
mediaType
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
470
540
|
* Send an event.emit WS message to the orchestrator and await the response.
|
|
471
541
|
*
|
|
472
542
|
* Used by the job runner to relay custom event emissions from the sandbox
|
|
@@ -628,6 +698,27 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
628
698
|
});
|
|
629
699
|
}
|
|
630
700
|
/**
|
|
701
|
+
* Relay a provenance bundle upload operation to the orchestrator. Maps the
|
|
702
|
+
* IPC `provenance.request` onto `requestProvenanceUploadUrl` (returns the
|
|
703
|
+
* presigned URL) or `sendProvenanceUploadComplete` (fire-and-forget) and
|
|
704
|
+
* returns the result on the IPC response shape.
|
|
705
|
+
*/
|
|
706
|
+
async relayProvenance(jobId, request) {
|
|
707
|
+
if (request.op === "complete") {
|
|
708
|
+
this.sendProvenanceUploadComplete(jobId, request.subjectName, request.subjectDigest, request.mediaType);
|
|
709
|
+
return {
|
|
710
|
+
type: "provenance.response",
|
|
711
|
+
requestId: request.requestId
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
const uploadUrl = await this.requestProvenanceUploadUrl(jobId, request.subjectDigest);
|
|
715
|
+
return {
|
|
716
|
+
type: "provenance.response",
|
|
717
|
+
requestId: request.requestId,
|
|
718
|
+
uploadUrl
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
631
722
|
* Relay a step-level approval request to the orchestrator. Sends a
|
|
632
723
|
* `step.approval-request` WS message and resolves with the orchestrator's
|
|
633
724
|
* `step.approval-resolved` mapped onto the IPC response shape. No client-side
|
|
@@ -814,7 +905,7 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
814
905
|
return;
|
|
815
906
|
}
|
|
816
907
|
const rawMsg = raw;
|
|
817
|
-
if (rawMsg.type === "cache.upload.response") {
|
|
908
|
+
if (rawMsg.type === "cache.upload.response" || rawMsg.type === "provenance.upload.response") {
|
|
818
909
|
const pending = this.pendingUploadRequests.get(rawMsg.requestId);
|
|
819
910
|
if (pending) {
|
|
820
911
|
this.pendingUploadRequests.delete(rawMsg.requestId);
|
|
@@ -1094,6 +1185,10 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
1094
1185
|
totalMemoryMb: Math.round(os.totalmem() / (1024 * 1024)),
|
|
1095
1186
|
cpuCount: os.cpus().length,
|
|
1096
1187
|
nodeVersion: process.versions.node,
|
|
1188
|
+
...(() => {
|
|
1189
|
+
const v = readAgentVersion();
|
|
1190
|
+
return v ? { version: v } : {};
|
|
1191
|
+
})(),
|
|
1097
1192
|
...(() => {
|
|
1098
1193
|
try {
|
|
1099
1194
|
const info = os.userInfo();
|
|
@@ -1202,14 +1297,14 @@ var init_console_capture = __esmMin((() => {
|
|
|
1202
1297
|
init_console_capture();
|
|
1203
1298
|
function safe(name, fallback = "unknown") {
|
|
1204
1299
|
switch (name) {
|
|
1205
|
-
case "version": return "0.1.
|
|
1206
|
-
case "buildCommit": return "
|
|
1207
|
-
case "sdkVersion": return "0.1.
|
|
1208
|
-
case "sdkBundleHash": return "
|
|
1209
|
-
case "sharedVersion": return "0.1.
|
|
1210
|
-
case "sharedBundleHash": return "
|
|
1211
|
-
case "engineVersion": return "0.1.
|
|
1212
|
-
case "engineBundleHash": return "
|
|
1300
|
+
case "version": return "0.1.17";
|
|
1301
|
+
case "buildCommit": return "5596f8a3c";
|
|
1302
|
+
case "sdkVersion": return "0.1.17";
|
|
1303
|
+
case "sdkBundleHash": return "df47ed5db86eaaa2de8394c0db08335f368e8d620a898cc409765f4545eb3972";
|
|
1304
|
+
case "sharedVersion": return "0.1.17";
|
|
1305
|
+
case "sharedBundleHash": return "9e8a753da73b26fb87d08817f70b4d836f67f599385fa268b2c1f68b97996f54";
|
|
1306
|
+
case "engineVersion": return "0.1.17";
|
|
1307
|
+
case "engineBundleHash": return "706d94fa54a47aea0f69bde105d40213befff401f717604aded2c62a1291cb51";
|
|
1213
1308
|
default: return fallback;
|
|
1214
1309
|
}
|
|
1215
1310
|
}
|
|
@@ -1766,7 +1861,7 @@ async function buildAssetDigestFromResolvedPaths(workDir, resolvedPaths) {
|
|
|
1766
1861
|
for (const rel of resolvedPaths) {
|
|
1767
1862
|
const abs = path.join(workDir, rel);
|
|
1768
1863
|
try {
|
|
1769
|
-
const content = await fs.readFile(abs, "utf-8");
|
|
1864
|
+
const content = await fs$1.readFile(abs, "utf-8");
|
|
1770
1865
|
parts.push(`${rel}\n${content}`);
|
|
1771
1866
|
} catch {
|
|
1772
1867
|
parts.push(`${rel}\n`);
|
|
@@ -1791,7 +1886,7 @@ async function loadWorkflowSource(workDir, sourceFile, expectedContentHash, reso
|
|
|
1791
1886
|
ensureLoaderHookRegistered();
|
|
1792
1887
|
const filePath = path.join(workDir, sourceFile);
|
|
1793
1888
|
if (expectedContentHash) {
|
|
1794
|
-
const rawSource = await fs.readFile(filePath, "utf-8");
|
|
1889
|
+
const rawSource = await fs$1.readFile(filePath, "utf-8");
|
|
1795
1890
|
let assetDigest;
|
|
1796
1891
|
if (resolvedHashFiles?.length) assetDigest = await buildAssetDigestFromResolvedPaths(workDir, resolvedHashFiles);
|
|
1797
1892
|
const actualHash = computeContentHash(rawSource, assetDigest);
|
|
@@ -1893,8 +1988,8 @@ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event
|
|
|
1893
1988
|
}
|
|
1894
1989
|
var AGENT_SDK_VERSION, AGENT_SDK_BUNDLE_HASH, hookRegistered;
|
|
1895
1990
|
var init_workflow_loader = __esmMin((() => {
|
|
1896
|
-
AGENT_SDK_VERSION = "0.1.
|
|
1897
|
-
AGENT_SDK_BUNDLE_HASH = "
|
|
1991
|
+
AGENT_SDK_VERSION = "0.1.17";
|
|
1992
|
+
AGENT_SDK_BUNDLE_HASH = "df47ed5db86eaaa2de8394c0db08335f368e8d620a898cc409765f4545eb3972";
|
|
1898
1993
|
hookRegistered = false;
|
|
1899
1994
|
}));
|
|
1900
1995
|
//#endregion
|
|
@@ -2060,24 +2155,24 @@ function resolveOrchestratorUrl(url) {
|
|
|
2060
2155
|
* have nothing to race; the defensive `rm` covers re-runs.
|
|
2061
2156
|
*/
|
|
2062
2157
|
async function moveScratchIntoRepo(scratchDir, workDir) {
|
|
2063
|
-
for (const child of await fs.readdir(scratchDir)) if (child === ".kici") {
|
|
2158
|
+
for (const child of await fs$1.readdir(scratchDir)) if (child === ".kici") {
|
|
2064
2159
|
const kiciScratch = join(scratchDir, ".kici");
|
|
2065
|
-
for (const sub of await fs.readdir(kiciScratch)) await moveInto(join(kiciScratch, sub), join(workDir, ".kici", sub));
|
|
2160
|
+
for (const sub of await fs$1.readdir(kiciScratch)) await moveInto(join(kiciScratch, sub), join(workDir, ".kici", sub));
|
|
2066
2161
|
} else await moveInto(join(scratchDir, child), join(workDir, child));
|
|
2067
2162
|
}
|
|
2068
2163
|
/** Move `src` to `dest`, creating the parent and clearing any stale dest. */
|
|
2069
2164
|
async function moveInto(src, dest) {
|
|
2070
2165
|
await mkdir(dirname(dest), { recursive: true });
|
|
2071
|
-
await fs.rm(dest, {
|
|
2166
|
+
await fs$1.rm(dest, {
|
|
2072
2167
|
recursive: true,
|
|
2073
2168
|
force: true
|
|
2074
2169
|
});
|
|
2075
|
-
await fs.rename(src, dest);
|
|
2170
|
+
await fs$1.rename(src, dest);
|
|
2076
2171
|
}
|
|
2077
2172
|
/** Best-effort cleanup of a settled scratch dir; logs and continues on failure. */
|
|
2078
2173
|
async function cleanupScratch(scratchDir) {
|
|
2079
2174
|
try {
|
|
2080
|
-
await fs.rm(scratchDir, {
|
|
2175
|
+
await fs$1.rm(scratchDir, {
|
|
2081
2176
|
recursive: true,
|
|
2082
2177
|
force: true
|
|
2083
2178
|
});
|
|
@@ -2111,7 +2206,7 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
|
|
|
2111
2206
|
const kiciDir = join(workDir, ".kici");
|
|
2112
2207
|
if (depsUrl.startsWith("file://")) {
|
|
2113
2208
|
const localPath = fileURLToPath(depsUrl);
|
|
2114
|
-
const data = await fs.readFile(localPath);
|
|
2209
|
+
const data = await fs$1.readFile(localPath);
|
|
2115
2210
|
if (depsHash) {
|
|
2116
2211
|
const actualHash = computeHash(data);
|
|
2117
2212
|
if (actualHash !== depsHash) throw new Error(`Dep tarball hash mismatch: expected ${depsHash}, got ${actualHash}`);
|
|
@@ -2270,7 +2365,7 @@ async function restoreSource(workDir, sourceTarUrl) {
|
|
|
2270
2365
|
let data;
|
|
2271
2366
|
if (sourceTarUrl.startsWith("file://")) {
|
|
2272
2367
|
const localPath = fileURLToPath(sourceTarUrl);
|
|
2273
|
-
data = await fs.readFile(localPath);
|
|
2368
|
+
data = await fs$1.readFile(localPath);
|
|
2274
2369
|
} else if (sourceTarUrl.startsWith("http://") || sourceTarUrl.startsWith("https://")) data = await downloadUrl(sourceTarUrl);
|
|
2275
2370
|
else throw new Error(`Unsupported source tarball URL scheme: ${sourceTarUrl}`);
|
|
2276
2371
|
await extractSourceTarball(data, workDir);
|
|
@@ -2339,6 +2434,28 @@ function findJobByName(workflow, jobName) {
|
|
|
2339
2434
|
async function evaluateDynamicFields(workflow, jobName, event, flags, timeoutMs = 6e4) {
|
|
2340
2435
|
const job = findJobByName(workflow, jobName);
|
|
2341
2436
|
const result = {};
|
|
2437
|
+
if (flags.dynamicMatrix && typeof job.matrix === "function") {
|
|
2438
|
+
const matrixContext = {
|
|
2439
|
+
$: (await import("zx")).$,
|
|
2440
|
+
ctx: {
|
|
2441
|
+
workflow: { name: workflow.name },
|
|
2442
|
+
job: {
|
|
2443
|
+
name: jobName,
|
|
2444
|
+
runsOn: job.runsOn
|
|
2445
|
+
}
|
|
2446
|
+
},
|
|
2447
|
+
log: {
|
|
2448
|
+
info: () => {},
|
|
2449
|
+
warn: () => {},
|
|
2450
|
+
error: () => {},
|
|
2451
|
+
debug: () => {}
|
|
2452
|
+
},
|
|
2453
|
+
env: { ...process.env }
|
|
2454
|
+
};
|
|
2455
|
+
let combos = expandMatrix(await withTimeout(() => job.matrix(matrixContext), timeoutMs, `dynamicMatrix for job '${jobName}'`));
|
|
2456
|
+
if (job.include || job.exclude) combos = applyIncludeExclude(combos, job.include, job.exclude);
|
|
2457
|
+
result.matrixValues = combos;
|
|
2458
|
+
}
|
|
2342
2459
|
if (flags.dynamicEnvironment && typeof job.environment === "function") {
|
|
2343
2460
|
const value = await withTimeout(() => job.environment(event), timeoutMs, `dynamicEnvironment for job '${jobName}'`);
|
|
2344
2461
|
if (value !== void 0 && value !== null) result.environmentName = value;
|
|
@@ -2541,6 +2658,7 @@ var MatrixExpansionError, DYNAMIC_FIELD_TIMEOUT_MS;
|
|
|
2541
2658
|
var init_dynamic_job_serializer = __esmMin((() => {
|
|
2542
2659
|
init_timeout_util();
|
|
2543
2660
|
MatrixExpansionError = class MatrixExpansionError extends Error {
|
|
2661
|
+
jobName;
|
|
2544
2662
|
name = "MatrixExpansionError";
|
|
2545
2663
|
constructor(jobName, message) {
|
|
2546
2664
|
super(message);
|
|
@@ -2814,7 +2932,7 @@ function decryptBuffer(encrypted, aesKey) {
|
|
|
2814
2932
|
*/
|
|
2815
2933
|
async function applyOverlay(config) {
|
|
2816
2934
|
const { tarballUrl, cliPublicKey, orchestratorPrivateKey, repoDir } = config;
|
|
2817
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "kici-overlay-"));
|
|
2935
|
+
const tmpDir = await fs$1.mkdtemp(path.join(os.tmpdir(), "kici-overlay-"));
|
|
2818
2936
|
try {
|
|
2819
2937
|
logger$7.info("Downloading overlay tarball", { url: tarballUrl.replace(/\?.*$/, "?[redacted]") });
|
|
2820
2938
|
let encryptedData;
|
|
@@ -2828,7 +2946,7 @@ async function applyOverlay(config) {
|
|
|
2828
2946
|
const decryptedData = decryptBuffer(encryptedData, aesKey);
|
|
2829
2947
|
logger$7.info("Extracting overlay tarball", { size: decryptedData.length });
|
|
2830
2948
|
const extractDir = path.join(tmpDir, "extracted");
|
|
2831
|
-
await fs.mkdir(extractDir, { recursive: true });
|
|
2949
|
+
await fs$1.mkdir(extractDir, { recursive: true });
|
|
2832
2950
|
try {
|
|
2833
2951
|
const readable = Readable.from(decryptedData);
|
|
2834
2952
|
await new Promise((resolve, reject) => {
|
|
@@ -2843,7 +2961,7 @@ async function applyOverlay(config) {
|
|
|
2843
2961
|
const manifestPath = path.join(extractDir, ".kici-overlay-tmp", "manifest.json");
|
|
2844
2962
|
let manifestContent;
|
|
2845
2963
|
try {
|
|
2846
|
-
manifestContent = await fs.readFile(manifestPath, "utf-8");
|
|
2964
|
+
manifestContent = await fs$1.readFile(manifestPath, "utf-8");
|
|
2847
2965
|
} catch {
|
|
2848
2966
|
throw new Error("Overlay manifest not found: expected .kici-overlay-tmp/manifest.json in tarball");
|
|
2849
2967
|
}
|
|
@@ -2864,15 +2982,15 @@ async function applyOverlay(config) {
|
|
|
2864
2982
|
for (const file of checksumFiles) {
|
|
2865
2983
|
const srcPath = path.join(extractDir, file);
|
|
2866
2984
|
const destPath = path.join(repoDir, file);
|
|
2867
|
-
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
2868
|
-
await fs.copyFile(srcPath, destPath);
|
|
2985
|
+
await fs$1.mkdir(path.dirname(destPath), { recursive: true });
|
|
2986
|
+
await fs$1.copyFile(srcPath, destPath);
|
|
2869
2987
|
filesApplied++;
|
|
2870
2988
|
}
|
|
2871
2989
|
let filesDeleted = 0;
|
|
2872
2990
|
for (const file of manifest.deletions) {
|
|
2873
2991
|
const targetPath = path.join(repoDir, file);
|
|
2874
2992
|
try {
|
|
2875
|
-
await fs.unlink(targetPath);
|
|
2993
|
+
await fs$1.unlink(targetPath);
|
|
2876
2994
|
filesDeleted++;
|
|
2877
2995
|
} catch {
|
|
2878
2996
|
logger$7.debug("Deletion target not found, skipping", { file });
|
|
@@ -2888,7 +3006,7 @@ async function applyOverlay(config) {
|
|
|
2888
3006
|
verified: true
|
|
2889
3007
|
};
|
|
2890
3008
|
} finally {
|
|
2891
|
-
await fs.rm(tmpDir, {
|
|
3009
|
+
await fs$1.rm(tmpDir, {
|
|
2892
3010
|
recursive: true,
|
|
2893
3011
|
force: true
|
|
2894
3012
|
}).catch(() => {});
|
|
@@ -3037,6 +3155,11 @@ var init_npm_registry_config = __esmMin((() => {}));
|
|
|
3037
3155
|
* clones the whole repo, so an in-repo sibling is present), and resolves
|
|
3038
3156
|
* `file:`/`link:`/`portal:` against a path — allowed when that path stays
|
|
3039
3157
|
* inside the cloned repo, rejected when it escapes the clone.
|
|
3158
|
+
* - yarn classic (v1) has no `workspace:` protocol and no `portal:` — it links
|
|
3159
|
+
* in-repo siblings by version range, not by a local specifier — so both are
|
|
3160
|
+
* rejected with guidance; `file:`/`link:` are allowed when the path stays
|
|
3161
|
+
* inside the clone, rejected when it escapes. (yarn berry is not yet
|
|
3162
|
+
* supported.)
|
|
3040
3163
|
*
|
|
3041
3164
|
* This module performs that classification so unresolvable specifiers fail
|
|
3042
3165
|
* fast with guidance rather than a cryptic install error.
|
|
@@ -3112,6 +3235,17 @@ function isInsideRepo(repoRoot, target) {
|
|
|
3112
3235
|
*/
|
|
3113
3236
|
async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
|
|
3114
3237
|
if (packageManager === PackageManager.Npm) return [...deps];
|
|
3238
|
+
if (packageManager === PackageManager.Yarn) {
|
|
3239
|
+
const unresolvable = [];
|
|
3240
|
+
for (const dep of deps) {
|
|
3241
|
+
if (dep.protocol === "workspace:" || dep.protocol === "portal:") {
|
|
3242
|
+
unresolvable.push(dep);
|
|
3243
|
+
continue;
|
|
3244
|
+
}
|
|
3245
|
+
if (!isInsideRepo(repoRoot, resolveLocalPath(kiciDir, dep))) unresolvable.push(dep);
|
|
3246
|
+
}
|
|
3247
|
+
return unresolvable;
|
|
3248
|
+
}
|
|
3115
3249
|
const hasWorkspaceFile = await fileExists$1(join(repoRoot, "pnpm-workspace.yaml"));
|
|
3116
3250
|
const unresolvable = [];
|
|
3117
3251
|
for (const dep of deps) {
|
|
@@ -3127,6 +3261,7 @@ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
|
|
|
3127
3261
|
function formatUnresolvableDepError(offenders, packageManager) {
|
|
3128
3262
|
const list = offenders.map((o) => `${o.name}: ${o.spec}`).join(", ");
|
|
3129
3263
|
if (packageManager === PackageManager.Npm) return `These .kici/ dependencies use local-protocol specifiers npm cannot resolve from a registry: ${list}. npm has no workspace protocol — pin a published version, publish the package to your registry, or use pnpm so an in-repo workspace sibling can be resolved.`;
|
|
3264
|
+
if (packageManager === PackageManager.Yarn) return `These .kici/ dependencies use specifiers yarn classic cannot resolve: ${list}. yarn classic has no workspace: or portal: protocol — reference an in-repo sibling by a version range (yarn links matching workspace members), use pnpm, or keep file:/link: paths inside this repository. (yarn berry support is planned.)`;
|
|
3130
3265
|
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.`;
|
|
3131
3266
|
}
|
|
3132
3267
|
/**
|
|
@@ -3160,6 +3295,102 @@ var init_validate_kici_deps = __esmMin((() => {
|
|
|
3160
3295
|
];
|
|
3161
3296
|
}));
|
|
3162
3297
|
//#endregion
|
|
3298
|
+
//#region src/execution/workspace-siblings.ts
|
|
3299
|
+
/**
|
|
3300
|
+
* In-repo workspace-sibling discovery for the agent's dependency handling.
|
|
3301
|
+
*
|
|
3302
|
+
* A pnpm or yarn-classic workspace lays out a `.kici/` member's `workspace:`
|
|
3303
|
+
* (pnpm) or version-range (yarn) siblings as symlinks pointing at package
|
|
3304
|
+
* directories that live inside the clone but outside `.kici/` and outside the
|
|
3305
|
+
* `node_modules` store. The dep-cache packer must travel those sibling dirs with
|
|
3306
|
+
* the closure (their symlinks would dangle otherwise), and the yarn install path
|
|
3307
|
+
* must build them (the install links a sibling but does not build it).
|
|
3308
|
+
*
|
|
3309
|
+
* `collectInRepoSiblings` walks a starting `node_modules` (and transitively each
|
|
3310
|
+
* discovered sibling's `node_modules`), returning each in-repo sibling directory
|
|
3311
|
+
* once, repo-root-relative, in breadth-first discovery order. The starting
|
|
3312
|
+
* `node_modules` is a parameter so it serves pnpm + yarn-standalone (seeded at
|
|
3313
|
+
* `.kici/node_modules`) and yarn-workspace-member (seeded at the hoisted root
|
|
3314
|
+
* `node_modules`).
|
|
3315
|
+
*/
|
|
3316
|
+
/**
|
|
3317
|
+
* The directory yarn lays `.kici`'s dependencies into. A standalone `.kici`
|
|
3318
|
+
* (own lockfile, no parent workspace) gets `.kici/node_modules`; a workspace
|
|
3319
|
+
* member hoists everything to the repo-root `node_modules`, leaving no
|
|
3320
|
+
* `.kici/node_modules`.
|
|
3321
|
+
*/
|
|
3322
|
+
function resolveYarnNodeModulesRoot(repoRoot, kiciDir) {
|
|
3323
|
+
const kiciNm = join(kiciDir, "node_modules");
|
|
3324
|
+
return existsSync(kiciNm) ? kiciNm : join(repoRoot, "node_modules");
|
|
3325
|
+
}
|
|
3326
|
+
/**
|
|
3327
|
+
* Walk `seedNodeModules` (and transitively each in-repo sibling's
|
|
3328
|
+
* `node_modules`) collecting the repo-root-relative directories of workspace
|
|
3329
|
+
* siblings — package dirs that live inside the clone but outside `.kici/` and
|
|
3330
|
+
* outside the repo-root `node_modules/` store. Returns each dir once, in
|
|
3331
|
+
* discovery (BFS) order.
|
|
3332
|
+
*/
|
|
3333
|
+
async function collectInRepoSiblings(workDir, kiciDir, seedNodeModules = join(kiciDir, "node_modules")) {
|
|
3334
|
+
const repoRoot = resolve(workDir);
|
|
3335
|
+
const kiciResolved = resolve(kiciDir);
|
|
3336
|
+
const rootNodeModules = resolve(join(workDir, "node_modules"));
|
|
3337
|
+
const found = /* @__PURE__ */ new Set();
|
|
3338
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3339
|
+
const queue = [seedNodeModules];
|
|
3340
|
+
while (queue.length > 0) {
|
|
3341
|
+
const nmDir = queue.shift();
|
|
3342
|
+
const real = await realpath(nmDir).catch(() => null);
|
|
3343
|
+
if (!real || visited.has(real)) continue;
|
|
3344
|
+
visited.add(real);
|
|
3345
|
+
for (const target of await resolveNodeModulesLinks(nmDir)) {
|
|
3346
|
+
if (!isInside(repoRoot, target)) continue;
|
|
3347
|
+
if (isInside(kiciResolved, target) || isInside(rootNodeModules, target)) continue;
|
|
3348
|
+
const rel = relative(workDir, target);
|
|
3349
|
+
if (!found.has(rel)) {
|
|
3350
|
+
found.add(rel);
|
|
3351
|
+
queue.push(join(target, "node_modules"));
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
return [...found];
|
|
3356
|
+
}
|
|
3357
|
+
/** Resolve every package symlink target under a `node_modules` dir (descending one level into `@scope` dirs). */
|
|
3358
|
+
async function resolveNodeModulesLinks(nmDir) {
|
|
3359
|
+
const targets = [];
|
|
3360
|
+
for (const entry of await readdir(nmDir).catch(() => [])) {
|
|
3361
|
+
if (entry.startsWith(".")) continue;
|
|
3362
|
+
const entryPath = join(nmDir, entry);
|
|
3363
|
+
if (entry.startsWith("@")) {
|
|
3364
|
+
for (const scoped of await readdir(entryPath).catch(() => [])) {
|
|
3365
|
+
const target = await resolveIfSymlink(join(entryPath, scoped));
|
|
3366
|
+
if (target) targets.push(target);
|
|
3367
|
+
}
|
|
3368
|
+
continue;
|
|
3369
|
+
}
|
|
3370
|
+
const target = await resolveIfSymlink(entryPath);
|
|
3371
|
+
if (target) targets.push(target);
|
|
3372
|
+
}
|
|
3373
|
+
return targets;
|
|
3374
|
+
}
|
|
3375
|
+
/** Return the real path of `p` if it is a symlink, else null. */
|
|
3376
|
+
async function resolveIfSymlink(p) {
|
|
3377
|
+
try {
|
|
3378
|
+
if (!(await lstat(p)).isSymbolicLink()) return null;
|
|
3379
|
+
return await realpath(p);
|
|
3380
|
+
} catch {
|
|
3381
|
+
return null;
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
/** Whether `target` is `root` itself or a path inside it. */
|
|
3385
|
+
function isInside(root, target) {
|
|
3386
|
+
const rel = relative(root, target);
|
|
3387
|
+
return rel === "" || !rel.startsWith("..") && !rel.startsWith(`..${sep}`) && !isAbsoluteRel(rel);
|
|
3388
|
+
}
|
|
3389
|
+
function isAbsoluteRel(rel) {
|
|
3390
|
+
return rel.length > 1 && rel[1] === ":";
|
|
3391
|
+
}
|
|
3392
|
+
var init_workspace_siblings = __esmMin((() => {}));
|
|
3393
|
+
//#endregion
|
|
3163
3394
|
//#region src/execution/dep-installer.ts
|
|
3164
3395
|
/**
|
|
3165
3396
|
* Inline dependency installation for graceful degradation.
|
|
@@ -3167,12 +3398,14 @@ var init_validate_kici_deps = __esmMin((() => {
|
|
|
3167
3398
|
* When the dep cache is unavailable or a download fails, the agent installs
|
|
3168
3399
|
* `.kici/` dependencies directly with the repository's package manager.
|
|
3169
3400
|
*
|
|
3170
|
-
* The package manager is detected from the cloned repo (npm / pnpm); the
|
|
3401
|
+
* The package manager is detected from the cloned repo (npm / pnpm / yarn); the
|
|
3171
3402
|
* presence of `.kici/package.json` signals that deps should be installed. npm
|
|
3172
3403
|
* is the default and ships with every Node.js install; pnpm is used when the
|
|
3173
3404
|
* repo is a pnpm workspace so a `.kici/` member can resolve in-repo
|
|
3174
|
-
* `workspace:` siblings. yarn
|
|
3175
|
-
*
|
|
3405
|
+
* `workspace:` siblings. yarn classic (v1) is supported for registry
|
|
3406
|
+
* dependencies and version-range workspace siblings (which it links but does
|
|
3407
|
+
* not build, so the agent builds the in-repo closure after install). yarn
|
|
3408
|
+
* berry (v2+) is not yet supported.
|
|
3176
3409
|
*
|
|
3177
3410
|
* Security: the install runs with an isolated per-invocation cache/store
|
|
3178
3411
|
* directory to prevent cache poisoning across build jobs — a malicious
|
|
@@ -3216,7 +3449,6 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
3216
3449
|
dir: kiciDir
|
|
3217
3450
|
});
|
|
3218
3451
|
process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, cwd=${kiciDir}\n`);
|
|
3219
|
-
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.");
|
|
3220
3452
|
await assertResolvableDeps({
|
|
3221
3453
|
kiciDir,
|
|
3222
3454
|
repoRoot,
|
|
@@ -3236,6 +3468,11 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
3236
3468
|
hasPrivateRegistry,
|
|
3237
3469
|
registryConfig
|
|
3238
3470
|
});
|
|
3471
|
+
else if (packageManager === PackageManager.Yarn) await runYarnInstall({
|
|
3472
|
+
kiciDir,
|
|
3473
|
+
hasPrivateRegistry,
|
|
3474
|
+
registryConfig
|
|
3475
|
+
});
|
|
3239
3476
|
else await runNpmInstall({
|
|
3240
3477
|
kiciDir,
|
|
3241
3478
|
hasPrivateRegistry,
|
|
@@ -3250,6 +3487,7 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
3250
3487
|
await registryConfig.cleanup();
|
|
3251
3488
|
}
|
|
3252
3489
|
if (packageManager === PackageManager.Pnpm && await kiciHasLocalProtocolDeps(kiciDir)) await buildWorkspaceClosure(repoRoot);
|
|
3490
|
+
if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir);
|
|
3253
3491
|
const durationMs = Date.now() - startTime;
|
|
3254
3492
|
process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
|
|
3255
3493
|
logger$6.info("Deps installed inline", {
|
|
@@ -3337,6 +3575,101 @@ async function runPnpmInstall(args) {
|
|
|
3337
3575
|
}).catch(() => {});
|
|
3338
3576
|
}
|
|
3339
3577
|
}
|
|
3578
|
+
/** Pure: argv for `yarn install` with an isolated cache folder. */
|
|
3579
|
+
function buildYarnInstallArgs(cacheDir, hasPrivateRegistry) {
|
|
3580
|
+
const a = [
|
|
3581
|
+
"install",
|
|
3582
|
+
"--cache-folder",
|
|
3583
|
+
cacheDir,
|
|
3584
|
+
"--non-interactive",
|
|
3585
|
+
"--no-progress"
|
|
3586
|
+
];
|
|
3587
|
+
if (hasPrivateRegistry) a.push("--ignore-scripts");
|
|
3588
|
+
return a;
|
|
3589
|
+
}
|
|
3590
|
+
/**
|
|
3591
|
+
* Run `yarn install` from `.kici/` with an isolated cache folder. yarn classic
|
|
3592
|
+
* reads the synthesized `.kici/.npmrc` (registry + `${VAR}` token expansion) for
|
|
3593
|
+
* private-registry auth. A workspace member hoists deps to the repo-root
|
|
3594
|
+
* node_modules; a standalone `.kici` gets `.kici/node_modules`. Not
|
|
3595
|
+
* `--frozen-lockfile` (resolved URLs in the lockfile may point at a different
|
|
3596
|
+
* registry than the synthesized `.npmrc`, e.g. localhost tunnel vs direct IP).
|
|
3597
|
+
*/
|
|
3598
|
+
async function runYarnInstall(args) {
|
|
3599
|
+
await assertYarnAvailable();
|
|
3600
|
+
const { nodeDir } = resolveNpm();
|
|
3601
|
+
const cacheDir = await mkdtemp(join(tmpdir(), "kici-yarn-cache-"));
|
|
3602
|
+
const env = envWithNodeOnPath(args.registryConfig.extraEnv, nodeDir);
|
|
3603
|
+
const argv = buildYarnInstallArgs(cacheDir, args.hasPrivateRegistry);
|
|
3604
|
+
try {
|
|
3605
|
+
process.stderr.write(`[dep-installer:trace] running: yarn ${argv.join(" ")}\n`);
|
|
3606
|
+
await execFileAsync("yarn", argv, {
|
|
3607
|
+
cwd: args.kiciDir,
|
|
3608
|
+
env,
|
|
3609
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
3610
|
+
maxBuffer: INSTALL_MAX_BUFFER
|
|
3611
|
+
});
|
|
3612
|
+
} finally {
|
|
3613
|
+
await rm(cacheDir, {
|
|
3614
|
+
recursive: true,
|
|
3615
|
+
force: true
|
|
3616
|
+
}).catch(() => {});
|
|
3617
|
+
}
|
|
3618
|
+
}
|
|
3619
|
+
/** Throw an actionable error when the repo needs yarn but it is not installed. */
|
|
3620
|
+
async function assertYarnAvailable() {
|
|
3621
|
+
try {
|
|
3622
|
+
await execFileAsync("yarn", ["--version"], {
|
|
3623
|
+
timeout: 3e4,
|
|
3624
|
+
cwd: tmpdir()
|
|
3625
|
+
});
|
|
3626
|
+
} catch (e) {
|
|
3627
|
+
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)})`);
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
/**
|
|
3631
|
+
* Build the in-repo workspace siblings `.kici` depends on (yarn links them on
|
|
3632
|
+
* install but does not build them). Walks siblings from the resolved
|
|
3633
|
+
* node_modules root and runs each sibling's `build` script in leaf-first
|
|
3634
|
+
* (reverse-discovery) order with a clean env (no synthesized registry tokens).
|
|
3635
|
+
* Deep cross-sibling build chains may build out of strict topological order —
|
|
3636
|
+
* real `.kici` closures are shallow.
|
|
3637
|
+
*/
|
|
3638
|
+
async function buildYarnWorkspaceClosure(repoRoot, kiciDir) {
|
|
3639
|
+
const siblings = await collectInRepoSiblings(repoRoot, kiciDir, resolveYarnNodeModulesRoot(repoRoot, kiciDir));
|
|
3640
|
+
if (siblings.length === 0) return;
|
|
3641
|
+
const { nodeDir } = resolveNpm();
|
|
3642
|
+
const env = envWithNodeOnPath({}, nodeDir);
|
|
3643
|
+
for (const rel of [...siblings].reverse()) {
|
|
3644
|
+
const sibDir = join(repoRoot, rel);
|
|
3645
|
+
if (!await siblingHasBuildScript(sibDir)) continue;
|
|
3646
|
+
process.stderr.write(`[dep-installer:trace] building yarn sibling: yarn --cwd ${sibDir} run build\n`);
|
|
3647
|
+
try {
|
|
3648
|
+
await execFileAsync("yarn", [
|
|
3649
|
+
"--cwd",
|
|
3650
|
+
sibDir,
|
|
3651
|
+
"run",
|
|
3652
|
+
"build"
|
|
3653
|
+
], {
|
|
3654
|
+
cwd: repoRoot,
|
|
3655
|
+
env,
|
|
3656
|
+
timeout: INSTALL_TIMEOUT_MS,
|
|
3657
|
+
maxBuffer: INSTALL_MAX_BUFFER
|
|
3658
|
+
});
|
|
3659
|
+
} catch (e) {
|
|
3660
|
+
logSubprocessStreams(e, []);
|
|
3661
|
+
throw new Error(`Failed to build .kici yarn workspace sibling ${rel}: ${describeExecError(e)}`);
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
/** Whether a sibling package.json declares a `build` script. */
|
|
3666
|
+
async function siblingHasBuildScript(sibDir) {
|
|
3667
|
+
try {
|
|
3668
|
+
return typeof JSON.parse(await readFile(join(sibDir, "package.json"), "utf-8")).scripts?.build === "string";
|
|
3669
|
+
} catch {
|
|
3670
|
+
return false;
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3340
3673
|
/**
|
|
3341
3674
|
* Build the in-repo dependency closure of the `.kici/` package so a
|
|
3342
3675
|
* `workspace:` sibling's build output exists before the workflow that imports
|
|
@@ -3380,7 +3713,10 @@ function describeExecError(e) {
|
|
|
3380
3713
|
/** Throw an actionable error when the repo needs pnpm but it is not installed. */
|
|
3381
3714
|
async function assertPnpmAvailable() {
|
|
3382
3715
|
try {
|
|
3383
|
-
await execFileAsync("pnpm", ["--version"], {
|
|
3716
|
+
await execFileAsync("pnpm", ["--version"], {
|
|
3717
|
+
timeout: 3e4,
|
|
3718
|
+
cwd: tmpdir()
|
|
3719
|
+
});
|
|
3384
3720
|
} catch (e) {
|
|
3385
3721
|
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)})`);
|
|
3386
3722
|
}
|
|
@@ -3394,6 +3730,7 @@ var logger$6, execFileAsync, INSTALL_TIMEOUT_MS, INSTALL_MAX_BUFFER;
|
|
|
3394
3730
|
var init_dep_installer = __esmMin((() => {
|
|
3395
3731
|
init_npm_registry_config();
|
|
3396
3732
|
init_validate_kici_deps();
|
|
3733
|
+
init_workspace_siblings();
|
|
3397
3734
|
logger$6 = createLogger({ prefix: "dep-installer" });
|
|
3398
3735
|
execFileAsync = promisify(execFile);
|
|
3399
3736
|
INSTALL_TIMEOUT_MS = 6e5;
|
|
@@ -3409,13 +3746,18 @@ var init_dep_installer = __esmMin((() => {
|
|
|
3409
3746
|
* **repo-root-relative** (cwd = the clone root) so restore is a single layout
|
|
3410
3747
|
* regardless of package manager:
|
|
3411
3748
|
*
|
|
3412
|
-
* - npm
|
|
3749
|
+
* - npm: just `.kici/node_modules`.
|
|
3413
3750
|
* - pnpm: `.kici/node_modules` plus the repo-root `node_modules/.pnpm` virtual
|
|
3414
3751
|
* store and the in-repo `workspace:` sibling package directories `.kici`
|
|
3415
3752
|
* depends on (with their built output). pnpm lays `.kici/node_modules` out as
|
|
3416
3753
|
* symlinks into the root store and into sibling dirs that live outside
|
|
3417
3754
|
* `.kici/`, so packing `.kici/node_modules` alone would capture dangling
|
|
3418
3755
|
* links — the store and siblings must travel together.
|
|
3756
|
+
* - yarn classic: the resolved node_modules root (standalone `.kici` →
|
|
3757
|
+
* `.kici/node_modules`; hoisted workspace member → the repo-root
|
|
3758
|
+
* `node_modules`) plus the in-repo version-range sibling package directories
|
|
3759
|
+
* `.kici` depends on (with their built output), whose symlinks would dangle
|
|
3760
|
+
* otherwise.
|
|
3419
3761
|
*
|
|
3420
3762
|
* Uses tar.gz (Node.js built-in zlib, no external binary) in portable mode to
|
|
3421
3763
|
* strip user/group info for cross-machine consistency; symlinks are preserved
|
|
@@ -3429,9 +3771,10 @@ var init_dep_installer = __esmMin((() => {
|
|
|
3429
3771
|
* @throws Error if `.kici/node_modules` does not exist.
|
|
3430
3772
|
*/
|
|
3431
3773
|
async function packNodeModules(kiciDir) {
|
|
3432
|
-
if (!existsSync(join(kiciDir, "node_modules"))) throw new Error(`node_modules not found at ${join(kiciDir, "node_modules")}`);
|
|
3433
3774
|
const workDir = dirname(kiciDir);
|
|
3434
3775
|
const packageManager = await detectPackageManagerFromManifests(workDir) ?? await detectPackageManagerFromManifests(kiciDir) ?? PackageManager.Npm;
|
|
3776
|
+
const nmRoot = packageManager === PackageManager.Yarn ? resolveYarnNodeModulesRoot(workDir, kiciDir) : join(kiciDir, "node_modules");
|
|
3777
|
+
if (!existsSync(nmRoot)) throw new Error(`node_modules not found at ${nmRoot}`);
|
|
3435
3778
|
const entries = await closureEntries(workDir, kiciDir, packageManager);
|
|
3436
3779
|
logger$5.info("Packing dependency closure into tarball", {
|
|
3437
3780
|
dir: workDir,
|
|
@@ -3460,85 +3803,31 @@ async function packNodeModules(kiciDir) {
|
|
|
3460
3803
|
};
|
|
3461
3804
|
}
|
|
3462
3805
|
/**
|
|
3463
|
-
* Compute the repo-root-relative tar entries for the dependency closure. npm
|
|
3464
|
-
*
|
|
3465
|
-
*
|
|
3806
|
+
* Compute the repo-root-relative tar entries for the dependency closure. npm
|
|
3807
|
+
* needs only `.kici/node_modules`. pnpm additionally needs the root store and
|
|
3808
|
+
* the in-repo workspace siblings `.kici` resolves. yarn classic packs the
|
|
3809
|
+
* resolved node_modules root (standalone → `.kici/node_modules`; hoisted
|
|
3810
|
+
* workspace member → root `node_modules`) plus the in-repo version-range
|
|
3811
|
+
* siblings, which it links but does not place in the store.
|
|
3466
3812
|
*/
|
|
3467
3813
|
async function closureEntries(workDir, kiciDir, packageManager) {
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
3473
|
-
return entries;
|
|
3474
|
-
}
|
|
3475
|
-
/**
|
|
3476
|
-
* Walk `.kici`'s `node_modules` (and transitively each in-repo sibling's
|
|
3477
|
-
* `node_modules`) collecting the repo-relative directories of `workspace:`
|
|
3478
|
-
* siblings — package dirs that live inside the clone but outside `.kici/` and
|
|
3479
|
-
* outside the root `node_modules/` store. Returns each dir once.
|
|
3480
|
-
*/
|
|
3481
|
-
async function collectInRepoSiblings(workDir, kiciDir) {
|
|
3482
|
-
const repoRoot = resolve(workDir);
|
|
3483
|
-
const kiciResolved = resolve(kiciDir);
|
|
3484
|
-
const rootNodeModules = resolve(join(workDir, "node_modules"));
|
|
3485
|
-
const found = /* @__PURE__ */ new Set();
|
|
3486
|
-
const visited = /* @__PURE__ */ new Set();
|
|
3487
|
-
const queue = [join(kiciDir, "node_modules")];
|
|
3488
|
-
while (queue.length > 0) {
|
|
3489
|
-
const nmDir = queue.shift();
|
|
3490
|
-
const real = await realpath(nmDir).catch(() => null);
|
|
3491
|
-
if (!real || visited.has(real)) continue;
|
|
3492
|
-
visited.add(real);
|
|
3493
|
-
for (const target of await resolveNodeModulesLinks(nmDir)) {
|
|
3494
|
-
if (!isInside(repoRoot, target)) continue;
|
|
3495
|
-
if (isInside(kiciResolved, target) || isInside(rootNodeModules, target)) continue;
|
|
3496
|
-
const rel = relative(workDir, target);
|
|
3497
|
-
if (!found.has(rel)) {
|
|
3498
|
-
found.add(rel);
|
|
3499
|
-
queue.push(join(target, "node_modules"));
|
|
3500
|
-
}
|
|
3501
|
-
}
|
|
3502
|
-
}
|
|
3503
|
-
return [...found];
|
|
3504
|
-
}
|
|
3505
|
-
/** Resolve every package symlink target under a `node_modules` dir (descending one level into `@scope` dirs). */
|
|
3506
|
-
async function resolveNodeModulesLinks(nmDir) {
|
|
3507
|
-
const targets = [];
|
|
3508
|
-
for (const entry of await readdir(nmDir).catch(() => [])) {
|
|
3509
|
-
if (entry.startsWith(".")) continue;
|
|
3510
|
-
const entryPath = join(nmDir, entry);
|
|
3511
|
-
if (entry.startsWith("@")) {
|
|
3512
|
-
for (const scoped of await readdir(entryPath).catch(() => [])) {
|
|
3513
|
-
const target = await resolveIfSymlink(join(entryPath, scoped));
|
|
3514
|
-
if (target) targets.push(target);
|
|
3515
|
-
}
|
|
3516
|
-
continue;
|
|
3517
|
-
}
|
|
3518
|
-
const target = await resolveIfSymlink(entryPath);
|
|
3519
|
-
if (target) targets.push(target);
|
|
3814
|
+
if (packageManager === PackageManager.Pnpm) {
|
|
3815
|
+
const entries = [relative(workDir, join(kiciDir, "node_modules"))];
|
|
3816
|
+
if (existsSync(join(workDir, "node_modules", ".pnpm"))) entries.push(join("node_modules", ".pnpm"));
|
|
3817
|
+
for (const sibling of await collectInRepoSiblings(workDir, kiciDir)) entries.push(sibling);
|
|
3818
|
+
return entries;
|
|
3520
3819
|
}
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
if (!(await lstat(p)).isSymbolicLink()) return null;
|
|
3527
|
-
return await realpath(p);
|
|
3528
|
-
} catch {
|
|
3529
|
-
return null;
|
|
3820
|
+
if (packageManager === PackageManager.Yarn) {
|
|
3821
|
+
const nmRoot = resolveYarnNodeModulesRoot(workDir, kiciDir);
|
|
3822
|
+
const entries = [relative(workDir, nmRoot)];
|
|
3823
|
+
for (const sibling of await collectInRepoSiblings(workDir, kiciDir, nmRoot)) entries.push(sibling);
|
|
3824
|
+
return entries;
|
|
3530
3825
|
}
|
|
3531
|
-
|
|
3532
|
-
/** Whether `target` is `root` itself or a path inside it. */
|
|
3533
|
-
function isInside(root, target) {
|
|
3534
|
-
const rel = relative(root, target);
|
|
3535
|
-
return rel === "" || !rel.startsWith("..") && !rel.startsWith(`..${sep}`) && !isAbsoluteRel(rel);
|
|
3536
|
-
}
|
|
3537
|
-
function isAbsoluteRel(rel) {
|
|
3538
|
-
return rel.length > 1 && rel[1] === ":";
|
|
3826
|
+
return [relative(workDir, join(kiciDir, "node_modules"))];
|
|
3539
3827
|
}
|
|
3540
3828
|
var logger$5;
|
|
3541
3829
|
var init_dep_packer = __esmMin((() => {
|
|
3830
|
+
init_workspace_siblings();
|
|
3542
3831
|
logger$5 = createLogger({ prefix: "dep-packer" });
|
|
3543
3832
|
}));
|
|
3544
3833
|
//#endregion
|
|
@@ -3682,6 +3971,8 @@ var init_secret_encryption = __esmMin((() => {
|
|
|
3682
3971
|
function buildRequest(dispatch, workDir) {
|
|
3683
3972
|
const jobConfig = dispatch.jobConfig;
|
|
3684
3973
|
return {
|
|
3974
|
+
runId: dispatch.runId,
|
|
3975
|
+
jobId: dispatch.jobId,
|
|
3685
3976
|
workDir,
|
|
3686
3977
|
repoUrl: dispatch.repoUrl,
|
|
3687
3978
|
ref: dispatch.ref,
|
|
@@ -3694,8 +3985,9 @@ function buildRequest(dispatch, workDir) {
|
|
|
3694
3985
|
depsUrl: dispatch.depsUrl,
|
|
3695
3986
|
depsHash: dispatch.depsHash,
|
|
3696
3987
|
workflowName: jobConfig.workflowName ?? "",
|
|
3697
|
-
jobName: jobConfig.name ?? "",
|
|
3988
|
+
jobName: jobConfig.baseJobName ?? jobConfig.name ?? "",
|
|
3698
3989
|
runsOn: jobConfig.runsOn ?? "",
|
|
3990
|
+
matrixValues: jobConfig.matrixValues,
|
|
3699
3991
|
secrets: dispatch.secrets,
|
|
3700
3992
|
namespacedSecrets: dispatch.namespacedSecrets,
|
|
3701
3993
|
sourceFile: jobConfig.source?.file,
|
|
@@ -3962,6 +4254,24 @@ function relayCacheRequest$1(msg, ctx) {
|
|
|
3962
4254
|
error: toErrorMessage(err)
|
|
3963
4255
|
}));
|
|
3964
4256
|
}
|
|
4257
|
+
/** Relay `provenance.request` and pipe the orchestrator response (or an error
|
|
4258
|
+
* response, or a "not configured" response when the callback isn't wired) back
|
|
4259
|
+
* into the sandbox runner. */
|
|
4260
|
+
function relayProvenanceRequest$1(msg, ctx) {
|
|
4261
|
+
if (!ctx.execOptions.onProvenanceRequest) {
|
|
4262
|
+
safeSendToChild(ctx.child, {
|
|
4263
|
+
type: "provenance.response",
|
|
4264
|
+
requestId: msg.requestId,
|
|
4265
|
+
error: "Provenance not available in this agent configuration"
|
|
4266
|
+
});
|
|
4267
|
+
return;
|
|
4268
|
+
}
|
|
4269
|
+
ctx.execOptions.onProvenanceRequest(msg).then((response) => safeSendToChild(ctx.child, response), (err) => safeSendToChild(ctx.child, {
|
|
4270
|
+
type: "provenance.response",
|
|
4271
|
+
requestId: msg.requestId,
|
|
4272
|
+
error: toErrorMessage(err)
|
|
4273
|
+
}));
|
|
4274
|
+
}
|
|
3965
4275
|
/** Relay `approval.request` and pipe the orchestrator's resolution (or a
|
|
3966
4276
|
* fail-closed reject when the callback isn't wired or the relay throws) back
|
|
3967
4277
|
* into the sandbox runner. */
|
|
@@ -4052,6 +4362,9 @@ function relayChildIpcMessage(msg, dispatch, ctx) {
|
|
|
4052
4362
|
case "cache.request":
|
|
4053
4363
|
relayCacheRequest$1(msg, ctx);
|
|
4054
4364
|
return;
|
|
4365
|
+
case "provenance.request":
|
|
4366
|
+
relayProvenanceRequest$1(msg, ctx);
|
|
4367
|
+
return;
|
|
4055
4368
|
case "approval.request":
|
|
4056
4369
|
relayApprovalRequest$1(msg, ctx);
|
|
4057
4370
|
return;
|
|
@@ -4476,6 +4789,32 @@ function relayCacheRequest(stream, options, cacheMsg) {
|
|
|
4476
4789
|
}));
|
|
4477
4790
|
}
|
|
4478
4791
|
/**
|
|
4792
|
+
* Relay provenance.request from the container runner to the orchestrator via
|
|
4793
|
+
* options.onProvenanceRequest, then write the response back through `stream`.
|
|
4794
|
+
* If the agent doesn't expose a provenance relay, write a structured error so
|
|
4795
|
+
* the runner doesn't hang.
|
|
4796
|
+
*/
|
|
4797
|
+
function relayProvenanceRequest(stream, options, provMsg) {
|
|
4798
|
+
const writeResponse = (response) => {
|
|
4799
|
+
try {
|
|
4800
|
+
stream.write(JSON.stringify(response) + "\n");
|
|
4801
|
+
} catch {}
|
|
4802
|
+
};
|
|
4803
|
+
if (!options.onProvenanceRequest) {
|
|
4804
|
+
writeResponse({
|
|
4805
|
+
type: "provenance.response",
|
|
4806
|
+
requestId: provMsg.requestId,
|
|
4807
|
+
error: "Provenance not available in this agent configuration"
|
|
4808
|
+
});
|
|
4809
|
+
return;
|
|
4810
|
+
}
|
|
4811
|
+
options.onProvenanceRequest(provMsg).then((response) => writeResponse(response), (err) => writeResponse({
|
|
4812
|
+
type: "provenance.response",
|
|
4813
|
+
requestId: provMsg.requestId,
|
|
4814
|
+
error: toErrorMessage(err)
|
|
4815
|
+
}));
|
|
4816
|
+
}
|
|
4817
|
+
/**
|
|
4479
4818
|
* Relay approval.request from the container runner to the orchestrator via
|
|
4480
4819
|
* options.onApprovalRequest, then write the resolution back through `stream`.
|
|
4481
4820
|
* If the agent doesn't expose an approval relay (or it throws), write a
|
|
@@ -4758,6 +5097,9 @@ var init_container_sandbox = __esmMin((() => {
|
|
|
4758
5097
|
case "cache.request":
|
|
4759
5098
|
relayCacheRequest(stream, options, msg);
|
|
4760
5099
|
return false;
|
|
5100
|
+
case "provenance.request":
|
|
5101
|
+
relayProvenanceRequest(stream, options, msg);
|
|
5102
|
+
return false;
|
|
4761
5103
|
case "approval.request":
|
|
4762
5104
|
relayApprovalRequest(stream, options, msg);
|
|
4763
5105
|
return false;
|
|
@@ -4857,7 +5199,7 @@ var job_runner_exports = /* @__PURE__ */ __exportAll({ JobRunner: () => JobRunne
|
|
|
4857
5199
|
*/
|
|
4858
5200
|
async function fileExists(p) {
|
|
4859
5201
|
try {
|
|
4860
|
-
await fs.access(p);
|
|
5202
|
+
await fs$1.access(p);
|
|
4861
5203
|
return true;
|
|
4862
5204
|
} catch {
|
|
4863
5205
|
return false;
|
|
@@ -4931,6 +5273,7 @@ var init_job_runner = __esmMin((() => {
|
|
|
4931
5273
|
_sendConcurrencyReport;
|
|
4932
5274
|
_sendApiRequest;
|
|
4933
5275
|
_requestUserCache;
|
|
5276
|
+
_relayProvenance;
|
|
4934
5277
|
_sendStepApproval;
|
|
4935
5278
|
/** Tracks running jobs for concurrency and cancellation */
|
|
4936
5279
|
activeJobs = /* @__PURE__ */ new Map();
|
|
@@ -4950,6 +5293,7 @@ var init_job_runner = __esmMin((() => {
|
|
|
4950
5293
|
this._sendConcurrencyReport = deps.sendConcurrencyReport;
|
|
4951
5294
|
this._sendApiRequest = deps.sendApiRequest;
|
|
4952
5295
|
this._requestUserCache = deps.requestUserCache;
|
|
5296
|
+
this._relayProvenance = deps.relayProvenance;
|
|
4953
5297
|
this._sendStepApproval = deps.sendStepApproval;
|
|
4954
5298
|
}
|
|
4955
5299
|
/**
|
|
@@ -4961,11 +5305,11 @@ var init_job_runner = __esmMin((() => {
|
|
|
4961
5305
|
async execute(dispatch) {
|
|
4962
5306
|
const { runId: _runId, jobId, jobConfig: _jobConfig } = dispatch;
|
|
4963
5307
|
const abortController = new AbortController();
|
|
4964
|
-
const workDir = await fs.mkdtemp(join(tmpdir(), "kici-"));
|
|
5308
|
+
const workDir = await fs$1.mkdtemp(join(tmpdir(), "kici-"));
|
|
4965
5309
|
const completionPromise = this.runJob(dispatch, workDir, abortController).finally(async () => {
|
|
4966
5310
|
this.activeJobs.delete(jobId);
|
|
4967
5311
|
this.activeSandbox = null;
|
|
4968
|
-
await fs.rm(workDir, {
|
|
5312
|
+
await fs$1.rm(workDir, {
|
|
4969
5313
|
recursive: true,
|
|
4970
5314
|
force: true
|
|
4971
5315
|
}).catch(() => {});
|
|
@@ -5190,6 +5534,7 @@ var init_job_runner = __esmMin((() => {
|
|
|
5190
5534
|
},
|
|
5191
5535
|
onApiRequest: this._sendApiRequest ? async (method, params) => this._sendApiRequest(method, params) : void 0,
|
|
5192
5536
|
onCacheRequest: this._requestUserCache ? async (request) => this._requestUserCache(jobId, request) : void 0,
|
|
5537
|
+
onProvenanceRequest: this._relayProvenance ? async (request) => this._relayProvenance(jobId, request) : void 0,
|
|
5193
5538
|
onApprovalRequest: this._sendStepApproval ? async (request) => this._sendStepApproval(dispatch.runId, dispatch.jobId, request) : void 0,
|
|
5194
5539
|
onSecretMount: (event) => {
|
|
5195
5540
|
this.emitRunEvent(runId, "step.secret_mount", {
|
|
@@ -5509,11 +5854,12 @@ var init_job_runner = __esmMin((() => {
|
|
|
5509
5854
|
const initResult = await runCaptured(initSink, async () => {
|
|
5510
5855
|
const { module } = await loadWorkflowSource(workDir, config.source, config.contentHash, config.resolvedHashFiles);
|
|
5511
5856
|
const workflow = extractWorkflow(module, config.workflowName);
|
|
5512
|
-
initLog(`Evaluating dynamic fields for job '${config.targetJobName}' (env=${config.dynamicEnv} environment=${config.dynamicEnvironment} concurrencyGroup=${config.dynamicConcurrencyGroup})`);
|
|
5857
|
+
initLog(`Evaluating dynamic fields for job '${config.targetJobName}' (env=${config.dynamicEnv} environment=${config.dynamicEnvironment} concurrencyGroup=${config.dynamicConcurrencyGroup} matrix=${config.dynamicMatrix ?? false})`);
|
|
5513
5858
|
return evaluateDynamicFields(workflow, config.targetJobName, config.event, {
|
|
5514
5859
|
dynamicEnvironment: config.dynamicEnvironment,
|
|
5515
5860
|
dynamicEnv: config.dynamicEnv,
|
|
5516
|
-
dynamicConcurrencyGroup: config.dynamicConcurrencyGroup
|
|
5861
|
+
dynamicConcurrencyGroup: config.dynamicConcurrencyGroup,
|
|
5862
|
+
dynamicMatrix: config.dynamicMatrix ?? false
|
|
5517
5863
|
}, config.timeoutMs);
|
|
5518
5864
|
});
|
|
5519
5865
|
logger$2.info("Init job completed successfully", {
|
|
@@ -5863,14 +6209,14 @@ var init_job_runner = __esmMin((() => {
|
|
|
5863
6209
|
*/
|
|
5864
6210
|
init_console_capture();
|
|
5865
6211
|
init_npm_resolver();
|
|
5866
|
-
const AGENT_VERSION = "0.1.
|
|
5867
|
-
const BUILD_COMMIT = "
|
|
5868
|
-
const SDK_VERSION = "0.1.
|
|
5869
|
-
const SDK_BUNDLE_HASH = "
|
|
5870
|
-
const SHARED_VERSION = "0.1.
|
|
5871
|
-
const SHARED_BUNDLE_HASH = "
|
|
5872
|
-
const ENGINE_VERSION = "0.1.
|
|
5873
|
-
const ENGINE_BUNDLE_HASH = "
|
|
6212
|
+
const AGENT_VERSION = "0.1.17";
|
|
6213
|
+
const BUILD_COMMIT = "5596f8a3c";
|
|
6214
|
+
const SDK_VERSION = "0.1.17";
|
|
6215
|
+
const SDK_BUNDLE_HASH = "df47ed5db86eaaa2de8394c0db08335f368e8d620a898cc409765f4545eb3972";
|
|
6216
|
+
const SHARED_VERSION = "0.1.17";
|
|
6217
|
+
const SHARED_BUNDLE_HASH = "9e8a753da73b26fb87d08817f70b4d836f67f599385fa268b2c1f68b97996f54";
|
|
6218
|
+
const ENGINE_VERSION = "0.1.17";
|
|
6219
|
+
const ENGINE_BUNDLE_HASH = "706d94fa54a47aea0f69bde105d40213befff401f717604aded2c62a1291cb51";
|
|
5874
6220
|
initTelemetry({
|
|
5875
6221
|
serviceName: "kici-agent",
|
|
5876
6222
|
otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|
|
@@ -5951,6 +6297,7 @@ await guardStartup(logger$1, async () => {
|
|
|
5951
6297
|
sendConcurrencyReport: (runId, jobId, group) => client.sendConcurrencyReport(runId, jobId, group),
|
|
5952
6298
|
sendApiRequest: (method, params) => client.sendApiRequest(method, params ?? {}),
|
|
5953
6299
|
requestUserCache: (jobId, request) => client.requestUserCache(jobId, request),
|
|
6300
|
+
relayProvenance: (jobId, request) => client.relayProvenance(jobId, request),
|
|
5954
6301
|
sendStepApproval: (runId, jobId, request) => client.sendStepApproval(runId, jobId, request)
|
|
5955
6302
|
});
|
|
5956
6303
|
/** Build and send an agent.status message with dynamic OS metadata. */
|