@kici-dev/agent 0.1.21 → 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/config.d.ts CHANGED
@@ -44,6 +44,7 @@ declare const configSchema: z.ZodObject<{
44
44
  }>>;
45
45
  otelExporterOtlpEndpoint: z.ZodOptional<z.ZodString>;
46
46
  concurrencyWaitTimeoutMs: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
47
+ isOrchestratorHost: z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<boolean, string | undefined>>;
47
48
  }, z.core.$strip>;
48
49
  /**
49
50
  * App configuration type. Includes computed agentId when not provided.
@@ -75,6 +76,7 @@ export declare const envDef: import("@kici-dev/shared/env").DefineEnvResult<{
75
76
  scalerIdleTimeoutMs: number;
76
77
  scalerPendingDispatchTimeoutMs: number;
77
78
  concurrencyWaitTimeoutMs: number;
79
+ isOrchestratorHost: boolean;
78
80
  agentId?: string | undefined;
79
81
  agentToken?: string | undefined;
80
82
  githubToken?: string | undefined;
@@ -1,5 +1,6 @@
1
1
  import type { AgentToOrchestratorMessage, JobDispatch } from '@kici-dev/engine';
2
2
  import type { AppConfig } from '../config.js';
3
+ import { buildNeedsContext } from '@kici-dev/sdk';
3
4
  import type { CacheRequestIpc, CacheResponseIpc, ProvenanceRequestIpc, ProvenanceResponseIpc, StepApprovalRequestIpc, StepApprovalResolvedIpc } from './sandbox/index.js';
4
5
  /**
5
6
  * Dependencies injected into JobRunner.
@@ -112,6 +113,15 @@ interface ActiveJob {
112
113
  completionPromise: Promise<void>;
113
114
  runId: string;
114
115
  }
116
+ /**
117
+ * Build the result-aware `ctx.needs` for a dynamic eval from its frozen upstream
118
+ * snapshot. Returns undefined for an event-only generator (no snapshot).
119
+ */
120
+ export declare function buildEvalNeedsContext(config: {
121
+ resultAware?: boolean;
122
+ declaredNeeds?: readonly unknown[];
123
+ upstreamSnapshot?: import('@kici-dev/engine').UpstreamSnapshot;
124
+ }): ReturnType<typeof buildNeedsContext> | undefined;
115
125
  /**
116
126
  * Top-level job execution orchestrator for the agent.
117
127
  *
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Cross-OS host reboot for the workflow-level `restartHost()` step.
3
+ *
4
+ * The agent runs ON the host it executes jobs for, so a `restartHost()` step
5
+ * reboots that host. `rebootCommandFor` is the pure OS→command mapping (kept
6
+ * pure for unit-testing); `issueReboot` spawns it detached with a tiny grace so
7
+ * the job's final flush completes before the box goes down.
8
+ *
9
+ * Rebooting needs host privilege (root/admin). If the primitive is denied, the
10
+ * spawn fails and the caller clears the orchestrator's reboot-pending flag and
11
+ * surfaces the error — the deadline sweep is the backstop.
12
+ */
13
+ /** The OS reboot primitive for a Node platform string. */
14
+ export declare function rebootCommandFor(platform: NodeJS.Platform): {
15
+ cmd: string;
16
+ args: string[];
17
+ };
18
+ /**
19
+ * Issue the OS reboot detached. Resolves once the child has been spawned (the
20
+ * box is going down; there is nothing to await). Rejects synchronously if the
21
+ * spawn itself fails (e.g. the binary is missing). A privilege denial usually
22
+ * surfaces as a non-zero exit AFTER spawn — logged, not thrown, because by then
23
+ * the box may already be on its way down.
24
+ */
25
+ export declare function issueReboot(platform?: NodeJS.Platform): Promise<void>;
26
+ //# sourceMappingURL=reboot.d.ts.map
@@ -193,8 +193,13 @@ export interface StepApprovalRequestIpc {
193
193
  }>;
194
194
  /** Human label for the gate. */
195
195
  reason: string;
196
- /** Per-gate timeout override (seconds) from the SDK `requireApproval.timeout`. */
196
+ /** Per-gate timeout override (seconds) from the SDK `approval.timeout`. */
197
197
  timeoutSeconds?: number;
198
+ /** Computed drift payload, present only for `when: 'drift'` gates. */
199
+ payload?: {
200
+ summaryMarkdown: string;
201
+ drift: unknown;
202
+ };
198
203
  }
199
204
  /** Which provenance upload operation to relay. */
200
205
  export type ProvenanceRequestOp = 'requestUploadUrl' | 'complete';
@@ -478,6 +483,10 @@ export interface JobExecutionRequest {
478
483
  branch?: string;
479
484
  /** Plain outputs from upstream jobs (keyed by job name, then by step name). For ctx.jobOutputs(). */
480
485
  upstreamJobOutputs?: Record<string, Record<string, unknown>>;
486
+ /** Terminal status of each upstream job (keyed by job name; per-child for fan-out). For ctx.needs.<job>.status. */
487
+ upstreamJobStatuses?: Record<string, import('@kici-dev/engine').ExecutionJobStatus>;
488
+ /** This job's declared upstream needs (normalized lock edges) used to shape ctx.needs for steps. */
489
+ jobNeeds?: readonly unknown[];
481
490
  /** Resolved private npm registries for `npm install` auth (token bytes already filled). */
482
491
  npmRegistries?: ReadonlyArray<{
483
492
  url: string;
@@ -90,11 +90,11 @@ export interface StepLoopOptions {
90
90
  */
91
91
  afterStepApplyEnvFiles?: () => Promise<void>;
92
92
  /**
93
- * Block a `requireApproval` step pending an orchestrator-side approval hold.
94
- * The runner sends the normalized requirement and awaits the resolution; the
95
- * agent keeps job heartbeats flowing during the wait so the agent isn't
96
- * reaped. Absent ⇒ approvals are not gated (CT / unit harnesses) and steps
97
- * run unconditionally.
93
+ * Block an `approval` step (`when: 'always'`) pending an orchestrator-side
94
+ * approval hold. The runner sends the normalized requirement and awaits the
95
+ * resolution; the agent keeps job heartbeats flowing during the wait so the
96
+ * agent isn't reaped. Absent ⇒ approvals are not gated (CT / unit harnesses)
97
+ * and steps run unconditionally.
98
98
  */
99
99
  awaitStepApproval?: (req: {
100
100
  stepIndex: number;
@@ -107,6 +107,28 @@ export interface StepLoopOptions {
107
107
  reason: string;
108
108
  timeoutSeconds?: number;
109
109
  }) => Promise<StepApprovalResolution>;
110
+ /**
111
+ * Block an `approval: { when: 'drift' }` step mid-execution: after `check()`
112
+ * returns drift in apply mode, send a payload-bearing step-approval and await
113
+ * the resolution. The payload carries the computed drift (`summaryMarkdown` +
114
+ * structured `drift`) so the operator approves the actual diff. Absent ⇒ the
115
+ * drift gate is not enforced (CT / unit harnesses) and the step applies.
116
+ */
117
+ awaitStepApprovalWithPayload?: (req: {
118
+ stepIndex: number;
119
+ stepName: string;
120
+ clauses: Array<{
121
+ team: string;
122
+ } | {
123
+ user: string;
124
+ }>;
125
+ reason: string;
126
+ timeoutSeconds?: number;
127
+ payload: {
128
+ summaryMarkdown: string;
129
+ drift: unknown;
130
+ };
131
+ }) => Promise<StepApprovalResolution>;
110
132
  }
111
133
  /** Outcome of an awaited step-level approval hold. */
112
134
  export interface StepApprovalResolution {
@@ -13,10 +13,21 @@
13
13
  * This file is compiled alongside the agent by rolldown (existing build), but
14
14
  * runs as a SEPARATE process spawned by the sandbox backend.
15
15
  */
16
+ import { ExecutionJobStatus } from '@kici-dev/engine';
16
17
  import type { StepContext } from '@kici-dev/sdk';
18
+ import type { NeedsContext } from '@kici-dev/sdk';
17
19
  import type { OutputsMap, StepRefMap, TrackedStepSecrets } from '@kici-dev/sdk';
18
20
  import type { RunnerToAgentMessage, JobExecutionRequest } from './ipc-protocol.js';
19
21
  import { LogMasker } from './log-masker.js';
22
+ /**
23
+ * Build `ctx.needs` for a job's steps from the dispatch envelope. Reconstructs
24
+ * an {@link UpstreamSnapshot} from `upstreamJobOutputs` (flat per single job;
25
+ * `byMatrix` / `byHost` envelopes per fan-out) + `upstreamJobStatuses` (keyed by
26
+ * each upstream job/child name), then resolves the job's declared needs into the
27
+ * `{ result, status }` / ordered-array shape via the shared SDK builder. Returns
28
+ * undefined when the job declares no needs.
29
+ */
30
+ export declare function buildStepNeedsContext(declaredNeeds: readonly unknown[] | undefined, upstreamJobOutputs: Record<string, Record<string, unknown>> | undefined, upstreamJobStatuses: Record<string, ExecutionJobStatus> | undefined): NeedsContext | undefined;
20
31
  /**
21
32
  * Create a StepContext natively inside the workflow runner.
22
33
  *
package/dist/index.js CHANGED
@@ -86,7 +86,8 @@ const envDef = defineEnv({
86
86
  scalerPendingDispatchTimeoutMs: z.coerce.number().default(6e4),
87
87
  executionMode: ExecutionMode.optional(),
88
88
  otelExporterOtlpEndpoint: z.string().optional(),
89
- concurrencyWaitTimeoutMs: z.coerce.number().int().min(1e3).default(36e5)
89
+ concurrencyWaitTimeoutMs: z.coerce.number().int().min(1e3).default(36e5),
90
+ isOrchestratorHost: z.string().optional().transform((v) => v === "true")
90
91
  }),
91
92
  envMap: {
92
93
  orchestratorUrl: "KICI_ORCHESTRATOR_URL",
@@ -97,6 +98,7 @@ const envDef = defineEnv({
97
98
  port: "KICI_PORT",
98
99
  logLevel: "KICI_LOG_LEVEL",
99
100
  agentToken: "KICI_AGENT_TOKEN",
101
+ isOrchestratorHost: "KICI_AGENT_IS_ORCHESTRATOR_HOST",
100
102
  githubToken: "KICI_GITHUB_TOKEN",
101
103
  maxLogSizeBytes: "KICI_MAX_LOG_SIZE_BYTES",
102
104
  defaultStepTimeoutMs: "KICI_DEFAULT_STEP_TIMEOUT_MS",
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$11 = createLogger({ prefix: "orchestrator-client" });
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$11.warn("connect() called while not disconnected", { state: this._state });
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$11.error("Failed to create WebSocket", { error: toErrorMessage(err) });
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$11.info("Connected to orchestrator, sending auth.request", {
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$11.info("Connected to orchestrator, sending agent.register (no token)", {
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$11.info("Orchestrator connection closed", {
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$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.", {
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$11.error(`Orchestrator WebSocket error: ${err.message}`);
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$11.warn("Malformed JSON received from orchestrator");
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$11.info("Authentication successful, sending agent.register", { connectionId: msg.connectionId });
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$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 });
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$11.info("Registration acknowledged by orchestrator", {
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$11.info("Job dispatch received", {
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$11.info("Job cancel received", {
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$11.info("Concurrency ack received", {
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$11.info("Step approval resolved", {
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$11.info("Fleet log collection requested", {
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$11.warn("Invalid message from orchestrator", { errors: parsed.error.issues });
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$11.info("Flushing event buffer", { count: events.length });
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$11.info("Flushing log buffer", { count: logLines.length });
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$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");
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$11.info("MMDS access blocked via iptables");
1141
+ logger$12.info("MMDS access blocked via iptables");
1139
1142
  } catch (err) {
1140
- 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) });
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$11.info("Config ACK sent to orchestrator", { agentId });
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$11.error("Not reconnecting: authentication permanently failed");
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$11.info("Scheduling reconnect", {
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.21";
1311
- case "buildCommit": return "8aeccc2c4";
1312
- case "sdkVersion": return "0.1.21";
1313
- case "sdkBundleHash": return "8308089347c304e41b457d3867b17bbff11d6b5cd9706b6823e7abdbd849f33f";
1314
- case "sharedVersion": return "0.1.21";
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";
1315
1318
  case "sharedBundleHash": return "991385c024392c395d3eb8a68946ef8ed3fcba96f2652f21c3164a54eafa1b1b";
1316
- case "engineVersion": return "0.1.21";
1317
- case "engineBundleHash": return "1b1f49acbbb66f045cfa8284406fce48de8975d9ec3a5685aff70313cd8232cc";
1319
+ case "engineVersion": return "0.1.22";
1320
+ case "engineBundleHash": return "ff74a3afb2b1db2870c03d47543642f64d04b660bebdcde6681f76d874f26757";
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.21";
2004
- AGENT_SDK_BUNDLE_HASH = "8308089347c304e41b457d3867b17bbff11d6b5cd9706b6823e7abdbd849f33f";
2083
+ AGENT_SDK_VERSION = "0.1.22";
2084
+ AGENT_SDK_BUNDLE_HASH = "4b26a42978f77e29db110d47523ec947d3d673c3109fc48141c1a62814bde7a1";
2005
2085
  hookRegistered = false;
2006
2086
  }));
2007
2087
  //#endregion
@@ -2550,7 +2630,8 @@ async function serializeJob(job, generatedNames, ctx, staticNames, allowedGroups
2550
2630
  * Validates against generatedNames union staticNames union allowedGroups.
2551
2631
  *
2552
2632
  * Returns the lock file representation: strings for concrete refs,
2553
- * 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).
2554
2635
  */
2555
2636
  function resolveNeeds(needs, generatedNames, staticNames, allowedGroups) {
2556
2637
  if (!needs || needs.length === 0) return [];
@@ -2565,7 +2646,7 @@ function resolveNeeds(needs, generatedNames, staticNames, allowedGroups) {
2565
2646
  if (!allowedGroups.has(groupRef.group)) throw new Error(`Dynamic group '${groupRef.group}' not found in workflow (available groups: ${[...allowedGroups].join(", ") || "none"})`);
2566
2647
  return {
2567
2648
  group: groupRef.group,
2568
- ifFailed: groupRef.ifFailed ?? "skip"
2649
+ runOn: resolveWhenToRunOn(groupRef.when)
2569
2650
  };
2570
2651
  }
2571
2652
  if (typeof dep === "object" && dep !== null && "group" in dep) {
@@ -2573,7 +2654,7 @@ function resolveNeeds(needs, generatedNames, staticNames, allowedGroups) {
2573
2654
  if (!allowedGroups.has(groupDep.group)) throw new Error(`Dynamic group '${groupDep.group}' not found in workflow (available groups: ${[...allowedGroups].join(", ") || "none"})`);
2574
2655
  return {
2575
2656
  group: groupDep.group,
2576
- ifFailed: groupDep.ifFailed ?? "skip"
2657
+ runOn: resolveWhenToRunOn(groupDep.when)
2577
2658
  };
2578
2659
  }
2579
2660
  if (typeof dep === "object" && dep !== null && "name" in dep && !("steps" in dep)) {
@@ -2581,7 +2662,7 @@ function resolveNeeds(needs, generatedNames, staticNames, allowedGroups) {
2581
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)`);
2582
2663
  return {
2583
2664
  name: namedDep.name,
2584
- ifFailed: namedDep.ifFailed ?? "skip"
2665
+ runOn: resolveWhenToRunOn(namedDep.when)
2585
2666
  };
2586
2667
  }
2587
2668
  if (isDynamicJobFn(dep)) throw new Error("Job dependency cannot be a DynamicJobFn");
@@ -4212,6 +4293,8 @@ function buildRequest(dispatch, workDir) {
4212
4293
  concurrencyEvaluationTimeoutMs: jobConfig.concurrencyEvaluationTimeoutMs,
4213
4294
  branch: dispatch.ref,
4214
4295
  upstreamJobOutputs: dispatch.upstreamJobOutputs,
4296
+ upstreamJobStatuses: dispatch.upstreamJobStatuses,
4297
+ jobNeeds: jobConfig.needs,
4215
4298
  npmRegistries: dispatch.npmRegistries,
4216
4299
  installEnvSecrets: dispatch.installEnvSecrets,
4217
4300
  jobIdShort: dispatch.jobId.slice(0, 8),
@@ -5394,7 +5477,10 @@ var init_sandbox = __esmMin((() => {
5394
5477
  }));
5395
5478
  //#endregion
5396
5479
  //#region src/execution/job-runner.ts
5397
- var job_runner_exports = /* @__PURE__ */ __exportAll({ JobRunner: () => JobRunner$1 });
5480
+ var job_runner_exports = /* @__PURE__ */ __exportAll({
5481
+ JobRunner: () => JobRunner$1,
5482
+ buildEvalNeedsContext: () => buildEvalNeedsContext
5483
+ });
5398
5484
  /**
5399
5485
  * Check if a file exists at the given path.
5400
5486
  */
@@ -6425,14 +6511,14 @@ var init_job_runner = __esmMin((() => {
6425
6511
  */
6426
6512
  init_console_capture();
6427
6513
  init_npm_resolver();
6428
- const AGENT_VERSION = "0.1.21";
6429
- const BUILD_COMMIT = "8aeccc2c4";
6430
- const SDK_VERSION = "0.1.21";
6431
- const SDK_BUNDLE_HASH = "8308089347c304e41b457d3867b17bbff11d6b5cd9706b6823e7abdbd849f33f";
6432
- const SHARED_VERSION = "0.1.21";
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";
6433
6519
  const SHARED_BUNDLE_HASH = "991385c024392c395d3eb8a68946ef8ed3fcba96f2652f21c3164a54eafa1b1b";
6434
- const ENGINE_VERSION = "0.1.21";
6435
- const ENGINE_BUNDLE_HASH = "1b1f49acbbb66f045cfa8284406fce48de8975d9ec3a5685aff70313cd8232cc";
6520
+ const ENGINE_VERSION = "0.1.22";
6521
+ const ENGINE_BUNDLE_HASH = "ff74a3afb2b1db2870c03d47543642f64d04b660bebdcde6681f76d874f26757";
6436
6522
  initTelemetry({
6437
6523
  serviceName: "kici-agent",
6438
6524
  otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT
@@ -6498,6 +6584,7 @@ await guardStartup(logger$1, async () => {
6498
6584
  }
6499
6585
  let isDraining = false;
6500
6586
  let idleShutdownTimer;
6587
+ let rebootIntent = false;
6501
6588
  let client;
6502
6589
  const jobRunner = new JobRunner({
6503
6590
  send: (msg) => client.send(msg),
@@ -6511,7 +6598,15 @@ await guardStartup(logger$1, async () => {
6511
6598
  sendJobContext: (runId, jobId, context) => client.sendJobContext(runId, jobId, context),
6512
6599
  sendRunEvent: (runId, eventType, opts) => client.sendRunEvent(runId, eventType, opts),
6513
6600
  sendConcurrencyReport: (runId, jobId, group) => client.sendConcurrencyReport(runId, jobId, group),
6514
- 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
+ },
6515
6610
  requestUserCache: (jobId, request) => client.requestUserCache(jobId, request),
6516
6611
  relayProvenance: (jobId, request) => client.relayProvenance(jobId, request),
6517
6612
  sendStepApproval: (runId, jobId, request) => client.sendStepApproval(runId, jobId, request)
@@ -6600,6 +6695,13 @@ await guardStartup(logger$1, async () => {
6600
6695
  jobsActive.add(-1);
6601
6696
  metricsReporter.collectAndSend().catch(() => {});
6602
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
+ }
6603
6705
  if (config.scalerManaged && jobRunner.activeJobs.size === 0) {
6604
6706
  if (client.state !== "registered") {
6605
6707
  logger$1.info("Scaler-managed agent idle but disconnected, deferring shutdown until reconnected");