@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 +2 -0
- package/dist/execution/job-runner.d.ts +10 -0
- package/dist/execution/reboot.d.ts +26 -0
- package/dist/execution/sandbox/ipc-protocol.d.ts +10 -1
- package/dist/execution/sandbox/step-loop.d.ts +27 -5
- package/dist/execution/sandbox/workflow-runner.d.ts +11 -0
- package/dist/index.js +3 -1
- package/dist/server.js +153 -51
- package/dist/workflow-runner.js +171 -48
- package/package.json +5 -5
- package/sbom.spdx.json +62 -62
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 `
|
|
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
|
|
94
|
-
* The runner sends the normalized requirement and awaits the
|
|
95
|
-
* agent keeps job heartbeats flowing during the wait so the
|
|
96
|
-
* reaped. Absent ⇒ approvals are not gated (CT / unit harnesses)
|
|
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$
|
|
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.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.
|
|
1317
|
-
case "engineBundleHash": return "
|
|
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.
|
|
2004
|
-
AGENT_SDK_BUNDLE_HASH = "
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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({
|
|
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.
|
|
6429
|
-
const BUILD_COMMIT = "
|
|
6430
|
-
const SDK_VERSION = "0.1.
|
|
6431
|
-
const SDK_BUNDLE_HASH = "
|
|
6432
|
-
const SHARED_VERSION = "0.1.
|
|
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.
|
|
6435
|
-
const ENGINE_BUNDLE_HASH = "
|
|
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) =>
|
|
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");
|