@getollie/cli 0.2.0 → 0.2.1

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/README.md CHANGED
@@ -60,6 +60,14 @@ DAYTONA_API_KEY=... ollie agent --executor daytona 'run the native test suite an
60
60
 
61
61
  Agent `--ndjson` emits one runner event per line. Agent workspace writes persist to the host.
62
62
 
63
+ Use repeatable `--mount <host-path:sandbox-path>` options to expose additional host directories read-write to the agent sandbox:
64
+
65
+ ```bash
66
+ ollie agent --mount ./data:/data --mount /tmp/cache:/cache 'process the data'
67
+ ```
68
+
69
+ Mount destinations must be normalized absolute sandbox paths outside `/workspace`, and mount destinations may not overlap. Relative host paths are resolved from the directory where Ollie is invoked. Additional mounts apply to the agent's primary sandbox tools, not delegated local or Daytona executors.
70
+
63
71
  The agent remains in the restricted workspace environment by default. `--executor local` adds a `delegateTask` tool that can start fresh subagents with native `bash`, file, package-manager, build, and test access. The local executor shares the current workspace, runs with the current user's permissions, inherits the current process environment, and is **not isolated**. It must be selected explicitly.
64
72
 
65
73
  `--executor daytona` runs delegated agents in an isolated Daytona sandbox. Ollie uploads the complete workspace before each delegated task, synchronizes all files back afterward, stops the sandbox between tasks while preserving its filesystem, and deletes it when the run finishes. Configure the SDK with `DAYTONA_API_KEY` and optionally `DAYTONA_API_URL`, `DAYTONA_TARGET`, or `OLLIE_DAYTONA_SNAPSHOT`.
@@ -68,9 +76,10 @@ The agent remains in the restricted workspace environment by default. `--executo
68
76
 
69
77
  ```bash
70
78
  ollie sandbox --cwd /path/to/workspace
79
+ ollie sandbox --cwd /path/to/workspace --mount ./data:/data
71
80
  ```
72
81
 
73
- The worker reserves stdout for newline-delimited request/response records and retains one sandbox until EOF, so in-session changes persist between requests. Its workspace writes persist to the host by default.
82
+ The worker reserves stdout for newline-delimited request/response records and retains one sandbox until EOF, so in-session changes persist between requests. Its workspace and additional mount writes persist to the host by default.
74
83
 
75
84
  ### Diagnostics
76
85
 
@@ -101,11 +110,13 @@ Configuration precedence is CLI flags, then environment variables, then defaults
101
110
  ```ts
102
111
  import { createJustBash, SANDBOX_WORKSPACE_PATH } from "@getollie/cli";
103
112
 
104
- const bash = createJustBash(process.cwd());
113
+ const bash = createJustBash(process.cwd(), {
114
+ mounts: [{ hostPath: "./data", mountPoint: "/data" }],
115
+ });
105
116
  const result = await bash.exec(`echo generated > ${SANDBOX_WORKSPACE_PATH}/output.txt`);
106
117
  ```
107
118
 
108
- The configured host directory is mounted read-write at `/workspace`, so writes persist directly to it. Symlinks are disabled, and Python and JavaScript support are enabled.
119
+ The configured host directory is mounted read-write at `/workspace`, and optional additional host directories can be mounted read-write elsewhere, so writes persist directly to them. Symlinks are disabled, and Python and JavaScript support are enabled.
109
120
 
110
121
  ### Sandbox Git
111
122
 
package/dist/cli.js CHANGED
@@ -8,7 +8,11 @@ import { Command } from "commander";
8
8
 
9
9
  // src/config.ts
10
10
  import { access, stat } from "fs/promises";
11
- import { resolve } from "path";
11
+ import { resolve as resolve2 } from "path";
12
+
13
+ // src/sandbox/mounts.ts
14
+ import { realpathSync, statSync } from "fs";
15
+ import { posix, resolve } from "path";
12
16
 
13
17
  // src/utils/errors.ts
14
18
  var CliError = class extends Error {
@@ -26,6 +30,64 @@ function getErrorMessage(error) {
26
30
  return String(error);
27
31
  }
28
32
 
33
+ // src/sandbox/mounts.ts
34
+ var SANDBOX_WORKSPACE_PATH = "/workspace";
35
+ function pathsOverlap(left, right) {
36
+ return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
37
+ }
38
+ function validateMountPoint(mountPoint) {
39
+ if (!posix.isAbsolute(mountPoint) || mountPoint === "/" || mountPoint.includes("\\") || mountPoint.includes("\0") || posix.normalize(mountPoint) !== mountPoint) {
40
+ throw new CliError(
41
+ `Mount point must be a normalized absolute sandbox path other than /: ${mountPoint}`
42
+ );
43
+ }
44
+ if (pathsOverlap(mountPoint, SANDBOX_WORKSPACE_PATH)) {
45
+ throw new CliError(`Mount point must be outside ${SANDBOX_WORKSPACE_PATH}: ${mountPoint}`);
46
+ }
47
+ }
48
+ function validateSandboxMounts(mounts) {
49
+ const validated = [];
50
+ for (const mount of mounts) {
51
+ validateMountPoint(mount.mountPoint);
52
+ let hostPath;
53
+ try {
54
+ hostPath = realpathSync(resolve(mount.hostPath));
55
+ if (!statSync(hostPath).isDirectory()) {
56
+ throw new Error("not a directory");
57
+ }
58
+ } catch {
59
+ throw new CliError(
60
+ `Mount source does not exist or is not a directory: ${resolve(mount.hostPath)}`
61
+ );
62
+ }
63
+ const conflict = validated.find(
64
+ (candidate) => pathsOverlap(candidate.mountPoint, mount.mountPoint)
65
+ );
66
+ if (conflict) {
67
+ throw new CliError(
68
+ `Mount points must not overlap: ${conflict.mountPoint} and ${mount.mountPoint}`
69
+ );
70
+ }
71
+ validated.push({ hostPath, mountPoint: mount.mountPoint });
72
+ }
73
+ return validated;
74
+ }
75
+ function parseSandboxMounts(specs = []) {
76
+ const mounts = specs.map((spec) => {
77
+ const separator = spec.lastIndexOf(":");
78
+ if (separator <= 0 || separator === spec.length - 1) {
79
+ throw new CliError(
80
+ `Invalid mount ${JSON.stringify(spec)}. Expected <host-path>:<sandbox-path>`
81
+ );
82
+ }
83
+ return {
84
+ hostPath: spec.slice(0, separator),
85
+ mountPoint: spec.slice(separator + 1)
86
+ };
87
+ });
88
+ return validateSandboxMounts(mounts);
89
+ }
90
+
29
91
  // src/config.ts
30
92
  var DEFAULT_PROVIDER = "openai";
31
93
  var DEFAULT_MODEL = "gpt-5.6";
@@ -54,7 +116,7 @@ function parseExecutor(value) {
54
116
  return executor;
55
117
  }
56
118
  async function resolveRunConfig(input = {}, env = process.env) {
57
- const cwd = resolve(input.cwd ?? env.OLLIE_CWD ?? process.cwd());
119
+ const cwd = resolve2(input.cwd ?? env.OLLIE_CWD ?? process.cwd());
58
120
  const stats = await stat(cwd).catch(() => null);
59
121
  if (!stats?.isDirectory()) {
60
122
  throw new CliError(`Working directory does not exist or is not a directory: ${cwd}`);
@@ -63,6 +125,7 @@ async function resolveRunConfig(input = {}, env = process.env) {
63
125
  const executor = parseExecutor(input.executor ?? env.OLLIE_EXECUTOR);
64
126
  return {
65
127
  cwd,
128
+ mounts: parseSandboxMounts(input.mount),
66
129
  provider: input.provider ?? env.OLLIE_PROVIDER ?? DEFAULT_PROVIDER,
67
130
  model: input.model ?? env.OLLIE_MODEL ?? DEFAULT_MODEL,
68
131
  executor,
@@ -99,23 +162,23 @@ async function resolveRunConfig(input = {}, env = process.env) {
99
162
  import { access as access2, stat as stat2 } from "fs/promises";
100
163
 
101
164
  // src/sandbox/just-bash.ts
102
- import { realpathSync, statSync } from "fs";
165
+ import { realpathSync as realpathSync2, statSync as statSync2 } from "fs";
103
166
  import { Bash, InMemoryFs, MountableFs, ReadWriteFs } from "just-bash";
104
167
 
105
168
  // src/sandbox/git/index.ts
106
- import { posix as posix2 } from "path";
169
+ import { posix as posix3 } from "path";
107
170
  import { createTwoFilesPatch } from "diff";
108
171
  import * as git from "isomorphic-git";
109
172
  import { defineCommand } from "just-bash";
110
173
 
111
174
  // src/sandbox/git/fs-adapter.ts
112
- import { posix } from "path";
175
+ import { posix as posix2 } from "path";
113
176
  var ROOT = "/workspace";
114
177
  function safePath(path) {
115
178
  if (path.includes("\0") || path.includes("\\")) {
116
179
  throw Object.assign(new Error("Invalid virtual Git path"), { code: "EINVAL" });
117
180
  }
118
- const normalized = posix.resolve(path);
181
+ const normalized = posix2.resolve(path);
119
182
  if (normalized !== ROOT && !normalized.startsWith(`${ROOT}/`)) {
120
183
  throw Object.assign(new Error("Git path is outside /workspace"), { code: "EACCES" });
121
184
  }
@@ -135,7 +198,7 @@ function normalizeError(error) {
135
198
  throw Object.assign(new Error("Virtual filesystem operation failed"), { code: "EIO" });
136
199
  }
137
200
  async function ensureParent(fs, path) {
138
- const parent = posix.dirname(path);
201
+ const parent = posix2.dirname(path);
139
202
  if (!await fs.exists(parent)) await fs.mkdir(parent, { recursive: true });
140
203
  }
141
204
  function statShape(stat4) {
@@ -370,9 +433,9 @@ function virtualPath(ctx, input = ".") {
370
433
  async function repositoryDir(ctx) {
371
434
  let current = virtualPath(ctx);
372
435
  while (current === WORKSPACE || current.startsWith(`${WORKSPACE}/`)) {
373
- if (await ctx.fs.exists(posix2.join(current, ".git"))) return current;
436
+ if (await ctx.fs.exists(posix3.join(current, ".git"))) return current;
374
437
  if (current === WORKSPACE) break;
375
- current = posix2.dirname(current);
438
+ current = posix3.dirname(current);
376
439
  }
377
440
  throw new Error("not a git repository (or any parent up to /workspace)");
378
441
  }
@@ -455,7 +518,7 @@ async function writeGlobalConfig(ctx, values) {
455
518
  ([section, entries]) => `[${section}]
456
519
  ${entries.map(([key, value]) => ` ${key} = ${value}`).join("\n")}`
457
520
  ).join("\n");
458
- await ctx.fs.mkdir(posix2.dirname(GLOBAL_CONFIG_PATH), { recursive: true });
521
+ await ctx.fs.mkdir(posix3.dirname(GLOBAL_CONFIG_PATH), { recursive: true });
459
522
  await ctx.fs.writeFile(GLOBAL_CONFIG_PATH, content ? `${content}
460
523
  ` : "");
461
524
  }
@@ -469,7 +532,7 @@ async function handleInit(context, args) {
469
532
  throw new Error("cannot create repository directory");
470
533
  });
471
534
  await git.init({ fs: context.fs, dir, defaultBranch: branchOpt.value ?? "main" });
472
- return ok(`Initialized empty Git repository in ${posix2.join(dir, ".git")}/
535
+ return ok(`Initialized empty Git repository in ${posix3.join(dir, ".git")}/
473
536
  `);
474
537
  }
475
538
  async function handleStatus(context, args) {
@@ -622,7 +685,7 @@ async function handleDiff(context, args) {
622
685
  const changed = cachedFlag.enabled ? headStatus !== stageStatus : workdirStatus !== stageStatus;
623
686
  if (!changed) continue;
624
687
  const oldBytes = cachedFlag.enabled ? head && headStatus !== 0 ? (await git.readBlob({ fs: context.fs, dir, oid: head, filepath })).blob : empty : await readStage(filepath);
625
- const newBytes = cachedFlag.enabled ? await readStage(filepath) : workdirStatus === 0 ? empty : await context.command.fs.readFileBuffer(posix2.join(dir, filepath));
688
+ const newBytes = cachedFlag.enabled ? await readStage(filepath) : workdirStatus === 0 ? empty : await context.command.fs.readFileBuffer(posix3.join(dir, filepath));
626
689
  patches.push(patch(filepath, oldBytes, newBytes));
627
690
  }
628
691
  return ok(patches.join(""));
@@ -689,7 +752,7 @@ async function handleCheckout(context, args) {
689
752
  const oid = await git.resolveRef({ fs: context.fs, dir, ref });
690
753
  for (const filepath of filepaths) {
691
754
  const blob = await readCheckoutBlob(context, dir, oid, filepath);
692
- await context.command.fs.writeFile(posix2.join(dir, filepath), blob);
755
+ await context.command.fs.writeFile(posix3.join(dir, filepath), blob);
693
756
  }
694
757
  return ok();
695
758
  }
@@ -816,7 +879,7 @@ async function handleClone(context, args) {
816
879
  const [rawUrl, destination, ...rest] = noCheckoutFlag.rest;
817
880
  if (!rawUrl || rest.length) usage("clone requires a URL and optional directory");
818
881
  const url = validateRemoteUrl(rawUrl);
819
- const inferred = posix2.basename(new URL(url).pathname).replace(/\.git$/, "") || "repository";
882
+ const inferred = posix3.basename(new URL(url).pathname).replace(/\.git$/, "") || "repository";
820
883
  const dir = virtualPath(context.command, destination ?? inferred);
821
884
  const depth = positiveInteger(depthOpt.value, "--depth");
822
885
  await git.clone({
@@ -980,30 +1043,35 @@ Supported commands: ${COMMANDS.join(", ")}
980
1043
  }
981
1044
 
982
1045
  // src/sandbox/just-bash.ts
983
- var SANDBOX_WORKSPACE_PATH = "/workspace";
1046
+ var SANDBOX_WORKSPACE_PATH2 = "/workspace";
984
1047
  function canonicalizeWorkspaceRoot(workspaceRoot) {
985
- const canonicalRoot = realpathSync(workspaceRoot);
986
- if (!statSync(canonicalRoot).isDirectory()) {
1048
+ const canonicalRoot = realpathSync2(workspaceRoot);
1049
+ if (!statSync2(canonicalRoot).isDirectory()) {
987
1050
  throw new Error("The sandbox workspace root must be a directory");
988
1051
  }
989
1052
  return canonicalRoot;
990
1053
  }
991
- function createWorkspaceFs(workspaceRoot) {
1054
+ function createWorkspaceFs(workspaceRoot, options) {
992
1055
  const root = canonicalizeWorkspaceRoot(workspaceRoot);
1056
+ const additionalMounts = validateSandboxMounts(options.mounts ?? []).map((mount) => ({
1057
+ mountPoint: mount.mountPoint,
1058
+ filesystem: new ReadWriteFs({ root: mount.hostPath, allowSymlinks: false })
1059
+ }));
993
1060
  return new MountableFs({
994
1061
  base: new InMemoryFs(),
995
1062
  mounts: [
996
1063
  {
997
- mountPoint: SANDBOX_WORKSPACE_PATH,
1064
+ mountPoint: SANDBOX_WORKSPACE_PATH2,
998
1065
  filesystem: new ReadWriteFs({ root, allowSymlinks: false })
999
- }
1066
+ },
1067
+ ...additionalMounts
1000
1068
  ]
1001
1069
  });
1002
1070
  }
1003
1071
  function createJustBash(workspaceRoot, options = {}) {
1004
1072
  return new Bash({
1005
- fs: createWorkspaceFs(workspaceRoot),
1006
- cwd: SANDBOX_WORKSPACE_PATH,
1073
+ fs: createWorkspaceFs(workspaceRoot, options),
1074
+ cwd: SANDBOX_WORKSPACE_PATH2,
1007
1075
  env: {
1008
1076
  HOME: "/home/user",
1009
1077
  TERM: "xterm-256color"
@@ -1353,7 +1421,7 @@ var ExecutorController = class {
1353
1421
  // src/executor/daytona.ts
1354
1422
  import { randomUUID } from "crypto";
1355
1423
  import { mkdir, readdir, readFile, rm, stat as stat3, writeFile } from "fs/promises";
1356
- import { dirname, isAbsolute, relative, resolve as resolve2, sep } from "path";
1424
+ import { dirname, isAbsolute, relative, resolve as resolve3, sep } from "path";
1357
1425
  import {
1358
1426
  Daytona,
1359
1427
  DaytonaProcessExecutionTimeoutError
@@ -1388,7 +1456,7 @@ async function listLocalFiles(root) {
1388
1456
  const files = /* @__PURE__ */ new Map();
1389
1457
  async function visit(directory) {
1390
1458
  for (const entry of await readdir(directory, { withFileTypes: true })) {
1391
- const absolutePath = resolve2(directory, entry.name);
1459
+ const absolutePath = resolve3(directory, entry.name);
1392
1460
  if (entry.isDirectory()) {
1393
1461
  await visit(absolutePath);
1394
1462
  } else if (entry.isFile()) {
@@ -1472,7 +1540,7 @@ ${marker}
1472
1540
  this.#disposed = true;
1473
1541
  }
1474
1542
  #resolvePath(path) {
1475
- const target = isAbsolute(path) ? resolve2(path) : resolve2(this.#workspacePath, path);
1543
+ const target = isAbsolute(path) ? resolve3(path) : resolve3(this.#workspacePath, path);
1476
1544
  if (target !== this.#workspacePath && !target.startsWith(`${this.#workspacePath}/`)) {
1477
1545
  throw new Error(`Path is outside the Daytona workspace: ${path}`);
1478
1546
  }
@@ -1488,7 +1556,7 @@ var DaytonaWorkspaceSynchronizer = class {
1488
1556
  #sandbox;
1489
1557
  #knownFiles = /* @__PURE__ */ new Set();
1490
1558
  constructor(cwd, remoteWorkspacePath, sandbox) {
1491
- this.#cwd = resolve2(cwd);
1559
+ this.#cwd = resolve3(cwd);
1492
1560
  this.#remoteWorkspacePath = remoteWorkspacePath;
1493
1561
  this.#sandbox = sandbox;
1494
1562
  }
@@ -1519,7 +1587,7 @@ var DaytonaWorkspaceSynchronizer = class {
1519
1587
  for (const [path, size] of localFiles) {
1520
1588
  signal?.throwIfAborted();
1521
1589
  await abortable(
1522
- this.#sandbox.fs.uploadFile(resolve2(this.#cwd, path), this.#remotePath(path)),
1590
+ this.#sandbox.fs.uploadFile(resolve3(this.#cwd, path), this.#remotePath(path)),
1523
1591
  signal
1524
1592
  );
1525
1593
  if (this.#knownFiles.has(path)) filesUpdated++;
@@ -1551,7 +1619,7 @@ var DaytonaWorkspaceSynchronizer = class {
1551
1619
  let bytesTransferred = 0;
1552
1620
  for (const path of this.#knownFiles) {
1553
1621
  if (remoteFiles.has(path)) continue;
1554
- await rm(resolve2(this.#cwd, path), { force: true });
1622
+ await rm(resolve3(this.#cwd, path), { force: true });
1555
1623
  filesDeleted++;
1556
1624
  }
1557
1625
  for (const path of remoteFiles) {
@@ -1581,7 +1649,7 @@ var DaytonaWorkspaceSynchronizer = class {
1581
1649
  return `${this.#remoteWorkspacePath}/${path}`;
1582
1650
  }
1583
1651
  #localPath(path) {
1584
- const target = resolve2(this.#cwd, path);
1652
+ const target = resolve3(this.#cwd, path);
1585
1653
  if (target !== this.#cwd && !target.startsWith(`${this.#cwd}${sep}`)) {
1586
1654
  throw new Error(`Daytona returned a path outside the workspace: ${path}`);
1587
1655
  }
@@ -1715,7 +1783,7 @@ Terminus-2 spirit:
1715
1783
  - Summarize final findings and important command results clearly.
1716
1784
 
1717
1785
  Runtime context:
1718
- - Working directory: ${SANDBOX_WORKSPACE_PATH}
1786
+ - Working directory: ${SANDBOX_WORKSPACE_PATH2}
1719
1787
  - Current time: ${options.now.toISOString()}
1720
1788
  - Maximum model/tool steps: ${options.maxSteps}
1721
1789
  - Per-command timeout: ${options.commandTimeoutMs} ms
@@ -1770,8 +1838,8 @@ async function createWorkspaceTools(options) {
1770
1838
  }
1771
1839
  async function createTools(options) {
1772
1840
  return createWorkspaceTools({
1773
- sandbox: createJustBash(options.cwd),
1774
- destination: SANDBOX_WORKSPACE_PATH,
1841
+ sandbox: createJustBash(options.cwd, { mounts: options.mounts }),
1842
+ destination: SANDBOX_WORKSPACE_PATH2,
1775
1843
  maxOutputBytes: options.maxOutputBytes,
1776
1844
  extraInstructions: "Use bash commands to inspect files, search, run tests, and gather terminal state. Sandbox-native git is available; local Git changes persist in the workspace, and git push has permanent remote effects, so destructive actions and pushes must only be used when explicitly requested. Keep commands bounded."
1777
1845
  });
@@ -1846,7 +1914,7 @@ var ModelTaskDelegator = class {
1846
1914
  // src/executor/local.ts
1847
1915
  import { spawn } from "child_process";
1848
1916
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
1849
- import { dirname as dirname2, isAbsolute as isAbsolute2, resolve as resolve3 } from "path";
1917
+ import { dirname as dirname2, isAbsolute as isAbsolute2, resolve as resolve4 } from "path";
1850
1918
  var LocalSandbox = class {
1851
1919
  #cwd;
1852
1920
  #commandTimeoutMs;
@@ -1855,7 +1923,7 @@ var LocalSandbox = class {
1855
1923
  #processes = /* @__PURE__ */ new Set();
1856
1924
  #disposed = false;
1857
1925
  constructor(options, signal) {
1858
- this.#cwd = resolve3(options.cwd);
1926
+ this.#cwd = resolve4(options.cwd);
1859
1927
  this.#commandTimeoutMs = options.commandTimeoutMs;
1860
1928
  this.#env = options.env ?? process.env;
1861
1929
  this.#signal = signal;
@@ -1968,7 +2036,7 @@ var LocalSandbox = class {
1968
2036
  this.#processes.clear();
1969
2037
  }
1970
2038
  #resolvePath(path) {
1971
- return isAbsolute2(path) ? path : resolve3(this.#cwd, path);
2039
+ return isAbsolute2(path) ? path : resolve4(this.#cwd, path);
1972
2040
  }
1973
2041
  #assertActive() {
1974
2042
  if (this.#disposed) throw new Error("Local executor session has been disposed");
@@ -1979,7 +2047,7 @@ var LocalExecutorSession = class {
1979
2047
  context;
1980
2048
  #disposed = false;
1981
2049
  constructor(options, signal) {
1982
- const workspacePath = resolve3(options.cwd);
2050
+ const workspacePath = resolve4(options.cwd);
1983
2051
  this.sandbox = new LocalSandbox(options, signal);
1984
2052
  this.context = {
1985
2053
  kind: "local",
@@ -2096,6 +2164,7 @@ async function runOneShot(options) {
2096
2164
  });
2097
2165
  const workspaceTools = await createTools({
2098
2166
  cwd: options.cwd,
2167
+ mounts: options.mounts,
2099
2168
  maxOutputBytes: options.maxOutputBytes
2100
2169
  });
2101
2170
  const executorProvider = createExecutorProvider(options);
@@ -2173,7 +2242,7 @@ async function runOneShot(options) {
2173
2242
 
2174
2243
  // src/sandbox/runtime.ts
2175
2244
  import { once } from "events";
2176
- import { posix as posix3 } from "path";
2245
+ import { posix as posix4 } from "path";
2177
2246
  import { createInterface } from "readline";
2178
2247
  function isRecord(value) {
2179
2248
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -2211,7 +2280,7 @@ function requireString(params, name) {
2211
2280
  return value;
2212
2281
  }
2213
2282
  function validateVirtualPath(path) {
2214
- if (!posix3.isAbsolute(path) || path.includes("\\") || path.includes("\0") || posix3.normalize(path) !== path || path.split("/").some((component) => component === "." || component === "..")) {
2283
+ if (!posix4.isAbsolute(path) || path.includes("\\") || path.includes("\0") || posix4.normalize(path) !== path || path.split("/").some((component) => component === "." || component === "..")) {
2215
2284
  throw new Error("Path must be a normalized absolute virtual path");
2216
2285
  }
2217
2286
  return path;
@@ -2272,7 +2341,7 @@ async function runWorker(options = {}) {
2272
2341
  const workspaceRoot = options.cwd ?? process.cwd();
2273
2342
  const input = options.input ?? process.stdin;
2274
2343
  const output = options.output ?? process.stdout;
2275
- const bash = createJustBash(workspaceRoot);
2344
+ const bash = createJustBash(workspaceRoot, { mounts: options.mounts });
2276
2345
  const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
2277
2346
  const close = () => lines.close();
2278
2347
  if (options.signal?.aborted) {
@@ -2323,8 +2392,18 @@ function truncateText(input, maxBytes) {
2323
2392
  function addExecOptions(command, ndjsonDescription = "emit one JSON result record") {
2324
2393
  return command.option("--cwd <path>", "working directory").option("--timeout <ms>", "command timeout in milliseconds").option("--max-output <bytes>", "maximum retained output").option("--ndjson", ndjsonDescription);
2325
2394
  }
2395
+ function collectOption(value, previous) {
2396
+ return [...previous ?? [], value];
2397
+ }
2398
+ function addMountOption(command) {
2399
+ return command.option(
2400
+ "--mount <host-path:sandbox-path>",
2401
+ "mount a host directory read-write (repeatable)",
2402
+ collectOption
2403
+ );
2404
+ }
2326
2405
  function addAgentOptions(command) {
2327
- return addExecOptions(command, "emit newline-delimited JSON event records").option("--provider <provider>", "model provider", "openai").option("--model <model>", "model id").option(
2406
+ return addMountOption(addExecOptions(command, "emit newline-delimited JSON event records")).option("--provider <provider>", "model provider", "openai").option("--model <model>", "model id").option(
2328
2407
  "--executor <executor>",
2329
2408
  "delegated executor: none, local, or daytona (local is unisolated; daytona requires DAYTONA_API_KEY)",
2330
2409
  "none"
@@ -2430,6 +2509,7 @@ async function runSandbox(options) {
2430
2509
  try {
2431
2510
  await runWorker({
2432
2511
  cwd: options.cwd,
2512
+ mounts: parseSandboxMounts(options.mount),
2433
2513
  signal: controller.signal
2434
2514
  });
2435
2515
  if (signalExitCode !== void 0) {
@@ -2458,7 +2538,10 @@ function createProgram() {
2458
2538
  agent.action(async (prompt, options) => {
2459
2539
  await runAgent(prompt, options);
2460
2540
  });
2461
- program.command("sandbox").description("run the persistent sandbox NDJSON command interface").option("--cwd <path>", "working directory").action(async (options) => {
2541
+ const sandbox = addMountOption(
2542
+ program.command("sandbox").description("run the persistent sandbox NDJSON command interface").option("--cwd <path>", "working directory")
2543
+ );
2544
+ sandbox.action(async (options) => {
2462
2545
  await runSandbox(options);
2463
2546
  });
2464
2547
  program.command("doctor").description("validate the local Ollie setup without making model calls").option("--cwd <path>", "working directory").option("--provider <provider>", "model provider", "openai").option("--model <model>", "model id").option("--json", "emit JSON report").action(async (options) => {