@kici-dev/agent 0.1.26 → 0.2.0

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.
Files changed (39) hide show
  1. package/dist/bootstrap/ensure-init-runner.d.ts +23 -22
  2. package/dist/bootstrap/payload-source.d.ts +32 -0
  3. package/dist/bootstrap/probe-platform.d.ts +21 -0
  4. package/dist/bootstrap/restage-agent.d.ts +43 -0
  5. package/dist/bootstrap/run-restage.d.ts +12 -0
  6. package/dist/bootstrap/s3-payload-source.d.ts +35 -0
  7. package/dist/bootstrap/ssh-exec.d.ts +14 -0
  8. package/dist/bootstrap/stage-agent-payload.d.ts +41 -0
  9. package/dist/checkout/changed-files.d.ts +34 -0
  10. package/dist/config.d.ts +42 -14
  11. package/dist/container-ts-loader-hook.js +147710 -0
  12. package/dist/execution/artifacts/artifact-engine.d.ts +51 -0
  13. package/dist/execution/dep-installer.d.ts +3 -3
  14. package/dist/execution/init-runner.d.ts +4 -4
  15. package/dist/execution/job-runner.d.ts +41 -6
  16. package/dist/execution/log-streamer.d.ts +17 -2
  17. package/dist/execution/rule-evaluator.d.ts +1 -12
  18. package/dist/execution/sandbox/container-hardening.d.ts +80 -0
  19. package/dist/execution/sandbox/container-sandbox.d.ts +54 -0
  20. package/dist/execution/sandbox/container-ts-loader-hook.d.ts +26 -0
  21. package/dist/execution/sandbox/env-sanitizer.d.ts +12 -3
  22. package/dist/execution/sandbox/fork-runner.d.ts +14 -0
  23. package/dist/execution/sandbox/index.d.ts +2 -1
  24. package/dist/execution/sandbox/ipc-protocol.d.ts +82 -6
  25. package/dist/execution/sandbox/step-loop.d.ts +4 -0
  26. package/dist/execution/sandbox/types.d.ts +25 -4
  27. package/dist/execution/sandbox/workflow-runner.d.ts +111 -5
  28. package/dist/execution/streaming-zx-log.d.ts +38 -0
  29. package/dist/execution/tmp-gc.d.ts +22 -7
  30. package/dist/execution/workflow-loader.d.ts +32 -1
  31. package/dist/index.js +50 -45
  32. package/dist/provenance/statement-builder.d.ts +3 -2
  33. package/dist/server.d.ts +1 -1
  34. package/dist/server.js +1360 -193
  35. package/dist/workflow-runner-bundle.js +215393 -0
  36. package/dist/workflow-runner.js +937 -256
  37. package/dist/ws/orchestrator-client.d.ts +105 -1
  38. package/package.json +14 -12
  39. package/sbom.spdx.json +1092 -1723
@@ -0,0 +1,51 @@
1
+ import type { ArtifactsApi } from '@kici-dev/sdk';
2
+ import type { ArtifactRejectReason, ArtifactUploadOutcome, ArtifactDownloadOutcome } from '@kici-dev/engine';
3
+ import { type CacheRoots } from '../cache/cache-engine.js';
4
+ /** Outcome of a begin-upload grant request. */
5
+ export interface ArtifactBeginUploadResult {
6
+ outcome: ArtifactUploadOutcome;
7
+ /** Presigned PUT URL — present only on `granted`. */
8
+ uploadUrl?: string;
9
+ /** Storage key echoed back on complete — present only on `granted`. */
10
+ storageKey?: string;
11
+ /** Enforcement-gate rejection reason — present only on an enforcement `rejected`. */
12
+ reason?: ArtifactRejectReason;
13
+ /**
14
+ * Non-enforcement refusal detail — present only on `rejected` when no
15
+ * enforcement `reason` applies (a name that violates the artifact-name
16
+ * contract, orchestrator misconfiguration, or an internal error). A safe,
17
+ * fixed string from the orchestrator; never a raw exception.
18
+ */
19
+ error?: string;
20
+ }
21
+ /** Outcome of a download lookup. */
22
+ export interface ArtifactDownloadLookup {
23
+ outcome: ArtifactDownloadOutcome;
24
+ /** Presigned GET URL — present only on `found`. */
25
+ downloadUrl?: string;
26
+ /** Artifact size in bytes — present only on `found`. */
27
+ sizeBytes?: number;
28
+ /** SHA-256 (hex) of the tarball bytes — present only on `found`. */
29
+ sha256?: string;
30
+ /**
31
+ * Internal-failure detail — present only on `not_found` when the outcome
32
+ * reflects an orchestrator failure rather than a genuinely missing artifact. A
33
+ * safe, fixed string from the orchestrator; never a raw exception.
34
+ */
35
+ error?: string;
36
+ }
37
+ /**
38
+ * Transport the artifacts engine uses to reach the orchestrator over IPC -> WS.
39
+ * Backed by the agent's request/response relay (mirrors {@link CacheTransport}).
40
+ */
41
+ export interface ArtifactTransport {
42
+ /** Request a presigned PUT for `name` at `declaredSizeBytes`; enforced before minting. */
43
+ beginUpload(name: string, declaredSizeBytes: number): Promise<ArtifactBeginUploadResult>;
44
+ /** Confirm the upload finished — records the DB row. */
45
+ completeUpload(name: string, sizeBytes: number, sha256: string, storageKey: string): Promise<void>;
46
+ /** Resolve a named artifact of this run to a presigned GET + its size/sha256. */
47
+ download(name: string): Promise<ArtifactDownloadLookup>;
48
+ }
49
+ /** Build the imperative `ctx.artifacts` API bound to a workDir + transport. */
50
+ export declare function createArtifactsApi(workDir: string, transport: ArtifactTransport, roots?: CacheRoots): ArtifactsApi;
51
+ //# sourceMappingURL=artifact-engine.d.ts.map
@@ -41,9 +41,9 @@ export interface InstallDepsOptions {
41
41
  * Install `.kici/` dependencies inline with the repo's package manager.
42
42
  *
43
43
  * Falls back to this when the dep cache is unavailable or a download fails.
44
- * The install runs with an isolated cache/store directory (created in
45
- * `os.tmpdir()`) to prevent cache poisoning between build jobs; the directory
46
- * is removed after installation.
44
+ * The install runs with an isolated cache/store directory (allocated under the
45
+ * global temp base, which honors `KICI_TMPDIR`) to prevent cache poisoning
46
+ * between build jobs; the directory is removed after installation.
47
47
  *
48
48
  * If `opts.npmRegistries` / `opts.installEnvSecrets` is provided, a job-scoped
49
49
  * `.kici/.npmrc` overlay is synthesized for the install, restored in `finally`,
@@ -5,8 +5,8 @@ import { type MatrixValues } from '@kici-dev/engine';
5
5
  * Only fields that were flagged as dynamic and successfully resolved are set.
6
6
  */
7
7
  export interface InitResult {
8
- /** Resolved bound-environment names, in merge order (one per `environments` element). */
9
- environmentNames?: string[];
8
+ /** Resolved bound-context names, in merge order (one per `contexts` element). */
9
+ contextNames?: string[];
10
10
  env?: Record<string, string>;
11
11
  concurrencyGroup?: string;
12
12
  /**
@@ -16,7 +16,7 @@ export interface InitResult {
16
16
  matrixValues?: MatrixValues[];
17
17
  }
18
18
  /**
19
- * Evaluate dynamic fields (environment, env, concurrencyGroup) on a job.
19
+ * Evaluate dynamic fields (context, env, concurrencyGroup) on a job.
20
20
  *
21
21
  * Only fields with their corresponding flag set to true AND whose property
22
22
  * on the job is a function will be evaluated. All evaluations happen in a
@@ -33,7 +33,7 @@ export interface InitResult {
33
33
  * @param timeoutMs - Timeout per dynamic function call (default 60_000ms)
34
34
  */
35
35
  export declare function evaluateDynamicFields(workflow: Workflow, jobName: string, event: Record<string, unknown>, flags: {
36
- dynamicEnvironment: boolean;
36
+ dynamicContext: boolean;
37
37
  dynamicEnv: boolean;
38
38
  dynamicConcurrencyGroup: boolean;
39
39
  dynamicMatrix?: boolean;
@@ -1,15 +1,13 @@
1
1
  import type { AgentToOrchestratorMessage, JobDispatch } from '@kici-dev/engine';
2
2
  import type { AppConfig } from '../config.js';
3
3
  import { buildNeedsContext } from '@kici-dev/sdk';
4
- import type { CacheRequestIpc, CacheResponseIpc, ProvenanceRequestIpc, ProvenanceResponseIpc, StepApprovalRequestIpc, StepApprovalResolvedIpc } from './sandbox/index.js';
4
+ import type { CacheRequestIpc, CacheResponseIpc, ProvenanceRequestIpc, ProvenanceResponseIpc, ArtifactRequestIpc, ArtifactResponseIpc, StepApprovalRequestIpc, StepApprovalResolvedIpc } from './sandbox/index.js';
5
5
  /**
6
6
  * Dependencies injected into JobRunner.
7
7
  */
8
8
  export interface JobRunnerDeps {
9
- /** Send function for WS messages (buffered) */
9
+ /** Send function for WS messages (buffered; replayed on reconnect) */
10
10
  send: (msg: AgentToOrchestratorMessage) => void;
11
- /** Send direct function (bypasses buffer, for protocol messages) */
12
- sendDirect: (msg: AgentToOrchestratorMessage) => void;
13
11
  /** Agent config */
14
12
  config: AppConfig;
15
13
  /** Request a pre-signed S3 upload URL from the orchestrator via WS request-response. */
@@ -99,6 +97,14 @@ export interface JobRunnerDeps {
99
97
  * compatibility (callers that don't support provenance).
100
98
  */
101
99
  relayProvenance?: (jobId: string, request: ProvenanceRequestIpc) => Promise<ProvenanceResponseIpc>;
100
+ /**
101
+ * Relay a user-facing artifact request to the orchestrator and await the
102
+ * response. Translates the sandbox `artifacts.request` IPC into the matching
103
+ * `artifacts.upload.request` / `.complete` / `artifacts.download.request` WS
104
+ * message and returns the orchestrator's response mapped back onto the IPC
105
+ * response shape. Optional for backward compatibility.
106
+ */
107
+ requestUserArtifact?: (jobId: string, request: ArtifactRequestIpc) => Promise<ArtifactResponseIpc>;
102
108
  /**
103
109
  * Relay a step-level approval request to the orchestrator and await the
104
110
  * resolution. Translates the sandbox `approval.request` IPC into a
@@ -113,6 +119,17 @@ interface ActiveJob {
113
119
  completionPromise: Promise<void>;
114
120
  runId: string;
115
121
  }
122
+ /**
123
+ * Derive the self-contained runner bundle path from the resolved runner path.
124
+ *
125
+ * The container backend mounts the runner as a single file into the customer
126
+ * job container, so it must run `workflow-runner-bundle.js` (zx + `@kici-dev/*`
127
+ * inlined) rather than the external `workflow-runner.js`, which cannot resolve
128
+ * its bare imports without the agent's node_modules / pnpm workspace. The
129
+ * bundle is a flat sibling emitted alongside the runner by build-service.mjs.
130
+ * bwrap / firecracker keep the external runner (they bind the workspace).
131
+ */
132
+ export declare function resolveRunnerBundlePath(runnerPath: string): string;
116
133
  /**
117
134
  * Build the result-aware `ctx.needs` for a dynamic eval from its frozen upstream
118
135
  * snapshot. Returns undefined for an event-only generator (no snapshot).
@@ -122,6 +139,23 @@ export declare function buildEvalNeedsContext(config: {
122
139
  declaredNeeds?: readonly unknown[];
123
140
  upstreamSnapshot?: import('@kici-dev/engine').UpstreamSnapshot;
124
141
  }): ReturnType<typeof buildNeedsContext> | undefined;
142
+ /**
143
+ * Resolve a job's workDir and its cleanup.
144
+ *
145
+ * - Default: a fresh `mkdtemp` the agent clones into and removes after the job.
146
+ * - **In-place profile** (`inPlace` config + a `file://` source): the source's
147
+ * real repo path used directly as the workDir, with **no clone** and **no
148
+ * removal**. This is the routed `deploy:stg` profile — the operator runs their
149
+ * own already-built working tree (module-relative `MONOREPO_ROOT`,
150
+ * `node_modules`, `dist` all present). Gated to `file://` so a
151
+ * Platform-connected agent (https sources) can never be pushed onto a tree,
152
+ * and read only from agent config (never a dispatch/wire value).
153
+ */
154
+ export declare function resolveJobWorkDir(inPlace: boolean, repoUrl: string | undefined): Promise<{
155
+ workDir: string;
156
+ cleanup: () => Promise<void>;
157
+ inPlace: boolean;
158
+ }>;
125
159
  /**
126
160
  * Top-level job execution orchestrator for the agent.
127
161
  *
@@ -138,7 +172,6 @@ export declare function buildEvalNeedsContext(config: {
138
172
  */
139
173
  export declare class JobRunner {
140
174
  private readonly send;
141
- private readonly sendDirect;
142
175
  private readonly config;
143
176
  private readonly requestUploadUrl;
144
177
  private readonly sendUploadComplete;
@@ -151,6 +184,7 @@ export declare class JobRunner {
151
184
  private readonly _sendApiRequest?;
152
185
  private readonly _requestUserCache?;
153
186
  private readonly _relayProvenance?;
187
+ private readonly _requestUserArtifact?;
154
188
  private readonly _sendStepApproval?;
155
189
  /** Tracks running jobs for concurrency and cancellation */
156
190
  readonly activeJobs: Map<string, ActiveJob>;
@@ -205,7 +239,8 @@ export declare class JobRunner {
205
239
  /**
206
240
  * Drive `sandbox.executeJob` with IPC callbacks wired to the WS pipeline.
207
241
  *
208
- * Lazily creates per-step LogStreamers, forwards step + log + event-emit +
242
+ * Lazily populates `logStreamers` (owned by the caller so a throw still
243
+ * leaves the buffered output flushable), forwards step + log + event-emit +
209
244
  * concurrency-report + api-request messages, and emits the
210
245
  * `agent.execution.start` / `agent.execution.end` lifecycle events.
211
246
  */
@@ -1,4 +1,4 @@
1
- import type { AgentLogChunk } from '@kici-dev/engine';
1
+ import { LogStream, type AgentLogChunk } from '@kici-dev/engine';
2
2
  interface LogStreamerOptions {
3
3
  /** Callback to send log.chunk messages */
4
4
  send: (msg: AgentLogChunk) => void;
@@ -47,6 +47,8 @@ export declare class LogStreamer {
47
47
  private flushTimer;
48
48
  private totalBytes;
49
49
  private truncated;
50
+ /** Which stream the currently-buffered lines came from. */
51
+ private bufferStream;
50
52
  /** Number of lines dropped due to backpressure (drop mode). */
51
53
  private droppedCount;
52
54
  /** Whether we are currently in a backpressured state (pause mode). */
@@ -73,8 +75,21 @@ export declare class LogStreamer {
73
75
  /**
74
76
  * Add a line to the buffer. Triggers flush if threshold reached,
75
77
  * otherwise schedules a timer-based flush.
78
+ *
79
+ * A chunk carries a single stream, so a kind flip closes the pending chunk
80
+ * before the new line is buffered. Chunks are delivered in order, so the
81
+ * stdout/stderr interleaving is preserved across that boundary.
82
+ *
83
+ * `bufferStream` is only advanced once the buffer is actually empty. Under
84
+ * pause-mode backpressure `flush()` deliberately leaves the buffer intact
85
+ * until the socket drains, so the flip cannot close the pending chunk;
86
+ * advancing the tag there would relabel the already-buffered lines as the
87
+ * newly-arrived stream. The buffer keeps the tag of its first lines instead,
88
+ * which means a chunk assembled while backpressured reports one stream for
89
+ * lines that came from both — stderr in such a chunk is under-reported as
90
+ * stdout. Separating them needs a queue of pending per-stream chunks.
76
91
  */
77
- addLine(line: string): void;
92
+ addLine(line: string, stream?: LogStream): void;
78
93
  /**
79
94
  * Flush buffered lines as a log.chunk message.
80
95
  * No-op if buffer is empty.
@@ -1,13 +1,2 @@
1
- import type { RuleContext, FanoutPosition } from '@kici-dev/sdk';
2
- /**
3
- * Create RuleContext for agent-side rule evaluation.
4
- *
5
- * @param event - Event payload from the dispatch message
6
- * @param changedFiles - List of files changed in this event
7
- * @param env - Merged environment variables
8
- * @param dispatchInputs - Operator dispatch inputs (`ctx.dispatchInputs`)
9
- * @param fanout - Fan-out position (`ctx.fanout`); undefined on a non-fan-out job
10
- */
11
- export declare function createRuleContext(event: Record<string, unknown>, changedFiles?: string[], env?: Record<string, string | undefined>, dispatchInputs?: Readonly<Record<string, string | number | boolean | null>>, fanout?: FanoutPosition): RuleContext;
12
- export { evaluateRules, type RuleEvaluationResult } from '@kici-dev/sdk';
1
+ export { createRuleContext, ChangedFilesUnavailableError, evaluateRules, type RuleEvaluationResult, } from '@kici-dev/sdk';
13
2
  //# sourceMappingURL=rule-evaluator.d.ts.map
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Container sandbox hardening posture builder.
3
+ *
4
+ * A pure function that produces the dockerode `HostConfig` fragment (plus an
5
+ * optional top-level `User`) applied to every per-job container sandbox. It
6
+ * brings the container backend to parity with the bare-metal bwrap sandbox
7
+ * (`fork-runner.ts`) — which runs rootless, with a read-only system tree,
8
+ * private dev/proc/tmp, and namespace isolation — and exceeds it with cgroup
9
+ * resource caps that bwrap (a namespace tool, not a cgroup tool) cannot set.
10
+ *
11
+ * The default posture ("hardened"):
12
+ * - `CapDrop: ['ALL']` — drop every Linux capability (no default add-back).
13
+ * - `SecurityOpt: ['no-new-privileges']` — a step can never gain privileges via
14
+ * setuid binaries, matching bwrap's `--new-session` lifecycle posture.
15
+ * - `PidsLimit` / `Memory` / `NanoCpus` — cgroup caps bounding fork-bomb, memory
16
+ * and CPU DoS against the host.
17
+ * - `Tmpfs: { '/tmp': '' }` — a private, non-persistent /tmp (mirrors bwrap's
18
+ * `--tmpfs /tmp`).
19
+ * - `User` — honored as configured on the image; an explicit override sets it,
20
+ * but a root image is never silently rewritten (parity is best-effort here:
21
+ * bwrap runs as the unprivileged invoker uid by construction).
22
+ * - `ReadonlyRootfs` — OPT-IN only (many images write outside /workspace: npm
23
+ * cache, /home, tool state), enabled via the resolved `readonlyRootfs` input.
24
+ * - `NetworkMode` — `none` for the isolated network posture (bwrap's
25
+ * `--unshare-net`); the default (bridge) otherwise.
26
+ *
27
+ * The `grant` input is the dispatch-resolved per-job escape hatch (allow-listed
28
+ * orchestrator-side). It is honored strictly additively on top of the hardened
29
+ * baseline: requested capabilities become `CapAdd` entries while `CapDrop:
30
+ * ['ALL']` still applies, and a grant may switch the network to `host` or set
31
+ * an explicit user / read-only rootfs. The builder never reads an allow-list —
32
+ * it applies whatever grant dispatch already resolved (single enforcement
33
+ * point). When `hardened` is false (the documented-temporary
34
+ * `KICI_SANDBOX_HARDENED` rollback affordance), the builder emits an empty
35
+ * posture, reproducing the unhardened container behavior.
36
+ */
37
+ import type Docker from 'dockerode';
38
+ import type { ResolvedSandboxGrant, SandboxNetworkMode } from '@kici-dev/engine';
39
+ export type { ResolvedSandboxGrant, SandboxNetworkMode };
40
+ /** Inputs to the hardening builder, already resolved from agent config + dispatch. */
41
+ export interface SandboxHardeningOptions {
42
+ /**
43
+ * Master switch. When false (the `KICI_SANDBOX_HARDENED=false` rollback
44
+ * affordance) the builder emits an empty posture — the legacy unhardened
45
+ * container behavior. Defaults are ON in the shipping config.
46
+ */
47
+ hardened: boolean;
48
+ /** Opt-in read-only rootfs (config `KICI_SANDBOX_READONLY_ROOTFS`). */
49
+ readonlyRootfs: boolean;
50
+ /** Explicit user override (config `KICI_SANDBOX_USER`); honors the image user when unset. */
51
+ user?: string;
52
+ /** Max PIDs in the container cgroup. */
53
+ pidsLimit: number;
54
+ /** Memory cap in bytes for the container cgroup. */
55
+ memoryBytes: number;
56
+ /** CPU cap in nano-CPUs (1 CPU = 1_000_000_000). */
57
+ nanoCpus: number;
58
+ /** Config-derived network posture (`isolated` → `none`, else `default`). */
59
+ networkMode: SandboxNetworkMode;
60
+ /** Optional dispatch-resolved escape hatch (Sub-wish B populates this at dispatch). */
61
+ grant?: ResolvedSandboxGrant;
62
+ }
63
+ /** The builder output: a HostConfig fragment merged into createContainer, plus an optional top-level User. */
64
+ export interface ContainerHardening {
65
+ hostConfig: Partial<Docker.HostConfig>;
66
+ user?: string;
67
+ }
68
+ /** Default cgroup caps — operator-visible constants, overridable via scaler `resources`. */
69
+ export declare const DEFAULT_PIDS_LIMIT = 512;
70
+ export declare const DEFAULT_MEMORY_BYTES: number;
71
+ export declare const DEFAULT_NANO_CPUS: number;
72
+ /**
73
+ * Build the hardened HostConfig fragment for a job sandbox container.
74
+ *
75
+ * Pure and side-effect-free: given resolved inputs it returns the fields the
76
+ * container-sandbox merges into `docker.createContainer`. When `hardened` is
77
+ * false it returns an empty posture (the rollback affordance).
78
+ */
79
+ export declare function buildContainerHardening(opts: SandboxHardeningOptions): ContainerHardening;
80
+ //# sourceMappingURL=container-hardening.d.ts.map
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import Docker from 'dockerode';
19
19
  import type { ExecutionSandbox, SandboxSetupOptions, JobExecutionOptions, JobExecutionResult } from './types.js';
20
+ import { type SandboxHardeningOptions } from './container-hardening.js';
20
21
  interface ContainerSandboxOptions {
21
22
  /** Dockerode instance (from orchestrator/scaler or created locally). */
22
23
  docker: Docker;
@@ -26,21 +27,39 @@ interface ContainerSandboxOptions {
26
27
  runnerPath: string;
27
28
  /** Mount target inside container (default: /opt/kici/workflow-runner.js). */
28
29
  runnerMountPath?: string;
30
+ /**
31
+ * Path to the pure-JS container loader-hook bundle on the HOST (bind-mounted
32
+ * read-only). Defaults to `container-ts-loader-hook.js` next to `runnerPath`.
33
+ */
34
+ hookPath?: string;
29
35
  /** Pre-sanitized environment variables for the container. */
30
36
  env: Record<string, string>;
31
37
  /** Whether to keep failed containers for debugging. */
32
38
  keepFailed?: boolean;
33
39
  /** Job ID for container labeling and orphan cleanup. */
34
40
  jobId?: string;
41
+ /**
42
+ * Resolved hardening posture for the job container (cap-drop, no-new-privileges,
43
+ * cgroup caps, tmpfs, user, network). When omitted, no hardening is applied —
44
+ * the production caller (job-runner) always supplies this from agent config so
45
+ * the secure-by-default posture is in force; leaving it optional keeps the
46
+ * constructor testable and lets non-production callers opt out explicitly.
47
+ */
48
+ hardening?: SandboxHardeningOptions;
35
49
  }
36
50
  export declare class ContainerSandbox implements ExecutionSandbox {
37
51
  private readonly docker;
38
52
  private readonly image;
39
53
  private readonly runnerPath;
40
54
  private readonly runnerMountPath;
55
+ /** Host path to the pure-JS container loader-hook bundle (bind-mounted :ro). */
56
+ private readonly hookHostPath;
41
57
  private readonly env;
42
58
  private readonly keepFailed;
43
59
  private readonly jobId;
60
+ private readonly hardening?;
61
+ /** Resolved container user (image-user override / grant), applied to createContainer + each exec. */
62
+ private resolvedUser?;
44
63
  /** The running container instance (set during setup). */
45
64
  private container;
46
65
  /** The active exec stream (set during executeJob, used for abort). */
@@ -50,7 +69,42 @@ export declare class ContainerSandbox implements ExecutionSandbox {
50
69
  /** Container name for logging/debugging. */
51
70
  private containerName;
52
71
  constructor(options: ContainerSandboxOptions);
72
+ /**
73
+ * Ensure the job's container image is present locally, pulling it on demand
74
+ * when it is not.
75
+ *
76
+ * dockerode's `createContainer` — unlike `docker run` / `podman run` — never
77
+ * auto-pulls a missing image; it fails with `(HTTP code 404) ... No such
78
+ * image`. A bare-metal executor that aggressively prunes unused images under
79
+ * disk pressure can leave a container job with nothing to run, so the agent
80
+ * pulls the image itself. Already-present images (the common case, and how
81
+ * private images pre-pulled with registry auth stay working) skip the pull.
82
+ */
83
+ private ensureImagePresent;
53
84
  setup(options: SandboxSetupOptions): Promise<void>;
85
+ /**
86
+ * Build the container's read-only bind list.
87
+ *
88
+ * The workspace is NOT bound here — it is a container-owned anonymous volume
89
+ * (`Volumes: { '/workspace': {} }` on the container config), so the container
90
+ * user can write it on every runtime with `CapDrop: ['ALL']` intact. This
91
+ * method binds the workflow runner (read-only) plus two parity affordances
92
+ * that mirror the bare-metal bwrap sandbox — both strictly additive and gated,
93
+ * so the default posture for a production (https-source, bridge-network) job is
94
+ * unchanged:
95
+ *
96
+ * - **`file://` clone-source dir(s)** (`options.extraReadOnlyBinds`): the
97
+ * workflow runner clones the repo from inside the container, so a local
98
+ * `file://` source dir must be exposed read-only or `git clone` fails.
99
+ * Empty for https/ssh remotes — mirrors fork-runner's `extraReadOnlyBinds`.
100
+ * - **Host name-resolution files under host networking**: when the effective
101
+ * network posture is `host` (`KICI_SANDBOX_NETWORK=host`, or a per-job host
102
+ * grant), bind the host's `/etc/hosts` (+ `/etc/nsswitch.conf`) read-only so
103
+ * an `/etc/hosts`-only name the host resolves — e.g. a private registry —
104
+ * resolves inside the container too. Mirrors fork-runner's
105
+ * `--ro-bind /etc/hosts` for the bwrap host-network mode.
106
+ */
107
+ private buildBinds;
54
108
  executeJob(options: JobExecutionOptions): Promise<JobExecutionResult>;
55
109
  /**
56
110
  * Phase 1 of executeJob: create the docker exec, start it in hijack mode,
@@ -0,0 +1,26 @@
1
+ type ResolveContext = {
2
+ parentURL?: string;
3
+ conditions: string[];
4
+ importAttributes: Record<string, string>;
5
+ };
6
+ type ResolveResult = {
7
+ url: string;
8
+ shortCircuit?: boolean;
9
+ format?: string | null;
10
+ };
11
+ type NextResolve = (specifier: string, context?: ResolveContext) => ResolveResult | Promise<ResolveResult>;
12
+ type LoadContext = {
13
+ format?: string | null;
14
+ importAttributes: Record<string, string>;
15
+ conditions: string[];
16
+ };
17
+ type LoadResult = {
18
+ format: string;
19
+ source?: string | ArrayBuffer | Uint8Array;
20
+ shortCircuit?: boolean;
21
+ };
22
+ type NextLoad = (url: string, context?: LoadContext) => LoadResult | Promise<LoadResult>;
23
+ export declare function resolve(specifier: string, context: ResolveContext, nextResolve: NextResolve): ResolveResult | Promise<ResolveResult>;
24
+ export declare function load(url: string, context: LoadContext, nextLoad: NextLoad): Promise<LoadResult>;
25
+ export {};
26
+ //# sourceMappingURL=container-ts-loader-hook.d.ts.map
@@ -9,8 +9,8 @@
9
9
  * 1. Allowed system vars from process.env
10
10
  * 2. Sandbox defaults (FORCE_COLOR=1, etc.)
11
11
  * 3. KICI_* system vars (orchestrator-generated, from userEnv)
12
- * 4. Org-level environment vars (from orchestrator via environmentVars)
13
- * 5. Source-level environment overrides (merged into environmentVars by orchestrator)
12
+ * 4. Org-level environment vars (from orchestrator via contextVars)
13
+ * 5. Source-level environment overrides (merged into contextVars by orchestrator)
14
14
  * 6. Job env (from lock file env field, evaluated by orchestrator)
15
15
  * 7. setEnv() calls (runtime -- applied at step execution, not here)
16
16
  */
@@ -32,12 +32,21 @@
32
32
  * PLATFORM_TOKEN, WEBHOOK_SECRET, etc.) are never included because they
33
33
  * are not in the allowlist.
34
34
  *
35
+ * The `trustedEnv` option (default false) selects the trusted fleet-agent
36
+ * profile: Layer 1 passes the ambient host env through instead of restricting
37
+ * to `ALLOWED_SYSTEM_VARS`, minus the agent's own KiCI identity/operational
38
+ * secrets (the whole `KICI_*` namespace + a small non-`KICI_` infra denylist).
39
+ * It is read ONLY from agent config (`KICI_TRUSTED_ENV`), never from a dispatch
40
+ * payload, so a workflow cannot request it. With it OFF the output is
41
+ * byte-identical to the allowlist-only behavior.
42
+ *
35
43
  * @param userEnv - Environment variables from workflow config and orchestrator
36
44
  * @param options - Optional extended options for environment layers
37
45
  * @returns A new Record with only safe environment variables
38
46
  */
39
47
  export declare function buildSanitizedEnv(userEnv: Record<string, string>, options?: {
40
- environmentVars?: Record<string, string>;
48
+ contextVars?: Record<string, string>;
41
49
  jobEnv?: Record<string, string>;
50
+ trustedEnv?: boolean;
42
51
  }): Record<string, string>;
43
52
  //# sourceMappingURL=env-sanitizer.d.ts.map
@@ -83,6 +83,20 @@ export declare function buildRequest(dispatch: JobDispatch, workDir: string): Jo
83
83
  * is simpler and more secure than selective blocking.
84
84
  */
85
85
  export declare function buildBwrapArgs(workDir: string, nodeExecPath: string, networkIsolation?: boolean, runnerPath?: string, extraReadOnlyBinds?: string[]): string[];
86
+ /**
87
+ * Derive the read-only bind path(s) for a `file://` clone source. The workflow
88
+ * runner clones the repo from inside the sandbox, so a local `file://` URL's
89
+ * source directory must be exposed read-only or `git clone` fails with
90
+ * `does not appear to be a git repository`. Returns `[]` for non-`file://`
91
+ * remotes (https/ssh need no host bind) and for a malformed `file://` URL (the
92
+ * clone then surfaces the real error rather than being masked here).
93
+ *
94
+ * Shared by both sandboxes that clone a local source: the bare-metal (bwrap)
95
+ * backend threads the result into `buildBwrapArgs`'s `extraReadOnlyBinds`, and
96
+ * the container backend binds each `<dir>:<dir>:ro` into the job container —
97
+ * the same clone-source affordance across both isolation models.
98
+ */
99
+ export declare function fileCloneSourceBinds(repoUrl: string | undefined): string[];
86
100
  /**
87
101
  * Spawn the workflow runner as a child process with IPC channel.
88
102
  *
@@ -5,10 +5,11 @@
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, CacheRequestIpc, CacheResponseIpc, ProvenanceRequestIpc, ProvenanceResponseIpc, StepApprovalRequestIpc, StepApprovalResolvedIpc, JobExecutionRequest, } from './ipc-protocol.js';
8
+ export type { RunnerToAgentMessage, AgentToRunnerMessage, EventEmitRequest, EventEmitResponse, CacheRequestIpc, CacheResponseIpc, ProvenanceRequestIpc, ProvenanceResponseIpc, ArtifactRequestIpc, ArtifactResponseIpc, 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';
12
12
  export { FirecrackerSandbox } from './firecracker-sandbox.js';
13
13
  export { ContainerSandbox } from './container-sandbox.js';
14
+ export { fileCloneSourceBinds } from './fork-runner.js';
14
15
  //# sourceMappingURL=index.d.ts.map
@@ -1,4 +1,4 @@
1
- import type { CheckMode, CheckStepOutcome } from '@kici-dev/engine';
1
+ import type { CheckMode, CheckStepOutcome, LogStream } from '@kici-dev/engine';
2
2
  import type { SandboxStepResult } from './types.js';
3
3
  /**
4
4
  * Structured clone auth. Wire-compatible with `gitAuthSchema` on the
@@ -78,6 +78,8 @@ interface LogLineMessage {
78
78
  type: 'log.line';
79
79
  stepIndex: number;
80
80
  line: string;
81
+ /** Which subprocess stream the line came from. Absent means stdout. */
82
+ stream?: LogStream;
81
83
  }
82
84
  /**
83
85
  * Discriminator for {@link StepSecretMountMessage} -- distinguishes a bare
@@ -248,7 +250,32 @@ export interface ProvenanceRequestIpc {
248
250
  /** Ephemeral public key JWK the envelope was signed with. `defer` only. */
249
251
  publicKey?: unknown;
250
252
  }
251
- export type RunnerToAgentMessage = ReadyMessage | StepStartMessage | StepCompleteMessage | LogLineMessage | StepSecretMountMessage | JobCompleteMessage | EventEmitRequest | ConcurrencyReportMessage | AgentApiRequestIpc | CacheRequestIpc | ProvenanceRequestIpc | StepApprovalRequestIpc;
253
+ /** Which artifact operation to relay. */
254
+ export type ArtifactRequestOp = 'beginUpload' | 'completeUpload' | 'download';
255
+ /**
256
+ * Request a user-facing artifact operation (runner -> agent). The agent relays
257
+ * it over the WS as an `artifacts.upload.request` / `.complete` /
258
+ * `artifacts.download.request` and pipes the response back as an
259
+ * {@link ArtifactResponseIpc}. Mirrors the {@link CacheRequestIpc} relay pattern.
260
+ */
261
+ export interface ArtifactRequestIpc {
262
+ type: 'artifacts.request';
263
+ /** UUID for correlating the response. */
264
+ requestId: string;
265
+ /** Which artifact operation to perform. */
266
+ op: ArtifactRequestOp;
267
+ /** Artifact name (all ops). */
268
+ name: string;
269
+ /** Packed tarball size in bytes — drives the enforcement gates. `beginUpload` only. */
270
+ declaredSizeBytes?: number;
271
+ /** Confirmed tarball size in bytes. `completeUpload` only. */
272
+ sizeBytes?: number;
273
+ /** SHA-256 of the tarball bytes. `completeUpload` only. */
274
+ sha256?: string;
275
+ /** Storage key echoed from the grant response. `completeUpload` only. */
276
+ storageKey?: string;
277
+ }
278
+ export type RunnerToAgentMessage = ReadyMessage | StepStartMessage | StepCompleteMessage | LogLineMessage | StepSecretMountMessage | JobCompleteMessage | EventEmitRequest | ConcurrencyReportMessage | AgentApiRequestIpc | CacheRequestIpc | ProvenanceRequestIpc | ArtifactRequestIpc | StepApprovalRequestIpc;
252
279
  /** Instruct the workflow runner to execute a job. */
253
280
  interface ExecuteMessage {
254
281
  type: 'execute';
@@ -353,7 +380,50 @@ export interface ProvenanceResponseIpc {
353
380
  /** Error description (present when the relay or orchestrator failed). */
354
381
  error?: string;
355
382
  }
356
- export type AgentToRunnerMessage = ExecuteMessage | AbortMessage | EventEmitResponse | ConcurrencyAckMessage | AgentApiResponseIpc | CacheResponseIpc | ProvenanceResponseIpc | StepApprovalResolvedIpc;
383
+ /**
384
+ * Response to an {@link ArtifactRequestIpc} (agent -> runner). Relayed from the
385
+ * orchestrator's `artifacts.upload.response` / `artifacts.download.response` WS
386
+ * message. Carries the union of the upload-grant (`uploadOutcome` / `uploadUrl`
387
+ * / `storageKey` / `reason`) and download-lookup (`downloadOutcome` /
388
+ * `downloadUrl` / `sizeBytes` / `sha256`) fields; `completeUpload` resolves with
389
+ * an empty (no-field) response. `error` is set when the relay itself failed;
390
+ * `rejectionDetail` carries an orchestrator-side non-enforcement explanation
391
+ * that the workflow-facing render surfaces without throwing a relay error.
392
+ */
393
+ export interface ArtifactResponseIpc {
394
+ type: 'artifacts.response';
395
+ /** Matches the original request's requestId. */
396
+ requestId: string;
397
+ /** beginUpload: `granted` (presigned PUT minted) or `rejected` (named reason). */
398
+ uploadOutcome?: 'granted' | 'rejected';
399
+ /** beginUpload: presigned PUT URL (present only on `granted`). */
400
+ uploadUrl?: string;
401
+ /** beginUpload: storage key to echo back on complete (present only on `granted`). */
402
+ storageKey?: string;
403
+ /** beginUpload: enforcement-gate refusal reason (present only on `rejected`). */
404
+ reason?: 'duplicate_name' | 'size_cap' | 'run_cap' | 'org_quota';
405
+ /**
406
+ * Orchestrator non-enforcement refusal detail — set when a `rejected` upload
407
+ * or a `not_found` download reflects a name that violates the artifact-name
408
+ * contract, or an orchestrator problem (artifacts not configured, unresolvable
409
+ * run, internal error), rather than an enforcement gate or a genuinely missing
410
+ * artifact. Distinct from `error` below, which is a relay/transport failure
411
+ * the sandbox turns into a thrown error: this field flows into the normal
412
+ * rejection render instead.
413
+ */
414
+ rejectionDetail?: string;
415
+ /** download: `found` (presigned GET minted) or `not_found`. */
416
+ downloadOutcome?: 'found' | 'not_found';
417
+ /** download: presigned GET URL (present only on `found`). */
418
+ downloadUrl?: string;
419
+ /** download: artifact size in bytes (present only on `found`). */
420
+ sizeBytes?: number;
421
+ /** download: SHA-256 of the tarball bytes (present only on `found`). */
422
+ sha256?: string;
423
+ /** Error description (present when the relay or orchestrator failed). */
424
+ error?: string;
425
+ }
426
+ export type AgentToRunnerMessage = ExecuteMessage | AbortMessage | EventEmitResponse | ConcurrencyAckMessage | AgentApiResponseIpc | CacheResponseIpc | ProvenanceResponseIpc | ArtifactResponseIpc | StepApprovalResolvedIpc;
357
427
  /**
358
428
  * All data the workflow runner needs to execute a job inside the sandbox.
359
429
  *
@@ -508,10 +578,10 @@ export interface JobExecutionRequest {
508
578
  orchestratorPrivateKey?: string;
509
579
  /** Base64-encoded X25519 public key for the run (for encrypting secret outputs). */
510
580
  runPublicKey?: string;
511
- /** Deployment environment name (resolved by orchestrator). */
512
- environment?: string;
581
+ /** Resolved context name (resolved by orchestrator). */
582
+ context?: string;
513
583
  /** Environment variables from orchestrator (org-level + source overrides, layers 4-5). */
514
- environmentVars?: Record<string, string>;
584
+ contextVars?: Record<string, string>;
515
585
  /** Job env from lock file env field (layer 6, evaluated by orchestrator). */
516
586
  jobEnv?: Record<string, string>;
517
587
  /** Whether this is a global workflow (dual-clone: workflow repo + source repo). */
@@ -528,6 +598,12 @@ export interface JobExecutionRequest {
528
598
  hasConcurrencyGroup?: boolean;
529
599
  /** Concurrency group evaluation timeout in milliseconds (default: 30000). */
530
600
  concurrencyEvaluationTimeoutMs?: number;
601
+ /**
602
+ * Orchestrator-resolved concurrency-slot wait timeout (ms), pushed on
603
+ * `job.dispatch` from the fleet-wide `cluster_settings.concurrency_wait_timeout_ms`.
604
+ * Absent for older orchestrators — the runner falls back to its own default.
605
+ */
606
+ concurrencyWaitTimeoutMs?: number;
531
607
  /** Git branch for concurrency group context. */
532
608
  branch?: string;
533
609
  /** Plain outputs from upstream jobs (keyed by job name, then by step name). For ctx.jobOutputs(). */