@cruxy/cli 0.12.0 → 0.13.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.
@@ -32,7 +32,11 @@ declare const parameters: z.ZodObject<{
32
32
  command?: string | undefined;
33
33
  }>;
34
34
  export interface RunTestsToolDeps {
35
- /** Execution seam (tests inject a fake; default spawns the real command). */
35
+ /**
36
+ * Execution seam (tests inject a fake). When unset, the runner is chosen per
37
+ * run from `ctx.sandbox`: present → {@link SandboxTestRunner} (run in the box,
38
+ * C.16), absent → host {@link CommandTestRunner} — unchanged.
39
+ */
36
40
  runner?: TestRunner;
37
41
  /** Detection seam (defaults to config + package.json detection). */
38
42
  detect?: (ctx: ToolContext) => TestCommand | null;
@@ -2,6 +2,7 @@ import { z } from "zod";
2
2
  import { ErrorCode } from "../errors/index.js";
3
3
  import { detectTestCommand } from "./detect.js";
4
4
  import { CommandTestRunner } from "./runner.js";
5
+ import { SandboxTestRunner } from "./sandbox-runner.js";
5
6
  /**
6
7
  * The `run_tests` tool (C.13): execute the project's test suite and return a
7
8
  * structured result the model can iterate on (edit → re-run → repeat). The
@@ -58,7 +59,6 @@ function renderResult(result, command, iteration) {
58
59
  }
59
60
  /** Build the `run_tests` tool. One instance = one session's iteration budget. */
60
61
  export function makeRunTestsTool(deps = {}) {
61
- const runner = deps.runner ?? new CommandTestRunner();
62
62
  const detect = deps.detect ??
63
63
  ((ctx) => detectTestCommand(ctx.cwd, ctx.config));
64
64
  const budget = new TestIterationBudget();
@@ -104,6 +104,13 @@ export function makeRunTestsTool(deps = {}) {
104
104
  error: decision.feedback ?? "test run denied by the user",
105
105
  };
106
106
  }
107
+ // Substrate chosen by ctx.sandbox (C.16): in the box when enabled, else
108
+ // host — identical result shape either way. An explicit deps.runner
109
+ // (tests) always wins. A sandbox that can't run throws (fail loud).
110
+ const runner = deps.runner ??
111
+ (ctx.sandbox
112
+ ? new SandboxTestRunner(ctx.sandbox)
113
+ : new CommandTestRunner());
107
114
  const result = await runner.run(resolved.command, {
108
115
  cwd: ctx.cwd,
109
116
  timeoutMs: ctx.config.shell.timeoutMs,
@@ -0,0 +1,16 @@
1
+ import type { SandboxService } from "../sandbox/index.js";
2
+ import type { TestRunner, TestRunOptions, TestRunResult } from "./types.js";
3
+ /**
4
+ * A {@link TestRunner} that runs the suite inside the C.16 sandbox instead of
5
+ * on the host. It maps the neutral sandbox {@link ExecResult} onto the exact
6
+ * same {@link TestRunResult} the host {@link CommandTestRunner} produces — so
7
+ * honest-green (`passed = exitCode === 0`), tail-biased capture, and the
8
+ * timeout note all hold identically inside the box. A container-start failure
9
+ * is NOT swallowed into a result: `sandbox.exec` throws a coded error that
10
+ * propagates (fail loud), never a fabricated pass.
11
+ */
12
+ export declare class SandboxTestRunner implements TestRunner {
13
+ private readonly sandbox;
14
+ constructor(sandbox: SandboxService);
15
+ run(command: string, opts: TestRunOptions): Promise<TestRunResult>;
16
+ }
@@ -0,0 +1,47 @@
1
+ import { parseFailures } from "./parse.js";
2
+ /**
3
+ * A {@link TestRunner} that runs the suite inside the C.16 sandbox instead of
4
+ * on the host. It maps the neutral sandbox {@link ExecResult} onto the exact
5
+ * same {@link TestRunResult} the host {@link CommandTestRunner} produces — so
6
+ * honest-green (`passed = exitCode === 0`), tail-biased capture, and the
7
+ * timeout note all hold identically inside the box. A container-start failure
8
+ * is NOT swallowed into a result: `sandbox.exec` throws a coded error that
9
+ * propagates (fail loud), never a fabricated pass.
10
+ */
11
+ export class SandboxTestRunner {
12
+ sandbox;
13
+ constructor(sandbox) {
14
+ this.sandbox = sandbox;
15
+ }
16
+ async run(command, opts) {
17
+ const result = await this.sandbox.exec(command, {
18
+ cwd: opts.cwd,
19
+ timeoutMs: opts.timeoutMs,
20
+ maxOutputBytes: opts.captureBytes,
21
+ capture: "tail", // failures live at the end of test output
22
+ });
23
+ if (result.timedOut) {
24
+ return {
25
+ passed: false,
26
+ exitCode: null,
27
+ durationMs: result.durationMs,
28
+ failures: [],
29
+ output: result.output +
30
+ `\n… [test run timed out after ${opts.timeoutMs}ms and was killed]`,
31
+ outputTruncated: result.outputTruncated,
32
+ };
33
+ }
34
+ // THE honest-green rule, unchanged by the substrate: exit 0 means passed.
35
+ const passed = result.exitCode === 0;
36
+ const parsed = passed ? { failures: [] } : parseFailures(result.output);
37
+ return {
38
+ passed,
39
+ exitCode: result.exitCode,
40
+ durationMs: result.durationMs,
41
+ ...(parsed.total !== undefined ? { total: parsed.total } : {}),
42
+ failures: parsed.failures,
43
+ output: result.output,
44
+ outputTruncated: result.outputTruncated,
45
+ };
46
+ }
47
+ }
@@ -26,9 +26,43 @@ export const runCommandTool = {
26
26
  error: decision.feedback ?? "command denied by the user",
27
27
  };
28
28
  }
29
- return runBounded(input.command, ctx);
29
+ // Substrate is chosen SOLELY by ctx.sandbox: present → run in the box (C.16),
30
+ // never on the host; absent → host, unchanged. There is no fallback path — a
31
+ // sandbox that can't run throws a coded error (see runSandboxed) rather than
32
+ // silently reaching runBounded.
33
+ return ctx.sandbox
34
+ ? runSandboxed(input.command, ctx)
35
+ : runBounded(input.command, ctx);
30
36
  },
31
37
  };
38
+ /**
39
+ * Run inside the sandbox and map the neutral ExecResult onto the IDENTICAL
40
+ * ToolResult the host path produces — same "exit code N" framing, same
41
+ * truncation note, same timeout message — so the tool is substrate-agnostic.
42
+ * A container-start / image failure throws a coded CruxyError from
43
+ * `sandbox.exec` and propagates; we deliberately do not catch it (fail loud).
44
+ */
45
+ function runSandboxed(command, ctx) {
46
+ const { timeoutMs, maxOutputBytes } = ctx.config.shell;
47
+ return ctx
48
+ .sandbox.exec(command, {
49
+ cwd: ctx.cwd,
50
+ timeoutMs,
51
+ maxOutputBytes,
52
+ capture: "head",
53
+ })
54
+ .then((result) => {
55
+ if (result.timedOut) {
56
+ return { ok: false, error: `timed out after ${timeoutMs}ms` };
57
+ }
58
+ const exit = result.exitCode ?? "unknown";
59
+ let output = `exit code ${exit}\n${result.output}`;
60
+ if (result.outputTruncated) {
61
+ output += `\n… [output truncated at ${maxOutputBytes} bytes]`;
62
+ }
63
+ return { ok: true, output };
64
+ });
65
+ }
32
66
  /** Spawn the command, capture bounded output, and enforce the timeout. */
33
67
  function runBounded(command, ctx) {
34
68
  const { timeoutMs, maxOutputBytes } = ctx.config.shell;
@@ -1,6 +1,7 @@
1
1
  import type { z, ZodTypeAny } from "zod";
2
2
  import type { CruxyConfig } from "../config/index.js";
3
3
  import type { ApprovalDecision } from "../approval/types.js";
4
+ import type { SandboxService } from "../sandbox/index.js";
4
5
  import type { logger } from "../utils/logger.js";
5
6
  /** The leveled logger instance shared across the CLI. */
6
7
  type Logger = typeof logger;
@@ -131,6 +132,15 @@ export interface ToolContext {
131
132
  * tools never call this.
132
133
  */
133
134
  requestApproval(action: ApproveAction): Promise<ApprovalDecision>;
135
+ /**
136
+ * Isolation substrate for the shell + test tools (C.16). Present ONLY when
137
+ * the sandbox is enabled; when set, `run_command`/`run_tests` execute the
138
+ * approved command inside the container and NEVER on the host. Its mere
139
+ * presence is the switch — there is no host fallback once it is set (an
140
+ * unavailable runtime fails loud at construction, before this is ever
141
+ * populated). Absent → host execution, unchanged.
142
+ */
143
+ sandbox?: SandboxService;
134
144
  }
135
145
  /**
136
146
  * The one interface every tool implements. `parameters` is a zod schema; it both
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cruxy/cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "an agentic coding CLI",
5
5
  "type": "module",
6
6
  "bin": {