@kici-dev/agent 0.1.15 → 0.1.16

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.
@@ -8,7 +8,7 @@ import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:pa
8
8
  import { $ } from "zx";
9
9
  import { createLogger, deriveSharedSecret, initZx, normalizeLineEndings, sha256, sha256File, toErrorMessage } from "@kici-dev/shared";
10
10
  import { CacheOutcome, CacheStepType, ExecutionJobStatus, ExecutionStepStatus, TimeoutReason } from "@kici-dev/engine";
11
- import { buildKiciApi, createStepSecrets, evaluateRules, isDynamicJobFn, normalizeCacheSpecs, resolveJobOutputs, resolveStepOutputs, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "@kici-dev/sdk";
11
+ import { buildKiciApi, createStepSecrets, evaluateRules, isDynamicJobFn, normalizeCacheSpecs, normalizeRequireApproval, resolveJobOutputs, resolveStepOutputs, setJobOutputsMap, setStepOutputsMap, setStepRefMap } from "@kici-dev/sdk";
12
12
  import { Readable, Transform } from "node:stream";
13
13
  import { pipeline } from "node:stream/promises";
14
14
  import { createGunzip } from "node:zlib";
@@ -1123,6 +1123,65 @@ async function evaluateStepRulesAndMaybeSkip(step, stepIndex, opts) {
1123
1123
  };
1124
1124
  }
1125
1125
  /**
1126
+ * Manual approval gate for a step. When the step declares `requireApproval`
1127
+ * and the harness wired `awaitStepApproval`, block until the orchestrator
1128
+ * resolves the hold. Returns a failed `StepIterationOutcome` (breaking the
1129
+ * loop) on reject/expired; returns null when approved or when no gate applies.
1130
+ */
1131
+ async function maybeGateStepApproval(step, stepIndex, opts) {
1132
+ if (step.requireApproval === void 0 || !opts.awaitStepApproval) return null;
1133
+ const normalized = normalizeRequireApproval(step.requireApproval);
1134
+ opts.sendIpc({
1135
+ type: "log.line",
1136
+ stepIndex,
1137
+ line: `[kici] Step '${step.name}' awaiting approval...`
1138
+ });
1139
+ const resolution = await opts.awaitStepApproval({
1140
+ stepIndex,
1141
+ stepName: step.name,
1142
+ clauses: normalized.clauses,
1143
+ reason: normalized.reason ?? `Approval required for step '${step.name}'`,
1144
+ timeoutSeconds: normalized.timeoutSeconds
1145
+ });
1146
+ if (resolution.outcome === "approved") {
1147
+ opts.sendIpc({
1148
+ type: "log.line",
1149
+ stepIndex,
1150
+ line: `[kici] Step '${step.name}' approved.`
1151
+ });
1152
+ return null;
1153
+ }
1154
+ const why = resolution.outcome === "expired" ? "approval expired" : `approval rejected${resolution.reason ? `: ${resolution.reason}` : ""}`;
1155
+ opts.sendIpc({
1156
+ type: "step.start",
1157
+ stepIndex,
1158
+ stepName: step.name
1159
+ });
1160
+ opts.sendIpc({
1161
+ type: "step.complete",
1162
+ stepIndex,
1163
+ status: ExecutionStepStatus.enum.failed,
1164
+ durationMs: 0
1165
+ });
1166
+ opts.sendIpc({
1167
+ type: "log.line",
1168
+ stepIndex,
1169
+ line: `[kici] Step '${step.name}' ${why}.`
1170
+ });
1171
+ await opts.disposeStepResources?.();
1172
+ return {
1173
+ result: {
1174
+ name: step.name,
1175
+ stepIndex,
1176
+ status: ExecutionStepStatus.enum.failed,
1177
+ durationMs: 0,
1178
+ error: { message: `Step '${step.name}' ${why}` }
1179
+ },
1180
+ shouldBreak: true,
1181
+ failedStepName: step.name
1182
+ };
1183
+ }
1184
+ /**
1126
1185
  * Run a single observer hook (beforeStep / afterStep). Failures only emit a
1127
1186
  * log line — they never change job status. Centralises the per-call boilerplate
1128
1187
  * so the per-step body can stay flat.
@@ -1169,6 +1228,8 @@ async function runStepIteration(step, stepIndex, opts) {
1169
1228
  shouldBreak: false
1170
1229
  };
1171
1230
  }
1231
+ const gate = await maybeGateStepApproval(step, stepIndex, opts);
1232
+ if (gate) return gate;
1172
1233
  if (opts.jobHooks?.beforeStep) await runObserverHook({
1173
1234
  hook: opts.jobHooks.beforeStep,
1174
1235
  hookType: "beforeStep",
@@ -2179,8 +2240,8 @@ function logSubprocessStreams(e, tokens) {
2179
2240
  * no Rolldown step at runtime. `@kici-dev/sdk` and host-repo deps resolve via
2180
2241
  * Node's normal ESM lookup against `.kici/node_modules/`.
2181
2242
  */
2182
- const AGENT_SDK_VERSION = "0.1.15";
2183
- const AGENT_SDK_BUNDLE_HASH = "cee24b67bbb4a1d2a770e4063dd37a9af33409faeb5ce32f936703b4431316dd";
2243
+ const AGENT_SDK_VERSION = "0.1.16";
2244
+ const AGENT_SDK_BUNDLE_HASH = "cd4b8e0d91efe578c96e7c746f89b44a3df8fcaebaec2185399168ef8eb4ebca";
2184
2245
  /**
2185
2246
  * Register the `@kici-dev/core/ts-loader-hook` oxc-transform ESM loader hook so
2186
2247
  * subsequent dynamic `import()` calls for `.ts` / `.tsx` files transform on the
@@ -2873,6 +2934,68 @@ function waitForCacheResponse(requestId) {
2873
2934
  });
2874
2935
  }
2875
2936
  /**
2937
+ * Pending promises for approval.resolved messages from the agent.
2938
+ * Key: requestId (correlates approval.request -> approval.resolved).
2939
+ */
2940
+ const pendingApprovalResolutions = /* @__PURE__ */ new Map();
2941
+ /**
2942
+ * Hard ceiling on how long the runner blocks a step waiting for an approval
2943
+ * resolution. The orchestrator enforces the real (org-/SDK-configured) expiry
2944
+ * and sends `expired` when it lapses; this is a safety net well above any sane
2945
+ * approval window so the runner cannot hang forever if the resolution is lost.
2946
+ */
2947
+ const APPROVAL_RESOLUTION_TIMEOUT_MS = 10080 * 60 * 1e3;
2948
+ /** Wait for an approval.resolved from the agent with the given requestId. */
2949
+ function waitForApprovalResolution(requestId) {
2950
+ return new Promise((resolve) => {
2951
+ const timer = setTimeout(() => {
2952
+ pendingApprovalResolutions.delete(requestId);
2953
+ resolve({
2954
+ type: "approval.resolved",
2955
+ requestId,
2956
+ outcome: "expired"
2957
+ });
2958
+ }, APPROVAL_RESOLUTION_TIMEOUT_MS);
2959
+ pendingApprovalResolutions.set(requestId, {
2960
+ resolve: (response) => {
2961
+ clearTimeout(timer);
2962
+ pendingApprovalResolutions.delete(requestId);
2963
+ resolve(response);
2964
+ },
2965
+ timer
2966
+ });
2967
+ });
2968
+ }
2969
+ /**
2970
+ * Build the `awaitStepApproval` callback the step loop uses to block on a
2971
+ * `requireApproval` step. Sends an `approval.request` IPC (relayed by the agent
2972
+ * over the WS as a `step.approval-request`) and awaits the matching
2973
+ * `approval.resolved`. A relay error is treated as a fail-closed reject.
2974
+ */
2975
+ function buildAwaitStepApproval() {
2976
+ return async (req) => {
2977
+ const requestId = randomUUID();
2978
+ sendMessage({
2979
+ type: "approval.request",
2980
+ requestId,
2981
+ stepIndex: req.stepIndex,
2982
+ stepName: req.stepName,
2983
+ clauses: req.clauses,
2984
+ reason: req.reason,
2985
+ ...req.timeoutSeconds !== void 0 && { timeoutSeconds: req.timeoutSeconds }
2986
+ });
2987
+ const resolution = await waitForApprovalResolution(requestId);
2988
+ if (resolution.error) return {
2989
+ outcome: "rejected",
2990
+ reason: resolution.error
2991
+ };
2992
+ return {
2993
+ outcome: resolution.outcome ?? "rejected",
2994
+ ...resolution.reason !== void 0 && { reason: resolution.reason }
2995
+ };
2996
+ };
2997
+ }
2998
+ /**
2876
2999
  * Build the {@link CacheTransport} the sandbox-side cache engine uses to reach
2877
3000
  * the orchestrator. Each method sends a `cache.request` IPC (relayed by the
2878
3001
  * agent over the WS as a `cache.user.*` message) and awaits the matching
@@ -2985,6 +3108,9 @@ function dispatchAgentMessage(msg) {
2985
3108
  } else if (msg.type === "cache.response") {
2986
3109
  const pending = pendingCacheResponses.get(msg.requestId);
2987
3110
  if (pending) pending.resolve(msg);
3111
+ } else if (msg.type === "approval.resolved") {
3112
+ const pending = pendingApprovalResolutions.get(msg.requestId);
3113
+ if (pending) pending.resolve(msg);
2988
3114
  }
2989
3115
  }
2990
3116
  if (isForkMode) process.on("message", (msg) => {
@@ -4093,7 +4219,8 @@ async function main() {
4093
4219
  if (disposeFn) await disposeFn();
4094
4220
  },
4095
4221
  beforeStepEnvFiles: stepEnvHooks.beforeStepEnvFiles,
4096
- afterStepApplyEnvFiles: stepEnvHooks.afterStepApplyEnvFiles
4222
+ afterStepApplyEnvFiles: stepEnvHooks.afterStepApplyEnvFiles,
4223
+ awaitStepApproval: buildAwaitStepApproval()
4097
4224
  });
4098
4225
  jobDeadline.clear();
4099
4226
  await maybeSaveJobCache(jobCacheSpecs, jobCacheRestore, cachePhaseDeps, !aborted && loopResult.status === ExecutionStepStatus.enum.success);
@@ -1,7 +1,7 @@
1
- import { type AgentToOrchestratorMessage, type JobDispatch, type JobCancel } from '@kici-dev/engine';
2
- import type { CacheRequestIpc, CacheResponseIpc } from '../execution/sandbox/index.js';
1
+ import { type AgentToOrchestratorMessage, type JobDispatch, type JobCancel, type FleetLogsRequest } from '@kici-dev/engine';
2
+ import type { CacheRequestIpc, CacheResponseIpc, StepApprovalRequestIpc, StepApprovalResolvedIpc } from '../execution/sandbox/index.js';
3
3
  export type ConnectionState = 'disconnected' | 'connecting' | 'authenticating' | 'registering' | 'registered';
4
- interface OrchestratorClientOptions {
4
+ export interface OrchestratorClientOptions {
5
5
  /** WebSocket URL of the orchestrator. */
6
6
  url: string;
7
7
  /** Agent's unique identifier. */
@@ -35,6 +35,18 @@ interface OrchestratorClientOptions {
35
35
  * doesn't have to read process.env directly.
36
36
  */
37
37
  scalerManaged?: boolean;
38
+ /**
39
+ * Supplies the inputs for the agent fleet mini-bundle on a fleet.logs.request.
40
+ * Returns the agent's resolved config (redacted inside the bundle assembler),
41
+ * the log directory, and the current Prometheus metrics text. Omitted in
42
+ * tests / contexts that don't participate in fleet collection — the handler
43
+ * then replies with an empty-config bundle.
44
+ */
45
+ getFleetBundleInputs?: () => Promise<{
46
+ config: Record<string, unknown>;
47
+ logDir?: string;
48
+ metricsText?: string;
49
+ }>;
38
50
  }
39
51
  /**
40
52
  * WebSocket client that connects the agent to the customer orchestrator.
@@ -70,6 +82,13 @@ export declare class OrchestratorClient {
70
82
  private readonly pendingApiRequests;
71
83
  /** Pending user-cache restore/save requests awaiting orchestrator response. */
72
84
  private readonly pendingUserCacheRequests;
85
+ /**
86
+ * Pending step-approval requests awaiting the orchestrator's resolution.
87
+ * No client-side timeout: the orchestrator owns the (org-/SDK-configured)
88
+ * expiry and sends `step.approval-resolved: expired` when it lapses. The
89
+ * workflow-runner carries an outer safety-net timeout.
90
+ */
91
+ private readonly pendingStepApprovals;
73
92
  /** Pending concurrency report requests awaiting orchestrator ack. */
74
93
  private readonly pendingConcurrencyRequests;
75
94
  private readonly url;
@@ -83,6 +102,7 @@ export declare class OrchestratorClient {
83
102
  private readonly getInFlightJobs?;
84
103
  private readonly roles;
85
104
  private readonly scalerManaged;
105
+ private readonly getFleetBundleInputs?;
86
106
  /** Timestamp when the connection was lost, used for gap marker outage duration. */
87
107
  private disconnectedAt;
88
108
  /** Set to true when auth.failure is received. Prevents retrying with a bad token. */
@@ -180,6 +200,12 @@ export declare class OrchestratorClient {
180
200
  deliveryId?: string;
181
201
  error?: string;
182
202
  }>;
203
+ /**
204
+ * Build this agent's fleet mini-bundle and stream it back to the orchestrator
205
+ * as ordered fleet.bundle.chunk frames (the WS frame cap forbids one frame).
206
+ * On failure, sends a single fleet.bundle.error. Public for unit testing.
207
+ */
208
+ streamFleetBundle(req: FleetLogsRequest): Promise<void>;
183
209
  /**
184
210
  * Send a typed API request to the orchestrator and await the response.
185
211
  *
@@ -202,6 +228,15 @@ export declare class OrchestratorClient {
202
228
  * Times out after 30 seconds for the round-trip ops.
203
229
  */
204
230
  requestUserCache(jobId: string, request: CacheRequestIpc): Promise<CacheResponseIpc>;
231
+ /**
232
+ * Relay a step-level approval request to the orchestrator. Sends a
233
+ * `step.approval-request` WS message and resolves with the orchestrator's
234
+ * `step.approval-resolved` mapped onto the IPC response shape. No client-side
235
+ * timeout — the orchestrator owns the approval expiry and replies with an
236
+ * `expired` outcome when it lapses. Rejects only on disconnect (the relay
237
+ * caller treats a rejection as a fail-closed reject).
238
+ */
239
+ sendStepApproval(runId: string, jobId: string, request: StepApprovalRequestIpc): Promise<StepApprovalResolvedIpc>;
205
240
  /**
206
241
  * Send a job.context message to the orchestrator.
207
242
  *
@@ -283,5 +318,4 @@ export declare class OrchestratorClient {
283
318
  private scheduleReconnect;
284
319
  private cancelReconnect;
285
320
  }
286
- export {};
287
321
  //# sourceMappingURL=orchestrator-client.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kici-dev/agent",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "description": "Customer-deployable agent for the KiCI CI/CD stack. Connects to an orchestrator, clones the workflow repo, executes steps, and streams logs back.",
5
5
  "keywords": [
6
6
  "ci",
@@ -54,6 +54,7 @@
54
54
  },
55
55
  "dependencies": {
56
56
  "@hono/node-server": "^2.0.0",
57
+ "archiver": "^7.0.1",
57
58
  "dockerode": "^5.0.0",
58
59
  "hono": "^4.12.18",
59
60
  "tar": "^7.5.13",
@@ -61,10 +62,10 @@
61
62
  "ws": "^8.20.0",
62
63
  "zod": "^4.3.6",
63
64
  "zx": "^8.8.5",
64
- "@kici-dev/core": "0.1.15",
65
- "@kici-dev/sdk": "0.1.15",
66
- "@kici-dev/engine": "0.1.15",
67
- "@kici-dev/shared": "0.1.15"
65
+ "@kici-dev/core": "0.1.16",
66
+ "@kici-dev/engine": "0.1.16",
67
+ "@kici-dev/shared": "0.1.16",
68
+ "@kici-dev/sdk": "0.1.16"
68
69
  },
69
70
  "kici": {
70
71
  "metrics": {
@@ -73,6 +74,7 @@
73
74
  }
74
75
  },
75
76
  "devDependencies": {
77
+ "@types/archiver": "^7.0.0",
76
78
  "@types/dockerode": "^4.0.1"
77
79
  },
78
80
  "scripts": {