@kici-dev/agent 0.1.21 → 0.1.23
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/bootstrap/api-intercept.d.ts +20 -0
- package/dist/bootstrap/ensure-init-runner.d.ts +39 -0
- package/dist/bootstrap/pre-boot-send.d.ts +25 -0
- package/dist/bootstrap/reach.d.ts +17 -0
- package/dist/bootstrap/ssh-exec.d.ts +58 -0
- package/dist/config.d.ts +2 -0
- package/dist/execution/job-runner.d.ts +22 -0
- package/dist/execution/reboot.d.ts +26 -0
- package/dist/execution/rule-evaluator.d.ts +4 -2
- package/dist/execution/sandbox/ipc-protocol.d.ts +24 -1
- package/dist/execution/sandbox/step-loop.d.ts +32 -6
- package/dist/execution/sandbox/workflow-runner.d.ts +17 -0
- package/dist/index.js +3 -1
- package/dist/server.js +486 -54
- package/dist/workflow-runner.js +234 -52
- package/package.json +5 -5
- package/sbom.spdx.json +62 -62
package/dist/server.js
CHANGED
|
@@ -12,7 +12,7 @@ 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, applyIncludeExclude, deriveOsArchLabels, expandMatrix, heartbeatSchema, hostLabel, mergeAutoLabels, orchestratorToAgentMessageSchema, parseHostPropertyAssignments, 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, parseHostPropertyAssignments, resolveRoleLabels, resolveWhenToRunOn, 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 * as fs$2 from "node:fs";
|
|
@@ -102,7 +102,8 @@ const envDef = defineEnv({
|
|
|
102
102
|
scalerPendingDispatchTimeoutMs: z.coerce.number().default(6e4),
|
|
103
103
|
executionMode: ExecutionMode.optional(),
|
|
104
104
|
otelExporterOtlpEndpoint: z.string().optional(),
|
|
105
|
-
concurrencyWaitTimeoutMs: z.coerce.number().int().min(1e3).default(36e5)
|
|
105
|
+
concurrencyWaitTimeoutMs: z.coerce.number().int().min(1e3).default(36e5),
|
|
106
|
+
isOrchestratorHost: z.string().optional().transform((v) => v === "true")
|
|
106
107
|
}),
|
|
107
108
|
envMap: {
|
|
108
109
|
orchestratorUrl: "KICI_ORCHESTRATOR_URL",
|
|
@@ -113,6 +114,7 @@ const envDef = defineEnv({
|
|
|
113
114
|
port: "KICI_PORT",
|
|
114
115
|
logLevel: "KICI_LOG_LEVEL",
|
|
115
116
|
agentToken: "KICI_AGENT_TOKEN",
|
|
117
|
+
isOrchestratorHost: "KICI_AGENT_IS_ORCHESTRATOR_HOST",
|
|
116
118
|
githubToken: "KICI_GITHUB_TOKEN",
|
|
117
119
|
maxLogSizeBytes: "KICI_MAX_LOG_SIZE_BYTES",
|
|
118
120
|
defaultStepTimeoutMs: "KICI_DEFAULT_STEP_TIMEOUT_MS",
|
|
@@ -286,7 +288,7 @@ var LogBuffer = class extends RingBuffer {
|
|
|
286
288
|
};
|
|
287
289
|
//#endregion
|
|
288
290
|
//#region src/ws/orchestrator-client.ts
|
|
289
|
-
const logger$
|
|
291
|
+
const logger$12 = createLogger({ prefix: "orchestrator-client" });
|
|
290
292
|
/**
|
|
291
293
|
* WebSocket client that connects the agent to the customer orchestrator.
|
|
292
294
|
*
|
|
@@ -403,7 +405,7 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
403
405
|
*/
|
|
404
406
|
connect() {
|
|
405
407
|
if (this._state !== "disconnected") {
|
|
406
|
-
logger$
|
|
408
|
+
logger$12.warn("connect() called while not disconnected", { state: this._state });
|
|
407
409
|
return;
|
|
408
410
|
}
|
|
409
411
|
this.intentionalDisconnect = false;
|
|
@@ -754,7 +756,8 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
754
756
|
stepName: request.stepName,
|
|
755
757
|
clauses: request.clauses,
|
|
756
758
|
reason: request.reason,
|
|
757
|
-
...request.timeoutSeconds !== void 0 && { timeoutSeconds: request.timeoutSeconds }
|
|
759
|
+
...request.timeoutSeconds !== void 0 && { timeoutSeconds: request.timeoutSeconds },
|
|
760
|
+
...request.payload !== void 0 && { payload: request.payload }
|
|
758
761
|
});
|
|
759
762
|
});
|
|
760
763
|
}
|
|
@@ -840,7 +843,7 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
840
843
|
}
|
|
841
844
|
});
|
|
842
845
|
} catch (err) {
|
|
843
|
-
logger$
|
|
846
|
+
logger$12.error("Failed to create WebSocket", { error: toErrorMessage(err) });
|
|
844
847
|
this._state = "disconnected";
|
|
845
848
|
this.scheduleReconnect();
|
|
846
849
|
return;
|
|
@@ -848,7 +851,7 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
848
851
|
this.ws.on("open", () => {
|
|
849
852
|
if (this.token) {
|
|
850
853
|
this._state = "authenticating";
|
|
851
|
-
logger$
|
|
854
|
+
logger$12.info("Connected to orchestrator, sending auth.request", {
|
|
852
855
|
url: this.url,
|
|
853
856
|
agentId: this.agentId
|
|
854
857
|
});
|
|
@@ -859,7 +862,7 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
859
862
|
}));
|
|
860
863
|
} else {
|
|
861
864
|
this._state = "registering";
|
|
862
|
-
logger$
|
|
865
|
+
logger$12.info("Connected to orchestrator, sending agent.register (no token)", {
|
|
863
866
|
url: this.url,
|
|
864
867
|
agentId: this.agentId
|
|
865
868
|
});
|
|
@@ -870,12 +873,12 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
870
873
|
this.handleMessage(data);
|
|
871
874
|
});
|
|
872
875
|
this.ws.on("close", (code, reason) => {
|
|
873
|
-
logger$
|
|
876
|
+
logger$12.info("Orchestrator connection closed", {
|
|
874
877
|
code,
|
|
875
878
|
reason: reason.toString()
|
|
876
879
|
});
|
|
877
880
|
if (code === WS_CLOSE_AGENT_AUTH_FAILED) {
|
|
878
|
-
logger$
|
|
881
|
+
logger$12.error("Orchestrator closed with auth-failed code -- token is invalid or revoked. NOT retrying.", {
|
|
879
882
|
code,
|
|
880
883
|
reason: reason.toString()
|
|
881
884
|
});
|
|
@@ -901,7 +904,7 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
901
904
|
if (!this.intentionalDisconnect) this.scheduleReconnect();
|
|
902
905
|
});
|
|
903
906
|
this.ws.on("error", (err) => {
|
|
904
|
-
logger$
|
|
907
|
+
logger$12.error(`Orchestrator WebSocket error: ${err.message}`);
|
|
905
908
|
if (this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.close();
|
|
906
909
|
});
|
|
907
910
|
}
|
|
@@ -910,7 +913,7 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
910
913
|
try {
|
|
911
914
|
raw = JSON.parse(data.toString());
|
|
912
915
|
} catch {
|
|
913
|
-
logger$
|
|
916
|
+
logger$12.warn("Malformed JSON received from orchestrator");
|
|
914
917
|
return;
|
|
915
918
|
}
|
|
916
919
|
const rawMsg = raw;
|
|
@@ -968,13 +971,13 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
968
971
|
switch (msg.type) {
|
|
969
972
|
case "auth.success":
|
|
970
973
|
if (this._state === "authenticating") {
|
|
971
|
-
logger$
|
|
974
|
+
logger$12.info("Authentication successful, sending agent.register", { connectionId: msg.connectionId });
|
|
972
975
|
this._state = "registering";
|
|
973
976
|
this.sendAgentRegister();
|
|
974
977
|
}
|
|
975
978
|
break;
|
|
976
979
|
case "auth.failure":
|
|
977
|
-
logger$
|
|
980
|
+
logger$12.error("Authentication FAILED -- token is invalid or expired. NOT retrying.", { reason: msg.reason });
|
|
978
981
|
this.authFailed = true;
|
|
979
982
|
this.intentionalDisconnect = true;
|
|
980
983
|
if (this.ws) {
|
|
@@ -984,7 +987,7 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
984
987
|
this._state = "disconnected";
|
|
985
988
|
break;
|
|
986
989
|
case "register.ack":
|
|
987
|
-
logger$
|
|
990
|
+
logger$12.info("Registration acknowledged by orchestrator", {
|
|
988
991
|
agentId: msg.agentId,
|
|
989
992
|
labels: msg.labels,
|
|
990
993
|
scalerManaged: msg.scalerManaged,
|
|
@@ -999,14 +1002,14 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
999
1002
|
this.sendConfigAck(msg.agentId);
|
|
1000
1003
|
break;
|
|
1001
1004
|
case "job.dispatch":
|
|
1002
|
-
logger$
|
|
1005
|
+
logger$12.info("Job dispatch received", {
|
|
1003
1006
|
runId: msg.runId,
|
|
1004
1007
|
jobId: msg.jobId
|
|
1005
1008
|
});
|
|
1006
1009
|
this.onJobDispatch(msg);
|
|
1007
1010
|
break;
|
|
1008
1011
|
case "job.cancel":
|
|
1009
|
-
logger$
|
|
1012
|
+
logger$12.info("Job cancel received", {
|
|
1010
1013
|
runId: msg.runId,
|
|
1011
1014
|
jobId: msg.jobId,
|
|
1012
1015
|
reason: msg.reason
|
|
@@ -1014,7 +1017,7 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
1014
1017
|
this.onJobCancel(msg);
|
|
1015
1018
|
break;
|
|
1016
1019
|
case "job.concurrency.ack": {
|
|
1017
|
-
logger$
|
|
1020
|
+
logger$12.info("Concurrency ack received", {
|
|
1018
1021
|
requestId: msg.requestId,
|
|
1019
1022
|
action: msg.action
|
|
1020
1023
|
});
|
|
@@ -1029,7 +1032,7 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
1029
1032
|
break;
|
|
1030
1033
|
}
|
|
1031
1034
|
case "step.approval-resolved": {
|
|
1032
|
-
logger$
|
|
1035
|
+
logger$12.info("Step approval resolved", {
|
|
1033
1036
|
requestId: msg.requestId,
|
|
1034
1037
|
runId: msg.runId,
|
|
1035
1038
|
jobId: msg.jobId,
|
|
@@ -1049,7 +1052,7 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
1049
1052
|
break;
|
|
1050
1053
|
}
|
|
1051
1054
|
case "fleet.logs.request":
|
|
1052
|
-
logger$
|
|
1055
|
+
logger$12.info("Fleet log collection requested", {
|
|
1053
1056
|
requestId: msg.requestId,
|
|
1054
1057
|
logWindowHours: msg.logWindowHours
|
|
1055
1058
|
});
|
|
@@ -1059,7 +1062,7 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
1059
1062
|
return;
|
|
1060
1063
|
}
|
|
1061
1064
|
if (heartbeatSchema.safeParse(raw).success) return;
|
|
1062
|
-
logger$
|
|
1065
|
+
logger$12.warn("Invalid message from orchestrator", { errors: parsed.error.issues });
|
|
1063
1066
|
}
|
|
1064
1067
|
flushBuffer() {
|
|
1065
1068
|
const events = this.eventBuffer.flush();
|
|
@@ -1073,11 +1076,11 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
1073
1076
|
}
|
|
1074
1077
|
this.disconnectedAt = null;
|
|
1075
1078
|
if (events.length > 0) {
|
|
1076
|
-
logger$
|
|
1079
|
+
logger$12.info("Flushing event buffer", { count: events.length });
|
|
1077
1080
|
for (const msg of events) this.sendDirect(msg);
|
|
1078
1081
|
}
|
|
1079
1082
|
if (logLines.length > 0) {
|
|
1080
|
-
logger$
|
|
1083
|
+
logger$12.info("Flushing log buffer", { count: logLines.length });
|
|
1081
1084
|
for (let i = 0; i < logLines.length; i += OrchestratorClient.LOG_BATCH_SIZE) {
|
|
1082
1085
|
const batch = logLines.slice(i, i + OrchestratorClient.LOG_BATCH_SIZE);
|
|
1083
1086
|
this.sendAgentLogMessage(batch);
|
|
@@ -1130,14 +1133,14 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
1130
1133
|
*/
|
|
1131
1134
|
blockMmdsAccess() {
|
|
1132
1135
|
if (process.getuid?.() !== 0) {
|
|
1133
|
-
logger$
|
|
1136
|
+
logger$12.info("MMDS iptables block skipped (non-root) — network isolation handled by orchestrator");
|
|
1134
1137
|
return;
|
|
1135
1138
|
}
|
|
1136
1139
|
try {
|
|
1137
1140
|
execSync("iptables -A OUTPUT -d 169.254.169.254 -j DROP", { timeout: 5e3 });
|
|
1138
|
-
logger$
|
|
1141
|
+
logger$12.info("MMDS access blocked via iptables");
|
|
1139
1142
|
} catch (err) {
|
|
1140
|
-
logger$
|
|
1143
|
+
logger$12.warn("Failed to block MMDS access via iptables", { error: toErrorMessage(err) });
|
|
1141
1144
|
}
|
|
1142
1145
|
}
|
|
1143
1146
|
/**
|
|
@@ -1151,7 +1154,7 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
1151
1154
|
messageId: `config-ack-${agentId}-${Date.now()}`,
|
|
1152
1155
|
agentId
|
|
1153
1156
|
}));
|
|
1154
|
-
logger$
|
|
1157
|
+
logger$12.info("Config ACK sent to orchestrator", { agentId });
|
|
1155
1158
|
}
|
|
1156
1159
|
}
|
|
1157
1160
|
startHeartbeat() {
|
|
@@ -1217,12 +1220,12 @@ var OrchestratorClient = class OrchestratorClient {
|
|
|
1217
1220
|
scheduleReconnect() {
|
|
1218
1221
|
this.cancelReconnect();
|
|
1219
1222
|
if (this.authFailed) {
|
|
1220
|
-
logger$
|
|
1223
|
+
logger$12.error("Not reconnecting: authentication permanently failed");
|
|
1221
1224
|
return;
|
|
1222
1225
|
}
|
|
1223
1226
|
const delay = this.getReconnectDelay();
|
|
1224
1227
|
this.reconnectAttempts++;
|
|
1225
|
-
logger$
|
|
1228
|
+
logger$12.info("Scheduling reconnect", {
|
|
1226
1229
|
attempt: this.reconnectAttempts,
|
|
1227
1230
|
delayMs: Math.round(delay)
|
|
1228
1231
|
});
|
|
@@ -1307,14 +1310,14 @@ var init_console_capture = __esmMin((() => {
|
|
|
1307
1310
|
init_console_capture();
|
|
1308
1311
|
function safe(name, fallback = "unknown") {
|
|
1309
1312
|
switch (name) {
|
|
1310
|
-
case "version": return "0.1.
|
|
1311
|
-
case "buildCommit": return "
|
|
1312
|
-
case "sdkVersion": return "0.1.
|
|
1313
|
-
case "sdkBundleHash": return "
|
|
1314
|
-
case "sharedVersion": return "0.1.
|
|
1313
|
+
case "version": return "0.1.23";
|
|
1314
|
+
case "buildCommit": return "4465935bb";
|
|
1315
|
+
case "sdkVersion": return "0.1.23";
|
|
1316
|
+
case "sdkBundleHash": return "3a83610d2d122b9f9b0f924b225a47050d0b35f00a8f3256a3e15f3ff3bf7ddc";
|
|
1317
|
+
case "sharedVersion": return "0.1.23";
|
|
1315
1318
|
case "sharedBundleHash": return "991385c024392c395d3eb8a68946ef8ed3fcba96f2652f21c3164a54eafa1b1b";
|
|
1316
|
-
case "engineVersion": return "0.1.
|
|
1317
|
-
case "engineBundleHash": return "
|
|
1319
|
+
case "engineVersion": return "0.1.23";
|
|
1320
|
+
case "engineBundleHash": return "4a0566c709180a6a8744ff3281640e596b8e661ddceb81327cd37d232d9768d5";
|
|
1318
1321
|
default: return fallback;
|
|
1319
1322
|
}
|
|
1320
1323
|
}
|
|
@@ -1571,6 +1574,83 @@ async function gcStaleAgentTmpDirs(base = tmpdir()) {
|
|
|
1571
1574
|
})];
|
|
1572
1575
|
}
|
|
1573
1576
|
//#endregion
|
|
1577
|
+
//#region src/execution/reboot.ts
|
|
1578
|
+
/**
|
|
1579
|
+
* Cross-OS host reboot for the workflow-level `restartHost()` step.
|
|
1580
|
+
*
|
|
1581
|
+
* The agent runs ON the host it executes jobs for, so a `restartHost()` step
|
|
1582
|
+
* reboots that host. `rebootCommandFor` is the pure OS→command mapping (kept
|
|
1583
|
+
* pure for unit-testing); `issueReboot` spawns it detached with a tiny grace so
|
|
1584
|
+
* the job's final flush completes before the box goes down.
|
|
1585
|
+
*
|
|
1586
|
+
* Rebooting needs host privilege (root/admin). If the primitive is denied, the
|
|
1587
|
+
* spawn fails and the caller clears the orchestrator's reboot-pending flag and
|
|
1588
|
+
* surfaces the error — the deadline sweep is the backstop.
|
|
1589
|
+
*/
|
|
1590
|
+
const logger$11 = createLogger({ prefix: "reboot" });
|
|
1591
|
+
/** The OS reboot primitive for a Node platform string. */
|
|
1592
|
+
function rebootCommandFor(platform) {
|
|
1593
|
+
switch (platform) {
|
|
1594
|
+
case "darwin": return {
|
|
1595
|
+
cmd: "shutdown",
|
|
1596
|
+
args: ["-r", "now"]
|
|
1597
|
+
};
|
|
1598
|
+
case "win32": return {
|
|
1599
|
+
cmd: "shutdown",
|
|
1600
|
+
args: [
|
|
1601
|
+
"/r",
|
|
1602
|
+
"/t",
|
|
1603
|
+
"0"
|
|
1604
|
+
]
|
|
1605
|
+
};
|
|
1606
|
+
default: return {
|
|
1607
|
+
cmd: "systemctl",
|
|
1608
|
+
args: ["reboot"]
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
/**
|
|
1613
|
+
* Issue the OS reboot detached. Resolves once the child has been spawned (the
|
|
1614
|
+
* box is going down; there is nothing to await). Rejects synchronously if the
|
|
1615
|
+
* spawn itself fails (e.g. the binary is missing). A privilege denial usually
|
|
1616
|
+
* surfaces as a non-zero exit AFTER spawn — logged, not thrown, because by then
|
|
1617
|
+
* the box may already be on its way down.
|
|
1618
|
+
*/
|
|
1619
|
+
function issueReboot(platform = process.platform) {
|
|
1620
|
+
const { cmd, args } = rebootCommandFor(platform);
|
|
1621
|
+
return new Promise((resolve, reject) => {
|
|
1622
|
+
try {
|
|
1623
|
+
const child = spawn(cmd, args, {
|
|
1624
|
+
detached: true,
|
|
1625
|
+
stdio: "ignore"
|
|
1626
|
+
});
|
|
1627
|
+
child.on("error", (err) => {
|
|
1628
|
+
logger$11.error("Reboot command failed to spawn", {
|
|
1629
|
+
cmd,
|
|
1630
|
+
args,
|
|
1631
|
+
error: String(err)
|
|
1632
|
+
});
|
|
1633
|
+
reject(err);
|
|
1634
|
+
});
|
|
1635
|
+
child.on("exit", (code) => {
|
|
1636
|
+
if (code !== 0 && code !== null) logger$11.warn("Reboot command exited non-zero (privilege denied?)", {
|
|
1637
|
+
cmd,
|
|
1638
|
+
args,
|
|
1639
|
+
code
|
|
1640
|
+
});
|
|
1641
|
+
});
|
|
1642
|
+
child.unref();
|
|
1643
|
+
logger$11.info("Issued host reboot", {
|
|
1644
|
+
cmd,
|
|
1645
|
+
args
|
|
1646
|
+
});
|
|
1647
|
+
setImmediate(resolve);
|
|
1648
|
+
} catch (err) {
|
|
1649
|
+
reject(err);
|
|
1650
|
+
}
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
//#endregion
|
|
1574
1654
|
//#region src/metrics/prometheus.ts
|
|
1575
1655
|
var prometheus_exports = /* @__PURE__ */ __exportAll({
|
|
1576
1656
|
cloneDurationSeconds: () => cloneDurationSeconds,
|
|
@@ -2000,8 +2080,8 @@ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event
|
|
|
2000
2080
|
}
|
|
2001
2081
|
var AGENT_SDK_VERSION, AGENT_SDK_BUNDLE_HASH, hookRegistered;
|
|
2002
2082
|
var init_workflow_loader = __esmMin((() => {
|
|
2003
|
-
AGENT_SDK_VERSION = "0.1.
|
|
2004
|
-
AGENT_SDK_BUNDLE_HASH = "
|
|
2083
|
+
AGENT_SDK_VERSION = "0.1.23";
|
|
2084
|
+
AGENT_SDK_BUNDLE_HASH = "3a83610d2d122b9f9b0f924b225a47050d0b35f00a8f3256a3e15f3ff3bf7ddc";
|
|
2005
2085
|
hookRegistered = false;
|
|
2006
2086
|
}));
|
|
2007
2087
|
//#endregion
|
|
@@ -2486,6 +2566,274 @@ var init_init_runner = __esmMin((() => {
|
|
|
2486
2566
|
init_timeout_util();
|
|
2487
2567
|
}));
|
|
2488
2568
|
//#endregion
|
|
2569
|
+
//#region src/bootstrap/ssh-exec.ts
|
|
2570
|
+
/**
|
|
2571
|
+
* Agent-side SSH helper for bootstrap bring-up.
|
|
2572
|
+
*
|
|
2573
|
+
* Lifts the discipline from `infra/prod/hw/ssh.sh` + the deploy-prod
|
|
2574
|
+
* `runOnBox` helper into the agent: the bring-up private key is piped into an
|
|
2575
|
+
* **ephemeral ssh-agent** (`ssh-add -` reads it from stdin) and the agent is
|
|
2576
|
+
* torn down in a `finally`, so the key never lands on disk and never enters a
|
|
2577
|
+
* long-lived process environment. The agent only holds the key for the
|
|
2578
|
+
* lifetime of the one `ssh` / `scp` invocation.
|
|
2579
|
+
*
|
|
2580
|
+
* `sshExec` runs a remote command; `sshPush` ships local bytes to a remote
|
|
2581
|
+
* path (`ssh 'cat > path'`). Both accept a resolved private key string
|
|
2582
|
+
* (supplied by the caller — the orchestrator resolves the scoped secret and
|
|
2583
|
+
* hands the key down). `sshExec` supports `{ stdin, port, hostKeyMode }` so it
|
|
2584
|
+
* can also drive a pre-boot dropbear / initramfs prompt (a forced-command
|
|
2585
|
+
* endpoint such as `cryptroot-unlock` on port 2222, which accepts the unlock
|
|
2586
|
+
* input on stdin and uses a transient host key distinct from the OS sshd key).
|
|
2587
|
+
*/
|
|
2588
|
+
/** Common `-o` flags every bootstrap SSH connection carries. */
|
|
2589
|
+
function baseSshOptions(hostKeyMode) {
|
|
2590
|
+
return [
|
|
2591
|
+
"-o",
|
|
2592
|
+
`StrictHostKeyChecking=${HOST_KEY_FLAG[hostKeyMode]}`,
|
|
2593
|
+
"-o",
|
|
2594
|
+
"ConnectTimeout=10",
|
|
2595
|
+
"-o",
|
|
2596
|
+
"BatchMode=yes"
|
|
2597
|
+
];
|
|
2598
|
+
}
|
|
2599
|
+
/** Resolve the `user@address` target + port from reach metadata. */
|
|
2600
|
+
function resolveTarget(reach, portOverride) {
|
|
2601
|
+
if (!reach.address) throw new Error(`host ${reach.agentId} has no SSH reach address declared`);
|
|
2602
|
+
const user = reach.sshUser ?? SSH_USER_DEFAULT;
|
|
2603
|
+
const port = portOverride ?? reach.sshPort ?? SSH_PORT_DEFAULT;
|
|
2604
|
+
return {
|
|
2605
|
+
dest: `${user}@${reach.address}`,
|
|
2606
|
+
port
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
/**
|
|
2610
|
+
* Run a command on the target over SSH using an ephemeral, in-memory key.
|
|
2611
|
+
*
|
|
2612
|
+
* The key is loaded into a per-call ssh-agent (never written to disk) and the
|
|
2613
|
+
* agent is killed in `finally`. The remote command's exit code / stdout /
|
|
2614
|
+
* stderr are returned verbatim — a non-zero exit is reported, not swallowed
|
|
2615
|
+
* (a pre-boot unlock legitimately drops the session, so the caller decides
|
|
2616
|
+
* what a "success" looks like).
|
|
2617
|
+
*/
|
|
2618
|
+
async function sshExec(reach, privateKey, command, opts = {}, deps = {}) {
|
|
2619
|
+
const spawnFn = deps.spawnFn ?? defaultSpawn;
|
|
2620
|
+
const hostKeyMode = opts.hostKeyMode ?? "accept-new";
|
|
2621
|
+
const { dest, port } = resolveTarget(reach, opts.port);
|
|
2622
|
+
return withEphemeralAgent(privateKey, spawnFn, async (env) => {
|
|
2623
|
+
return spawnFn("ssh", [
|
|
2624
|
+
...baseSshOptions(hostKeyMode),
|
|
2625
|
+
"-p",
|
|
2626
|
+
String(port),
|
|
2627
|
+
dest,
|
|
2628
|
+
command
|
|
2629
|
+
], {
|
|
2630
|
+
env,
|
|
2631
|
+
stdin: opts.stdin
|
|
2632
|
+
});
|
|
2633
|
+
});
|
|
2634
|
+
}
|
|
2635
|
+
/**
|
|
2636
|
+
* Ship local bytes to a remote path over SSH (`ssh 'cat > path'`), using the
|
|
2637
|
+
* same ephemeral-key discipline. Throws on a non-zero exit (a push must
|
|
2638
|
+
* succeed end-to-end, unlike a pre-boot unlock).
|
|
2639
|
+
*/
|
|
2640
|
+
async function sshPush(reach, privateKey, localBytes, remotePath, opts = {}, deps = {}) {
|
|
2641
|
+
const spawnFn = deps.spawnFn ?? defaultSpawn;
|
|
2642
|
+
const hostKeyMode = opts.hostKeyMode ?? "accept-new";
|
|
2643
|
+
const { dest, port } = resolveTarget(reach, opts.port);
|
|
2644
|
+
const result = await withEphemeralAgent(privateKey, spawnFn, async (env) => {
|
|
2645
|
+
return spawnFn("ssh", [
|
|
2646
|
+
...baseSshOptions(hostKeyMode),
|
|
2647
|
+
"-p",
|
|
2648
|
+
String(port),
|
|
2649
|
+
dest,
|
|
2650
|
+
`cat > '${remotePath.replace(/'/g, `'\\''`)}'`
|
|
2651
|
+
], {
|
|
2652
|
+
env,
|
|
2653
|
+
stdin: localBytes
|
|
2654
|
+
});
|
|
2655
|
+
});
|
|
2656
|
+
if (result.exitCode !== 0) throw new Error(`sshPush(${reach.agentId}:${remotePath}): exit ${result.exitCode}${result.stderr ? `\n${result.stderr}` : ""}`);
|
|
2657
|
+
}
|
|
2658
|
+
/**
|
|
2659
|
+
* Start a per-call ephemeral ssh-agent, load the key via stdin (never a file),
|
|
2660
|
+
* run `body` with `SSH_AUTH_SOCK` in env, and kill the agent in `finally`.
|
|
2661
|
+
*/
|
|
2662
|
+
async function withEphemeralAgent(privateKey, spawnFn, body) {
|
|
2663
|
+
const baseEnv = { ...process.env };
|
|
2664
|
+
const start = await spawnFn("ssh-agent", ["-s"], { env: baseEnv });
|
|
2665
|
+
if (start.exitCode !== 0) throw new Error(`ssh-agent start failed: exit ${start.exitCode}\n${start.stderr}`);
|
|
2666
|
+
const sock = parseAgentSocket(start.stdout);
|
|
2667
|
+
const pid = parseAgentPid(start.stdout);
|
|
2668
|
+
const agentEnv = {
|
|
2669
|
+
...baseEnv,
|
|
2670
|
+
SSH_AUTH_SOCK: sock,
|
|
2671
|
+
...pid ? { SSH_AGENT_PID: pid } : {},
|
|
2672
|
+
SSH_ASKPASS: "/bin/false",
|
|
2673
|
+
DISPLAY: ""
|
|
2674
|
+
};
|
|
2675
|
+
try {
|
|
2676
|
+
const add = await spawnFn("ssh-add", ["-"], {
|
|
2677
|
+
env: agentEnv,
|
|
2678
|
+
stdin: privateKey.endsWith("\n") ? privateKey : `${privateKey}\n`
|
|
2679
|
+
});
|
|
2680
|
+
if (add.exitCode !== 0) throw new Error(`ssh-add failed: exit ${add.exitCode}\n${add.stderr}`);
|
|
2681
|
+
return await body(agentEnv);
|
|
2682
|
+
} finally {
|
|
2683
|
+
await spawnFn("ssh-agent", ["-k"], { env: agentEnv }).catch(() => {});
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
/** Extract `SSH_AUTH_SOCK=<path>;` from `ssh-agent -s` output. */
|
|
2687
|
+
function parseAgentSocket(out) {
|
|
2688
|
+
const m = out.match(/SSH_AUTH_SOCK=([^;\n]+)/);
|
|
2689
|
+
if (!m) throw new Error("ssh-agent -s did not emit SSH_AUTH_SOCK");
|
|
2690
|
+
return m[1];
|
|
2691
|
+
}
|
|
2692
|
+
/** Extract `SSH_AGENT_PID=<n>;` from `ssh-agent -s` output (best-effort). */
|
|
2693
|
+
function parseAgentPid(out) {
|
|
2694
|
+
return out.match(/SSH_AGENT_PID=([^;\n]+)/)?.[1];
|
|
2695
|
+
}
|
|
2696
|
+
var defaultSpawn, SSH_USER_DEFAULT, SSH_PORT_DEFAULT, HOST_KEY_FLAG;
|
|
2697
|
+
var init_ssh_exec = __esmMin((() => {
|
|
2698
|
+
defaultSpawn = (command, args, opts) => new Promise((resolve, reject) => {
|
|
2699
|
+
const child = spawn(command, args, {
|
|
2700
|
+
env: opts.env,
|
|
2701
|
+
stdio: [
|
|
2702
|
+
opts.stdin !== void 0 ? "pipe" : "ignore",
|
|
2703
|
+
"pipe",
|
|
2704
|
+
"pipe"
|
|
2705
|
+
]
|
|
2706
|
+
});
|
|
2707
|
+
let stdout = "";
|
|
2708
|
+
let stderr = "";
|
|
2709
|
+
child.stdout?.on("data", (b) => {
|
|
2710
|
+
stdout += b.toString();
|
|
2711
|
+
});
|
|
2712
|
+
child.stderr?.on("data", (b) => {
|
|
2713
|
+
stderr += b.toString();
|
|
2714
|
+
});
|
|
2715
|
+
child.on("error", reject);
|
|
2716
|
+
child.on("close", (code) => resolve({
|
|
2717
|
+
exitCode: code ?? -1,
|
|
2718
|
+
stdout,
|
|
2719
|
+
stderr
|
|
2720
|
+
}));
|
|
2721
|
+
if (opts.stdin !== void 0) child.stdin?.end(opts.stdin);
|
|
2722
|
+
});
|
|
2723
|
+
SSH_USER_DEFAULT = "root";
|
|
2724
|
+
SSH_PORT_DEFAULT = 22;
|
|
2725
|
+
HOST_KEY_FLAG = {
|
|
2726
|
+
"accept-new": "accept-new",
|
|
2727
|
+
strict: "yes"
|
|
2728
|
+
};
|
|
2729
|
+
}));
|
|
2730
|
+
//#endregion
|
|
2731
|
+
//#region src/bootstrap/ensure-init-runner.ts
|
|
2732
|
+
/**
|
|
2733
|
+
* Build the launcher script that starts the init-runner on the target with its
|
|
2734
|
+
* bootstrap env. Detached (`setsid … &`) so the SSH session can return while
|
|
2735
|
+
* the agent keeps running and dials the orchestrator.
|
|
2736
|
+
*/
|
|
2737
|
+
function buildLauncher(material, agentCommand) {
|
|
2738
|
+
return [
|
|
2739
|
+
"#!/usr/bin/env bash",
|
|
2740
|
+
"set -euo pipefail",
|
|
2741
|
+
`setsid env ${[
|
|
2742
|
+
`KICI_AGENT_TOKEN=${shQuote(material.bootstrapToken)}`,
|
|
2743
|
+
`KICI_AGENT_ID=${shQuote(material.targetAgentId)}`,
|
|
2744
|
+
`KICI_ORCHESTRATOR_URL=${shQuote(material.orchestratorUrl)}`,
|
|
2745
|
+
`KICI_LABELS=${shQuote(material.labels.join(","))}`,
|
|
2746
|
+
"KICI_EXECUTION_MODE=bare-metal",
|
|
2747
|
+
"KICI_PORT=0"
|
|
2748
|
+
].join(" \\\n ")} \\`,
|
|
2749
|
+
` ${agentCommand} >/tmp/kici-init-runner.log 2>&1 &`,
|
|
2750
|
+
"echo \"init-runner started pid=$!\""
|
|
2751
|
+
].join("\n");
|
|
2752
|
+
}
|
|
2753
|
+
/** Single-quote a value for safe embedding in the launcher's env assignment. */
|
|
2754
|
+
function shQuote(v) {
|
|
2755
|
+
return `'${v.replace(/'/g, `'\\''`)}'`;
|
|
2756
|
+
}
|
|
2757
|
+
/**
|
|
2758
|
+
* Bring up a temporary init-runner on `targetAgentId`. Returns `{ broughtUp }`:
|
|
2759
|
+
* false when the target already had a live agent (the orchestrator no-op'd),
|
|
2760
|
+
* true when this call dropped + started the init-runner.
|
|
2761
|
+
*/
|
|
2762
|
+
async function ensureInitRunner(transport, targetAgentId, deps = {}) {
|
|
2763
|
+
const material = await transport("kici.ensureInitRunner", { targetAgentId });
|
|
2764
|
+
if (!material.broughtUp) return { broughtUp: false };
|
|
2765
|
+
const { reach, privateKey, bootstrapToken, orchestratorUrl, labels } = material;
|
|
2766
|
+
if (!reach || !privateKey || !bootstrapToken || !orchestratorUrl || !labels) throw new Error(`orchestrator returned incomplete bring-up material for ${targetAgentId}`);
|
|
2767
|
+
const agentCommand = deps.agentCommand ?? DEFAULT_AGENT_COMMAND;
|
|
2768
|
+
await sshPush(reach, privateKey, buildLauncher({
|
|
2769
|
+
bootstrapToken,
|
|
2770
|
+
targetAgentId,
|
|
2771
|
+
orchestratorUrl,
|
|
2772
|
+
labels
|
|
2773
|
+
}, agentCommand), LAUNCHER_REMOTE_PATH, {}, deps);
|
|
2774
|
+
const run = await sshExec(reach, privateKey, `chmod 0700 ${LAUNCHER_REMOTE_PATH} && ${LAUNCHER_REMOTE_PATH}`, {}, deps);
|
|
2775
|
+
if (run.exitCode !== 0) throw new Error(`init-runner launch on ${targetAgentId} failed: exit ${run.exitCode}${run.stderr ? `\n${run.stderr}` : ""}`);
|
|
2776
|
+
return { broughtUp: true };
|
|
2777
|
+
}
|
|
2778
|
+
var DEFAULT_AGENT_COMMAND, LAUNCHER_REMOTE_PATH;
|
|
2779
|
+
var init_ensure_init_runner = __esmMin((() => {
|
|
2780
|
+
init_ssh_exec();
|
|
2781
|
+
DEFAULT_AGENT_COMMAND = "kici-agent";
|
|
2782
|
+
LAUNCHER_REMOTE_PATH = "/tmp/kici-init-runner.sh";
|
|
2783
|
+
}));
|
|
2784
|
+
//#endregion
|
|
2785
|
+
//#region src/bootstrap/pre-boot-send.ts
|
|
2786
|
+
/**
|
|
2787
|
+
* Ship a pre-boot input to the target's dropbear/initramfs SSH channel. The
|
|
2788
|
+
* input plaintext is resolved server-side and never logged. Resolves once the
|
|
2789
|
+
* send completes (the SSH session legitimately drops as the box boots).
|
|
2790
|
+
*/
|
|
2791
|
+
async function preBootSend(transport, targetAgentId, opts, deps = {}) {
|
|
2792
|
+
const material = await transport("kici.preBootSend", {
|
|
2793
|
+
targetAgentId,
|
|
2794
|
+
inputSecret: opts.inputSecret,
|
|
2795
|
+
...opts.port !== void 0 ? { port: opts.port } : {},
|
|
2796
|
+
...opts.command !== void 0 ? { command: opts.command } : {}
|
|
2797
|
+
});
|
|
2798
|
+
await sshExec(material.reach, material.privateKey, material.command, {
|
|
2799
|
+
stdin: material.input,
|
|
2800
|
+
port: material.port,
|
|
2801
|
+
hostKeyMode: "accept-new"
|
|
2802
|
+
}, deps);
|
|
2803
|
+
}
|
|
2804
|
+
var init_pre_boot_send = __esmMin((() => {
|
|
2805
|
+
init_ssh_exec();
|
|
2806
|
+
}));
|
|
2807
|
+
//#endregion
|
|
2808
|
+
//#region src/bootstrap/api-intercept.ts
|
|
2809
|
+
/**
|
|
2810
|
+
* Wrap the orchestrator API transport so the two bootstrap methods are handled
|
|
2811
|
+
* in-process (SSH transport here; privileged resolve relayed to the
|
|
2812
|
+
* orchestrator). `relay` is the raw orchestrator transport (the WS
|
|
2813
|
+
* `sendApiRequest`).
|
|
2814
|
+
*/
|
|
2815
|
+
function withBootstrapInterception(relay, deps = {}) {
|
|
2816
|
+
return async (method, params = {}) => {
|
|
2817
|
+
if (method === ENSURE_INIT_RUNNER) return ensureInitRunner(relay, String(params.targetAgentId ?? ""), deps);
|
|
2818
|
+
if (method === PRE_BOOT_SEND) {
|
|
2819
|
+
await preBootSend(relay, String(params.targetAgentId ?? ""), {
|
|
2820
|
+
inputSecret: String(params.inputSecret ?? ""),
|
|
2821
|
+
...typeof params.port === "number" ? { port: params.port } : {},
|
|
2822
|
+
...typeof params.command === "string" ? { command: params.command } : {}
|
|
2823
|
+
}, deps);
|
|
2824
|
+
return;
|
|
2825
|
+
}
|
|
2826
|
+
return relay(method, params);
|
|
2827
|
+
};
|
|
2828
|
+
}
|
|
2829
|
+
var ENSURE_INIT_RUNNER, PRE_BOOT_SEND;
|
|
2830
|
+
var init_api_intercept = __esmMin((() => {
|
|
2831
|
+
init_ensure_init_runner();
|
|
2832
|
+
init_pre_boot_send();
|
|
2833
|
+
ENSURE_INIT_RUNNER = "kici.ensureInitRunner";
|
|
2834
|
+
PRE_BOOT_SEND = "kici.preBootSend";
|
|
2835
|
+
}));
|
|
2836
|
+
//#endregion
|
|
2489
2837
|
//#region src/execution/dynamic-job-serializer.ts
|
|
2490
2838
|
/**
|
|
2491
2839
|
* Convert an array of SDK Job objects into LockJob format for the orchestrator.
|
|
@@ -2550,7 +2898,8 @@ async function serializeJob(job, generatedNames, ctx, staticNames, allowedGroups
|
|
|
2550
2898
|
* Validates against generatedNames union staticNames union allowedGroups.
|
|
2551
2899
|
*
|
|
2552
2900
|
* Returns the lock file representation: strings for concrete refs,
|
|
2553
|
-
* NeedsEntry for { name,
|
|
2901
|
+
* NeedsEntry for { name, when }, NeedsGroupEntry for group refs (each `when`
|
|
2902
|
+
* normalized to a runOn status-set).
|
|
2554
2903
|
*/
|
|
2555
2904
|
function resolveNeeds(needs, generatedNames, staticNames, allowedGroups) {
|
|
2556
2905
|
if (!needs || needs.length === 0) return [];
|
|
@@ -2565,7 +2914,7 @@ function resolveNeeds(needs, generatedNames, staticNames, allowedGroups) {
|
|
|
2565
2914
|
if (!allowedGroups.has(groupRef.group)) throw new Error(`Dynamic group '${groupRef.group}' not found in workflow (available groups: ${[...allowedGroups].join(", ") || "none"})`);
|
|
2566
2915
|
return {
|
|
2567
2916
|
group: groupRef.group,
|
|
2568
|
-
|
|
2917
|
+
runOn: resolveWhenToRunOn(groupRef.when)
|
|
2569
2918
|
};
|
|
2570
2919
|
}
|
|
2571
2920
|
if (typeof dep === "object" && dep !== null && "group" in dep) {
|
|
@@ -2573,7 +2922,7 @@ function resolveNeeds(needs, generatedNames, staticNames, allowedGroups) {
|
|
|
2573
2922
|
if (!allowedGroups.has(groupDep.group)) throw new Error(`Dynamic group '${groupDep.group}' not found in workflow (available groups: ${[...allowedGroups].join(", ") || "none"})`);
|
|
2574
2923
|
return {
|
|
2575
2924
|
group: groupDep.group,
|
|
2576
|
-
|
|
2925
|
+
runOn: resolveWhenToRunOn(groupDep.when)
|
|
2577
2926
|
};
|
|
2578
2927
|
}
|
|
2579
2928
|
if (typeof dep === "object" && dep !== null && "name" in dep && !("steps" in dep)) {
|
|
@@ -2581,7 +2930,7 @@ function resolveNeeds(needs, generatedNames, staticNames, allowedGroups) {
|
|
|
2581
2930
|
if (!allNames.has(namedDep.name)) throw new Error(`Job dependency '${namedDep.name}' not found in workflow jobs (checked: ${generatedNames.size} generated, ${staticNames.size} static)`);
|
|
2582
2931
|
return {
|
|
2583
2932
|
name: namedDep.name,
|
|
2584
|
-
|
|
2933
|
+
runOn: resolveWhenToRunOn(namedDep.when)
|
|
2585
2934
|
};
|
|
2586
2935
|
}
|
|
2587
2936
|
if (isDynamicJobFn(dep)) throw new Error("Job dependency cannot be a DynamicJobFn");
|
|
@@ -2606,7 +2955,13 @@ function serializeSteps(steps) {
|
|
|
2606
2955
|
name: step.name || `step-${index}`,
|
|
2607
2956
|
hasOutputs: !!step.outputs,
|
|
2608
2957
|
...step.continueOnError ? { continueOnError: true } : {},
|
|
2609
|
-
...step.timeout ? { timeout: step.timeout } : {}
|
|
2958
|
+
...step.timeout ? { timeout: step.timeout } : {},
|
|
2959
|
+
...step.retry ? { retry: {
|
|
2960
|
+
maxAttempts: step.retry.maxAttempts,
|
|
2961
|
+
delayMs: step.retry.delayMs,
|
|
2962
|
+
backoff: step.retry.backoff,
|
|
2963
|
+
maxDelayMs: step.retry.maxDelayMs
|
|
2964
|
+
} } : {}
|
|
2610
2965
|
};
|
|
2611
2966
|
});
|
|
2612
2967
|
}
|
|
@@ -4182,6 +4537,9 @@ function buildRequest(dispatch, workDir) {
|
|
|
4182
4537
|
matrixValues: jobConfig.matrixValues,
|
|
4183
4538
|
host: jobConfig.host,
|
|
4184
4539
|
agent: jobConfig.agent,
|
|
4540
|
+
dispatchInputs: jobConfig.dispatchInputs,
|
|
4541
|
+
fanoutIndex: jobConfig.fanoutIndex,
|
|
4542
|
+
fanoutTotal: jobConfig.fanoutTotal,
|
|
4185
4543
|
secrets: dispatch.secrets,
|
|
4186
4544
|
namespacedSecrets: dispatch.namespacedSecrets,
|
|
4187
4545
|
sourceFile: jobConfig.source?.file,
|
|
@@ -4212,6 +4570,8 @@ function buildRequest(dispatch, workDir) {
|
|
|
4212
4570
|
concurrencyEvaluationTimeoutMs: jobConfig.concurrencyEvaluationTimeoutMs,
|
|
4213
4571
|
branch: dispatch.ref,
|
|
4214
4572
|
upstreamJobOutputs: dispatch.upstreamJobOutputs,
|
|
4573
|
+
upstreamJobStatuses: dispatch.upstreamJobStatuses,
|
|
4574
|
+
jobNeeds: jobConfig.needs,
|
|
4215
4575
|
npmRegistries: dispatch.npmRegistries,
|
|
4216
4576
|
installEnvSecrets: dispatch.installEnvSecrets,
|
|
4217
4577
|
jobIdShort: dispatch.jobId.slice(0, 8),
|
|
@@ -5394,7 +5754,10 @@ var init_sandbox = __esmMin((() => {
|
|
|
5394
5754
|
}));
|
|
5395
5755
|
//#endregion
|
|
5396
5756
|
//#region src/execution/job-runner.ts
|
|
5397
|
-
var job_runner_exports = /* @__PURE__ */ __exportAll({
|
|
5757
|
+
var job_runner_exports = /* @__PURE__ */ __exportAll({
|
|
5758
|
+
JobRunner: () => JobRunner$1,
|
|
5759
|
+
buildEvalNeedsContext: () => buildEvalNeedsContext
|
|
5760
|
+
});
|
|
5398
5761
|
/**
|
|
5399
5762
|
* Check if a file exists at the given path.
|
|
5400
5763
|
*/
|
|
@@ -5457,6 +5820,8 @@ var init_job_runner = __esmMin((() => {
|
|
|
5457
5820
|
init_source_packer();
|
|
5458
5821
|
init_source_restore();
|
|
5459
5822
|
init_init_runner();
|
|
5823
|
+
init_api_intercept();
|
|
5824
|
+
init_ensure_init_runner();
|
|
5460
5825
|
init_timeout_util();
|
|
5461
5826
|
init_dynamic_job_serializer();
|
|
5462
5827
|
init_log_streamer();
|
|
@@ -5569,6 +5934,10 @@ var init_job_runner = __esmMin((() => {
|
|
|
5569
5934
|
await this.handleDynamicJobFn(dispatch, workDir, abortController);
|
|
5570
5935
|
return true;
|
|
5571
5936
|
}
|
|
5937
|
+
if (jobConfig.bringupOnly === true) {
|
|
5938
|
+
await this.handleBringupJob(dispatch);
|
|
5939
|
+
return true;
|
|
5940
|
+
}
|
|
5572
5941
|
if (jobConfig.buildOnly === true) {
|
|
5573
5942
|
if (jobConfig.fullRepo) {
|
|
5574
5943
|
logger$2.warn("Build job received for fullRepo run -- skipping (should not happen)", {
|
|
@@ -5741,7 +6110,7 @@ var init_job_runner = __esmMin((() => {
|
|
|
5741
6110
|
reason: ack.reason
|
|
5742
6111
|
};
|
|
5743
6112
|
},
|
|
5744
|
-
onApiRequest: this._sendApiRequest ? async (method, params) => this._sendApiRequest(method, params) : void 0,
|
|
6113
|
+
onApiRequest: this._sendApiRequest ? withBootstrapInterception(async (method, params) => this._sendApiRequest(method, params)) : void 0,
|
|
5745
6114
|
onCacheRequest: this._requestUserCache ? async (request) => this._requestUserCache(jobId, request) : void 0,
|
|
5746
6115
|
onProvenanceRequest: this._relayProvenance ? async (request) => this._relayProvenance(jobId, request) : void 0,
|
|
5747
6116
|
onApprovalRequest: this._sendStepApproval ? async (request) => this._sendStepApproval(dispatch.runId, dispatch.jobId, request) : void 0,
|
|
@@ -5803,6 +6172,53 @@ var init_job_runner = __esmMin((() => {
|
|
|
5803
6172
|
}, result.secretOutputs);
|
|
5804
6173
|
}
|
|
5805
6174
|
/**
|
|
6175
|
+
* Handle a bring-up job: bring up a temporary init-runner on a declared-but-
|
|
6176
|
+
* un-agented host over SSH (fresh-box bootstrap convergence). The orchestrator
|
|
6177
|
+
* dispatches this synthetic `__bringup__` job to an agent holding the
|
|
6178
|
+
* `kici:capability:ssh-transport` capability; here we run the agent-side
|
|
6179
|
+
* `ensureInitRunner` helper (the privileged resolve is relayed to the
|
|
6180
|
+
* orchestrator, the SSH transport happens in this agent process — never
|
|
6181
|
+
* reaching workflow code). No clone, no sandbox — the init-runner then
|
|
6182
|
+
* connects under the target's agent id and the orchestrator's pinned-hold
|
|
6183
|
+
* drains the target's bootstrap steps onto it.
|
|
6184
|
+
*/
|
|
6185
|
+
async handleBringupJob(dispatch) {
|
|
6186
|
+
const { runId, jobId, jobConfig } = dispatch;
|
|
6187
|
+
const targetAgentId = String(jobConfig.bringupTarget ?? "");
|
|
6188
|
+
logger$2.info("Starting bring-up job", {
|
|
6189
|
+
jobId,
|
|
6190
|
+
runId,
|
|
6191
|
+
targetAgentId
|
|
6192
|
+
});
|
|
6193
|
+
this.sendJobStatus(dispatch, ExecutionJobStatus.enum.running);
|
|
6194
|
+
const streamer = this.createStepStreamer(dispatch, 0);
|
|
6195
|
+
this.sendStepStatus(dispatch, 0, "bring-up", ExecutionStepStatus.enum.running);
|
|
6196
|
+
try {
|
|
6197
|
+
if (!targetAgentId) throw new Error("bring-up job missing bringupTarget");
|
|
6198
|
+
if (!this._sendApiRequest) throw new Error("bring-up job requires an orchestrator API transport");
|
|
6199
|
+
streamer.addLine(`Bringing up init-runner on ${targetAgentId}…`);
|
|
6200
|
+
const result = await ensureInitRunner(async (method, params) => this._sendApiRequest(method, params), targetAgentId);
|
|
6201
|
+
streamer.addLine(result.broughtUp ? `Init-runner brought up on ${targetAgentId}.` : `${targetAgentId} already has a live agent — no bring-up needed.`);
|
|
6202
|
+
await streamer.flush();
|
|
6203
|
+
this.sendStepStatus(dispatch, 0, "bring-up", ExecutionStepStatus.enum.success, void 0, streamer.getTotalBytes());
|
|
6204
|
+
streamer.destroy();
|
|
6205
|
+
this.sendJobStatus(dispatch, ExecutionJobStatus.enum.success);
|
|
6206
|
+
} catch (err) {
|
|
6207
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6208
|
+
streamer.addLine(`Bring-up failed: ${message}`);
|
|
6209
|
+
await streamer.flush();
|
|
6210
|
+
this.sendStepStatus(dispatch, 0, "bring-up", ExecutionStepStatus.enum.failed, void 0, streamer.getTotalBytes());
|
|
6211
|
+
streamer.destroy();
|
|
6212
|
+
this.sendJobStatus(dispatch, ExecutionJobStatus.enum.failed, { error: message });
|
|
6213
|
+
logger$2.warn("Bring-up job failed", {
|
|
6214
|
+
jobId,
|
|
6215
|
+
runId,
|
|
6216
|
+
targetAgentId,
|
|
6217
|
+
error: message
|
|
6218
|
+
});
|
|
6219
|
+
}
|
|
6220
|
+
}
|
|
6221
|
+
/**
|
|
5806
6222
|
* Handle a build-only job.
|
|
5807
6223
|
*
|
|
5808
6224
|
* Build jobs install dependencies, pack them into a tarball,
|
|
@@ -6195,7 +6611,7 @@ var init_job_runner = __esmMin((() => {
|
|
|
6195
6611
|
error: (msg, ..._args) => evalLog(`ERROR: ${msg}`),
|
|
6196
6612
|
debug: (msg, ..._args) => evalLog(`DEBUG: ${msg}`)
|
|
6197
6613
|
};
|
|
6198
|
-
const kici = buildKiciApi(this._sendApiRequest ? (method, params) => this._sendApiRequest(method, params ?? {}) : () => Promise.reject(/* @__PURE__ */ new Error("Agent API not available")));
|
|
6614
|
+
const kici = buildKiciApi(this._sendApiRequest ? withBootstrapInterception((method, params) => this._sendApiRequest(method, params ?? {})) : () => Promise.reject(/* @__PURE__ */ new Error("Agent API not available")));
|
|
6199
6615
|
const lockJobs = await runCaptured(evalSink, async () => {
|
|
6200
6616
|
const { module } = await loadWorkflowSource(workDir, config.source.file, config.contentHash, config.resolvedHashFiles);
|
|
6201
6617
|
evalLog("Workflow loaded");
|
|
@@ -6425,14 +6841,14 @@ var init_job_runner = __esmMin((() => {
|
|
|
6425
6841
|
*/
|
|
6426
6842
|
init_console_capture();
|
|
6427
6843
|
init_npm_resolver();
|
|
6428
|
-
const AGENT_VERSION = "0.1.
|
|
6429
|
-
const BUILD_COMMIT = "
|
|
6430
|
-
const SDK_VERSION = "0.1.
|
|
6431
|
-
const SDK_BUNDLE_HASH = "
|
|
6432
|
-
const SHARED_VERSION = "0.1.
|
|
6844
|
+
const AGENT_VERSION = "0.1.23";
|
|
6845
|
+
const BUILD_COMMIT = "4465935bb";
|
|
6846
|
+
const SDK_VERSION = "0.1.23";
|
|
6847
|
+
const SDK_BUNDLE_HASH = "3a83610d2d122b9f9b0f924b225a47050d0b35f00a8f3256a3e15f3ff3bf7ddc";
|
|
6848
|
+
const SHARED_VERSION = "0.1.23";
|
|
6433
6849
|
const SHARED_BUNDLE_HASH = "991385c024392c395d3eb8a68946ef8ed3fcba96f2652f21c3164a54eafa1b1b";
|
|
6434
|
-
const ENGINE_VERSION = "0.1.
|
|
6435
|
-
const ENGINE_BUNDLE_HASH = "
|
|
6850
|
+
const ENGINE_VERSION = "0.1.23";
|
|
6851
|
+
const ENGINE_BUNDLE_HASH = "4a0566c709180a6a8744ff3281640e596b8e661ddceb81327cd37d232d9768d5";
|
|
6436
6852
|
initTelemetry({
|
|
6437
6853
|
serviceName: "kici-agent",
|
|
6438
6854
|
otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
|
|
@@ -6498,6 +6914,7 @@ await guardStartup(logger$1, async () => {
|
|
|
6498
6914
|
}
|
|
6499
6915
|
let isDraining = false;
|
|
6500
6916
|
let idleShutdownTimer;
|
|
6917
|
+
let rebootIntent = false;
|
|
6501
6918
|
let client;
|
|
6502
6919
|
const jobRunner = new JobRunner({
|
|
6503
6920
|
send: (msg) => client.send(msg),
|
|
@@ -6511,7 +6928,15 @@ await guardStartup(logger$1, async () => {
|
|
|
6511
6928
|
sendJobContext: (runId, jobId, context) => client.sendJobContext(runId, jobId, context),
|
|
6512
6929
|
sendRunEvent: (runId, eventType, opts) => client.sendRunEvent(runId, eventType, opts),
|
|
6513
6930
|
sendConcurrencyReport: (runId, jobId, group) => client.sendConcurrencyReport(runId, jobId, group),
|
|
6514
|
-
sendApiRequest: (method, params) =>
|
|
6931
|
+
sendApiRequest: async (method, params) => {
|
|
6932
|
+
if (method === "host.requestReboot") {
|
|
6933
|
+
if (config.isOrchestratorHost) throw new Error("refusing to reboot the orchestrator host");
|
|
6934
|
+
const result = await client.sendApiRequest(method, params ?? {});
|
|
6935
|
+
rebootIntent = true;
|
|
6936
|
+
return result;
|
|
6937
|
+
}
|
|
6938
|
+
return client.sendApiRequest(method, params ?? {});
|
|
6939
|
+
},
|
|
6515
6940
|
requestUserCache: (jobId, request) => client.requestUserCache(jobId, request),
|
|
6516
6941
|
relayProvenance: (jobId, request) => client.relayProvenance(jobId, request),
|
|
6517
6942
|
sendStepApproval: (runId, jobId, request) => client.sendStepApproval(runId, jobId, request)
|
|
@@ -6600,6 +7025,13 @@ await guardStartup(logger$1, async () => {
|
|
|
6600
7025
|
jobsActive.add(-1);
|
|
6601
7026
|
metricsReporter.collectAndSend().catch(() => {});
|
|
6602
7027
|
sendAgentStatus();
|
|
7028
|
+
if (rebootIntent) {
|
|
7029
|
+
rebootIntent = false;
|
|
7030
|
+
issueReboot().catch((err) => {
|
|
7031
|
+
logger$1.error("Host reboot failed; cancelling reboot-pending flag", { error: toErrorMessage(err) });
|
|
7032
|
+
client.sendApiRequest("host.cancelReboot", {}).catch(() => {});
|
|
7033
|
+
});
|
|
7034
|
+
}
|
|
6603
7035
|
if (config.scalerManaged && jobRunner.activeJobs.size === 0) {
|
|
6604
7036
|
if (client.state !== "registered") {
|
|
6605
7037
|
logger$1.info("Scaler-managed agent idle but disconnected, deferring shutdown until reconnected");
|