@kici-dev/agent 0.1.20 → 0.1.22

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/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, 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";
@@ -66,6 +66,7 @@ const envDef = defineEnv({
66
66
  orchestratorUrl: z.string().url().min(1, "KICI_ORCHESTRATOR_URL is required"),
67
67
  agentId: z.string().optional(),
68
68
  labels: z.string().default("").transform((s) => s.split(",").filter(Boolean)),
69
+ properties: z.string().default("").transform((s) => parseHostPropertyAssignments(s.split(",").filter(Boolean))),
69
70
  roles: z.string().optional().transform((s) => {
70
71
  if (s === void 0) return void 0;
71
72
  if (s === "") return [];
@@ -101,16 +102,19 @@ const envDef = defineEnv({
101
102
  scalerPendingDispatchTimeoutMs: z.coerce.number().default(6e4),
102
103
  executionMode: ExecutionMode.optional(),
103
104
  otelExporterOtlpEndpoint: z.string().optional(),
104
- 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")
105
107
  }),
106
108
  envMap: {
107
109
  orchestratorUrl: "KICI_ORCHESTRATOR_URL",
108
110
  agentId: "KICI_AGENT_ID",
109
111
  labels: "KICI_LABELS",
112
+ properties: "KICI_PROPERTIES",
110
113
  roles: "KICI_ROLES",
111
114
  port: "KICI_PORT",
112
115
  logLevel: "KICI_LOG_LEVEL",
113
116
  agentToken: "KICI_AGENT_TOKEN",
117
+ isOrchestratorHost: "KICI_AGENT_IS_ORCHESTRATOR_HOST",
114
118
  githubToken: "KICI_GITHUB_TOKEN",
115
119
  maxLogSizeBytes: "KICI_MAX_LOG_SIZE_BYTES",
116
120
  defaultStepTimeoutMs: "KICI_DEFAULT_STEP_TIMEOUT_MS",
@@ -134,6 +138,7 @@ const envDef = defineEnv({
134
138
  * - KICI_ORCHESTRATOR_URL (required)
135
139
  * - KICI_AGENT_ID (optional, auto-generated from hostname-uuid8)
136
140
  * - KICI_LABELS (comma-separated, e.g. "linux,docker"). Labels with 'kici-' prefix are reserved.
141
+ * - KICI_PROPERTIES (comma-separated key=value host-vars, e.g. "region=eu,cores=8,gpu=true"). Typed (bool/number/string), reported into the host roster.
137
142
  * - KICI_ROLES (comma-separated agent roles, e.g. "builder,init-runner". undefined=all, empty=execution-only)
138
143
  * - KICI_PORT (default: 8080)
139
144
  * - KICI_LOG_LEVEL (default: info)
@@ -177,6 +182,7 @@ function agentClientConnectionOptions(config) {
177
182
  url: config.orchestratorUrl,
178
183
  agentId: config.agentId,
179
184
  labels: config.labels,
185
+ properties: config.properties,
180
186
  scalerManaged: config.scalerManaged,
181
187
  token: config.agentToken
182
188
  };
@@ -282,7 +288,7 @@ var LogBuffer = class extends RingBuffer {
282
288
  };
283
289
  //#endregion
284
290
  //#region src/ws/orchestrator-client.ts
285
- const logger$11 = createLogger({ prefix: "orchestrator-client" });
291
+ const logger$12 = createLogger({ prefix: "orchestrator-client" });
286
292
  /**
287
293
  * WebSocket client that connects the agent to the customer orchestrator.
288
294
  *
@@ -329,6 +335,7 @@ var OrchestratorClient = class OrchestratorClient {
329
335
  url;
330
336
  agentId;
331
337
  labels;
338
+ properties;
332
339
  onJobDispatch;
333
340
  onJobCancel;
334
341
  token;
@@ -356,6 +363,7 @@ var OrchestratorClient = class OrchestratorClient {
356
363
  this.url = options.url;
357
364
  this.agentId = options.agentId;
358
365
  this.labels = options.labels;
366
+ this.properties = options.properties ?? {};
359
367
  this.onJobDispatch = options.onJobDispatch;
360
368
  this.onJobCancel = options.onJobCancel;
361
369
  this.token = options.token;
@@ -397,7 +405,7 @@ var OrchestratorClient = class OrchestratorClient {
397
405
  */
398
406
  connect() {
399
407
  if (this._state !== "disconnected") {
400
- logger$11.warn("connect() called while not disconnected", { state: this._state });
408
+ logger$12.warn("connect() called while not disconnected", { state: this._state });
401
409
  return;
402
410
  }
403
411
  this.intentionalDisconnect = false;
@@ -748,7 +756,8 @@ var OrchestratorClient = class OrchestratorClient {
748
756
  stepName: request.stepName,
749
757
  clauses: request.clauses,
750
758
  reason: request.reason,
751
- ...request.timeoutSeconds !== void 0 && { timeoutSeconds: request.timeoutSeconds }
759
+ ...request.timeoutSeconds !== void 0 && { timeoutSeconds: request.timeoutSeconds },
760
+ ...request.payload !== void 0 && { payload: request.payload }
752
761
  });
753
762
  });
754
763
  }
@@ -834,7 +843,7 @@ var OrchestratorClient = class OrchestratorClient {
834
843
  }
835
844
  });
836
845
  } catch (err) {
837
- logger$11.error("Failed to create WebSocket", { error: toErrorMessage(err) });
846
+ logger$12.error("Failed to create WebSocket", { error: toErrorMessage(err) });
838
847
  this._state = "disconnected";
839
848
  this.scheduleReconnect();
840
849
  return;
@@ -842,7 +851,7 @@ var OrchestratorClient = class OrchestratorClient {
842
851
  this.ws.on("open", () => {
843
852
  if (this.token) {
844
853
  this._state = "authenticating";
845
- logger$11.info("Connected to orchestrator, sending auth.request", {
854
+ logger$12.info("Connected to orchestrator, sending auth.request", {
846
855
  url: this.url,
847
856
  agentId: this.agentId
848
857
  });
@@ -853,7 +862,7 @@ var OrchestratorClient = class OrchestratorClient {
853
862
  }));
854
863
  } else {
855
864
  this._state = "registering";
856
- logger$11.info("Connected to orchestrator, sending agent.register (no token)", {
865
+ logger$12.info("Connected to orchestrator, sending agent.register (no token)", {
857
866
  url: this.url,
858
867
  agentId: this.agentId
859
868
  });
@@ -864,12 +873,12 @@ var OrchestratorClient = class OrchestratorClient {
864
873
  this.handleMessage(data);
865
874
  });
866
875
  this.ws.on("close", (code, reason) => {
867
- logger$11.info("Orchestrator connection closed", {
876
+ logger$12.info("Orchestrator connection closed", {
868
877
  code,
869
878
  reason: reason.toString()
870
879
  });
871
880
  if (code === WS_CLOSE_AGENT_AUTH_FAILED) {
872
- logger$11.error("Orchestrator closed with auth-failed code -- token is invalid or revoked. NOT retrying.", {
881
+ logger$12.error("Orchestrator closed with auth-failed code -- token is invalid or revoked. NOT retrying.", {
873
882
  code,
874
883
  reason: reason.toString()
875
884
  });
@@ -895,7 +904,7 @@ var OrchestratorClient = class OrchestratorClient {
895
904
  if (!this.intentionalDisconnect) this.scheduleReconnect();
896
905
  });
897
906
  this.ws.on("error", (err) => {
898
- logger$11.error(`Orchestrator WebSocket error: ${err.message}`);
907
+ logger$12.error(`Orchestrator WebSocket error: ${err.message}`);
899
908
  if (this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.close();
900
909
  });
901
910
  }
@@ -904,7 +913,7 @@ var OrchestratorClient = class OrchestratorClient {
904
913
  try {
905
914
  raw = JSON.parse(data.toString());
906
915
  } catch {
907
- logger$11.warn("Malformed JSON received from orchestrator");
916
+ logger$12.warn("Malformed JSON received from orchestrator");
908
917
  return;
909
918
  }
910
919
  const rawMsg = raw;
@@ -962,13 +971,13 @@ var OrchestratorClient = class OrchestratorClient {
962
971
  switch (msg.type) {
963
972
  case "auth.success":
964
973
  if (this._state === "authenticating") {
965
- logger$11.info("Authentication successful, sending agent.register", { connectionId: msg.connectionId });
974
+ logger$12.info("Authentication successful, sending agent.register", { connectionId: msg.connectionId });
966
975
  this._state = "registering";
967
976
  this.sendAgentRegister();
968
977
  }
969
978
  break;
970
979
  case "auth.failure":
971
- logger$11.error("Authentication FAILED -- token is invalid or expired. NOT retrying.", { reason: msg.reason });
980
+ logger$12.error("Authentication FAILED -- token is invalid or expired. NOT retrying.", { reason: msg.reason });
972
981
  this.authFailed = true;
973
982
  this.intentionalDisconnect = true;
974
983
  if (this.ws) {
@@ -978,7 +987,7 @@ var OrchestratorClient = class OrchestratorClient {
978
987
  this._state = "disconnected";
979
988
  break;
980
989
  case "register.ack":
981
- logger$11.info("Registration acknowledged by orchestrator", {
990
+ logger$12.info("Registration acknowledged by orchestrator", {
982
991
  agentId: msg.agentId,
983
992
  labels: msg.labels,
984
993
  scalerManaged: msg.scalerManaged,
@@ -993,14 +1002,14 @@ var OrchestratorClient = class OrchestratorClient {
993
1002
  this.sendConfigAck(msg.agentId);
994
1003
  break;
995
1004
  case "job.dispatch":
996
- logger$11.info("Job dispatch received", {
1005
+ logger$12.info("Job dispatch received", {
997
1006
  runId: msg.runId,
998
1007
  jobId: msg.jobId
999
1008
  });
1000
1009
  this.onJobDispatch(msg);
1001
1010
  break;
1002
1011
  case "job.cancel":
1003
- logger$11.info("Job cancel received", {
1012
+ logger$12.info("Job cancel received", {
1004
1013
  runId: msg.runId,
1005
1014
  jobId: msg.jobId,
1006
1015
  reason: msg.reason
@@ -1008,7 +1017,7 @@ var OrchestratorClient = class OrchestratorClient {
1008
1017
  this.onJobCancel(msg);
1009
1018
  break;
1010
1019
  case "job.concurrency.ack": {
1011
- logger$11.info("Concurrency ack received", {
1020
+ logger$12.info("Concurrency ack received", {
1012
1021
  requestId: msg.requestId,
1013
1022
  action: msg.action
1014
1023
  });
@@ -1023,7 +1032,7 @@ var OrchestratorClient = class OrchestratorClient {
1023
1032
  break;
1024
1033
  }
1025
1034
  case "step.approval-resolved": {
1026
- logger$11.info("Step approval resolved", {
1035
+ logger$12.info("Step approval resolved", {
1027
1036
  requestId: msg.requestId,
1028
1037
  runId: msg.runId,
1029
1038
  jobId: msg.jobId,
@@ -1043,7 +1052,7 @@ var OrchestratorClient = class OrchestratorClient {
1043
1052
  break;
1044
1053
  }
1045
1054
  case "fleet.logs.request":
1046
- logger$11.info("Fleet log collection requested", {
1055
+ logger$12.info("Fleet log collection requested", {
1047
1056
  requestId: msg.requestId,
1048
1057
  logWindowHours: msg.logWindowHours
1049
1058
  });
@@ -1053,7 +1062,7 @@ var OrchestratorClient = class OrchestratorClient {
1053
1062
  return;
1054
1063
  }
1055
1064
  if (heartbeatSchema.safeParse(raw).success) return;
1056
- logger$11.warn("Invalid message from orchestrator", { errors: parsed.error.issues });
1065
+ logger$12.warn("Invalid message from orchestrator", { errors: parsed.error.issues });
1057
1066
  }
1058
1067
  flushBuffer() {
1059
1068
  const events = this.eventBuffer.flush();
@@ -1067,11 +1076,11 @@ var OrchestratorClient = class OrchestratorClient {
1067
1076
  }
1068
1077
  this.disconnectedAt = null;
1069
1078
  if (events.length > 0) {
1070
- logger$11.info("Flushing event buffer", { count: events.length });
1079
+ logger$12.info("Flushing event buffer", { count: events.length });
1071
1080
  for (const msg of events) this.sendDirect(msg);
1072
1081
  }
1073
1082
  if (logLines.length > 0) {
1074
- logger$11.info("Flushing log buffer", { count: logLines.length });
1083
+ logger$12.info("Flushing log buffer", { count: logLines.length });
1075
1084
  for (let i = 0; i < logLines.length; i += OrchestratorClient.LOG_BATCH_SIZE) {
1076
1085
  const batch = logLines.slice(i, i + OrchestratorClient.LOG_BATCH_SIZE);
1077
1086
  this.sendAgentLogMessage(batch);
@@ -1124,14 +1133,14 @@ var OrchestratorClient = class OrchestratorClient {
1124
1133
  */
1125
1134
  blockMmdsAccess() {
1126
1135
  if (process.getuid?.() !== 0) {
1127
- logger$11.info("MMDS iptables block skipped (non-root) — network isolation handled by orchestrator");
1136
+ logger$12.info("MMDS iptables block skipped (non-root) — network isolation handled by orchestrator");
1128
1137
  return;
1129
1138
  }
1130
1139
  try {
1131
1140
  execSync("iptables -A OUTPUT -d 169.254.169.254 -j DROP", { timeout: 5e3 });
1132
- logger$11.info("MMDS access blocked via iptables");
1141
+ logger$12.info("MMDS access blocked via iptables");
1133
1142
  } catch (err) {
1134
- logger$11.warn("Failed to block MMDS access via iptables", { error: toErrorMessage(err) });
1143
+ logger$12.warn("Failed to block MMDS access via iptables", { error: toErrorMessage(err) });
1135
1144
  }
1136
1145
  }
1137
1146
  /**
@@ -1145,7 +1154,7 @@ var OrchestratorClient = class OrchestratorClient {
1145
1154
  messageId: `config-ack-${agentId}-${Date.now()}`,
1146
1155
  agentId
1147
1156
  }));
1148
- logger$11.info("Config ACK sent to orchestrator", { agentId });
1157
+ logger$12.info("Config ACK sent to orchestrator", { agentId });
1149
1158
  }
1150
1159
  }
1151
1160
  startHeartbeat() {
@@ -1205,17 +1214,18 @@ var OrchestratorClient = class OrchestratorClient {
1205
1214
  })()
1206
1215
  };
1207
1216
  if (inFlightJobs.length > 0) msg.inFlightJobs = inFlightJobs;
1217
+ if (Object.keys(this.properties).length > 0) msg.properties = this.properties;
1208
1218
  this.ws.send(JSON.stringify(msg));
1209
1219
  }
1210
1220
  scheduleReconnect() {
1211
1221
  this.cancelReconnect();
1212
1222
  if (this.authFailed) {
1213
- logger$11.error("Not reconnecting: authentication permanently failed");
1223
+ logger$12.error("Not reconnecting: authentication permanently failed");
1214
1224
  return;
1215
1225
  }
1216
1226
  const delay = this.getReconnectDelay();
1217
1227
  this.reconnectAttempts++;
1218
- logger$11.info("Scheduling reconnect", {
1228
+ logger$12.info("Scheduling reconnect", {
1219
1229
  attempt: this.reconnectAttempts,
1220
1230
  delayMs: Math.round(delay)
1221
1231
  });
@@ -1300,14 +1310,14 @@ var init_console_capture = __esmMin((() => {
1300
1310
  init_console_capture();
1301
1311
  function safe(name, fallback = "unknown") {
1302
1312
  switch (name) {
1303
- case "version": return "0.1.20";
1304
- case "buildCommit": return "f12ea0665";
1305
- case "sdkVersion": return "0.1.20";
1306
- case "sdkBundleHash": return "8308089347c304e41b457d3867b17bbff11d6b5cd9706b6823e7abdbd849f33f";
1307
- case "sharedVersion": return "0.1.20";
1308
- case "sharedBundleHash": return "5f2c220f24d166b0f13d0620a2e80d19689ac8b683a12154daef44e938fd46a7";
1309
- case "engineVersion": return "0.1.20";
1310
- case "engineBundleHash": return "79ce14640d1798eaaa7cb3aa6c4bc325da3bff1e9f4716936e19614eabb1d858";
1313
+ case "version": return "0.1.22";
1314
+ case "buildCommit": return "5afd16303";
1315
+ case "sdkVersion": return "0.1.22";
1316
+ case "sdkBundleHash": return "4b26a42978f77e29db110d47523ec947d3d673c3109fc48141c1a62814bde7a1";
1317
+ case "sharedVersion": return "0.1.22";
1318
+ case "sharedBundleHash": return "991385c024392c395d3eb8a68946ef8ed3fcba96f2652f21c3164a54eafa1b1b";
1319
+ case "engineVersion": return "0.1.22";
1320
+ case "engineBundleHash": return "ff74a3afb2b1db2870c03d47543642f64d04b660bebdcde6681f76d874f26757";
1311
1321
  default: return fallback;
1312
1322
  }
1313
1323
  }
@@ -1564,6 +1574,83 @@ async function gcStaleAgentTmpDirs(base = tmpdir()) {
1564
1574
  })];
1565
1575
  }
1566
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
1567
1654
  //#region src/metrics/prometheus.ts
1568
1655
  var prometheus_exports = /* @__PURE__ */ __exportAll({
1569
1656
  cloneDurationSeconds: () => cloneDurationSeconds,
@@ -1993,8 +2080,8 @@ async function extractStepsFromDynamicJob(workflow, dynamicIndex, jobName, event
1993
2080
  }
1994
2081
  var AGENT_SDK_VERSION, AGENT_SDK_BUNDLE_HASH, hookRegistered;
1995
2082
  var init_workflow_loader = __esmMin((() => {
1996
- AGENT_SDK_VERSION = "0.1.20";
1997
- AGENT_SDK_BUNDLE_HASH = "8308089347c304e41b457d3867b17bbff11d6b5cd9706b6823e7abdbd849f33f";
2083
+ AGENT_SDK_VERSION = "0.1.22";
2084
+ AGENT_SDK_BUNDLE_HASH = "4b26a42978f77e29db110d47523ec947d3d673c3109fc48141c1a62814bde7a1";
1998
2085
  hookRegistered = false;
1999
2086
  }));
2000
2087
  //#endregion
@@ -2543,7 +2630,8 @@ async function serializeJob(job, generatedNames, ctx, staticNames, allowedGroups
2543
2630
  * Validates against generatedNames union staticNames union allowedGroups.
2544
2631
  *
2545
2632
  * Returns the lock file representation: strings for concrete refs,
2546
- * NeedsEntry for { name, ifFailed }, NeedsGroupEntry for group refs.
2633
+ * NeedsEntry for { name, when }, NeedsGroupEntry for group refs (each `when`
2634
+ * normalized to a runOn status-set).
2547
2635
  */
2548
2636
  function resolveNeeds(needs, generatedNames, staticNames, allowedGroups) {
2549
2637
  if (!needs || needs.length === 0) return [];
@@ -2558,7 +2646,7 @@ function resolveNeeds(needs, generatedNames, staticNames, allowedGroups) {
2558
2646
  if (!allowedGroups.has(groupRef.group)) throw new Error(`Dynamic group '${groupRef.group}' not found in workflow (available groups: ${[...allowedGroups].join(", ") || "none"})`);
2559
2647
  return {
2560
2648
  group: groupRef.group,
2561
- ifFailed: groupRef.ifFailed ?? "skip"
2649
+ runOn: resolveWhenToRunOn(groupRef.when)
2562
2650
  };
2563
2651
  }
2564
2652
  if (typeof dep === "object" && dep !== null && "group" in dep) {
@@ -2566,7 +2654,7 @@ function resolveNeeds(needs, generatedNames, staticNames, allowedGroups) {
2566
2654
  if (!allowedGroups.has(groupDep.group)) throw new Error(`Dynamic group '${groupDep.group}' not found in workflow (available groups: ${[...allowedGroups].join(", ") || "none"})`);
2567
2655
  return {
2568
2656
  group: groupDep.group,
2569
- ifFailed: groupDep.ifFailed ?? "skip"
2657
+ runOn: resolveWhenToRunOn(groupDep.when)
2570
2658
  };
2571
2659
  }
2572
2660
  if (typeof dep === "object" && dep !== null && "name" in dep && !("steps" in dep)) {
@@ -2574,7 +2662,7 @@ function resolveNeeds(needs, generatedNames, staticNames, allowedGroups) {
2574
2662
  if (!allNames.has(namedDep.name)) throw new Error(`Job dependency '${namedDep.name}' not found in workflow jobs (checked: ${generatedNames.size} generated, ${staticNames.size} static)`);
2575
2663
  return {
2576
2664
  name: namedDep.name,
2577
- ifFailed: namedDep.ifFailed ?? "skip"
2665
+ runOn: resolveWhenToRunOn(namedDep.when)
2578
2666
  };
2579
2667
  }
2580
2668
  if (isDynamicJobFn(dep)) throw new Error("Job dependency cannot be a DynamicJobFn");
@@ -4188,6 +4276,7 @@ function buildRequest(dispatch, workDir) {
4188
4276
  checkout: jobConfig.checkout ?? true,
4189
4277
  isTestRun: jobConfig.isTestRun ?? false,
4190
4278
  fullRepo: jobConfig.fullRepo ?? false,
4279
+ checkMode: jobConfig.checkMode,
4191
4280
  tarballUrl: jobConfig.tarballUrl,
4192
4281
  cliPublicKey: jobConfig.cliPublicKey,
4193
4282
  orchestratorPrivateKey: jobConfig.orchestratorPrivateKey,
@@ -4204,6 +4293,8 @@ function buildRequest(dispatch, workDir) {
4204
4293
  concurrencyEvaluationTimeoutMs: jobConfig.concurrencyEvaluationTimeoutMs,
4205
4294
  branch: dispatch.ref,
4206
4295
  upstreamJobOutputs: dispatch.upstreamJobOutputs,
4296
+ upstreamJobStatuses: dispatch.upstreamJobStatuses,
4297
+ jobNeeds: jobConfig.needs,
4207
4298
  npmRegistries: dispatch.npmRegistries,
4208
4299
  installEnvSecrets: dispatch.installEnvSecrets,
4209
4300
  jobIdShort: dispatch.jobId.slice(0, 8),
@@ -4525,6 +4616,9 @@ function relayChildIpcMessage(msg, dispatch, ctx) {
4525
4616
  ...msg.error && { error: msg.error },
4526
4617
  ...msg.secretsAccessed && { secretsAccessed: msg.secretsAccessed },
4527
4618
  ...msg.step_type && { step_type: msg.step_type },
4619
+ ...msg.checkOutcome !== void 0 && { checkOutcome: msg.checkOutcome },
4620
+ ...msg.driftSummary !== void 0 && { driftSummary: msg.driftSummary },
4621
+ ...msg.drift !== void 0 && { drift: msg.drift },
4528
4622
  ...msg.data && msg.data
4529
4623
  });
4530
4624
  return;
@@ -5249,6 +5343,9 @@ var init_container_sandbox = __esmMin((() => {
5249
5343
  ...msg.error && { error: msg.error },
5250
5344
  ...msg.secretsAccessed && { secretsAccessed: msg.secretsAccessed },
5251
5345
  ...msg.step_type && { step_type: msg.step_type },
5346
+ ...msg.checkOutcome !== void 0 && { checkOutcome: msg.checkOutcome },
5347
+ ...msg.driftSummary !== void 0 && { driftSummary: msg.driftSummary },
5348
+ ...msg.drift !== void 0 && { drift: msg.drift },
5252
5349
  ...msg.data && msg.data
5253
5350
  });
5254
5351
  stepResults.push({
@@ -5380,7 +5477,10 @@ var init_sandbox = __esmMin((() => {
5380
5477
  }));
5381
5478
  //#endregion
5382
5479
  //#region src/execution/job-runner.ts
5383
- var job_runner_exports = /* @__PURE__ */ __exportAll({ JobRunner: () => JobRunner$1 });
5480
+ var job_runner_exports = /* @__PURE__ */ __exportAll({
5481
+ JobRunner: () => JobRunner$1,
5482
+ buildEvalNeedsContext: () => buildEvalNeedsContext
5483
+ });
5384
5484
  /**
5385
5485
  * Check if a file exists at the given path.
5386
5486
  */
@@ -5764,12 +5864,17 @@ var init_job_runner = __esmMin((() => {
5764
5864
  stepsTotal.add(1, { status: stepResult.status });
5765
5865
  if (stepResult.durationMs > 0) stepDurationSeconds.record(stepResult.durationMs / 1e3);
5766
5866
  }
5767
- if (result.status === ExecutionJobStatus.enum.failed) logger$2.error("Sandbox returned failed result", {
5768
- durationMs: result.durationMs,
5769
- stepCount: result.stepResults.length,
5770
- steps: result.stepResults.map((r) => `${r.name}:${r.status}`).join(","),
5771
- logStreamerKeys: [...logStreamers.keys()].join(",")
5772
- });
5867
+ if (result.status === ExecutionJobStatus.enum.failed) {
5868
+ const stepErrors = result.stepResults.filter((r) => r.error).map((r) => `${r.name}: ${r.error.message}`).join(" | ");
5869
+ logger$2.error("Sandbox returned failed result", {
5870
+ durationMs: result.durationMs,
5871
+ stepCount: result.stepResults.length,
5872
+ steps: result.stepResults.map((r) => `${r.name}:${r.status}`).join(","),
5873
+ logStreamerKeys: [...logStreamers.keys()].join(","),
5874
+ ...result.error && { error: result.error },
5875
+ ...stepErrors && { stepErrors }
5876
+ });
5877
+ }
5773
5878
  this.sendJobStatus(dispatch, result.status, {
5774
5879
  durationMs: result.durationMs,
5775
5880
  ...result.error && { error: result.error },
@@ -6406,14 +6511,14 @@ var init_job_runner = __esmMin((() => {
6406
6511
  */
6407
6512
  init_console_capture();
6408
6513
  init_npm_resolver();
6409
- const AGENT_VERSION = "0.1.20";
6410
- const BUILD_COMMIT = "f12ea0665";
6411
- const SDK_VERSION = "0.1.20";
6412
- const SDK_BUNDLE_HASH = "8308089347c304e41b457d3867b17bbff11d6b5cd9706b6823e7abdbd849f33f";
6413
- const SHARED_VERSION = "0.1.20";
6414
- const SHARED_BUNDLE_HASH = "5f2c220f24d166b0f13d0620a2e80d19689ac8b683a12154daef44e938fd46a7";
6415
- const ENGINE_VERSION = "0.1.20";
6416
- const ENGINE_BUNDLE_HASH = "79ce14640d1798eaaa7cb3aa6c4bc325da3bff1e9f4716936e19614eabb1d858";
6514
+ const AGENT_VERSION = "0.1.22";
6515
+ const BUILD_COMMIT = "5afd16303";
6516
+ const SDK_VERSION = "0.1.22";
6517
+ const SDK_BUNDLE_HASH = "4b26a42978f77e29db110d47523ec947d3d673c3109fc48141c1a62814bde7a1";
6518
+ const SHARED_VERSION = "0.1.22";
6519
+ const SHARED_BUNDLE_HASH = "991385c024392c395d3eb8a68946ef8ed3fcba96f2652f21c3164a54eafa1b1b";
6520
+ const ENGINE_VERSION = "0.1.22";
6521
+ const ENGINE_BUNDLE_HASH = "ff74a3afb2b1db2870c03d47543642f64d04b660bebdcde6681f76d874f26757";
6417
6522
  initTelemetry({
6418
6523
  serviceName: "kici-agent",
6419
6524
  otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
@@ -6479,6 +6584,7 @@ await guardStartup(logger$1, async () => {
6479
6584
  }
6480
6585
  let isDraining = false;
6481
6586
  let idleShutdownTimer;
6587
+ let rebootIntent = false;
6482
6588
  let client;
6483
6589
  const jobRunner = new JobRunner({
6484
6590
  send: (msg) => client.send(msg),
@@ -6492,7 +6598,15 @@ await guardStartup(logger$1, async () => {
6492
6598
  sendJobContext: (runId, jobId, context) => client.sendJobContext(runId, jobId, context),
6493
6599
  sendRunEvent: (runId, eventType, opts) => client.sendRunEvent(runId, eventType, opts),
6494
6600
  sendConcurrencyReport: (runId, jobId, group) => client.sendConcurrencyReport(runId, jobId, group),
6495
- sendApiRequest: (method, params) => client.sendApiRequest(method, params ?? {}),
6601
+ sendApiRequest: async (method, params) => {
6602
+ if (method === "host.requestReboot") {
6603
+ if (config.isOrchestratorHost) throw new Error("refusing to reboot the orchestrator host");
6604
+ const result = await client.sendApiRequest(method, params ?? {});
6605
+ rebootIntent = true;
6606
+ return result;
6607
+ }
6608
+ return client.sendApiRequest(method, params ?? {});
6609
+ },
6496
6610
  requestUserCache: (jobId, request) => client.requestUserCache(jobId, request),
6497
6611
  relayProvenance: (jobId, request) => client.relayProvenance(jobId, request),
6498
6612
  sendStepApproval: (runId, jobId, request) => client.sendStepApproval(runId, jobId, request)
@@ -6581,6 +6695,13 @@ await guardStartup(logger$1, async () => {
6581
6695
  jobsActive.add(-1);
6582
6696
  metricsReporter.collectAndSend().catch(() => {});
6583
6697
  sendAgentStatus();
6698
+ if (rebootIntent) {
6699
+ rebootIntent = false;
6700
+ issueReboot().catch((err) => {
6701
+ logger$1.error("Host reboot failed; cancelling reboot-pending flag", { error: toErrorMessage(err) });
6702
+ client.sendApiRequest("host.cancelReboot", {}).catch(() => {});
6703
+ });
6704
+ }
6584
6705
  if (config.scalerManaged && jobRunner.activeJobs.size === 0) {
6585
6706
  if (client.state !== "registered") {
6586
6707
  logger$1.info("Scaler-managed agent idle but disconnected, deferring shutdown until reconnected");