@kici-dev/agent 0.1.27 → 0.3.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.
- package/dist/bootstrap/ensure-init-runner.d.ts +23 -22
- package/dist/bootstrap/payload-source.d.ts +32 -0
- package/dist/bootstrap/probe-platform.d.ts +21 -0
- package/dist/bootstrap/restage-agent.d.ts +43 -0
- package/dist/bootstrap/run-restage.d.ts +12 -0
- package/dist/bootstrap/s3-payload-source.d.ts +35 -0
- package/dist/bootstrap/ssh-exec.d.ts +14 -0
- package/dist/bootstrap/stage-agent-payload.d.ts +41 -0
- package/dist/checkout/changed-files.d.ts +34 -0
- package/dist/config.d.ts +36 -14
- package/dist/container-ts-loader-hook.js +147710 -0
- package/dist/execution/artifacts/artifact-engine.d.ts +51 -0
- package/dist/execution/dep-installer.d.ts +3 -3
- package/dist/execution/job-runner.d.ts +24 -6
- package/dist/execution/log-streamer.d.ts +17 -2
- package/dist/execution/rule-evaluator.d.ts +1 -12
- package/dist/execution/sandbox/container-hardening.d.ts +80 -0
- package/dist/execution/sandbox/container-sandbox.d.ts +54 -0
- package/dist/execution/sandbox/container-ts-loader-hook.d.ts +26 -0
- package/dist/execution/sandbox/fork-runner.d.ts +14 -0
- package/dist/execution/sandbox/index.d.ts +2 -1
- package/dist/execution/sandbox/ipc-protocol.d.ts +79 -3
- package/dist/execution/sandbox/step-loop.d.ts +4 -0
- package/dist/execution/sandbox/types.d.ts +25 -4
- package/dist/execution/sandbox/workflow-runner.d.ts +111 -5
- package/dist/execution/streaming-zx-log.d.ts +11 -3
- package/dist/execution/tmp-gc.d.ts +22 -7
- package/dist/execution/workflow-loader.d.ts +32 -1
- package/dist/index.js +44 -45
- package/dist/provenance/statement-builder.d.ts +3 -2
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1256 -171
- package/dist/workflow-runner-bundle.js +215403 -0
- package/dist/workflow-runner.js +886 -244
- package/dist/ws/orchestrator-client.d.ts +105 -1
- package/package.json +14 -12
- package/sbom.spdx.json +1090 -1721
|
@@ -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 (
|
|
45
|
-
* `
|
|
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`,
|
|
@@ -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).
|
|
@@ -155,7 +172,6 @@ export declare function resolveJobWorkDir(inPlace: boolean, repoUrl: string | un
|
|
|
155
172
|
*/
|
|
156
173
|
export declare class JobRunner {
|
|
157
174
|
private readonly send;
|
|
158
|
-
private readonly sendDirect;
|
|
159
175
|
private readonly config;
|
|
160
176
|
private readonly requestUploadUrl;
|
|
161
177
|
private readonly sendUploadComplete;
|
|
@@ -168,6 +184,7 @@ export declare class JobRunner {
|
|
|
168
184
|
private readonly _sendApiRequest?;
|
|
169
185
|
private readonly _requestUserCache?;
|
|
170
186
|
private readonly _relayProvenance?;
|
|
187
|
+
private readonly _requestUserArtifact?;
|
|
171
188
|
private readonly _sendStepApproval?;
|
|
172
189
|
/** Tracks running jobs for concurrency and cancellation */
|
|
173
190
|
readonly activeJobs: Map<string, ActiveJob>;
|
|
@@ -222,7 +239,8 @@ export declare class JobRunner {
|
|
|
222
239
|
/**
|
|
223
240
|
* Drive `sandbox.executeJob` with IPC callbacks wired to the WS pipeline.
|
|
224
241
|
*
|
|
225
|
-
* Lazily
|
|
242
|
+
* Lazily populates `logStreamers` (owned by the caller so a throw still
|
|
243
|
+
* leaves the buffered output flushable), forwards step + log + event-emit +
|
|
226
244
|
* concurrency-report + api-request messages, and emits the
|
|
227
245
|
* `agent.execution.start` / `agent.execution.end` lifecycle events.
|
|
228
246
|
*/
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
*
|
|
@@ -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(). */
|
|
@@ -208,6 +208,10 @@ interface StepLoopResult {
|
|
|
208
208
|
stepResults: SandboxStepResult[];
|
|
209
209
|
failureReason?: string;
|
|
210
210
|
}
|
|
211
|
+
/** Most lines of a failing step's error message that reach the run log. */
|
|
212
|
+
export declare const STEP_FAILURE_LOG_MAX_LINES = 100;
|
|
213
|
+
/** Most characters of a failing step's error message that reach the run log. */
|
|
214
|
+
export declare const STEP_FAILURE_LOG_MAX_CHARS = 8192;
|
|
211
215
|
/**
|
|
212
216
|
* Per-step iteration outcome returned by `runStepIteration`.
|
|
213
217
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { JobDispatch } from '@kici-dev/engine';
|
|
2
|
-
import type { EventEmitRequest, EventEmitResponse, ConcurrencyReportMessage, ConcurrencyAckMessage, CacheRequestIpc, CacheResponseIpc, ProvenanceRequestIpc, ProvenanceResponseIpc, StepApprovalRequestIpc, StepApprovalResolvedIpc } from './ipc-protocol.js';
|
|
1
|
+
import type { JobDispatch, LogStream } from '@kici-dev/engine';
|
|
2
|
+
import type { EventEmitRequest, EventEmitResponse, ConcurrencyReportMessage, ConcurrencyAckMessage, CacheRequestIpc, CacheResponseIpc, ProvenanceRequestIpc, ProvenanceResponseIpc, ArtifactRequestIpc, ArtifactResponseIpc, StepApprovalRequestIpc, StepApprovalResolvedIpc } from './ipc-protocol.js';
|
|
3
3
|
/**
|
|
4
4
|
* Common interface for all execution sandbox backends.
|
|
5
5
|
*
|
|
@@ -50,6 +50,15 @@ export interface SandboxSetupOptions {
|
|
|
50
50
|
workDir: string;
|
|
51
51
|
/** Sanitized environment variables (user env + secrets, NO agent credentials). */
|
|
52
52
|
env: Record<string, string>;
|
|
53
|
+
/**
|
|
54
|
+
* Extra read-only host paths to expose in the sandbox beyond the workspace +
|
|
55
|
+
* runner — the `file://` clone-source dir(s) so the in-sandbox `git clone`
|
|
56
|
+
* can read a local source. Derived from the dispatch `repoUrl` by the
|
|
57
|
+
* job-runner (empty for https/ssh remotes). The container backend binds each
|
|
58
|
+
* as `<dir>:<dir>:ro`; the bare-metal backend derives its own equivalent
|
|
59
|
+
* inside `executeJob`, so it ignores this field.
|
|
60
|
+
*/
|
|
61
|
+
extraReadOnlyBinds?: string[];
|
|
53
62
|
}
|
|
54
63
|
/** Options for executing a job inside the sandbox. */
|
|
55
64
|
export interface JobExecutionOptions {
|
|
@@ -57,8 +66,11 @@ export interface JobExecutionOptions {
|
|
|
57
66
|
dispatch: JobDispatch;
|
|
58
67
|
/** Callback for real-time step status updates (start, success, failed). */
|
|
59
68
|
onStepStatus: (stepIndex: number, name: string, state: string, data?: Record<string, unknown>) => void;
|
|
60
|
-
/**
|
|
61
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Callback for real-time log line forwarding. `stream` names the subprocess
|
|
71
|
+
* stream the line came from; absent means stdout.
|
|
72
|
+
*/
|
|
73
|
+
onLogLine: (stepIndex: number, line: string, stream?: LogStream) => void;
|
|
62
74
|
/** Abort signal for cancellation. */
|
|
63
75
|
signal: AbortSignal;
|
|
64
76
|
/**
|
|
@@ -97,6 +109,15 @@ export interface JobExecutionOptions {
|
|
|
97
109
|
* working — the runner falls back to a "not configured" error response.
|
|
98
110
|
*/
|
|
99
111
|
onProvenanceRequest?: (request: ProvenanceRequestIpc) => Promise<ProvenanceResponseIpc>;
|
|
112
|
+
/**
|
|
113
|
+
* Callback for relaying a user-facing artifact request from the sandbox to the
|
|
114
|
+
* orchestrator. The sandbox runner sends `artifacts.request` IPC; the agent
|
|
115
|
+
* wraps it in the matching `artifacts.upload.*` / `artifacts.download.*` WS
|
|
116
|
+
* message and forwards to the orchestrator. Optional so harnesses that don't
|
|
117
|
+
* thread artifacts keep working — the runner falls back to a "not configured"
|
|
118
|
+
* error response.
|
|
119
|
+
*/
|
|
120
|
+
onArtifactRequest?: (request: ArtifactRequestIpc) => Promise<ArtifactResponseIpc>;
|
|
100
121
|
/**
|
|
101
122
|
* Callback for relaying a step-level approval request from the sandbox to the
|
|
102
123
|
* orchestrator. The sandbox runner sends `approval.request` IPC; the agent
|