@kici-dev/agent 0.1.15 → 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/diagnostics/mini-bundle.d.ts +9 -0
- 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 +18 -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 +101 -3
- package/dist/execution/sandbox/step-loop.d.ts +24 -1
- package/dist/execution/sandbox/types.d.ts +18 -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 +698 -146
- package/dist/version.d.ts +2 -0
- package/dist/workflow-runner.js +877 -45
- package/dist/ws/orchestrator-client.d.ts +53 -4
- package/package.json +8 -5
- package/sbom.spdx.json +1957 -152
package/dist/server.js
CHANGED
|
@@ -3,24 +3,26 @@ import { dirname as __cjs_dirname } from "node:path";
|
|
|
3
3
|
__cjs_dirname(__cjs_fileURLToPath(import.meta.url));
|
|
4
4
|
import { register } from "node:module";
|
|
5
5
|
import crypto$1, { createCipheriv, createHash, createPublicKey, diffieHellman, generateKeyPairSync, hkdfSync, randomBytes, randomUUID } from "node:crypto";
|
|
6
|
+
import * as os$1 from "node:os";
|
|
6
7
|
import os, { hostname, tmpdir } from "node:os";
|
|
7
8
|
import { PassThrough, Readable, Transform, Writable } from "node:stream";
|
|
8
9
|
import { serve } from "@hono/node-server";
|
|
9
10
|
import { Hono } from "hono";
|
|
10
11
|
import winston from "winston";
|
|
11
|
-
import { RingBuffer, createHealthRoutes, createLogger, createMeter, createMetricsRoutes, deriveSharedSecret, getPrometheusExporter, getReconnectDelay, getRequestContext, guardStartup, initTelemetry, logger, normalizeLineEndings, requestContext, setServiceName, setupGracefulShutdown, sha256, sha256File, toErrorMessage, validateRequiredTools } from "@kici-dev/shared";
|
|
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";
|
|
12
13
|
import { z } from "zod";
|
|
13
14
|
import { LOGGER_ENV_VARS, defineEnv, validateUnknownKiciVars } from "@kici-dev/shared/env";
|
|
14
|
-
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";
|
|
15
16
|
import { execFile, execFileSync, execSync, fork, spawn } from "node:child_process";
|
|
16
17
|
import WebSocket from "ws";
|
|
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";
|
|
17
22
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
18
23
|
import { format, promisify } from "node:util";
|
|
19
|
-
import { existsSync } from "node:fs";
|
|
20
|
-
import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
21
24
|
import { gcStaleTmpDirs } from "@kici-dev/core/tmp-gc";
|
|
22
|
-
import {
|
|
23
|
-
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";
|
|
24
26
|
import Docker from "dockerode";
|
|
25
27
|
import { buildKiciApi, isDynamicGroupRef, isDynamicJobFn, isStaticArray, isStaticObject } from "@kici-dev/sdk";
|
|
26
28
|
import { c, x } from "tar";
|
|
@@ -177,6 +179,75 @@ function agentClientConnectionOptions(config) {
|
|
|
177
179
|
};
|
|
178
180
|
}
|
|
179
181
|
//#endregion
|
|
182
|
+
//#region src/diagnostics/mini-bundle.ts
|
|
183
|
+
/**
|
|
184
|
+
* Agent fleet mini-bundle assembler.
|
|
185
|
+
*
|
|
186
|
+
* Builds an in-memory ZIP of the agent's recent logs, system info, redacted
|
|
187
|
+
* config, and current Prometheus metrics text, streamed to the orchestrator on
|
|
188
|
+
* a fleet.logs.request. No diagnostics runner exists agent-side, so this is a
|
|
189
|
+
* lean subset of the orchestrator's createDebugBundle.
|
|
190
|
+
*/
|
|
191
|
+
async function buildAgentMiniBundle(opts) {
|
|
192
|
+
const archive = archiver("zip", { zlib: { level: 6 } });
|
|
193
|
+
const chunks = [];
|
|
194
|
+
archive.on("data", (d) => chunks.push(d));
|
|
195
|
+
const done = new Promise((resolve, reject) => {
|
|
196
|
+
archive.on("end", resolve);
|
|
197
|
+
archive.on("error", reject);
|
|
198
|
+
});
|
|
199
|
+
archive.append(JSON.stringify({
|
|
200
|
+
kind: "agent",
|
|
201
|
+
agentId: opts.agentId,
|
|
202
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
203
|
+
}, null, 2), { name: "manifest.json" });
|
|
204
|
+
archive.append(JSON.stringify(redactConfig(opts.config), null, 2), { name: "config/config.json" });
|
|
205
|
+
archive.append(JSON.stringify({
|
|
206
|
+
hostname: os$1.hostname(),
|
|
207
|
+
platform: process.platform,
|
|
208
|
+
arch: process.arch,
|
|
209
|
+
nodeVersion: process.version,
|
|
210
|
+
cpus: os$1.cpus().length,
|
|
211
|
+
totalmem: os$1.totalmem(),
|
|
212
|
+
freemem: os$1.freemem(),
|
|
213
|
+
uptime: os$1.uptime()
|
|
214
|
+
}, null, 2), { name: "system/info.json" });
|
|
215
|
+
if (opts.metricsText) archive.append(opts.metricsText, { name: "system/metrics.txt" });
|
|
216
|
+
if (opts.logDir) await addLogsToArchive(archive, opts.logDir, opts.logWindowHours);
|
|
217
|
+
await archive.finalize();
|
|
218
|
+
await done;
|
|
219
|
+
return Buffer.concat(chunks);
|
|
220
|
+
}
|
|
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
|
|
180
251
|
//#region src/ws/event-buffer.ts
|
|
181
252
|
/**
|
|
182
253
|
* In-memory buffer for agent-to-orchestrator messages during disconnection.
|
|
@@ -243,6 +314,13 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
243
314
|
pendingApiRequests = /* @__PURE__ */ new Map();
|
|
244
315
|
/** Pending user-cache restore/save requests awaiting orchestrator response. */
|
|
245
316
|
pendingUserCacheRequests = /* @__PURE__ */ new Map();
|
|
317
|
+
/**
|
|
318
|
+
* Pending step-approval requests awaiting the orchestrator's resolution.
|
|
319
|
+
* No client-side timeout: the orchestrator owns the (org-/SDK-configured)
|
|
320
|
+
* expiry and sends `step.approval-resolved: expired` when it lapses. The
|
|
321
|
+
* workflow-runner carries an outer safety-net timeout.
|
|
322
|
+
*/
|
|
323
|
+
pendingStepApprovals = /* @__PURE__ */ new Map();
|
|
246
324
|
/** Pending concurrency report requests awaiting orchestrator ack. */
|
|
247
325
|
pendingConcurrencyRequests = /* @__PURE__ */ new Map();
|
|
248
326
|
url;
|
|
@@ -256,6 +334,7 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
256
334
|
getInFlightJobs;
|
|
257
335
|
roles;
|
|
258
336
|
scalerManaged;
|
|
337
|
+
getFleetBundleInputs;
|
|
259
338
|
/** Timestamp when the connection was lost, used for gap marker outage duration. */
|
|
260
339
|
disconnectedAt = null;
|
|
261
340
|
/** Set to true when auth.failure is received. Prevents retrying with a bad token. */
|
|
@@ -282,6 +361,7 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
282
361
|
this.getInFlightJobs = options.getInFlightJobs;
|
|
283
362
|
this.roles = options.roles;
|
|
284
363
|
this.scalerManaged = options.scalerManaged ?? false;
|
|
364
|
+
this.getFleetBundleInputs = options.getFleetBundleInputs;
|
|
285
365
|
this.eventBuffer = new EventBuffer({ maxSize: options.maxBufferSize ?? 5e3 });
|
|
286
366
|
this.logBuffer = new LogBuffer({ maxLines: options.maxLogBufferLines ?? 1e4 });
|
|
287
367
|
}
|
|
@@ -416,6 +496,47 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
416
496
|
});
|
|
417
497
|
}
|
|
418
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
|
+
/**
|
|
419
540
|
* Send an event.emit WS message to the orchestrator and await the response.
|
|
420
541
|
*
|
|
421
542
|
* Used by the job runner to relay custom event emissions from the sandbox
|
|
@@ -453,6 +574,36 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
453
574
|
});
|
|
454
575
|
}
|
|
455
576
|
/**
|
|
577
|
+
* Build this agent's fleet mini-bundle and stream it back to the orchestrator
|
|
578
|
+
* as ordered fleet.bundle.chunk frames (the WS frame cap forbids one frame).
|
|
579
|
+
* On failure, sends a single fleet.bundle.error. Public for unit testing.
|
|
580
|
+
*/
|
|
581
|
+
async streamFleetBundle(req) {
|
|
582
|
+
try {
|
|
583
|
+
const inputs = await this.getFleetBundleInputs?.() ?? { config: {} };
|
|
584
|
+
const buf = await buildAgentMiniBundle({
|
|
585
|
+
agentId: this.agentId,
|
|
586
|
+
logDir: inputs.logDir,
|
|
587
|
+
logWindowHours: req.logWindowHours,
|
|
588
|
+
config: inputs.config,
|
|
589
|
+
metricsText: inputs.metricsText
|
|
590
|
+
});
|
|
591
|
+
for (const f of chunkBuffer(buf)) this.sendDirect({
|
|
592
|
+
type: "fleet.bundle.chunk",
|
|
593
|
+
requestId: req.requestId,
|
|
594
|
+
seq: f.seq,
|
|
595
|
+
isLast: f.isLast,
|
|
596
|
+
dataB64: f.dataB64
|
|
597
|
+
});
|
|
598
|
+
} catch (err) {
|
|
599
|
+
this.sendDirect({
|
|
600
|
+
type: "fleet.bundle.error",
|
|
601
|
+
requestId: req.requestId,
|
|
602
|
+
message: toErrorMessage(err)
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
/**
|
|
456
607
|
* Send a typed API request to the orchestrator and await the response.
|
|
457
608
|
*
|
|
458
609
|
* This is the transport layer for the agent private API. The SDK's typed
|
|
@@ -547,6 +698,58 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
547
698
|
});
|
|
548
699
|
}
|
|
549
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
|
+
/**
|
|
722
|
+
* Relay a step-level approval request to the orchestrator. Sends a
|
|
723
|
+
* `step.approval-request` WS message and resolves with the orchestrator's
|
|
724
|
+
* `step.approval-resolved` mapped onto the IPC response shape. No client-side
|
|
725
|
+
* timeout — the orchestrator owns the approval expiry and replies with an
|
|
726
|
+
* `expired` outcome when it lapses. Rejects only on disconnect (the relay
|
|
727
|
+
* caller treats a rejection as a fail-closed reject).
|
|
728
|
+
*/
|
|
729
|
+
async sendStepApproval(runId, jobId, request) {
|
|
730
|
+
const messageId = randomUUID();
|
|
731
|
+
return new Promise((resolve, reject) => {
|
|
732
|
+
this.pendingStepApprovals.set(messageId, {
|
|
733
|
+
resolve: (response) => resolve({
|
|
734
|
+
...response,
|
|
735
|
+
requestId: request.requestId
|
|
736
|
+
}),
|
|
737
|
+
reject
|
|
738
|
+
});
|
|
739
|
+
this.sendDirect({
|
|
740
|
+
type: "step.approval-request",
|
|
741
|
+
messageId,
|
|
742
|
+
runId,
|
|
743
|
+
jobId,
|
|
744
|
+
stepIndex: request.stepIndex,
|
|
745
|
+
stepName: request.stepName,
|
|
746
|
+
clauses: request.clauses,
|
|
747
|
+
reason: request.reason,
|
|
748
|
+
...request.timeoutSeconds !== void 0 && { timeoutSeconds: request.timeoutSeconds }
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
550
753
|
* Send a job.context message to the orchestrator.
|
|
551
754
|
*
|
|
552
755
|
* Conveys execution environment details (runtime, sandbox type, env vars)
|
|
@@ -684,6 +887,8 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
684
887
|
this.pendingUserCacheRequests.clear();
|
|
685
888
|
for (const [_id, pending] of this.pendingConcurrencyRequests) pending.reject(/* @__PURE__ */ new Error("WebSocket disconnected"));
|
|
686
889
|
this.pendingConcurrencyRequests.clear();
|
|
890
|
+
for (const [_id, pending] of this.pendingStepApprovals) pending.reject(/* @__PURE__ */ new Error("WebSocket disconnected"));
|
|
891
|
+
this.pendingStepApprovals.clear();
|
|
687
892
|
if (!this.intentionalDisconnect) this.scheduleReconnect();
|
|
688
893
|
});
|
|
689
894
|
this.ws.on("error", (err) => {
|
|
@@ -700,7 +905,7 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
700
905
|
return;
|
|
701
906
|
}
|
|
702
907
|
const rawMsg = raw;
|
|
703
|
-
if (rawMsg.type === "cache.upload.response") {
|
|
908
|
+
if (rawMsg.type === "cache.upload.response" || rawMsg.type === "provenance.upload.response") {
|
|
704
909
|
const pending = this.pendingUploadRequests.get(rawMsg.requestId);
|
|
705
910
|
if (pending) {
|
|
706
911
|
this.pendingUploadRequests.delete(rawMsg.requestId);
|
|
@@ -814,6 +1019,33 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
814
1019
|
}
|
|
815
1020
|
break;
|
|
816
1021
|
}
|
|
1022
|
+
case "step.approval-resolved": {
|
|
1023
|
+
logger$11.info("Step approval resolved", {
|
|
1024
|
+
requestId: msg.requestId,
|
|
1025
|
+
runId: msg.runId,
|
|
1026
|
+
jobId: msg.jobId,
|
|
1027
|
+
stepIndex: msg.stepIndex,
|
|
1028
|
+
outcome: msg.outcome
|
|
1029
|
+
});
|
|
1030
|
+
const pending = this.pendingStepApprovals.get(msg.requestId);
|
|
1031
|
+
if (pending) {
|
|
1032
|
+
this.pendingStepApprovals.delete(msg.requestId);
|
|
1033
|
+
pending.resolve({
|
|
1034
|
+
type: "approval.resolved",
|
|
1035
|
+
requestId: msg.requestId,
|
|
1036
|
+
outcome: msg.outcome,
|
|
1037
|
+
...msg.reason !== void 0 && { reason: msg.reason }
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
break;
|
|
1041
|
+
}
|
|
1042
|
+
case "fleet.logs.request":
|
|
1043
|
+
logger$11.info("Fleet log collection requested", {
|
|
1044
|
+
requestId: msg.requestId,
|
|
1045
|
+
logWindowHours: msg.logWindowHours
|
|
1046
|
+
});
|
|
1047
|
+
this.streamFleetBundle(msg);
|
|
1048
|
+
break;
|
|
817
1049
|
}
|
|
818
1050
|
return;
|
|
819
1051
|
}
|
|
@@ -953,6 +1185,10 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
953
1185
|
totalMemoryMb: Math.round(os.totalmem() / (1024 * 1024)),
|
|
954
1186
|
cpuCount: os.cpus().length,
|
|
955
1187
|
nodeVersion: process.versions.node,
|
|
1188
|
+
...(() => {
|
|
1189
|
+
const v = readAgentVersion();
|
|
1190
|
+
return v ? { version: v } : {};
|
|
1191
|
+
})(),
|
|
956
1192
|
...(() => {
|
|
957
1193
|
try {
|
|
958
1194
|
const info = os.userInfo();
|
|
@@ -1061,14 +1297,14 @@ var init_console_capture = __esmMin((() => {
|
|
|
1061
1297
|
init_console_capture();
|
|
1062
1298
|
function safe(name, fallback = "unknown") {
|
|
1063
1299
|
switch (name) {
|
|
1064
|
-
case "version": return "0.1.
|
|
1065
|
-
case "buildCommit": return "
|
|
1066
|
-
case "sdkVersion": return "0.1.
|
|
1067
|
-
case "sdkBundleHash": return "
|
|
1068
|
-
case "sharedVersion": return "0.1.
|
|
1069
|
-
case "sharedBundleHash": return "
|
|
1070
|
-
case "engineVersion": return "0.1.
|
|
1071
|
-
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";
|
|
1072
1308
|
default: return fallback;
|
|
1073
1309
|
}
|
|
1074
1310
|
}
|
|
@@ -1625,7 +1861,7 @@ async function buildAssetDigestFromResolvedPaths(workDir, resolvedPaths) {
|
|
|
1625
1861
|
for (const rel of resolvedPaths) {
|
|
1626
1862
|
const abs = path.join(workDir, rel);
|
|
1627
1863
|
try {
|
|
1628
|
-
const content = await fs.readFile(abs, "utf-8");
|
|
1864
|
+
const content = await fs$1.readFile(abs, "utf-8");
|
|
1629
1865
|
parts.push(`${rel}\n${content}`);
|
|
1630
1866
|
} catch {
|
|
1631
1867
|
parts.push(`${rel}\n`);
|
|
@@ -1650,7 +1886,7 @@ async function loadWorkflowSource(workDir, sourceFile, expectedContentHash, reso
|
|
|
1650
1886
|
ensureLoaderHookRegistered();
|
|
1651
1887
|
const filePath = path.join(workDir, sourceFile);
|
|
1652
1888
|
if (expectedContentHash) {
|
|
1653
|
-
const rawSource = await fs.readFile(filePath, "utf-8");
|
|
1889
|
+
const rawSource = await fs$1.readFile(filePath, "utf-8");
|
|
1654
1890
|
let assetDigest;
|
|
1655
1891
|
if (resolvedHashFiles?.length) assetDigest = await buildAssetDigestFromResolvedPaths(workDir, resolvedHashFiles);
|
|
1656
1892
|
const actualHash = computeContentHash(rawSource, assetDigest);
|
|
@@ -1752,8 +1988,8 @@ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event
|
|
|
1752
1988
|
}
|
|
1753
1989
|
var AGENT_SDK_VERSION, AGENT_SDK_BUNDLE_HASH, hookRegistered;
|
|
1754
1990
|
var init_workflow_loader = __esmMin((() => {
|
|
1755
|
-
AGENT_SDK_VERSION = "0.1.
|
|
1756
|
-
AGENT_SDK_BUNDLE_HASH = "
|
|
1991
|
+
AGENT_SDK_VERSION = "0.1.17";
|
|
1992
|
+
AGENT_SDK_BUNDLE_HASH = "df47ed5db86eaaa2de8394c0db08335f368e8d620a898cc409765f4545eb3972";
|
|
1757
1993
|
hookRegistered = false;
|
|
1758
1994
|
}));
|
|
1759
1995
|
//#endregion
|
|
@@ -1919,24 +2155,24 @@ function resolveOrchestratorUrl(url) {
|
|
|
1919
2155
|
* have nothing to race; the defensive `rm` covers re-runs.
|
|
1920
2156
|
*/
|
|
1921
2157
|
async function moveScratchIntoRepo(scratchDir, workDir) {
|
|
1922
|
-
for (const child of await fs.readdir(scratchDir)) if (child === ".kici") {
|
|
2158
|
+
for (const child of await fs$1.readdir(scratchDir)) if (child === ".kici") {
|
|
1923
2159
|
const kiciScratch = join(scratchDir, ".kici");
|
|
1924
|
-
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));
|
|
1925
2161
|
} else await moveInto(join(scratchDir, child), join(workDir, child));
|
|
1926
2162
|
}
|
|
1927
2163
|
/** Move `src` to `dest`, creating the parent and clearing any stale dest. */
|
|
1928
2164
|
async function moveInto(src, dest) {
|
|
1929
2165
|
await mkdir(dirname(dest), { recursive: true });
|
|
1930
|
-
await fs.rm(dest, {
|
|
2166
|
+
await fs$1.rm(dest, {
|
|
1931
2167
|
recursive: true,
|
|
1932
2168
|
force: true
|
|
1933
2169
|
});
|
|
1934
|
-
await fs.rename(src, dest);
|
|
2170
|
+
await fs$1.rename(src, dest);
|
|
1935
2171
|
}
|
|
1936
2172
|
/** Best-effort cleanup of a settled scratch dir; logs and continues on failure. */
|
|
1937
2173
|
async function cleanupScratch(scratchDir) {
|
|
1938
2174
|
try {
|
|
1939
|
-
await fs.rm(scratchDir, {
|
|
2175
|
+
await fs$1.rm(scratchDir, {
|
|
1940
2176
|
recursive: true,
|
|
1941
2177
|
force: true
|
|
1942
2178
|
});
|
|
@@ -1970,7 +2206,7 @@ async function restoreDeps(workDir, depsUrl, depsHash) {
|
|
|
1970
2206
|
const kiciDir = join(workDir, ".kici");
|
|
1971
2207
|
if (depsUrl.startsWith("file://")) {
|
|
1972
2208
|
const localPath = fileURLToPath(depsUrl);
|
|
1973
|
-
const data = await fs.readFile(localPath);
|
|
2209
|
+
const data = await fs$1.readFile(localPath);
|
|
1974
2210
|
if (depsHash) {
|
|
1975
2211
|
const actualHash = computeHash(data);
|
|
1976
2212
|
if (actualHash !== depsHash) throw new Error(`Dep tarball hash mismatch: expected ${depsHash}, got ${actualHash}`);
|
|
@@ -2129,7 +2365,7 @@ async function restoreSource(workDir, sourceTarUrl) {
|
|
|
2129
2365
|
let data;
|
|
2130
2366
|
if (sourceTarUrl.startsWith("file://")) {
|
|
2131
2367
|
const localPath = fileURLToPath(sourceTarUrl);
|
|
2132
|
-
data = await fs.readFile(localPath);
|
|
2368
|
+
data = await fs$1.readFile(localPath);
|
|
2133
2369
|
} else if (sourceTarUrl.startsWith("http://") || sourceTarUrl.startsWith("https://")) data = await downloadUrl(sourceTarUrl);
|
|
2134
2370
|
else throw new Error(`Unsupported source tarball URL scheme: ${sourceTarUrl}`);
|
|
2135
2371
|
await extractSourceTarball(data, workDir);
|
|
@@ -2198,6 +2434,28 @@ function findJobByName(workflow, jobName) {
|
|
|
2198
2434
|
async function evaluateDynamicFields(workflow, jobName, event, flags, timeoutMs = 6e4) {
|
|
2199
2435
|
const job = findJobByName(workflow, jobName);
|
|
2200
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
|
+
}
|
|
2201
2459
|
if (flags.dynamicEnvironment && typeof job.environment === "function") {
|
|
2202
2460
|
const value = await withTimeout(() => job.environment(event), timeoutMs, `dynamicEnvironment for job '${jobName}'`);
|
|
2203
2461
|
if (value !== void 0 && value !== null) result.environmentName = value;
|
|
@@ -2400,6 +2658,7 @@ var MatrixExpansionError, DYNAMIC_FIELD_TIMEOUT_MS;
|
|
|
2400
2658
|
var init_dynamic_job_serializer = __esmMin((() => {
|
|
2401
2659
|
init_timeout_util();
|
|
2402
2660
|
MatrixExpansionError = class MatrixExpansionError extends Error {
|
|
2661
|
+
jobName;
|
|
2403
2662
|
name = "MatrixExpansionError";
|
|
2404
2663
|
constructor(jobName, message) {
|
|
2405
2664
|
super(message);
|
|
@@ -2673,7 +2932,7 @@ function decryptBuffer(encrypted, aesKey) {
|
|
|
2673
2932
|
*/
|
|
2674
2933
|
async function applyOverlay(config) {
|
|
2675
2934
|
const { tarballUrl, cliPublicKey, orchestratorPrivateKey, repoDir } = config;
|
|
2676
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "kici-overlay-"));
|
|
2935
|
+
const tmpDir = await fs$1.mkdtemp(path.join(os.tmpdir(), "kici-overlay-"));
|
|
2677
2936
|
try {
|
|
2678
2937
|
logger$7.info("Downloading overlay tarball", { url: tarballUrl.replace(/\?.*$/, "?[redacted]") });
|
|
2679
2938
|
let encryptedData;
|
|
@@ -2687,7 +2946,7 @@ async function applyOverlay(config) {
|
|
|
2687
2946
|
const decryptedData = decryptBuffer(encryptedData, aesKey);
|
|
2688
2947
|
logger$7.info("Extracting overlay tarball", { size: decryptedData.length });
|
|
2689
2948
|
const extractDir = path.join(tmpDir, "extracted");
|
|
2690
|
-
await fs.mkdir(extractDir, { recursive: true });
|
|
2949
|
+
await fs$1.mkdir(extractDir, { recursive: true });
|
|
2691
2950
|
try {
|
|
2692
2951
|
const readable = Readable.from(decryptedData);
|
|
2693
2952
|
await new Promise((resolve, reject) => {
|
|
@@ -2702,7 +2961,7 @@ async function applyOverlay(config) {
|
|
|
2702
2961
|
const manifestPath = path.join(extractDir, ".kici-overlay-tmp", "manifest.json");
|
|
2703
2962
|
let manifestContent;
|
|
2704
2963
|
try {
|
|
2705
|
-
manifestContent = await fs.readFile(manifestPath, "utf-8");
|
|
2964
|
+
manifestContent = await fs$1.readFile(manifestPath, "utf-8");
|
|
2706
2965
|
} catch {
|
|
2707
2966
|
throw new Error("Overlay manifest not found: expected .kici-overlay-tmp/manifest.json in tarball");
|
|
2708
2967
|
}
|
|
@@ -2723,15 +2982,15 @@ async function applyOverlay(config) {
|
|
|
2723
2982
|
for (const file of checksumFiles) {
|
|
2724
2983
|
const srcPath = path.join(extractDir, file);
|
|
2725
2984
|
const destPath = path.join(repoDir, file);
|
|
2726
|
-
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
2727
|
-
await fs.copyFile(srcPath, destPath);
|
|
2985
|
+
await fs$1.mkdir(path.dirname(destPath), { recursive: true });
|
|
2986
|
+
await fs$1.copyFile(srcPath, destPath);
|
|
2728
2987
|
filesApplied++;
|
|
2729
2988
|
}
|
|
2730
2989
|
let filesDeleted = 0;
|
|
2731
2990
|
for (const file of manifest.deletions) {
|
|
2732
2991
|
const targetPath = path.join(repoDir, file);
|
|
2733
2992
|
try {
|
|
2734
|
-
await fs.unlink(targetPath);
|
|
2993
|
+
await fs$1.unlink(targetPath);
|
|
2735
2994
|
filesDeleted++;
|
|
2736
2995
|
} catch {
|
|
2737
2996
|
logger$7.debug("Deletion target not found, skipping", { file });
|
|
@@ -2747,7 +3006,7 @@ async function applyOverlay(config) {
|
|
|
2747
3006
|
verified: true
|
|
2748
3007
|
};
|
|
2749
3008
|
} finally {
|
|
2750
|
-
await fs.rm(tmpDir, {
|
|
3009
|
+
await fs$1.rm(tmpDir, {
|
|
2751
3010
|
recursive: true,
|
|
2752
3011
|
force: true
|
|
2753
3012
|
}).catch(() => {});
|
|
@@ -2896,6 +3155,11 @@ var init_npm_registry_config = __esmMin((() => {}));
|
|
|
2896
3155
|
* clones the whole repo, so an in-repo sibling is present), and resolves
|
|
2897
3156
|
* `file:`/`link:`/`portal:` against a path — allowed when that path stays
|
|
2898
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.)
|
|
2899
3163
|
*
|
|
2900
3164
|
* This module performs that classification so unresolvable specifiers fail
|
|
2901
3165
|
* fast with guidance rather than a cryptic install error.
|
|
@@ -2971,6 +3235,17 @@ function isInsideRepo(repoRoot, target) {
|
|
|
2971
3235
|
*/
|
|
2972
3236
|
async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
|
|
2973
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
|
+
}
|
|
2974
3249
|
const hasWorkspaceFile = await fileExists$1(join(repoRoot, "pnpm-workspace.yaml"));
|
|
2975
3250
|
const unresolvable = [];
|
|
2976
3251
|
for (const dep of deps) {
|
|
@@ -2986,6 +3261,7 @@ async function findUnresolvableDeps(deps, packageManager, kiciDir, repoRoot) {
|
|
|
2986
3261
|
function formatUnresolvableDepError(offenders, packageManager) {
|
|
2987
3262
|
const list = offenders.map((o) => `${o.name}: ${o.spec}`).join(", ");
|
|
2988
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.)`;
|
|
2989
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.`;
|
|
2990
3266
|
}
|
|
2991
3267
|
/**
|
|
@@ -3019,6 +3295,102 @@ var init_validate_kici_deps = __esmMin((() => {
|
|
|
3019
3295
|
];
|
|
3020
3296
|
}));
|
|
3021
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
|
|
3022
3394
|
//#region src/execution/dep-installer.ts
|
|
3023
3395
|
/**
|
|
3024
3396
|
* Inline dependency installation for graceful degradation.
|
|
@@ -3026,12 +3398,14 @@ var init_validate_kici_deps = __esmMin((() => {
|
|
|
3026
3398
|
* When the dep cache is unavailable or a download fails, the agent installs
|
|
3027
3399
|
* `.kici/` dependencies directly with the repository's package manager.
|
|
3028
3400
|
*
|
|
3029
|
-
* 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
|
|
3030
3402
|
* presence of `.kici/package.json` signals that deps should be installed. npm
|
|
3031
3403
|
* is the default and ships with every Node.js install; pnpm is used when the
|
|
3032
3404
|
* repo is a pnpm workspace so a `.kici/` member can resolve in-repo
|
|
3033
|
-
* `workspace:` siblings. yarn
|
|
3034
|
-
*
|
|
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.
|
|
3035
3409
|
*
|
|
3036
3410
|
* Security: the install runs with an isolated per-invocation cache/store
|
|
3037
3411
|
* directory to prevent cache poisoning across build jobs — a malicious
|
|
@@ -3075,7 +3449,6 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
3075
3449
|
dir: kiciDir
|
|
3076
3450
|
});
|
|
3077
3451
|
process.stderr.write(`[dep-installer:trace] starting install: pm=${packageManager}, cwd=${kiciDir}\n`);
|
|
3078
|
-
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.");
|
|
3079
3452
|
await assertResolvableDeps({
|
|
3080
3453
|
kiciDir,
|
|
3081
3454
|
repoRoot,
|
|
@@ -3095,6 +3468,11 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
3095
3468
|
hasPrivateRegistry,
|
|
3096
3469
|
registryConfig
|
|
3097
3470
|
});
|
|
3471
|
+
else if (packageManager === PackageManager.Yarn) await runYarnInstall({
|
|
3472
|
+
kiciDir,
|
|
3473
|
+
hasPrivateRegistry,
|
|
3474
|
+
registryConfig
|
|
3475
|
+
});
|
|
3098
3476
|
else await runNpmInstall({
|
|
3099
3477
|
kiciDir,
|
|
3100
3478
|
hasPrivateRegistry,
|
|
@@ -3109,6 +3487,7 @@ async function installDeps(kiciDir, opts = {}) {
|
|
|
3109
3487
|
await registryConfig.cleanup();
|
|
3110
3488
|
}
|
|
3111
3489
|
if (packageManager === PackageManager.Pnpm && await kiciHasLocalProtocolDeps(kiciDir)) await buildWorkspaceClosure(repoRoot);
|
|
3490
|
+
if (packageManager === PackageManager.Yarn) await buildYarnWorkspaceClosure(repoRoot, kiciDir);
|
|
3112
3491
|
const durationMs = Date.now() - startTime;
|
|
3113
3492
|
process.stderr.write(`[dep-installer:trace] install complete: ${durationMs}ms\n`);
|
|
3114
3493
|
logger$6.info("Deps installed inline", {
|
|
@@ -3196,6 +3575,101 @@ async function runPnpmInstall(args) {
|
|
|
3196
3575
|
}).catch(() => {});
|
|
3197
3576
|
}
|
|
3198
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
|
+
}
|
|
3199
3673
|
/**
|
|
3200
3674
|
* Build the in-repo dependency closure of the `.kici/` package so a
|
|
3201
3675
|
* `workspace:` sibling's build output exists before the workflow that imports
|
|
@@ -3239,7 +3713,10 @@ function describeExecError(e) {
|
|
|
3239
3713
|
/** Throw an actionable error when the repo needs pnpm but it is not installed. */
|
|
3240
3714
|
async function assertPnpmAvailable() {
|
|
3241
3715
|
try {
|
|
3242
|
-
await execFileAsync("pnpm", ["--version"], {
|
|
3716
|
+
await execFileAsync("pnpm", ["--version"], {
|
|
3717
|
+
timeout: 3e4,
|
|
3718
|
+
cwd: tmpdir()
|
|
3719
|
+
});
|
|
3243
3720
|
} catch (e) {
|
|
3244
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)})`);
|
|
3245
3722
|
}
|
|
@@ -3253,6 +3730,7 @@ var logger$6, execFileAsync, INSTALL_TIMEOUT_MS, INSTALL_MAX_BUFFER;
|
|
|
3253
3730
|
var init_dep_installer = __esmMin((() => {
|
|
3254
3731
|
init_npm_registry_config();
|
|
3255
3732
|
init_validate_kici_deps();
|
|
3733
|
+
init_workspace_siblings();
|
|
3256
3734
|
logger$6 = createLogger({ prefix: "dep-installer" });
|
|
3257
3735
|
execFileAsync = promisify(execFile);
|
|
3258
3736
|
INSTALL_TIMEOUT_MS = 6e5;
|
|
@@ -3268,13 +3746,18 @@ var init_dep_installer = __esmMin((() => {
|
|
|
3268
3746
|
* **repo-root-relative** (cwd = the clone root) so restore is a single layout
|
|
3269
3747
|
* regardless of package manager:
|
|
3270
3748
|
*
|
|
3271
|
-
* - npm
|
|
3749
|
+
* - npm: just `.kici/node_modules`.
|
|
3272
3750
|
* - pnpm: `.kici/node_modules` plus the repo-root `node_modules/.pnpm` virtual
|
|
3273
3751
|
* store and the in-repo `workspace:` sibling package directories `.kici`
|
|
3274
3752
|
* depends on (with their built output). pnpm lays `.kici/node_modules` out as
|
|
3275
3753
|
* symlinks into the root store and into sibling dirs that live outside
|
|
3276
3754
|
* `.kici/`, so packing `.kici/node_modules` alone would capture dangling
|
|
3277
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.
|
|
3278
3761
|
*
|
|
3279
3762
|
* Uses tar.gz (Node.js built-in zlib, no external binary) in portable mode to
|
|
3280
3763
|
* strip user/group info for cross-machine consistency; symlinks are preserved
|
|
@@ -3288,9 +3771,10 @@ var init_dep_installer = __esmMin((() => {
|
|
|
3288
3771
|
* @throws Error if `.kici/node_modules` does not exist.
|
|
3289
3772
|
*/
|
|
3290
3773
|
async function packNodeModules(kiciDir) {
|
|
3291
|
-
if (!existsSync(join(kiciDir, "node_modules"))) throw new Error(`node_modules not found at ${join(kiciDir, "node_modules")}`);
|
|
3292
3774
|
const workDir = dirname(kiciDir);
|
|
3293
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}`);
|
|
3294
3778
|
const entries = await closureEntries(workDir, kiciDir, packageManager);
|
|
3295
3779
|
logger$5.info("Packing dependency closure into tarball", {
|
|
3296
3780
|
dir: workDir,
|
|
@@ -3319,85 +3803,31 @@ async function packNodeModules(kiciDir) {
|
|
|
3319
3803
|
};
|
|
3320
3804
|
}
|
|
3321
3805
|
/**
|
|
3322
|
-
* Compute the repo-root-relative tar entries for the dependency closure. npm
|
|
3323
|
-
*
|
|
3324
|
-
*
|
|
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.
|
|
3325
3812
|
*/
|
|
3326
3813
|
async function closureEntries(workDir, kiciDir, packageManager) {
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
return entries;
|
|
3333
|
-
}
|
|
3334
|
-
/**
|
|
3335
|
-
* Walk `.kici`'s `node_modules` (and transitively each in-repo sibling's
|
|
3336
|
-
* `node_modules`) collecting the repo-relative directories of `workspace:`
|
|
3337
|
-
* siblings — package dirs that live inside the clone but outside `.kici/` and
|
|
3338
|
-
* outside the root `node_modules/` store. Returns each dir once.
|
|
3339
|
-
*/
|
|
3340
|
-
async function collectInRepoSiblings(workDir, kiciDir) {
|
|
3341
|
-
const repoRoot = resolve(workDir);
|
|
3342
|
-
const kiciResolved = resolve(kiciDir);
|
|
3343
|
-
const rootNodeModules = resolve(join(workDir, "node_modules"));
|
|
3344
|
-
const found = /* @__PURE__ */ new Set();
|
|
3345
|
-
const visited = /* @__PURE__ */ new Set();
|
|
3346
|
-
const queue = [join(kiciDir, "node_modules")];
|
|
3347
|
-
while (queue.length > 0) {
|
|
3348
|
-
const nmDir = queue.shift();
|
|
3349
|
-
const real = await realpath(nmDir).catch(() => null);
|
|
3350
|
-
if (!real || visited.has(real)) continue;
|
|
3351
|
-
visited.add(real);
|
|
3352
|
-
for (const target of await resolveNodeModulesLinks(nmDir)) {
|
|
3353
|
-
if (!isInside(repoRoot, target)) continue;
|
|
3354
|
-
if (isInside(kiciResolved, target) || isInside(rootNodeModules, target)) continue;
|
|
3355
|
-
const rel = relative(workDir, target);
|
|
3356
|
-
if (!found.has(rel)) {
|
|
3357
|
-
found.add(rel);
|
|
3358
|
-
queue.push(join(target, "node_modules"));
|
|
3359
|
-
}
|
|
3360
|
-
}
|
|
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;
|
|
3361
3819
|
}
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
for (const entry of await readdir(nmDir).catch(() => [])) {
|
|
3368
|
-
if (entry.startsWith(".")) continue;
|
|
3369
|
-
const entryPath = join(nmDir, entry);
|
|
3370
|
-
if (entry.startsWith("@")) {
|
|
3371
|
-
for (const scoped of await readdir(entryPath).catch(() => [])) {
|
|
3372
|
-
const target = await resolveIfSymlink(join(entryPath, scoped));
|
|
3373
|
-
if (target) targets.push(target);
|
|
3374
|
-
}
|
|
3375
|
-
continue;
|
|
3376
|
-
}
|
|
3377
|
-
const target = await resolveIfSymlink(entryPath);
|
|
3378
|
-
if (target) targets.push(target);
|
|
3379
|
-
}
|
|
3380
|
-
return targets;
|
|
3381
|
-
}
|
|
3382
|
-
/** Return the real path of `p` if it is a symlink, else null. */
|
|
3383
|
-
async function resolveIfSymlink(p) {
|
|
3384
|
-
try {
|
|
3385
|
-
if (!(await lstat(p)).isSymbolicLink()) return null;
|
|
3386
|
-
return await realpath(p);
|
|
3387
|
-
} catch {
|
|
3388
|
-
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;
|
|
3389
3825
|
}
|
|
3390
|
-
|
|
3391
|
-
/** Whether `target` is `root` itself or a path inside it. */
|
|
3392
|
-
function isInside(root, target) {
|
|
3393
|
-
const rel = relative(root, target);
|
|
3394
|
-
return rel === "" || !rel.startsWith("..") && !rel.startsWith(`..${sep}`) && !isAbsoluteRel(rel);
|
|
3395
|
-
}
|
|
3396
|
-
function isAbsoluteRel(rel) {
|
|
3397
|
-
return rel.length > 1 && rel[1] === ":";
|
|
3826
|
+
return [relative(workDir, join(kiciDir, "node_modules"))];
|
|
3398
3827
|
}
|
|
3399
3828
|
var logger$5;
|
|
3400
3829
|
var init_dep_packer = __esmMin((() => {
|
|
3830
|
+
init_workspace_siblings();
|
|
3401
3831
|
logger$5 = createLogger({ prefix: "dep-packer" });
|
|
3402
3832
|
}));
|
|
3403
3833
|
//#endregion
|
|
@@ -3541,6 +3971,8 @@ var init_secret_encryption = __esmMin((() => {
|
|
|
3541
3971
|
function buildRequest(dispatch, workDir) {
|
|
3542
3972
|
const jobConfig = dispatch.jobConfig;
|
|
3543
3973
|
return {
|
|
3974
|
+
runId: dispatch.runId,
|
|
3975
|
+
jobId: dispatch.jobId,
|
|
3544
3976
|
workDir,
|
|
3545
3977
|
repoUrl: dispatch.repoUrl,
|
|
3546
3978
|
ref: dispatch.ref,
|
|
@@ -3553,8 +3985,9 @@ function buildRequest(dispatch, workDir) {
|
|
|
3553
3985
|
depsUrl: dispatch.depsUrl,
|
|
3554
3986
|
depsHash: dispatch.depsHash,
|
|
3555
3987
|
workflowName: jobConfig.workflowName ?? "",
|
|
3556
|
-
jobName: jobConfig.name ?? "",
|
|
3988
|
+
jobName: jobConfig.baseJobName ?? jobConfig.name ?? "",
|
|
3557
3989
|
runsOn: jobConfig.runsOn ?? "",
|
|
3990
|
+
matrixValues: jobConfig.matrixValues,
|
|
3558
3991
|
secrets: dispatch.secrets,
|
|
3559
3992
|
namespacedSecrets: dispatch.namespacedSecrets,
|
|
3560
3993
|
sourceFile: jobConfig.source?.file,
|
|
@@ -3821,6 +4254,42 @@ function relayCacheRequest$1(msg, ctx) {
|
|
|
3821
4254
|
error: toErrorMessage(err)
|
|
3822
4255
|
}));
|
|
3823
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
|
+
}
|
|
4275
|
+
/** Relay `approval.request` and pipe the orchestrator's resolution (or a
|
|
4276
|
+
* fail-closed reject when the callback isn't wired or the relay throws) back
|
|
4277
|
+
* into the sandbox runner. */
|
|
4278
|
+
function relayApprovalRequest$1(msg, ctx) {
|
|
4279
|
+
if (!ctx.execOptions.onApprovalRequest) {
|
|
4280
|
+
safeSendToChild(ctx.child, {
|
|
4281
|
+
type: "approval.resolved",
|
|
4282
|
+
requestId: msg.requestId,
|
|
4283
|
+
error: "Approvals not available in this agent configuration"
|
|
4284
|
+
});
|
|
4285
|
+
return;
|
|
4286
|
+
}
|
|
4287
|
+
ctx.execOptions.onApprovalRequest(msg).then((response) => safeSendToChild(ctx.child, response), (err) => safeSendToChild(ctx.child, {
|
|
4288
|
+
type: "approval.resolved",
|
|
4289
|
+
requestId: msg.requestId,
|
|
4290
|
+
error: toErrorMessage(err)
|
|
4291
|
+
}));
|
|
4292
|
+
}
|
|
3824
4293
|
/** Resolve the result promise for `job.complete` IPC messages. Encrypts
|
|
3825
4294
|
* secret outputs (when a runPublicKey is available) and overrides status to
|
|
3826
4295
|
* `cancelled` if a cancel was already in flight. */
|
|
@@ -3893,6 +4362,12 @@ function relayChildIpcMessage(msg, dispatch, ctx) {
|
|
|
3893
4362
|
case "cache.request":
|
|
3894
4363
|
relayCacheRequest$1(msg, ctx);
|
|
3895
4364
|
return;
|
|
4365
|
+
case "provenance.request":
|
|
4366
|
+
relayProvenanceRequest$1(msg, ctx);
|
|
4367
|
+
return;
|
|
4368
|
+
case "approval.request":
|
|
4369
|
+
relayApprovalRequest$1(msg, ctx);
|
|
4370
|
+
return;
|
|
3896
4371
|
case "job.complete":
|
|
3897
4372
|
handleJobComplete(msg, dispatch, ctx);
|
|
3898
4373
|
return;
|
|
@@ -4314,6 +4789,58 @@ function relayCacheRequest(stream, options, cacheMsg) {
|
|
|
4314
4789
|
}));
|
|
4315
4790
|
}
|
|
4316
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
|
+
/**
|
|
4818
|
+
* Relay approval.request from the container runner to the orchestrator via
|
|
4819
|
+
* options.onApprovalRequest, then write the resolution back through `stream`.
|
|
4820
|
+
* If the agent doesn't expose an approval relay (or it throws), write a
|
|
4821
|
+
* fail-closed reject so the runner doesn't hang.
|
|
4822
|
+
*/
|
|
4823
|
+
function relayApprovalRequest(stream, options, approvalMsg) {
|
|
4824
|
+
const writeResponse = (response) => {
|
|
4825
|
+
try {
|
|
4826
|
+
stream.write(JSON.stringify(response) + "\n");
|
|
4827
|
+
} catch {}
|
|
4828
|
+
};
|
|
4829
|
+
if (!options.onApprovalRequest) {
|
|
4830
|
+
writeResponse({
|
|
4831
|
+
type: "approval.resolved",
|
|
4832
|
+
requestId: approvalMsg.requestId,
|
|
4833
|
+
error: "Approvals not available in this agent configuration"
|
|
4834
|
+
});
|
|
4835
|
+
return;
|
|
4836
|
+
}
|
|
4837
|
+
options.onApprovalRequest(approvalMsg).then((response) => writeResponse(response), (err) => writeResponse({
|
|
4838
|
+
type: "approval.resolved",
|
|
4839
|
+
requestId: approvalMsg.requestId,
|
|
4840
|
+
error: toErrorMessage(err)
|
|
4841
|
+
}));
|
|
4842
|
+
}
|
|
4843
|
+
/**
|
|
4317
4844
|
* Apply a job.complete message to the mutable runner state: capture status,
|
|
4318
4845
|
* merge any bulk-reported step results, propagate plain outputs, and encrypt
|
|
4319
4846
|
* secret outputs if a run public key is available.
|
|
@@ -4570,6 +5097,12 @@ var init_container_sandbox = __esmMin((() => {
|
|
|
4570
5097
|
case "cache.request":
|
|
4571
5098
|
relayCacheRequest(stream, options, msg);
|
|
4572
5099
|
return false;
|
|
5100
|
+
case "provenance.request":
|
|
5101
|
+
relayProvenanceRequest(stream, options, msg);
|
|
5102
|
+
return false;
|
|
5103
|
+
case "approval.request":
|
|
5104
|
+
relayApprovalRequest(stream, options, msg);
|
|
5105
|
+
return false;
|
|
4573
5106
|
case "job.complete":
|
|
4574
5107
|
applyJobComplete(msg, stepResults, state, options);
|
|
4575
5108
|
return true;
|
|
@@ -4666,7 +5199,7 @@ var job_runner_exports = /* @__PURE__ */ __exportAll({ JobRunner: () => JobRunne
|
|
|
4666
5199
|
*/
|
|
4667
5200
|
async function fileExists(p) {
|
|
4668
5201
|
try {
|
|
4669
|
-
await fs.access(p);
|
|
5202
|
+
await fs$1.access(p);
|
|
4670
5203
|
return true;
|
|
4671
5204
|
} catch {
|
|
4672
5205
|
return false;
|
|
@@ -4740,6 +5273,8 @@ var init_job_runner = __esmMin((() => {
|
|
|
4740
5273
|
_sendConcurrencyReport;
|
|
4741
5274
|
_sendApiRequest;
|
|
4742
5275
|
_requestUserCache;
|
|
5276
|
+
_relayProvenance;
|
|
5277
|
+
_sendStepApproval;
|
|
4743
5278
|
/** Tracks running jobs for concurrency and cancellation */
|
|
4744
5279
|
activeJobs = /* @__PURE__ */ new Map();
|
|
4745
5280
|
/** Active sandbox for the current job (used for abort). */
|
|
@@ -4758,6 +5293,8 @@ var init_job_runner = __esmMin((() => {
|
|
|
4758
5293
|
this._sendConcurrencyReport = deps.sendConcurrencyReport;
|
|
4759
5294
|
this._sendApiRequest = deps.sendApiRequest;
|
|
4760
5295
|
this._requestUserCache = deps.requestUserCache;
|
|
5296
|
+
this._relayProvenance = deps.relayProvenance;
|
|
5297
|
+
this._sendStepApproval = deps.sendStepApproval;
|
|
4761
5298
|
}
|
|
4762
5299
|
/**
|
|
4763
5300
|
* Execute a dispatched job through its full lifecycle.
|
|
@@ -4768,11 +5305,11 @@ var init_job_runner = __esmMin((() => {
|
|
|
4768
5305
|
async execute(dispatch) {
|
|
4769
5306
|
const { runId: _runId, jobId, jobConfig: _jobConfig } = dispatch;
|
|
4770
5307
|
const abortController = new AbortController();
|
|
4771
|
-
const workDir = await fs.mkdtemp(join(tmpdir(), "kici-"));
|
|
5308
|
+
const workDir = await fs$1.mkdtemp(join(tmpdir(), "kici-"));
|
|
4772
5309
|
const completionPromise = this.runJob(dispatch, workDir, abortController).finally(async () => {
|
|
4773
5310
|
this.activeJobs.delete(jobId);
|
|
4774
5311
|
this.activeSandbox = null;
|
|
4775
|
-
await fs.rm(workDir, {
|
|
5312
|
+
await fs$1.rm(workDir, {
|
|
4776
5313
|
recursive: true,
|
|
4777
5314
|
force: true
|
|
4778
5315
|
}).catch(() => {});
|
|
@@ -4997,6 +5534,8 @@ var init_job_runner = __esmMin((() => {
|
|
|
4997
5534
|
},
|
|
4998
5535
|
onApiRequest: this._sendApiRequest ? async (method, params) => this._sendApiRequest(method, params) : void 0,
|
|
4999
5536
|
onCacheRequest: this._requestUserCache ? async (request) => this._requestUserCache(jobId, request) : void 0,
|
|
5537
|
+
onProvenanceRequest: this._relayProvenance ? async (request) => this._relayProvenance(jobId, request) : void 0,
|
|
5538
|
+
onApprovalRequest: this._sendStepApproval ? async (request) => this._sendStepApproval(dispatch.runId, dispatch.jobId, request) : void 0,
|
|
5000
5539
|
onSecretMount: (event) => {
|
|
5001
5540
|
this.emitRunEvent(runId, "step.secret_mount", {
|
|
5002
5541
|
jobId,
|
|
@@ -5315,11 +5854,12 @@ var init_job_runner = __esmMin((() => {
|
|
|
5315
5854
|
const initResult = await runCaptured(initSink, async () => {
|
|
5316
5855
|
const { module } = await loadWorkflowSource(workDir, config.source, config.contentHash, config.resolvedHashFiles);
|
|
5317
5856
|
const workflow = extractWorkflow(module, config.workflowName);
|
|
5318
|
-
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})`);
|
|
5319
5858
|
return evaluateDynamicFields(workflow, config.targetJobName, config.event, {
|
|
5320
5859
|
dynamicEnvironment: config.dynamicEnvironment,
|
|
5321
5860
|
dynamicEnv: config.dynamicEnv,
|
|
5322
|
-
dynamicConcurrencyGroup: config.dynamicConcurrencyGroup
|
|
5861
|
+
dynamicConcurrencyGroup: config.dynamicConcurrencyGroup,
|
|
5862
|
+
dynamicMatrix: config.dynamicMatrix ?? false
|
|
5323
5863
|
}, config.timeoutMs);
|
|
5324
5864
|
});
|
|
5325
5865
|
logger$2.info("Init job completed successfully", {
|
|
@@ -5669,14 +6209,14 @@ var init_job_runner = __esmMin((() => {
|
|
|
5669
6209
|
*/
|
|
5670
6210
|
init_console_capture();
|
|
5671
6211
|
init_npm_resolver();
|
|
5672
|
-
const AGENT_VERSION = "0.1.
|
|
5673
|
-
const BUILD_COMMIT = "
|
|
5674
|
-
const SDK_VERSION = "0.1.
|
|
5675
|
-
const SDK_BUNDLE_HASH = "
|
|
5676
|
-
const SHARED_VERSION = "0.1.
|
|
5677
|
-
const SHARED_BUNDLE_HASH = "
|
|
5678
|
-
const ENGINE_VERSION = "0.1.
|
|
5679
|
-
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";
|
|
5680
6220
|
initTelemetry({
|
|
5681
6221
|
serviceName: "kici-agent",
|
|
5682
6222
|
otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|
|
@@ -5685,6 +6225,26 @@ const { connectionStatus, jobsActive, jobsTotal } = await Promise.resolve().then
|
|
|
5685
6225
|
const { JobRunner } = await Promise.resolve().then(() => (init_job_runner(), job_runner_exports));
|
|
5686
6226
|
setServiceName("agent");
|
|
5687
6227
|
const logger$1 = createLogger({ prefix: "agent" });
|
|
6228
|
+
/**
|
|
6229
|
+
* Serialize the agent's current Prometheus metrics to text. The OTel
|
|
6230
|
+
* PrometheusExporter exposes no direct serialize method, so the metrics are
|
|
6231
|
+
* piped through its request handler with a mock ServerResponse. Returns an
|
|
6232
|
+
* empty string when no exporter is configured. Shared by the /metrics health
|
|
6233
|
+
* route and the fleet mini-bundle.
|
|
6234
|
+
*/
|
|
6235
|
+
async function serializeAgentMetrics() {
|
|
6236
|
+
const exporter = getPrometheusExporter();
|
|
6237
|
+
if (!exporter) return "";
|
|
6238
|
+
return new Promise((resolve) => {
|
|
6239
|
+
exporter.getMetricsRequestHandler({}, {
|
|
6240
|
+
statusCode: 200,
|
|
6241
|
+
setHeader: () => {},
|
|
6242
|
+
end: (data) => {
|
|
6243
|
+
resolve(typeof data === "string" ? data : data ? data.toString() : "");
|
|
6244
|
+
}
|
|
6245
|
+
});
|
|
6246
|
+
});
|
|
6247
|
+
}
|
|
5688
6248
|
installConsoleCapture();
|
|
5689
6249
|
await guardStartup(logger$1, async () => {
|
|
5690
6250
|
const config = loadConfig();
|
|
@@ -5736,7 +6296,9 @@ await guardStartup(logger$1, async () => {
|
|
|
5736
6296
|
sendRunEvent: (runId, eventType, opts) => client.sendRunEvent(runId, eventType, opts),
|
|
5737
6297
|
sendConcurrencyReport: (runId, jobId, group) => client.sendConcurrencyReport(runId, jobId, group),
|
|
5738
6298
|
sendApiRequest: (method, params) => client.sendApiRequest(method, params ?? {}),
|
|
5739
|
-
requestUserCache: (jobId, request) => client.requestUserCache(jobId, request)
|
|
6299
|
+
requestUserCache: (jobId, request) => client.requestUserCache(jobId, request),
|
|
6300
|
+
relayProvenance: (jobId, request) => client.relayProvenance(jobId, request),
|
|
6301
|
+
sendStepApproval: (runId, jobId, request) => client.sendStepApproval(runId, jobId, request)
|
|
5740
6302
|
});
|
|
5741
6303
|
/** Build and send an agent.status message with dynamic OS metadata. */
|
|
5742
6304
|
function sendAgentStatus() {
|
|
@@ -5752,6 +6314,11 @@ await guardStartup(logger$1, async () => {
|
|
|
5752
6314
|
}
|
|
5753
6315
|
client = new OrchestratorClient({
|
|
5754
6316
|
...agentClientConnectionOptions(config),
|
|
6317
|
+
getFleetBundleInputs: async () => ({
|
|
6318
|
+
config,
|
|
6319
|
+
logDir: process.env.KICI_LOG_DIR,
|
|
6320
|
+
metricsText: await serializeAgentMetrics()
|
|
6321
|
+
}),
|
|
5755
6322
|
onJobDispatch: (dispatch) => {
|
|
5756
6323
|
const reqId = dispatch.requestId ?? randomUUID();
|
|
5757
6324
|
requestContext.run({
|
|
@@ -5899,25 +6466,10 @@ await guardStartup(logger$1, async () => {
|
|
|
5899
6466
|
metricsReporter.start();
|
|
5900
6467
|
const app = new Hono();
|
|
5901
6468
|
const healthRoutes = createHealthRoutes$1({
|
|
5902
|
-
getMetrics: async () => {
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
body: ""
|
|
5907
|
-
};
|
|
5908
|
-
return new Promise((resolve) => {
|
|
5909
|
-
exporter.getMetricsRequestHandler({}, {
|
|
5910
|
-
statusCode: 200,
|
|
5911
|
-
setHeader: () => {},
|
|
5912
|
-
end: (data) => {
|
|
5913
|
-
resolve({
|
|
5914
|
-
contentType: "text/plain",
|
|
5915
|
-
body: typeof data === "string" ? data : data ? data.toString() : ""
|
|
5916
|
-
});
|
|
5917
|
-
}
|
|
5918
|
-
});
|
|
5919
|
-
});
|
|
5920
|
-
},
|
|
6469
|
+
getMetrics: async () => ({
|
|
6470
|
+
contentType: "text/plain",
|
|
6471
|
+
body: await serializeAgentMetrics()
|
|
6472
|
+
}),
|
|
5921
6473
|
getStatus: () => ({
|
|
5922
6474
|
agentId: config.agentId,
|
|
5923
6475
|
connected: client.state === "registered",
|