@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
@@ -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
- /** Callback for real-time log line forwarding. */
61
- onLogLine: (stepIndex: number, line: string) => void;
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
@@ -13,12 +13,15 @@
13
13
  * This file is compiled alongside the agent by rolldown (existing build), but
14
14
  * runs as a SEPARATE process spawned by the sandbox backend.
15
15
  */
16
+ import { $ } from 'zx';
17
+ import { type TempScope } from '@kici-dev/core/tmp';
16
18
  import { ExecutionJobStatus } from '@kici-dev/engine';
17
- import type { StepContext } from '@kici-dev/sdk';
18
- import type { NeedsContext, FanoutPosition } from '@kici-dev/sdk';
19
+ import type { Step, StepInput, StepContext } from '@kici-dev/sdk';
20
+ import type { NeedsContext, FanoutPosition, EventDefinition } from '@kici-dev/sdk';
19
21
  import type { OutputsMap, StepRefMap, TrackedStepSecrets } from '@kici-dev/sdk';
20
22
  import type { RunnerToAgentMessage, JobExecutionRequest } from './ipc-protocol.js';
21
23
  import { LogMasker } from './log-masker.js';
24
+ import { type RuleEvaluationResult } from '../rule-evaluator.js';
22
25
  /**
23
26
  * Build `ctx.needs` for a job's steps from the dispatch envelope. Reconstructs
24
27
  * an {@link UpstreamSnapshot} from `upstreamJobOutputs` (flat per single job;
@@ -28,6 +31,62 @@ import { LogMasker } from './log-masker.js';
28
31
  * undefined when the job declares no needs.
29
32
  */
30
33
  export declare function buildStepNeedsContext(declaredNeeds: readonly unknown[] | undefined, upstreamJobOutputs: Record<string, Record<string, unknown>> | undefined, upstreamJobStatuses: Record<string, ExecutionJobStatus> | undefined): NeedsContext | undefined;
34
+ /**
35
+ * Build a fresh zx `$` shell bound to the sandbox working directory and the
36
+ * sanitized environment (process.env was set by the parent via env-sanitizer
37
+ * before spawning this process). This is the single shell-construction code
38
+ * path shared by step execution (`createSandboxStepContext`) and the per-job
39
+ * init phase (`runInitPhase`), so init commands run through the identical shell
40
+ * steps use — same cwd, same live sanitized env, same masked log streaming.
41
+ *
42
+ * The shell binds `process.env` by reference (not a copy), so a same-step
43
+ * ctx.setEnv / ctx.addPath mutation — which applyEnvDelta writes to live
44
+ * process.env — is visible to that step's own subprocesses, matching the
45
+ * "visible to this step" SDK contract.
46
+ *
47
+ * Intercept zx subprocess output via the log callback: zx does NOT write child
48
+ * stdout/stderr to process.stdout — it pipes to an internal VoidStream and only
49
+ * calls $.log() with { kind: 'stdout'|'stderr', verbose }. We override the log
50
+ * function (`makeStreamingZxLog`) to capture both kinds and send them as masked
51
+ * IPC log.line messages tagged with `stepIndex`.
52
+ *
53
+ * The shell is built with `verbose: true` so ordinary subprocess output is
54
+ * flagged `verbose: true` (captured) while a step that opts into
55
+ * `$({ quiet: true })` — e.g. a `sops -d` credential decrypt — is flagged
56
+ * `verbose: false` and dropped by `makeStreamingZxLog`. That gate is what keeps
57
+ * a decrypted secret out of the streamed run log; a `verbose: false` base would
58
+ * flag ordinary output `verbose: false` too and drop every line.
59
+ *
60
+ * IMPORTANT: The log function must be passed in the zx$() config, not set on the
61
+ * returned function. zx$() returns a plain function (not the proxy $), so setting
62
+ * step$.log would only set it on the function object and NOT propagate to the
63
+ * AsyncLocalStorage store that zx uses for ProcessPromise snapshots.
64
+ */
65
+ export declare function buildSandboxShell(cwd: string, stepIndex: number, maskedSendFn: (msg: RunnerToAgentMessage) => void): typeof $;
66
+ /**
67
+ * Resolve the on-the-wire event name for `ctx.emit`. Accepts either an ad-hoc
68
+ * event-name string or a `defineEvent()` definition object (passed by the typed
69
+ * emit overload); a definition resolves to its `.name`, a string passes through.
70
+ */
71
+ export declare function resolveEmitEventName(nameOrDefinition: string | EventDefinition): string;
72
+ /**
73
+ * Sanitize a raw identifier into a valid temp label: lowercase, every
74
+ * non-`[a-z0-9-]` char to `-`, falling back to `'step'` when the result is
75
+ * empty. Applied to both the caller-supplied `ctx.mktemp(label)` and the
76
+ * default step-id label so a friendly-but-irregular label never rejects at the
77
+ * scope. Mirrors the SDK test builder's `sanitizeTempLabel`.
78
+ */
79
+ export declare function sanitizeTempLabel(raw: string): string;
80
+ /**
81
+ * Drain the job-scoped temp allocator once at job end.
82
+ *
83
+ * Called after the step loop and cancel-path hooks, immediately before the
84
+ * terminal `job.complete` emit, so a single call reclaims every `ctx.mktemp` /
85
+ * `ctx.mktempFile` allocation on success, failure, and cancel/timeout alike. A
86
+ * cleanup failure must NEVER change the job's terminal status, so any error is
87
+ * swallowed into `warn` rather than thrown out of `main()`.
88
+ */
89
+ export declare function drainJobTempScope(scope: TempScope, warn: (message: string) => void): Promise<void>;
31
90
  /**
32
91
  * Create a StepContext natively inside the workflow runner.
33
92
  *
@@ -35,7 +94,7 @@ export declare function buildStepNeedsContext(declaredNeeds: readonly unknown[]
35
94
  * NOT serialized across the process boundary. This means zx $ runs natively
36
95
  * inside this process with full shell access.
37
96
  */
38
- export declare function createSandboxStepContext(workDir: string, stepIndex: number, stepName: string, request: JobExecutionRequest, maskedSendFn: (msg: RunnerToAgentMessage) => void, outputsMap: OutputsMap, refMap: StepRefMap, operatorSecretKeys: Set<string>, secretOutputs: Map<string, string>, jobOutputsMap: OutputsMap, secrets: TrackedStepSecrets, masker: LogMasker, signal: AbortSignal): StepContext;
97
+ export declare function createSandboxStepContext(workDir: string, stepIndex: number, stepName: string, request: JobExecutionRequest, maskedSendFn: (msg: RunnerToAgentMessage) => void, outputsMap: OutputsMap, refMap: StepRefMap, operatorSecretKeys: Set<string>, secretOutputs: Map<string, string>, jobOutputsMap: OutputsMap, secrets: TrackedStepSecrets, masker: LogMasker, signal: AbortSignal, jobTempScope: TempScope): StepContext;
39
98
  /**
40
99
  * Derive the fan-out position (`ctx.fanout`) from a dispatch request. Returns
41
100
  * `undefined` for a non-fan-out job (no `fanoutTotal`), so `ctx.fanout` is only
@@ -44,12 +103,59 @@ export declare function createSandboxStepContext(workDir: string, stepIndex: num
44
103
  export declare function deriveFanout(request: JobExecutionRequest): FanoutPosition | undefined;
45
104
  /** Raw provider webhook body for ctx.rawPayload — nested in the envelope. */
46
105
  export declare function rawPayloadFromEvent(event: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
106
+ /**
107
+ * Mutable state threaded through {@link coerceStep} for one job normalization
108
+ * pass: the shared `step-N` counter, the bare-function → name ref map, and the
109
+ * set of step objects this pass has already auto-named.
110
+ */
111
+ export interface CoerceState {
112
+ counter: number;
113
+ refMap: StepRefMap;
114
+ autoNamed: WeakSet<object>;
115
+ }
116
+ /**
117
+ * Coerce one raw entry (bare function or Step) into a normalized Step, naming
118
+ * anonymous steps `step-N` with a counter shared across the whole flattened
119
+ * sequence (parallel children inline) — matching the compiler's transformSteps
120
+ * enumeration so the flat-stepIndex invariant holds agent ↔ orchestrator.
121
+ *
122
+ * Unnamed steps are named by MUTATING the original object so the SDK's
123
+ * late-bound `.result` proxy (which reads `step.name` at access time) resolves.
124
+ * A step object reused twice in one pass gets a clone with a fresh name on the
125
+ * repeat encounter — mirroring the compiler's per-occurrence naming so lock
126
+ * parity holds; the original keeps its first binding.
127
+ */
128
+ export declare function coerceStep(stepOrFn: StepInput, state: CoerceState): Step<any>;
129
+ /**
130
+ * Resolve `event.changedFiles` for rule evaluation before job/step rules run.
131
+ * Ground truth is the agent's own clone (`computeChangedFiles`); the
132
+ * orchestrator's already-fetched list (status `'fetched'`) is a free fast-path.
133
+ * Only runs when a rule could read `ctx.changedFiles` — rule-less jobs skip the
134
+ * git cost. Diff-less events (schedule/tag/manual) resolve to `'unavailable'`.
135
+ */
136
+ export declare function resolveChangedFilesForRules(request: JobExecutionRequest, workDir: string, hasRules: boolean): Promise<void>;
137
+ /**
138
+ * Decide the terminal `job.complete` for job-level rule evaluation, or `null`
139
+ * when the job should proceed to step execution.
140
+ *
141
+ * - `allPassed` → `null` (run the steps).
142
+ * - a rule's `check()` **threw** (`evaluationError`) → FAIL: the gate could not
143
+ * be evaluated, so treating "couldn't decide" as "don't run" would be a false
144
+ * green. Report `status: failed` with the error surfaced.
145
+ * - a rule cleanly returned `false` → clean skip: `status: success` with every
146
+ * step marked skipped (a gate that evaluated and said "don't run").
147
+ *
148
+ * Pure and exported so the decision is unit-testable without `process.exit`.
149
+ */
150
+ export declare function buildJobRuleCompletion(ruleResult: RuleEvaluationResult, normalizedSteps: Step[]): (RunnerToAgentMessage & {
151
+ type: 'job.complete';
152
+ }) | null;
47
153
  /**
48
154
  * Build the step loop's KICI_ENV/KICI_PATH callbacks with a per-step delta-file
49
155
  * pair (keyed by step index). `beforeStepEnvFiles(stepIndex)` lazily creates the
50
156
  * step's pair and points the runner's process.env at it (each step's zx $
51
- * snapshots process.env at context creation, which happens AFTER this
52
- * before-hook, so the shell sees them; the pre-fork env allowlist does not
157
+ * reads live process.env at spawn, which happens AFTER this before-hook, so
158
+ * the shell sees them; the pre-fork env allowlist does not
53
159
  * re-filter runtime-set vars). `afterStepApplyEnvFiles(stepIndex)` applies that
54
160
  * step's delta and releases the pair.
55
161
  *
@@ -0,0 +1,38 @@
1
+ import { LogStream } from '@kici-dev/engine';
2
+ /**
3
+ * Shared factory for the zx `log` callback that streams a subprocess's
4
+ * stdout/stderr into the captured/streamed run log, line by line, via `emit`.
5
+ *
6
+ * zx does NOT write child stdout/stderr to `process.stdout`; it pipes the
7
+ * child stdio to an internal VoidStream and surfaces each chunk through the
8
+ * shell's `log` callback as `{ kind: 'stdout' | 'stderr', data, verbose }`.
9
+ * The `verbose` flag is zx's per-invocation quiet/verbose decision:
10
+ *
11
+ * - stdout entries: `verbose = !piped && (snapshot.verbose && !snapshot.quiet)`
12
+ * - stderr entries: `verbose = !snapshot.quiet`
13
+ *
14
+ * so a step that opts into `$({ quiet: true })` (e.g. a `sops -d` decrypt of a
15
+ * credential) produces `verbose: false` entries. This factory HONORS that flag
16
+ * — it skips `verbose: false` entries — which is what makes `{ quiet: true }`
17
+ * actually suppress sensitive output from the run log. Without the gate, a
18
+ * decrypted-secret line leaks into the persisted/streamed log (zx's own default
19
+ * log function gates on the same flag: `if (!entry.verbose) return`).
20
+ *
21
+ * IMPORTANT: the shell that installs this callback MUST be constructed with
22
+ * `verbose: true`. With `verbose: true` zx flags ordinary (non-quiet)
23
+ * subprocess output `verbose: true` (captured) and a `{ quiet: true }` call
24
+ * `verbose: false` (suppressed). A `verbose: false` base would flag ordinary
25
+ * output `verbose: false` too, and this gate would then drop every line.
26
+ *
27
+ * The returned callback owns one line buffer PER STREAM, so partial chunks are
28
+ * coalesced into whole lines before `emit` is called. The buffers are separate
29
+ * because the two streams are independent pipes: a stdout chunk ending mid-line
30
+ * and a stderr chunk arriving next would otherwise concatenate into a single
31
+ * spliced line attributed to whichever kind completed it.
32
+ *
33
+ * `emit` receives the originating stream alongside the line so a diagnostic
34
+ * written to stderr stays distinguishable from ordinary progress output all the
35
+ * way to the persisted run log.
36
+ */
37
+ export declare function makeStreamingZxLog(emit: (line: string, stream: LogStream) => void): (entry: unknown) => void;
38
+ //# sourceMappingURL=streaming-zx-log.d.ts.map
@@ -1,17 +1,32 @@
1
1
  /**
2
2
  * Startup garbage collection for this agent's own temp-directory families.
3
3
  *
4
- * Job workdirs (`kici-<6 random chars>`, see job-runner.ts) and isolated
5
- * pnpm stores (`kici-pnpm-store-*`, see dep-installer.ts) clean themselves
6
- * up in `finally` blocks but a hard process death (SIGKILL, OOM kill)
7
- * skips those, and on a long-lived bare-metal agent the leftovers then
8
- * accumulate forever. Collecting anything older than a day at startup is
9
- * safe on shared hosts: no job lives remotely that long (job timeouts are
10
- * minutes), so a concurrent agent's in-flight dirs are never eligible.
4
+ * Bare job workdirs (`kici-<6 random chars>`, see job-runner.ts), labeled
5
+ * allocator dirs (`kici-<label>-<6 random chars>`, minted by the global temp
6
+ * allocator `@kici-dev/core/tmp`, which guarantees every allocation carries
7
+ * the `kici-` prefix), and isolated pnpm stores (`kici-pnpm-store-*`, see
8
+ * dep-installer.ts) clean themselves up in `finally` blocks but a hard
9
+ * process death (SIGKILL, OOM kill) skips those, and on a long-lived
10
+ * bare-metal agent the leftovers then accumulate forever. Collecting anything
11
+ * older than a day at startup is safe on shared hosts: no job lives remotely
12
+ * that long (job timeouts are minutes), so a concurrent agent's in-flight
13
+ * dirs are never eligible.
14
+ *
15
+ * The deterministic persistent caches (`kici-agent-payloads`, `kici-data`,
16
+ * `kici-scaler-ledger`) also live directly under the temp root and can be far
17
+ * older than a day, so they are explicitly excluded — `kici-scaler-ledger`
18
+ * even structurally matches the allocator pattern (`ledger` is a 6-char label
19
+ * suffix), which a regex alone cannot distinguish.
11
20
  */
12
21
  /**
13
22
  * Collect this agent's stale temp dirs. `base` is overridable for tests;
14
23
  * production callers use the default temp root. Never throws.
24
+ *
25
+ * The default base is `kiciTmpBase()`, not the OS temp root, so it scans the
26
+ * exact directory the agent now writes payloads/clones/pnpm stores into: the
27
+ * global temp allocator and the payload cache both honor `KICI_TMPDIR` via the
28
+ * same helper. If the GC scanned a different root, stale-temp reaping would
29
+ * silently stop working whenever `KICI_TMPDIR` is set.
15
30
  */
16
31
  export declare function gcStaleAgentTmpDirs(base?: string): Promise<string[]>;
17
32
  //# sourceMappingURL=tmp-gc.d.ts.map
@@ -6,7 +6,37 @@
6
6
  * no Rolldown step at runtime. `@kici-dev/sdk` and host-repo deps resolve via
7
7
  * Node's normal ESM lookup against `.kici/node_modules/`.
8
8
  */
9
- import type { Workflow, StepInput, DynamicJobFn } from '@kici-dev/sdk';
9
+ import type { Workflow, StepInput, DynamicJobFn, OutputsMap, StepRefMap } from '@kici-dev/sdk';
10
+ /**
11
+ * SDK output-map setters resolved from a specific `@kici-dev/sdk` instance.
12
+ * The agent uses these to wire the workflow module's OWN SDK copy (a different
13
+ * physical module than the agent's bundled SDK) to the same output maps the
14
+ * step loop mutates, so within-job `.result` proxies — which read that copy's
15
+ * module-global `_stepOutputsMap` — resolve at access time.
16
+ */
17
+ export interface SdkOutputSetters {
18
+ setStepOutputsMap: (map: OutputsMap) => void;
19
+ setStepRefMap: (map: StepRefMap) => void;
20
+ setJobOutputsMap: (map: OutputsMap) => void;
21
+ }
22
+ /**
23
+ * Resolve the `@kici-dev/sdk` instance the workflow module itself imports.
24
+ *
25
+ * The workflow's `.result` proxies read the module-global step-outputs map of
26
+ * whichever SDK copy the workflow file resolves — which is generally a
27
+ * different physical module than the agent's bundled SDK (the workflow is
28
+ * imported from the cloned source tree and resolves its deps against that
29
+ * tree's `node_modules`). Resolving via `createRequire(workflowFilePath)` walks
30
+ * `node_modules` from the workflow file exactly the way the workflow's own
31
+ * `import '@kici-dev/sdk'` does — including any hoisted copy — so the returned
32
+ * setters mutate the SAME module-global map object the proxies read. Node caches
33
+ * ESM modules by resolved URL, so importing that path yields the workflow's live
34
+ * singleton, not a fresh copy.
35
+ *
36
+ * Falls back to the agent's bundled setters when resolution fails (mirrors
37
+ * `resolveSdkSetters` in the compiler's test runner).
38
+ */
39
+ export declare function resolveWorkflowSdkSetters(workflowFilePath: string): Promise<SdkOutputSetters>;
10
40
  /**
11
41
  * Compile schema version — must match `@kici-dev/compiler` lockfile/hasher.ts.
12
42
  * Mixed into the content hash so compilation-approach changes produce different
@@ -33,6 +63,7 @@ export declare function ensureLoaderHookRegistered(): void;
33
63
  */
34
64
  export declare function loadWorkflowSource(workDir: string, sourceFile: string, expectedContentHash?: string, resolvedHashFiles?: string[]): Promise<{
35
65
  module: Record<string, unknown>;
66
+ sdkSetters: SdkOutputSetters;
36
67
  }>;
37
68
  /**
38
69
  * Extract a workflow by name from a module's exports.
package/dist/index.js CHANGED
@@ -5,7 +5,8 @@ import { z } from "zod";
5
5
  import { LOGGER_ENV_VARS, defineEnv, validateUnknownKiciVars } from "@kici-dev/shared/env";
6
6
  import { KNOWN_ROLES, parseHostPropertyAssignments, validateNoReservedLabels } from "@kici-dev/engine";
7
7
  import { execFile } from "node:child_process";
8
- import { access, cp, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
8
+ import { access, cp, lstat, mkdir, readFile, readdir, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
9
+ import { makeTempDir } from "@kici-dev/core/tmp";
9
10
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
10
11
  import { promisify } from "node:util";
11
12
  import { createLogger, sha256, toErrorMessage } from "@kici-dev/shared";
@@ -15,6 +16,7 @@ import { parse, stringify } from "yaml";
15
16
  import { Readable, Transform } from "node:stream";
16
17
  import { pipeline } from "node:stream/promises";
17
18
  import { createGunzip } from "node:zlib";
19
+ import { HOME_ANCHOR, REPO_ANCHOR } from "@kici-dev/core";
18
20
  import { c, x } from "tar";
19
21
  import https from "node:https";
20
22
  import http from "node:http";
@@ -85,8 +87,18 @@ const envDef = defineEnv({
85
87
  dockerKeepFailed: z.string().default("false").transform((s) => s === "true"),
86
88
  jobHeartbeatIntervalMs: z.coerce.number().default(6e4),
87
89
  backpressureMode: z.enum(["pause", "drop"]).default("pause"),
90
+ agentPayloadDir: z.string().optional(),
91
+ agentCommand: z.string().optional(),
88
92
  sandbox: z.string().default("false").transform((s) => s === "true"),
93
+ trustedEnv: z.string().default("false").transform((s) => s === "true"),
94
+ inPlace: z.string().default("false").transform((s) => s === "true"),
89
95
  sandboxNetwork: z.enum(["isolated", "host"]).default("isolated"),
96
+ sandboxHardened: z.string().default("true").transform((s) => s !== "false"),
97
+ sandboxReadonlyRootfs: z.string().default("false").transform((s) => s === "true"),
98
+ sandboxUser: z.string().optional(),
99
+ sandboxPidsLimit: z.coerce.number().int().positive().default(512),
100
+ sandboxMemoryBytes: z.coerce.number().int().positive().default(2 * 1024 * 1024 * 1024),
101
+ sandboxNanoCpus: z.coerce.number().int().positive().default(2 * 1e9),
90
102
  scalerManaged: z.string().optional().transform((s) => s === "1"),
91
103
  scalerIdleTimeoutMs: z.coerce.number().default(5e3),
92
104
  scalerPendingDispatchTimeoutMs: z.coerce.number().default(6e4),
@@ -111,8 +123,18 @@ const envDef = defineEnv({
111
123
  dockerKeepFailed: "KICI_DOCKER_KEEP_FAILED",
112
124
  jobHeartbeatIntervalMs: "KICI_JOB_HEARTBEAT_INTERVAL_MS",
113
125
  backpressureMode: "KICI_BACKPRESSURE_MODE",
126
+ agentPayloadDir: "KICI_AGENT_PAYLOAD_DIR",
127
+ agentCommand: "KICI_AGENT_COMMAND",
114
128
  sandbox: "KICI_SANDBOX",
129
+ trustedEnv: "KICI_TRUSTED_ENV",
130
+ inPlace: "KICI_IN_PLACE",
115
131
  sandboxNetwork: "KICI_SANDBOX_NETWORK",
132
+ sandboxHardened: "KICI_SANDBOX_HARDENED",
133
+ sandboxReadonlyRootfs: "KICI_SANDBOX_READONLY_ROOTFS",
134
+ sandboxUser: "KICI_SANDBOX_USER",
135
+ sandboxPidsLimit: "KICI_SANDBOX_PIDS_LIMIT",
136
+ sandboxMemoryBytes: "KICI_SANDBOX_MEMORY_BYTES",
137
+ sandboxNanoCpus: "KICI_SANDBOX_NANO_CPUS",
116
138
  scalerManaged: "KICI_SCALER_MANAGED",
117
139
  scalerIdleTimeoutMs: "KICI_SCALER_IDLE_TIMEOUT",
118
140
  scalerPendingDispatchTimeoutMs: "KICI_SCALER_PENDING_DISPATCH_TIMEOUT",
@@ -140,7 +162,15 @@ const envDef = defineEnv({
140
162
  * - KICI_JOB_HEARTBEAT_INTERVAL_MS (default: 60000)
141
163
  * - KICI_BACKPRESSURE_MODE (default: pause, options: pause | drop)
142
164
  * - KICI_SANDBOX (default: false) — enable bubblewrap (bwrap) namespace isolation for bare-metal execution
143
- * - KICI_SANDBOX_NETWORK (default: isolated, options: isolated | host) when sandbox=true, controls bwrap network namespace
165
+ * - KICI_TRUSTED_ENV (default: false) — trusted fleet-agent profile: pass the ambient host env (minus the agent's own KiCI identity secrets) through to steps
166
+ * - KICI_IN_PLACE (default: false) — in-place no-clone profile: for a file:// source, use the real repo path as workDir and skip the clone (the routed deploy:stg profile)
167
+ * - KICI_SANDBOX_NETWORK (default: isolated, options: isolated | host) — sandbox network posture for BOTH backends: the bwrap network namespace (when sandbox=true) and the container job network. `host` shares the host network (container backend also binds host /etc/hosts read-only for name resolution); applies to the container backend under the default hardened posture (KICI_SANDBOX_HARDENED=true)
168
+ * - KICI_SANDBOX_HARDENED (default: true) — hardened-by-default job containers (CapDrop ALL, no-new-privileges, cgroup caps, tmpfs /tmp); set false to roll back to the legacy unhardened posture
169
+ * - KICI_SANDBOX_READONLY_ROOTFS (default: false) — opt-in read-only container rootfs (/tmp stays a writable tmpfs)
170
+ * - KICI_SANDBOX_USER (optional) — container user override (uid, uid:gid, or name); honors the image user when unset
171
+ * - KICI_SANDBOX_PIDS_LIMIT (default: 512) — max PIDs in the job container cgroup
172
+ * - KICI_SANDBOX_MEMORY_BYTES (default: 2 GiB) — memory cap in bytes for the job container cgroup
173
+ * - KICI_SANDBOX_NANO_CPUS (default: 2 CPUs) — CPU cap in nano-CPUs for the job container cgroup
144
174
  * - KICI_SCALER_MANAGED (set to "1" by the orchestrator's auto-scaler — agent self-shuts down on idle)
145
175
  * - KICI_SCALER_IDLE_TIMEOUT (ms, default 5000) — how long a scaler-managed agent waits before shutdown after going idle
146
176
  * - KICI_SCALER_PENDING_DISPATCH_TIMEOUT (ms, default 60000) — extended idle window when register.ack signals a queued bound job
@@ -149,7 +179,7 @@ const envDef = defineEnv({
149
179
  */
150
180
  function loadConfig() {
151
181
  const data = envDef.parse();
152
- if (!data.scalerManaged) validateNoReservedLabels(data.labels, "KICI_LABELS");
182
+ if (!data.scalerManaged && !data.agentToken) validateNoReservedLabels(data.labels, "KICI_LABELS");
153
183
  validateUnknownKiciVars([...envDef.listKnownEnvVars(), ...LOGGER_ENV_VARS]);
154
184
  return {
155
185
  ...data,
@@ -358,7 +388,7 @@ async function applyYarnrcBerryConfig(args) {
358
388
  const hasPrivateRegistry = registries.length > 0 || Object.keys(installEnvSecrets).length > 0;
359
389
  const yarnrcPath = join(args.kiciDir, ".yarnrc.yml");
360
390
  const { raw: original, doc } = await readOriginalYarnrc(yarnrcPath);
361
- const cacheFolder = await mkdtemp(join(tmpdir(), "kici-yarn-berry-cache-"));
391
+ const { path: cacheFolder, cleanup: cleanupCache } = await makeTempDir("yarn-berry-cache");
362
392
  const merged = {
363
393
  ...doc,
364
394
  nodeLinker: "node-modules",
@@ -395,10 +425,7 @@ async function applyYarnrcBerryConfig(args) {
395
425
  if (original === null) await unlink(yarnrcPath).catch(() => {});
396
426
  else await writeFile(yarnrcPath, original, { encoding: "utf8" });
397
427
  } catch {}
398
- await rm(cacheFolder, {
399
- recursive: true,
400
- force: true
401
- }).catch(() => {});
428
+ await cleanupCache().catch(() => {});
402
429
  };
403
430
  return {
404
431
  extraEnv: {
@@ -750,9 +777,9 @@ async function detectKiciYarnFlavor(repoRoot, kiciDir) {
750
777
  * Install `.kici/` dependencies inline with the repo's package manager.
751
778
  *
752
779
  * Falls back to this when the dep cache is unavailable or a download fails.
753
- * The install runs with an isolated cache/store directory (created in
754
- * `os.tmpdir()`) to prevent cache poisoning between build jobs; the directory
755
- * is removed after installation.
780
+ * The install runs with an isolated cache/store directory (allocated under the
781
+ * global temp base, which honors `KICI_TMPDIR`) to prevent cache poisoning
782
+ * between build jobs; the directory is removed after installation.
756
783
  *
757
784
  * If `opts.npmRegistries` / `opts.installEnvSecrets` is provided, a job-scoped
758
785
  * `.kici/.npmrc` overlay is synthesized for the install, restored in `finally`,
@@ -841,7 +868,7 @@ function envWithNodeOnPath(extraEnv, nodeDir) {
841
868
  /** Run `npm install` in `.kici/` with an isolated cache directory. */
842
869
  async function runNpmInstall(args) {
843
870
  const { npmCliPath, nodeExe, nodeDir } = resolveNpm();
844
- const cacheDir = await mkdtemp(join(tmpdir(), "kici-npm-cache-"));
871
+ const { path: cacheDir, cleanup } = await makeTempDir("npm-cache");
845
872
  const env = envWithNodeOnPath(args.registryConfig.extraEnv, nodeDir);
846
873
  const buildArgs = (...prefix) => {
847
874
  const a = [
@@ -866,10 +893,7 @@ async function runNpmInstall(args) {
866
893
  maxBuffer: INSTALL_MAX_BUFFER
867
894
  });
868
895
  } finally {
869
- await rm(cacheDir, {
870
- recursive: true,
871
- force: true
872
- }).catch(() => {});
896
+ await cleanup().catch(() => {});
873
897
  }
874
898
  }
875
899
  /**
@@ -883,7 +907,7 @@ async function runNpmInstall(args) {
883
907
  async function runPnpmInstall(args) {
884
908
  await assertPnpmAvailable();
885
909
  const { nodeDir } = resolveNpm();
886
- const storeDir = await mkdtemp(join(tmpdir(), "kici-pnpm-store-"));
910
+ const { path: storeDir, cleanup } = await makeTempDir("pnpm-store");
887
911
  const env = envWithNodeOnPath(args.registryConfig.extraEnv, nodeDir);
888
912
  const argv = [
889
913
  "install",
@@ -903,10 +927,7 @@ async function runPnpmInstall(args) {
903
927
  maxBuffer: INSTALL_MAX_BUFFER
904
928
  });
905
929
  } finally {
906
- await rm(storeDir, {
907
- recursive: true,
908
- force: true
909
- }).catch(() => {});
930
+ await cleanup().catch(() => {});
910
931
  }
911
932
  }
912
933
  /** Pure: argv for `yarn install` with an isolated cache folder. */
@@ -932,7 +953,7 @@ function buildYarnInstallArgs(cacheDir, hasPrivateRegistry) {
932
953
  async function runYarnInstall(args) {
933
954
  await assertYarnAvailable();
934
955
  const { nodeDir } = resolveNpm();
935
- const cacheDir = await mkdtemp(join(tmpdir(), "kici-yarn-cache-"));
956
+ const { path: cacheDir, cleanup } = await makeTempDir("yarn-cache");
936
957
  const env = envWithNodeOnPath(args.registryConfig.extraEnv, nodeDir);
937
958
  const argv = buildYarnInstallArgs(cacheDir, args.hasPrivateRegistry);
938
959
  try {
@@ -944,10 +965,7 @@ async function runYarnInstall(args) {
944
965
  maxBuffer: INSTALL_MAX_BUFFER
945
966
  });
946
967
  } finally {
947
- await rm(cacheDir, {
948
- recursive: true,
949
- force: true
950
- }).catch(() => {});
968
+ await cleanup().catch(() => {});
951
969
  }
952
970
  }
953
971
  /** Pure: argv for a berry `yarn install`. Cache + linker live in .yarnrc.yml. */
@@ -1218,10 +1236,6 @@ var init_download = __esmMin((() => {
1218
1236
  const logger = createLogger({ prefix: "cache-engine" });
1219
1237
  /** Download timeout for a presigned cache GET: 5 minutes. */
1220
1238
  const DOWNLOAD_TIMEOUT_MS = 300 * 1e3;
1221
- /** Anchor prefix for repo-root-relative cache entries inside the tar. */
1222
- const REPO_ANCHOR = "__repo__";
1223
- /** Anchor prefix for home-relative (`~`) cache entries inside the tar. */
1224
- const HOME_ANCHOR = "__home__";
1225
1239
  /**
1226
1240
  * Resolve a cache path. `~`-prefixed -> home root; otherwise repo-root-relative.
1227
1241
  * Rejects absolute paths and `..` escapes so a workflow cannot read or clobber
@@ -1262,7 +1276,7 @@ function anchorEntries(workDir, paths, roots) {
1262
1276
  */
1263
1277
  async function packCachePaths(workDir, paths, roots) {
1264
1278
  const entries = anchorEntries(workDir, paths, roots);
1265
- const staging = await mkdtemp(join(tmpdir(), "kici-cache-pack-"));
1279
+ const { path: staging, cleanup } = await makeTempDir("cache-pack");
1266
1280
  try {
1267
1281
  const topLevel = /* @__PURE__ */ new Set();
1268
1282
  for (const e of entries) {
@@ -1293,10 +1307,7 @@ async function packCachePaths(workDir, paths, roots) {
1293
1307
  hash
1294
1308
  };
1295
1309
  } finally {
1296
- await rm(staging, {
1297
- recursive: true,
1298
- force: true
1299
- });
1310
+ await cleanup();
1300
1311
  }
1301
1312
  }
1302
1313
  /** Move the extracted `__repo__` / `__home__` groups from a scratch dir into place. */
@@ -1327,7 +1338,7 @@ async function extractCacheTarball(tarball, workDir, expectedHash, roots) {
1327
1338
  if (actual !== expectedHash) throw new Error(`Cache tarball checksum mismatch: expected ${expectedHash}, got ${actual}`);
1328
1339
  const home = roots?.home ?? homedir();
1329
1340
  await mkdir(workDir, { recursive: true });
1330
- const scratch = await mkdtemp(join(tmpdir(), "kici-cache-extract-"));
1341
+ const { path: scratch, cleanup } = await makeTempDir("cache-extract");
1331
1342
  try {
1332
1343
  await new Promise((res, rej) => {
1333
1344
  Readable.from(tarball).pipe(x({
@@ -1337,10 +1348,7 @@ async function extractCacheTarball(tarball, workDir, expectedHash, roots) {
1337
1348
  });
1338
1349
  await moveAnchoredGroups(scratch, workDir, home);
1339
1350
  } finally {
1340
- await rm(scratch, {
1341
- recursive: true,
1342
- force: true
1343
- });
1351
+ await cleanup();
1344
1352
  }
1345
1353
  }
1346
1354
  /**
@@ -1359,17 +1367,14 @@ async function downloadAndExtractCache(url, workDir, expectedHash, roots) {
1359
1367
  cb(null, chunk);
1360
1368
  } });
1361
1369
  await mkdir(workDir, { recursive: true });
1362
- const scratch = await mkdtemp(join(tmpdir(), "kici-cache-extract-"));
1370
+ const { path: scratch, cleanup } = await makeTempDir("cache-extract");
1363
1371
  try {
1364
1372
  await pipeline(Readable.fromWeb(response.body), hashTransform, createGunzip(), x({ cwd: scratch }));
1365
1373
  const digest = hash.digest("hex");
1366
1374
  if (digest !== expectedHash) throw new Error(`Cache tarball checksum mismatch on download: expected ${expectedHash}, got ${digest}`);
1367
1375
  await moveAnchoredGroups(scratch, workDir, home);
1368
1376
  } finally {
1369
- await rm(scratch, {
1370
- recursive: true,
1371
- force: true
1372
- });
1377
+ await cleanup();
1373
1378
  }
1374
1379
  }
1375
1380
  /** Build the imperative `ctx.cache` API bound to a workDir + transport. */
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Build a SLSA v1.0 in-toto provenance statement from the server-truth identity
3
3
  * token claims plus the caller-supplied subject. The build context comes
4
- * entirely from the JWT claims (Platform-minted, unforgeable), so the
4
+ * entirely from the JWT claims (minted server-side by the orchestrator,
5
+ * unforgeable), so the
5
6
  * statement's identity equals the token's identity by construction.
6
7
  */
7
8
  import { type KiciProvenanceStatement } from '@kici-dev/engine/provenance/schema';
8
9
  import type { SourceOrigin } from '@kici-dev/engine';
9
- /** The KiCI identity-token claims the builder reads (Platform server-truth). */
10
+ /** The KiCI identity-token claims the builder reads (minter server-truth). */
10
11
  export interface ProvenanceTokenClaims {
11
12
  iss: string;
12
13
  repository?: string | null;
package/dist/server.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Startup sequence:
5
5
  * 1. Load config
6
6
  * 2. Create logger
7
- * 3. Create JobRunner with send/sendDirect callbacks
7
+ * 3. Create JobRunner with send callbacks
8
8
  * 4. Create OrchestratorClient with dispatch and cancel handlers
9
9
  * 5. Add WS log transport (if not scaler-managed)
10
10
  * 6. Connect OrchestratorClient