@kici-dev/agent 0.1.14 → 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.
package/README.md CHANGED
@@ -1 +1,13 @@
1
- TBD
1
+ # @kici-dev/agent
2
+
3
+ Customer-deployable agent for the KiCI CI/CD stack. Connects to an orchestrator, clones the workflow repository, executes steps, and streams logs back.
4
+
5
+ You normally don't install this package directly: agents are spawned by the orchestrator's auto-scaler or run from the published container image `quay.io/kici-dev/kici-agent`.
6
+
7
+ Part of [KiCI](https://kici.dev) — CI/CD workflows as TypeScript code: author them with full language power, dry-run them locally, and run them on your own infrastructure.
8
+
9
+ ## Links
10
+
11
+ - Documentation: <https://docs.kici.dev/operator/agent/getting-started/>
12
+ - Source: <https://github.com/kici-dev/kici-public/tree/main/packages/agent>
13
+ - License: AGPL-3.0-only
@@ -0,0 +1,9 @@
1
+ export interface AgentMiniBundleOptions {
2
+ agentId: string;
3
+ logDir?: string;
4
+ logWindowHours: number;
5
+ config: Record<string, unknown>;
6
+ metricsText?: string;
7
+ }
8
+ export declare function buildAgentMiniBundle(opts: AgentMiniBundleOptions): Promise<Buffer>;
9
+ //# sourceMappingURL=mini-bundle.d.ts.map
@@ -0,0 +1,62 @@
1
+ import type { CacheSpec, CacheRestoreResult } from '@kici-dev/sdk';
2
+ /** Override roots — exposed for tests so the home destination is sandboxable. */
3
+ export interface CacheRoots {
4
+ /** Home root override (defaults to `os.homedir()`). */
5
+ home?: string;
6
+ }
7
+ /**
8
+ * Resolve a cache path. `~`-prefixed -> home root; otherwise repo-root-relative.
9
+ * Rejects absolute paths and `..` escapes so a workflow cannot read or clobber
10
+ * files outside its tree / home.
11
+ */
12
+ export declare function resolveCachePath(workDir: string, p: string, roots?: CacheRoots): string;
13
+ /**
14
+ * Pack the spec's paths into a gzip tarball + its SHA-256.
15
+ *
16
+ * Each path is copied into a staging dir under its anchor prefix
17
+ * (`__repo__/<rel>` or `__home__/<rel>`), the staging dir is tarred (portable
18
+ * mode strips uid/gid/mtime), and the staging dir is removed. The resulting
19
+ * tarball self-describes which root each entry restores to.
20
+ */
21
+ export declare function packCachePaths(workDir: string, paths: string[], roots?: CacheRoots): Promise<{
22
+ tarball: Buffer;
23
+ hash: string;
24
+ }>;
25
+ /**
26
+ * Extract a cache tarball buffer, verifying its SHA-256 first, then move each
27
+ * anchored group into place (repo entries under `workDir`, home entries under
28
+ * the home root). Extracts into a scratch dir so a partial restore never
29
+ * leaves half-written paths in the live tree.
30
+ */
31
+ export declare function extractCacheTarball(tarball: Buffer, workDir: string, expectedHash: string, roots?: CacheRoots): Promise<void>;
32
+ /**
33
+ * Stream-download a presigned URL, verify its SHA-256 on the fly (mirrors
34
+ * dep-restore's response -> hash -> gunzip -> tar pipeline), then move the
35
+ * anchored groups into place. Extracts into a scratch dir so a failed download
36
+ * never half-writes the live tree.
37
+ */
38
+ export declare function downloadAndExtractCache(url: string, workDir: string, expectedHash: string, roots?: CacheRoots): Promise<void>;
39
+ /**
40
+ * Transport the cache engine uses to reach the orchestrator over IPC -> WS.
41
+ * Backed by the agent's request/response relay (added to the IPC protocol in a
42
+ * later wiring task).
43
+ */
44
+ export interface CacheTransport {
45
+ restore(key: string, restoreKeys?: string[]): Promise<{
46
+ hit: boolean;
47
+ matchedKey?: string;
48
+ downloadUrl?: string;
49
+ tarHash?: string;
50
+ }>;
51
+ beginSave(key: string): Promise<{
52
+ skip: boolean;
53
+ uploadUrl?: string;
54
+ }>;
55
+ completeSave(key: string, tarHash: string, sizeBytes: number): Promise<void>;
56
+ }
57
+ /** Build the imperative `ctx.cache` API bound to a workDir + transport. */
58
+ export declare function createCacheApi(workDir: string, transport: CacheTransport, roots?: CacheRoots): {
59
+ restore(spec: CacheSpec): Promise<CacheRestoreResult>;
60
+ save(spec: CacheSpec): Promise<void>;
61
+ };
62
+ //# sourceMappingURL=cache-engine.d.ts.map
@@ -0,0 +1,29 @@
1
+ import type { CacheSpec, CacheApi } from '@kici-dev/sdk';
2
+ import type { RunnerToAgentMessage } from '../sandbox/ipc-protocol.js';
3
+ /** The restore outcome remembered per spec key so the save phase can skip exact hits. */
4
+ export interface CacheRestoreOutcome {
5
+ hit: boolean;
6
+ matchedKey?: string;
7
+ }
8
+ export interface CachePhaseDeps {
9
+ /** Imperative cache API (the same one bound to `ctx.cache`). */
10
+ cache: CacheApi;
11
+ /** Emit a runner→agent IPC message (the masked send). */
12
+ sendIpc: (msg: RunnerToAgentMessage) => void;
13
+ /** Monotonic pseudo-step index allocator (continues after real steps + hooks). */
14
+ nextStepIndex: () => number;
15
+ }
16
+ /**
17
+ * Restore every spec, surfacing each as a `cache:restore` pseudo-step. Returns
18
+ * a map keyed by spec key recording whether the EXACT key hit (so the save
19
+ * phase can skip a redundant save of an entry that already exists).
20
+ */
21
+ export declare function restoreCacheSpecs(specs: CacheSpec[], deps: CachePhaseDeps): Promise<Map<string, CacheRestoreOutcome>>;
22
+ /**
23
+ * Save every spec whose EXACT key did not already hit on restore (immutable +
24
+ * no redundant save), surfacing each as a `cache:save` pseudo-step. A spec
25
+ * whose restore matched a different key via a `restoreKeys` prefix is still
26
+ * saved under its exact key.
27
+ */
28
+ export declare function saveCacheSpecs(specs: CacheSpec[], restoreResults: Map<string, CacheRestoreOutcome>, deps: CachePhaseDeps): Promise<void>;
29
+ //# sourceMappingURL=cache-phase.d.ts.map
@@ -0,0 +1,9 @@
1
+ /**
2
+ * User-facing cache engine barrel (sandbox-side).
3
+ *
4
+ * Re-exports the pure pack/extract/checksum helpers and the imperative
5
+ * `ctx.cache` API factory plus its transport interface.
6
+ */
7
+ export { createCacheApi, packCachePaths, extractCacheTarball, downloadAndExtractCache, resolveCachePath, type CacheTransport, type CacheRoots, } from './cache-engine.js';
8
+ export { restoreCacheSpecs, saveCacheSpecs, type CachePhaseDeps, type CacheRestoreOutcome, } from './cache-phase.js';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -18,6 +18,17 @@
18
18
  import type { $ as Shell } from 'zx';
19
19
  import type { Job, Logger } from '@kici-dev/sdk';
20
20
  import type { LockJob } from '@kici-dev/engine';
21
+ /**
22
+ * Thrown when resolving a job's dynamic matrix fails (the matrix function threw
23
+ * or timed out, or returned an unsupported value). Lets the agent attribute the
24
+ * failure to the matrix_expansion init-failure category instead of the generic
25
+ * dynamic_eval bucket.
26
+ */
27
+ export declare class MatrixExpansionError extends Error {
28
+ readonly jobName: string;
29
+ readonly name = "MatrixExpansionError";
30
+ constructor(jobName: string, message: string);
31
+ }
21
32
  /** Maximum number of jobs a single DynamicJobFn can generate. */
22
33
  export declare const MAX_DYNAMIC_JOBS = 100;
23
34
  /**
@@ -27,7 +38,7 @@ export declare const MAX_DYNAMIC_JOBS = 100;
27
38
  * we thread it through the serializer rather than re-creating it.
28
39
  */
29
40
  export interface SerializerContext {
30
- /** Webhook event payload — passed as the first argument to env/environment/concurrencyGroup fns. */
41
+ /** Normalized event envelope — passed as the first argument to env/environment/concurrencyGroup fns. */
31
42
  event: Record<string, unknown>;
32
43
  /** zx shell — passed to dynamic matrix fns. */
33
44
  $: typeof Shell;
@@ -0,0 +1,64 @@
1
+ import type { $ as Shell } from 'zx';
2
+ import type { GenericInitConfig, CacheSpec } from '@kici-dev/sdk';
3
+ import { TimeoutReason } from '@kici-dev/engine';
4
+ import type { RunnerToAgentMessage } from '../sandbox/ipc-protocol.js';
5
+ /**
6
+ * Cache engine port (P2). Init reuses the same imperative cache API bound to
7
+ * `ctx.cache` (and used by the declarative job/step cache phase) — restore the
8
+ * spec's paths before the command, save them after on an exact-key miss. The
9
+ * shape mirrors `@kici-dev/sdk`'s `CacheApi` (`restore`/`save`).
10
+ */
11
+ export interface InitCachePort {
12
+ restore(spec: CacheSpec): Promise<{
13
+ hit: boolean;
14
+ matchedKey?: string;
15
+ }>;
16
+ save(spec: CacheSpec): Promise<void>;
17
+ }
18
+ /**
19
+ * Env-handoff port (P1). Mirrors the KICI_ENV / KICI_PATH file contract that
20
+ * steps use: before the command the agent allocates fresh KICI_ENV / KICI_PATH
21
+ * files and points the shell env at them (`beginCapture`); after a successful
22
+ * command it reads + parses those files into an EnvDelta and applies it through
23
+ * `applyEnvDelta` (operator-secret guard + PATH-prepend order), then truncates
24
+ * the files (`applyDelta`). P1 owns the file lifecycle and the parse; the init
25
+ * phase only sequences the two calls around the command.
26
+ */
27
+ export interface InitEnvPort {
28
+ /** Allocate fresh KICI_ENV/KICI_PATH files for this init; the shell's env points at them. */
29
+ beginCapture(): Promise<void>;
30
+ /** Read+parse the files, apply via applyEnvDelta (operator-secret guard + masking), then truncate. */
31
+ applyDelta(): Promise<void>;
32
+ }
33
+ export interface RunInitPhaseOptions {
34
+ /** Init specs to run, in order. Empty / undefined => no-op (ok:true). */
35
+ specs: GenericInitConfig[];
36
+ /**
37
+ * Returns the sandbox shell to run the i-th init command with.
38
+ * The workflow-runner builds a fresh zx$ shell (cwd=clone root, env=process.env,
39
+ * log->IPC) per init so each spec's `env` overlay + KICI_ENV/KICI_PATH files apply.
40
+ */
41
+ shellFor: (spec: GenericInitConfig, index: number) => typeof Shell;
42
+ /** Masked IPC sender (same as the step loop's maskedSend). */
43
+ sendIpc: (msg: RunnerToAgentMessage) => void;
44
+ /** stepIndex for init:0; subsequent specs use base+1, base+2, … (after user steps + hook indices). */
45
+ stepIndexBase: number;
46
+ /** Cache engine (P2). When a spec sets `cache`, restore before / save-on-miss after. */
47
+ cache?: InitCachePort;
48
+ /** Env-handoff port (P1). When set, capture before each init and apply the delta after success. */
49
+ env?: InitEnvPort;
50
+ }
51
+ export interface RunInitPhaseResult {
52
+ ok: boolean;
53
+ /** Index of the init spec that failed (when ok=false). */
54
+ failedInitIndex?: number;
55
+ /** Failure message (when ok=false). */
56
+ error?: string;
57
+ /** True when the failure was a wall-clock timeout (init exceeded `timeout`). */
58
+ timedOut?: boolean;
59
+ /** Distinct P3 timeout reason (`job_timeout`) when `timedOut` is true. */
60
+ reason?: TimeoutReason;
61
+ }
62
+ /** Run all init specs in order; stop + fail at the first non-zero / timeout. */
63
+ export declare function runInitPhase(opts: RunInitPhaseOptions): Promise<RunInitPhaseResult>;
64
+ //# sourceMappingURL=init-phase.d.ts.map
@@ -21,7 +21,7 @@ export interface InitResult {
21
21
  *
22
22
  * @param workflow - The extracted Workflow object
23
23
  * @param jobName - Name of the job whose dynamic fields to evaluate
24
- * @param event - Normalized webhook event data, passed as argument to dynamic functions
24
+ * @param event - Normalized event envelope same shape every dynamic-function call site receives.
25
25
  * @param flags - Which fields are dynamic and need evaluation
26
26
  * @param timeoutMs - Timeout per dynamic function call (default 60_000ms)
27
27
  */
@@ -1,5 +1,6 @@
1
1
  import type { AgentToOrchestratorMessage, JobDispatch } from '@kici-dev/engine';
2
2
  import type { AppConfig } from '../config.js';
3
+ import type { CacheRequestIpc, CacheResponseIpc, StepApprovalRequestIpc, StepApprovalResolvedIpc } from './sandbox/index.js';
3
4
  /**
4
5
  * Dependencies injected into JobRunner.
5
6
  */
@@ -81,6 +82,23 @@ export interface JobRunnerDeps {
81
82
  * Optional for backward compatibility.
82
83
  */
83
84
  sendApiRequest?: (method: string, params?: Record<string, unknown>) => Promise<unknown>;
85
+ /**
86
+ * Relay a user-facing cache request to the orchestrator and await the
87
+ * response. Translates the sandbox `cache.request` IPC into the matching
88
+ * `cache.user.restore.request` / `cache.user.save.request` /
89
+ * `cache.user.save.complete` WS message and returns the orchestrator's
90
+ * `cache.user.*.response` mapped back onto the IPC response shape.
91
+ * Optional for backward compatibility (callers that don't support the cache).
92
+ */
93
+ requestUserCache?: (jobId: string, request: CacheRequestIpc) => Promise<CacheResponseIpc>;
94
+ /**
95
+ * Relay a step-level approval request to the orchestrator and await the
96
+ * resolution. Translates the sandbox `approval.request` IPC into a
97
+ * `step.approval-request` WS message and returns the orchestrator's
98
+ * `step.approval-resolved` mapped onto the IPC response shape. Optional for
99
+ * backward compatibility (callers that don't support approvals).
100
+ */
101
+ sendStepApproval?: (runId: string, jobId: string, request: StepApprovalRequestIpc) => Promise<StepApprovalResolvedIpc>;
84
102
  }
85
103
  interface ActiveJob {
86
104
  abortController: AbortController;
@@ -114,6 +132,8 @@ export declare class JobRunner {
114
132
  private readonly _sendRunEvent;
115
133
  private readonly _sendConcurrencyReport;
116
134
  private readonly _sendApiRequest?;
135
+ private readonly _requestUserCache?;
136
+ private readonly _sendStepApproval?;
117
137
  /** Tracks running jobs for concurrency and cancellation */
118
138
  readonly activeJobs: Map<string, ActiveJob>;
119
139
  /** Active sandbox for the current job (used for abort). */
@@ -240,6 +260,16 @@ export declare class JobRunner {
240
260
  * Emit a run.event message to the orchestrator for infrastructure lifecycle tracking.
241
261
  */
242
262
  private emitRunEvent;
263
+ /**
264
+ * Emit a `cache.restore` / `cache.save` run event for a cache pseudo-step.
265
+ *
266
+ * The cache phase tags its `step.complete` IPC with a {@link CacheStepType}
267
+ * `step_type` and a `data.cacheOutcome` ({@link CacheOutcome}); when one of
268
+ * those terminal pseudo-step statuses arrives here, mirror it onto the run
269
+ * timeline as a `run.event` so hit/miss/saved/skipped/error is recorded for
270
+ * the dashboard. A no-op for regular steps and hooks.
271
+ */
272
+ private maybeEmitCacheRunEvent;
243
273
  /**
244
274
  * Create a LogStreamer for a synthetic step (build, evaluate, etc.).
245
275
  */
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared environment-delta applier.
3
+ *
4
+ * This is the single code path behind the JS API (ctx.setEnv / ctx.addPath) and
5
+ * the shell-side KICI_ENV / KICI_PATH file contract. Routing both through one
6
+ * helper guarantees they share the operator-secret guard and the PATH-prepend
7
+ * ordering. The generic init phase applies its captured delta through the same
8
+ * function so the init command and a step export env identically.
9
+ */
10
+ /** A parsed environment delta to apply to process.env. */
11
+ export interface EnvDelta {
12
+ /** KEY -> value pairs to set (last-write-wins). */
13
+ env: Record<string, string>;
14
+ /** Directories to prepend to PATH, in order (first entry ends up first on PATH). */
15
+ pathPrepends: string[];
16
+ }
17
+ /** Outcome of applying a delta -- used for logging / tests. */
18
+ export interface ApplyEnvDeltaResult {
19
+ /** Keys that were applied to the target env. */
20
+ appliedKeys: string[];
21
+ /** Keys that were rejected because they collide with an operator secret. */
22
+ rejectedKeys: string[];
23
+ /** Directories prepended to PATH (in the order they ended up on PATH). */
24
+ appliedPaths: string[];
25
+ }
26
+ /** Options controlling how a delta is applied. */
27
+ export interface ApplyEnvDeltaOptions {
28
+ /** Keys that must never be overridden (operator-injected secrets + reserved ctx keys). */
29
+ operatorSecretKeys: Set<string>;
30
+ /** Environment object to mutate. Defaults to process.env. */
31
+ target?: NodeJS.ProcessEnv;
32
+ /** Invoked once per rejected key (e.g. to emit a masked log warning). */
33
+ onReject?: (key: string) => void;
34
+ }
35
+ /**
36
+ * Apply an environment delta to `target` (defaults to process.env), honoring the
37
+ * operator-secret guard.
38
+ *
39
+ * - env keys present in `operatorSecretKeys` are rejected (never override an
40
+ * operator secret); `onReject` fires once per rejected key.
41
+ * - pathPrepends are applied so the FIRST array entry ends up FIRST on PATH.
42
+ */
43
+ export declare function applyEnvDelta(delta: EnvDelta, options: ApplyEnvDeltaOptions): ApplyEnvDeltaResult;
44
+ //# sourceMappingURL=env-delta.d.ts.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * KICI_ENV / KICI_PATH temp-file contract.
3
+ *
4
+ * Before each step the agent points the KICI_ENV and KICI_PATH env vars at fresh
5
+ * temp files. A step's shell commands append `KEY=value` lines to $KICI_ENV and
6
+ * one directory per line to $KICI_PATH. After the step the agent parses both
7
+ * files into an EnvDelta and feeds it through applyEnvDelta() -- the same path
8
+ * the JS API (ctx.setEnv / ctx.addPath) uses -- then truncates the files for the
9
+ * next step.
10
+ *
11
+ * Format v1: single-line `KEY=value` for env (no embedded newlines); one
12
+ * directory per line for path. Blank lines and lines without `=` are ignored.
13
+ */
14
+ import type { EnvDelta } from './env-delta.js';
15
+ /** The pair of temp files the step's shell appends to. */
16
+ export interface EnvFiles {
17
+ /** Path of the file steps append `KEY=value` lines to (KICI_ENV). */
18
+ envFile: string;
19
+ /** Path of the file steps append one directory per line to (KICI_PATH). */
20
+ pathFile: string;
21
+ }
22
+ /**
23
+ * Parse `KEY=value` lines into a record. Blank lines, lines without `=`, and
24
+ * lines with an empty key are ignored. The split is on the first `=` only, so a
25
+ * value may contain `=`. The key is trimmed; the value is taken verbatim after
26
+ * the first `=`. Last assignment to a key wins.
27
+ */
28
+ export declare function parseEnvFileContent(content: string): Record<string, string>;
29
+ /** Parse one trimmed directory per non-blank line, preserving order. */
30
+ export declare function parsePathFileContent(content: string): string[];
31
+ /** Create fresh, empty env + path files inside a private temp dir under `baseDir`. */
32
+ export declare function createEnvFiles(baseDir: string): Promise<EnvFiles>;
33
+ /** Convenience: create env files under the OS temp dir. */
34
+ export declare function createEnvFilesInTmp(): Promise<EnvFiles>;
35
+ /** Read + parse both files into an EnvDelta. Missing/empty files yield an empty delta. */
36
+ export declare function readEnvDelta(files: EnvFiles): Promise<EnvDelta>;
37
+ /** Truncate both files to empty so the next step starts clean. */
38
+ export declare function truncateEnvFiles(files: EnvFiles): Promise<void>;
39
+ //# sourceMappingURL=env-file.d.ts.map
@@ -5,7 +5,7 @@
5
5
  * import { BareMetalSandbox, ContainerSandbox, buildSanitizedEnv } from './sandbox/index.js';
6
6
  */
7
7
  export type { ExecutionSandbox, SandboxSetupOptions, JobExecutionOptions, JobExecutionResult, SandboxStepResult, } from './types.js';
8
- export type { RunnerToAgentMessage, AgentToRunnerMessage, EventEmitRequest, EventEmitResponse, JobExecutionRequest, } from './ipc-protocol.js';
8
+ export type { RunnerToAgentMessage, AgentToRunnerMessage, EventEmitRequest, EventEmitResponse, CacheRequestIpc, CacheResponseIpc, StepApprovalRequestIpc, StepApprovalResolvedIpc, JobExecutionRequest, } from './ipc-protocol.js';
9
9
  export { buildSanitizedEnv } from './env-sanitizer.js';
10
10
  export { ALLOWED_SYSTEM_VARS, KICI_AGENT_ENV_PREFIX, AGENT_REQUIRED_KICI_VARS, } from '@kici-dev/engine';
11
11
  export { BareMetalSandbox } from './bare-metal-sandbox.js';
@@ -41,6 +41,12 @@ interface StepCompleteMessage {
41
41
  step_type?: string;
42
42
  /** Secret key names accessed by this step via ctx.secrets.get() or ctx.secrets.expose(). Never contains values. */
43
43
  secretsAccessed?: string[];
44
+ /**
45
+ * Structured per-step metadata forwarded to the orchestrator's `step.status`
46
+ * `data` field (and persisted for the dashboard timeline). The cache
47
+ * pseudo-steps carry `{ cacheOutcome, key, matchedKey?, bytes? }` here.
48
+ */
49
+ data?: Record<string, unknown>;
44
50
  }
45
51
  /** A single log line from step execution. */
46
52
  interface LogLineMessage {
@@ -123,7 +129,63 @@ export interface AgentApiRequestIpc {
123
129
  /** Method-specific parameters. */
124
130
  params: Record<string, unknown>;
125
131
  }
126
- export type RunnerToAgentMessage = ReadyMessage | StepStartMessage | StepCompleteMessage | LogLineMessage | StepSecretMountMessage | JobCompleteMessage | EventEmitRequest | ConcurrencyReportMessage | AgentApiRequestIpc;
132
+ /**
133
+ * Operation requested by a user-facing cache IPC message.
134
+ *
135
+ * - `restore` — look up a cache entry (exact key + prefix fallbacks).
136
+ * - `beginSave` — request a presigned PUT (declined when the immutable key exists).
137
+ * - `completeSave` — confirm the upload so the orchestrator commits temp -> final.
138
+ */
139
+ export type CacheRequestOp = 'restore' | 'beginSave' | 'completeSave';
140
+ /**
141
+ * Request a user-facing cache operation (runner -> agent). The sandbox runner
142
+ * can't open a WS, so it sends this IPC; the agent relays it to the
143
+ * orchestrator over the WS as a `cache.user.*` message and pipes the response
144
+ * back as a {@link CacheResponseIpc}. Mirrors the {@link AgentApiRequestIpc}
145
+ * relay pattern.
146
+ */
147
+ export interface CacheRequestIpc {
148
+ type: 'cache.request';
149
+ /** UUID for correlating the response. */
150
+ requestId: string;
151
+ /** Which cache operation to perform. */
152
+ op: CacheRequestOp;
153
+ /** Exact cache key (all ops). */
154
+ key: string;
155
+ /** Ordered prefix fallbacks (newest matching entry wins). `restore` only. */
156
+ restoreKeys?: string[];
157
+ /** SHA-256 of the uploaded tarball bytes. `completeSave` only. */
158
+ tarHash?: string;
159
+ /** Tarball size in bytes (drives quota accounting). `completeSave` only. */
160
+ sizeBytes?: number;
161
+ }
162
+ /**
163
+ * Request a step-level approval hold (runner -> agent). The sandbox runner
164
+ * blocks the step loop before a `requireApproval` step; the agent relays this
165
+ * as a `step.approval-request` WS message and pipes the orchestrator's
166
+ * resolution back as a {@link StepApprovalResolvedIpc}. Mirrors the
167
+ * {@link CacheRequestIpc} relay pattern.
168
+ */
169
+ export interface StepApprovalRequestIpc {
170
+ type: 'approval.request';
171
+ /** UUID for correlating the response. */
172
+ requestId: string;
173
+ /** Step index within the job. */
174
+ stepIndex: number;
175
+ /** Step name (for the hold reason / logs). */
176
+ stepName: string;
177
+ /** AND-list of approver clauses (empty = any approval-capable member). */
178
+ clauses: Array<{
179
+ team: string;
180
+ } | {
181
+ user: string;
182
+ }>;
183
+ /** Human label for the gate. */
184
+ reason: string;
185
+ /** Per-gate timeout override (seconds) from the SDK `requireApproval.timeout`. */
186
+ timeoutSeconds?: number;
187
+ }
188
+ export type RunnerToAgentMessage = ReadyMessage | StepStartMessage | StepCompleteMessage | LogLineMessage | StepSecretMountMessage | JobCompleteMessage | EventEmitRequest | ConcurrencyReportMessage | AgentApiRequestIpc | CacheRequestIpc | StepApprovalRequestIpc;
127
189
  /** Instruct the workflow runner to execute a job. */
128
190
  interface ExecuteMessage {
129
191
  type: 'execute';
@@ -170,7 +232,51 @@ export interface AgentApiResponseIpc {
170
232
  /** Error description (present on failure). */
171
233
  error?: string;
172
234
  }
173
- export type AgentToRunnerMessage = ExecuteMessage | AbortMessage | EventEmitResponse | ConcurrencyAckMessage | AgentApiResponseIpc;
235
+ /**
236
+ * Response to a {@link CacheRequestIpc} (agent -> runner). Relayed from the
237
+ * orchestrator's `cache.user.*.response` WS message. Carries the union of the
238
+ * restore-response (`hit` / `matchedKey` / `downloadUrl` / `tarHash`) and
239
+ * save-response (`skip` / `uploadUrl`) fields; `completeSave` resolves with an
240
+ * empty (no-field) response. `error` is set when the relay or orchestrator
241
+ * failed.
242
+ */
243
+ export interface CacheResponseIpc {
244
+ type: 'cache.response';
245
+ /** Matches the original request's requestId. */
246
+ requestId: string;
247
+ /** Restore: true when an entry matched (exact or prefix). */
248
+ hit?: boolean;
249
+ /** Restore: full key that matched (exact key or matched prefix entry's key). */
250
+ matchedKey?: string;
251
+ /** Restore: presigned GET URL for the matched tarball (present only on hit). */
252
+ downloadUrl?: string;
253
+ /** Restore: SHA-256 of the tarball bytes for download integrity verification. */
254
+ tarHash?: string;
255
+ /** Save: true when the immutable key already exists (upload skipped). */
256
+ skip?: boolean;
257
+ /** Save: presigned PUT URL to the temp object (absent when `skip`). */
258
+ uploadUrl?: string;
259
+ /** Error description (present when the relay or orchestrator failed). */
260
+ error?: string;
261
+ }
262
+ /**
263
+ * Resolution of a step-level approval hold (agent -> runner). Relayed from the
264
+ * orchestrator's `step.approval-resolved` WS message. On `approved` the runner
265
+ * runs the step; on `rejected`/`expired` it fails the job. `error` is set when
266
+ * the relay itself failed (treated as a fail-closed reject by the runner).
267
+ */
268
+ export interface StepApprovalResolvedIpc {
269
+ type: 'approval.resolved';
270
+ /** Matches the original request's requestId. */
271
+ requestId: string;
272
+ /** Outcome of the hold. */
273
+ outcome?: 'approved' | 'rejected' | 'expired';
274
+ /** Optional human reason (e.g. the reject reason). */
275
+ reason?: string;
276
+ /** Error description (present when the relay or orchestrator failed). */
277
+ error?: string;
278
+ }
279
+ export type AgentToRunnerMessage = ExecuteMessage | AbortMessage | EventEmitResponse | ConcurrencyAckMessage | AgentApiResponseIpc | CacheResponseIpc | StepApprovalResolvedIpc;
174
280
  /**
175
281
  * All data the workflow runner needs to execute a job inside the sandbox.
176
282
  *
@@ -238,9 +344,11 @@ export interface JobExecutionRequest {
238
344
  maxLogSizeBytes?: number;
239
345
  /** Default step timeout in milliseconds. */
240
346
  defaultStepTimeoutMs?: number;
347
+ /** Total job wall-clock timeout in milliseconds (init + all steps + hooks). When set, the runner aborts the job on breach and reports TimeoutReason.job_timeout. From the lock job's `timeout`. */
348
+ jobTimeoutMs?: number;
241
349
  /** Container configuration passthrough (for container-aware steps). */
242
350
  container?: Record<string, unknown>;
243
- /** Webhook event payload for rule evaluation context. */
351
+ /** Normalized event envelope (type/action/targetBranch/payload…) for rule evaluation and step context. */
244
352
  event?: Record<string, unknown>;
245
353
  /** Git provider that originated the triggering event (e.g. 'github', 'forgejo'). */
246
354
  provider?: string;
@@ -0,0 +1,13 @@
1
+ import { TimeoutReason } from '@kici-dev/engine';
2
+ /** Handle returned by armJobDeadline; call clear() to cancel the timer. */
3
+ export interface JobDeadlineHandle {
4
+ clear(): void;
5
+ }
6
+ /**
7
+ * Arm a job-level wall-clock deadline. When `timeoutMs` is set and elapses
8
+ * before clear() is called, invokes `onTimeout` with the distinct
9
+ * `job_timeout` reason and the configured budget. A no-op when `timeoutMs`
10
+ * is undefined (no job-level cap configured).
11
+ */
12
+ export declare function armJobDeadline(timeoutMs: number | undefined, onTimeout: (reason: TimeoutReason, timeoutMs: number) => void): JobDeadlineHandle;
13
+ //# sourceMappingURL=job-deadline.d.ts.map
@@ -8,6 +8,7 @@
8
8
  import type { Step, StepContext, HookInput, OutputsMap, StepSecretMountRecord } from '@kici-dev/sdk';
9
9
  import type { RunnerToAgentMessage } from './ipc-protocol.js';
10
10
  import type { SandboxStepResult } from './types.js';
11
+ import { type CachePhaseDeps } from '../cache/index.js';
11
12
  /** Job-level hooks passed to the step loop. */
12
13
  export interface JobHooks {
13
14
  beforeStep?: HookInput;
@@ -18,7 +19,7 @@ export interface JobHooks {
18
19
  cleanup?: HookInput;
19
20
  }
20
21
  /** Options for the step execution loop. */
21
- interface StepLoopOptions {
22
+ export interface StepLoopOptions {
22
23
  steps: Step[];
23
24
  /** Factory that creates a StepContext for a given step index and name. */
24
25
  createStepContext: (stepIndex: number, stepName: string) => StepContext;
@@ -31,8 +32,24 @@ interface StepLoopOptions {
31
32
  env: Record<string, string | undefined>;
32
33
  /** Job-level hooks. */
33
34
  jobHooks?: JobHooks;
35
+ /**
36
+ * Declarative cache phase dependencies (cache API + IPC + pseudo-step index
37
+ * allocator). When set, each step's own `cache` specs are restored before the
38
+ * step's `run` and saved after (on an exact-key miss), surfacing as
39
+ * `cache:restore` / `cache:save` pseudo-steps. Absent ⇒ no step-level cache.
40
+ */
41
+ cachePhaseDeps?: CachePhaseDeps;
34
42
  /** Abort check callback. Returns true if job was aborted. */
35
43
  isAborted?: () => boolean;
44
+ /**
45
+ * Aborted when the job-level wall-clock deadline (the lock job's `timeout`)
46
+ * is breached. Threaded into each step's run race so an in-flight step is
47
+ * interrupted immediately on breach — the between-steps `isAborted()` check
48
+ * alone cannot unwind a single long-running step that has no per-step
49
+ * `timeout`. When the signal fires, the step rejects with a job_timeout
50
+ * error and the loop stops.
51
+ */
52
+ jobDeadlineSignal?: AbortSignal;
36
53
  /** Job start time (epoch ms) for outcome metadata duration. */
37
54
  startTime?: number;
38
55
  /**
@@ -54,6 +71,41 @@ interface StepLoopOptions {
54
71
  * orchestrator can persist the audit trail alongside `secretsAccessed`.
55
72
  */
56
73
  getSecretMountRecords?: () => StepSecretMountRecord[];
74
+ /**
75
+ * Before a step's run function executes, point KICI_ENV / KICI_PATH at fresh
76
+ * temp files for this step. Invoked once per executed step (NOT for rule-skipped
77
+ * steps). The workflow-runner owns the file lifecycle.
78
+ */
79
+ beforeStepEnvFiles?: () => Promise<void>;
80
+ /**
81
+ * After a step's run function completes (success OR failure), read the
82
+ * KICI_ENV / KICI_PATH files, apply the delta via applyEnvDelta, and truncate
83
+ * them for the next step. Never throws -- errors are logged by the wired impl.
84
+ */
85
+ afterStepApplyEnvFiles?: () => Promise<void>;
86
+ /**
87
+ * Block a `requireApproval` step pending an orchestrator-side approval hold.
88
+ * The runner sends the normalized requirement and awaits the resolution; the
89
+ * agent keeps job heartbeats flowing during the wait so the agent isn't
90
+ * reaped. Absent ⇒ approvals are not gated (CT / unit harnesses) and steps
91
+ * run unconditionally.
92
+ */
93
+ awaitStepApproval?: (req: {
94
+ stepIndex: number;
95
+ stepName: string;
96
+ clauses: Array<{
97
+ team: string;
98
+ } | {
99
+ user: string;
100
+ }>;
101
+ reason: string;
102
+ timeoutSeconds?: number;
103
+ }) => Promise<StepApprovalResolution>;
104
+ }
105
+ /** Outcome of an awaited step-level approval hold. */
106
+ export interface StepApprovalResolution {
107
+ outcome: 'approved' | 'rejected' | 'expired';
108
+ reason?: string;
57
109
  }
58
110
  /** Result of the step execution loop. */
59
111
  interface StepLoopResult {
@@ -1,5 +1,5 @@
1
1
  import type { JobDispatch } from '@kici-dev/engine';
2
- import type { EventEmitRequest, EventEmitResponse, ConcurrencyReportMessage, ConcurrencyAckMessage } from './ipc-protocol.js';
2
+ import type { EventEmitRequest, EventEmitResponse, ConcurrencyReportMessage, ConcurrencyAckMessage, CacheRequestIpc, CacheResponseIpc, StepApprovalRequestIpc, StepApprovalResolvedIpc } from './ipc-protocol.js';
3
3
  /**
4
4
  * Common interface for all execution sandbox backends.
5
5
  *
@@ -79,6 +79,25 @@ export interface JobExecutionOptions {
79
79
  * Optional for backward compatibility (callers that don't support the agent API).
80
80
  */
81
81
  onApiRequest?: (method: string, params: Record<string, unknown>) => Promise<unknown>;
82
+ /**
83
+ * Callback for relaying a user-facing cache request from the sandbox to the
84
+ * orchestrator. The sandbox runner sends `cache.request` IPC; the agent wraps
85
+ * it in the matching `cache.user.*` WS message and forwards to the
86
+ * orchestrator. Returns the orchestrator's response (or an error response).
87
+ *
88
+ * Optional so backends / harnesses that don't thread the cache through keep
89
+ * working — the runner falls back to a "not configured" cache response.
90
+ */
91
+ onCacheRequest?: (request: CacheRequestIpc) => Promise<CacheResponseIpc>;
92
+ /**
93
+ * Callback for relaying a step-level approval request from the sandbox to the
94
+ * orchestrator. The sandbox runner sends `approval.request` IPC; the agent
95
+ * wraps it in a `step.approval-request` WS message and forwards to the
96
+ * orchestrator, awaiting the `step.approval-resolved` response which it pipes
97
+ * back as `approval.resolved`. Optional so harnesses that don't thread
98
+ * approvals keep working — the runner falls back to a fail-closed reject.
99
+ */
100
+ onApprovalRequest?: (request: StepApprovalRequestIpc) => Promise<StepApprovalResolvedIpc>;
82
101
  /**
83
102
  * Callback fired once per `ctx.secrets.mountFile` / `exposeFile` call the
84
103
  * workflow runner performs. Carries only key names + the resulting path /