@getollie/cli 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -7,8 +7,8 @@ import { CommanderError } from "commander";
7
7
  import { Command } from "commander";
8
8
 
9
9
  // src/config.ts
10
- import { access, stat } from "fs/promises";
11
- import { resolve as resolve2 } from "path";
10
+ import { access, realpath, stat } from "fs/promises";
11
+ import { posix as posix2, resolve as resolve2 } from "path";
12
12
 
13
13
  // src/sandbox/mounts.ts
14
14
  import { realpathSync, statSync } from "fs";
@@ -32,36 +32,43 @@ function getErrorMessage(error) {
32
32
 
33
33
  // src/sandbox/mounts.ts
34
34
  var SANDBOX_WORKSPACE_PATH = "/workspace";
35
- function pathsOverlap(left, right) {
35
+ function sandboxPathsOverlap(left, right) {
36
36
  return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
37
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
- );
38
+ function sandboxPathContains(parent, child) {
39
+ return parent === child || child.startsWith(`${parent}/`);
40
+ }
41
+ function validateSandboxPath(path, label = "Sandbox path") {
42
+ if (!posix.isAbsolute(path) || path.includes("\\") || path.includes("\0") || path.split("/").includes("..") || posix.normalize(path) !== path) {
43
+ throw new CliError(`${label} must be a normalized absolute sandbox path: ${path}`);
43
44
  }
44
- if (pathsOverlap(mountPoint, SANDBOX_WORKSPACE_PATH)) {
45
- throw new CliError(`Mount point must be outside ${SANDBOX_WORKSPACE_PATH}: ${mountPoint}`);
45
+ return path;
46
+ }
47
+ function validateMountPoint(mountPoint) {
48
+ validateSandboxPath(mountPoint, "Mount point");
49
+ if (mountPoint === "/") {
50
+ throw new CliError(`Mount point must be a normalized absolute sandbox path other than /: ${mountPoint}`);
46
51
  }
47
52
  }
48
- function validateSandboxMounts(mounts) {
53
+ function validateSandboxMounts(mounts, options = {}) {
54
+ const reservedMountPoint = options.reservedMountPoint === void 0 ? SANDBOX_WORKSPACE_PATH : options.reservedMountPoint;
49
55
  const validated = [];
50
56
  for (const mount of mounts) {
51
57
  validateMountPoint(mount.mountPoint);
58
+ if (reservedMountPoint && sandboxPathsOverlap(mount.mountPoint, reservedMountPoint)) {
59
+ throw new CliError(`Mount point must be outside ${reservedMountPoint}: ${mount.mountPoint}`);
60
+ }
52
61
  let hostPath;
53
62
  try {
54
63
  hostPath = realpathSync(resolve(mount.hostPath));
55
- if (!statSync(hostPath).isDirectory()) {
56
- throw new Error("not a directory");
57
- }
64
+ if (!statSync(hostPath).isDirectory()) throw new Error("not a directory");
58
65
  } catch {
59
66
  throw new CliError(
60
- `Mount source does not exist or is not a directory: ${resolve(mount.hostPath)}`
67
+ `Mount source host path does not exist or is not a directory: ${resolve(mount.hostPath)}`
61
68
  );
62
69
  }
63
70
  const conflict = validated.find(
64
- (candidate) => pathsOverlap(candidate.mountPoint, mount.mountPoint)
71
+ (candidate) => sandboxPathsOverlap(candidate.mountPoint, mount.mountPoint)
65
72
  );
66
73
  if (conflict) {
67
74
  throw new CliError(
@@ -72,7 +79,7 @@ function validateSandboxMounts(mounts) {
72
79
  }
73
80
  return validated;
74
81
  }
75
- function parseSandboxMounts(specs = []) {
82
+ function parseSandboxMounts(specs = [], options) {
76
83
  const mounts = specs.map((spec) => {
77
84
  const separator = spec.lastIndexOf(":");
78
85
  if (separator <= 0 || separator === spec.length - 1) {
@@ -80,12 +87,9 @@ function parseSandboxMounts(specs = []) {
80
87
  `Invalid mount ${JSON.stringify(spec)}. Expected <host-path>:<sandbox-path>`
81
88
  );
82
89
  }
83
- return {
84
- hostPath: spec.slice(0, separator),
85
- mountPoint: spec.slice(separator + 1)
86
- };
90
+ return { hostPath: spec.slice(0, separator), mountPoint: spec.slice(separator + 1) };
87
91
  });
88
- return validateSandboxMounts(mounts);
92
+ return validateSandboxMounts(mounts, options);
89
93
  }
90
94
 
91
95
  // src/config.ts
@@ -96,9 +100,7 @@ var DEFAULT_COMMAND_TIMEOUT_MS = 3e4;
96
100
  var DEFAULT_MAX_OUTPUT_BYTES = 65536;
97
101
  var DEFAULT_EXECUTOR = "none";
98
102
  function parseNumber(name, value, fallback) {
99
- if (value === void 0 || value === "") {
100
- return fallback;
101
- }
103
+ if (value === void 0 || value === "") return fallback;
102
104
  const parsed = typeof value === "number" ? value : Number.parseInt(value, 10);
103
105
  if (!Number.isFinite(parsed) || parsed < 0) {
104
106
  throw new CliError(`${name} must be a non-negative integer`);
@@ -115,40 +117,82 @@ function parseExecutor(value) {
115
117
  }
116
118
  return executor;
117
119
  }
118
- async function resolveRunConfig(input = {}, env = process.env) {
119
- const cwd = resolve2(input.cwd ?? env.OLLIE_CWD ?? process.cwd());
120
- const stats = await stat(cwd).catch(() => null);
120
+ async function resolveHostDirectory(value, label) {
121
+ const absolute = resolve2(value);
122
+ const stats = await stat(absolute).catch(() => null);
121
123
  if (!stats?.isDirectory()) {
122
- throw new CliError(`Working directory does not exist or is not a directory: ${cwd}`);
124
+ throw new CliError(`${label} host path does not exist or is not a directory: ${absolute}`);
125
+ }
126
+ await access(absolute);
127
+ return realpath(absolute);
128
+ }
129
+ async function resolveRunConfig(input = {}, env = process.env) {
130
+ const configuredHostCwd = input.cwd ?? env.OLLIE_CWD;
131
+ const configuredSandboxCwd = input.sandboxCwd ?? env.OLLIE_SANDBOX_CWD;
132
+ const usedImplicitCwd = configuredHostCwd === void 0 && configuredSandboxCwd === void 0;
133
+ const hostCwdValue = configuredHostCwd ?? (usedImplicitCwd ? process.cwd() : void 0);
134
+ const explicitMounts = parseSandboxMounts(input.mount, {
135
+ reservedMountPoint: hostCwdValue ? "/workspace" : null
136
+ });
137
+ let workspace;
138
+ let mounts;
139
+ let sandboxCwd;
140
+ if (hostCwdValue) {
141
+ workspace = {
142
+ hostPath: await resolveHostDirectory(hostCwdValue, "Working directory"),
143
+ mountPoint: "/workspace"
144
+ };
145
+ mounts = explicitMounts;
146
+ sandboxCwd = configuredSandboxCwd ?? "/workspace";
147
+ } else {
148
+ if (!configuredSandboxCwd) {
149
+ throw new CliError("Either --cwd or --sandbox-cwd must be specified.");
150
+ }
151
+ sandboxCwd = configuredSandboxCwd;
152
+ validateSandboxPath(sandboxCwd, "Sandbox cwd");
153
+ const containing2 = explicitMounts.find(
154
+ (mount) => sandboxPathContains(mount.mountPoint, sandboxCwd)
155
+ );
156
+ if (!containing2) {
157
+ const destinations = explicitMounts.map((mount) => mount.mountPoint).join(", ") || "(none)";
158
+ throw new CliError(
159
+ `Sandbox cwd ${sandboxCwd} is not contained by the primary workspace or any configured mount. Configured mount destinations: ${destinations}`
160
+ );
161
+ }
162
+ workspace = containing2;
163
+ mounts = explicitMounts.filter((mount) => mount !== containing2);
123
164
  }
124
- await access(cwd);
165
+ validateSandboxPath(sandboxCwd, "Sandbox cwd");
166
+ const allMounts = [workspace, ...mounts];
167
+ const containing = allMounts.find((mount) => sandboxPathContains(mount.mountPoint, sandboxCwd));
168
+ if (!containing) {
169
+ const destinations = allMounts.map((mount) => mount.mountPoint).join(", ");
170
+ throw new CliError(
171
+ `Sandbox cwd ${sandboxCwd} is not contained by the primary workspace or any configured mount. Configured mount destinations: ${destinations}`
172
+ );
173
+ }
174
+ const mappedRelative = posix2.relative(containing.mountPoint, sandboxCwd);
175
+ await resolveHostDirectory(resolve2(containing.hostPath, mappedRelative), "Sandbox cwd mapped");
176
+ const workspaceRelativeCwd = sandboxPathContains(workspace.mountPoint, sandboxCwd) ? posix2.relative(workspace.mountPoint, sandboxCwd) || "." : null;
125
177
  const executor = parseExecutor(input.executor ?? env.OLLIE_EXECUTOR);
126
178
  return {
127
- cwd,
128
- mounts: parseSandboxMounts(input.mount),
179
+ cwd: workspace.hostPath,
180
+ workspaceMountPoint: workspace.mountPoint,
181
+ sandboxCwd,
182
+ workspaceRelativeCwd,
183
+ mounts,
184
+ usedImplicitCwd,
129
185
  provider: input.provider ?? env.OLLIE_PROVIDER ?? DEFAULT_PROVIDER,
130
186
  model: input.model ?? env.OLLIE_MODEL ?? DEFAULT_MODEL,
131
187
  executor,
132
- maxSteps: clamp(
133
- parseNumber("max steps", input.maxSteps ?? env.OLLIE_MAX_STEPS, DEFAULT_MAX_STEPS),
134
- 1,
135
- 100
136
- ),
188
+ maxSteps: clamp(parseNumber("max steps", input.maxSteps ?? env.OLLIE_MAX_STEPS, DEFAULT_MAX_STEPS), 1, 100),
137
189
  commandTimeoutMs: clamp(
138
- parseNumber(
139
- "command timeout",
140
- input.timeout ?? env.OLLIE_COMMAND_TIMEOUT_MS,
141
- DEFAULT_COMMAND_TIMEOUT_MS
142
- ),
190
+ parseNumber("command timeout", input.timeout ?? env.OLLIE_COMMAND_TIMEOUT_MS, DEFAULT_COMMAND_TIMEOUT_MS),
143
191
  1,
144
192
  3e5
145
193
  ),
146
194
  maxOutputBytes: clamp(
147
- parseNumber(
148
- "max output",
149
- input.maxOutput ?? env.OLLIE_MAX_OUTPUT_BYTES,
150
- DEFAULT_MAX_OUTPUT_BYTES
151
- ),
195
+ parseNumber("max output", input.maxOutput ?? env.OLLIE_MAX_OUTPUT_BYTES, DEFAULT_MAX_OUTPUT_BYTES),
152
196
  1,
153
197
  2e6
154
198
  ),
@@ -166,19 +210,19 @@ import { realpathSync as realpathSync2, statSync as statSync2 } from "fs";
166
210
  import { Bash, InMemoryFs, MountableFs, ReadWriteFs } from "just-bash";
167
211
 
168
212
  // src/sandbox/git/index.ts
169
- import { posix as posix3 } from "path";
213
+ import { posix as posix4 } from "path";
170
214
  import { createTwoFilesPatch } from "diff";
171
215
  import * as git from "isomorphic-git";
172
216
  import { defineCommand } from "just-bash";
173
217
 
174
218
  // src/sandbox/git/fs-adapter.ts
175
- import { posix as posix2 } from "path";
219
+ import { posix as posix3 } from "path";
176
220
  var ROOT = "/workspace";
177
221
  function safePath(path) {
178
222
  if (path.includes("\0") || path.includes("\\")) {
179
223
  throw Object.assign(new Error("Invalid virtual Git path"), { code: "EINVAL" });
180
224
  }
181
- const normalized = posix2.resolve(path);
225
+ const normalized = posix3.resolve(path);
182
226
  if (normalized !== ROOT && !normalized.startsWith(`${ROOT}/`)) {
183
227
  throw Object.assign(new Error("Git path is outside /workspace"), { code: "EACCES" });
184
228
  }
@@ -198,7 +242,7 @@ function normalizeError(error) {
198
242
  throw Object.assign(new Error("Virtual filesystem operation failed"), { code: "EIO" });
199
243
  }
200
244
  async function ensureParent(fs, path) {
201
- const parent = posix2.dirname(path);
245
+ const parent = posix3.dirname(path);
202
246
  if (!await fs.exists(parent)) await fs.mkdir(parent, { recursive: true });
203
247
  }
204
248
  function statShape(stat4) {
@@ -433,9 +477,9 @@ function virtualPath(ctx, input = ".") {
433
477
  async function repositoryDir(ctx) {
434
478
  let current = virtualPath(ctx);
435
479
  while (current === WORKSPACE || current.startsWith(`${WORKSPACE}/`)) {
436
- if (await ctx.fs.exists(posix3.join(current, ".git"))) return current;
480
+ if (await ctx.fs.exists(posix4.join(current, ".git"))) return current;
437
481
  if (current === WORKSPACE) break;
438
- current = posix3.dirname(current);
482
+ current = posix4.dirname(current);
439
483
  }
440
484
  throw new Error("not a git repository (or any parent up to /workspace)");
441
485
  }
@@ -518,7 +562,7 @@ async function writeGlobalConfig(ctx, values) {
518
562
  ([section, entries]) => `[${section}]
519
563
  ${entries.map(([key, value]) => ` ${key} = ${value}`).join("\n")}`
520
564
  ).join("\n");
521
- await ctx.fs.mkdir(posix3.dirname(GLOBAL_CONFIG_PATH), { recursive: true });
565
+ await ctx.fs.mkdir(posix4.dirname(GLOBAL_CONFIG_PATH), { recursive: true });
522
566
  await ctx.fs.writeFile(GLOBAL_CONFIG_PATH, content ? `${content}
523
567
  ` : "");
524
568
  }
@@ -532,7 +576,7 @@ async function handleInit(context, args) {
532
576
  throw new Error("cannot create repository directory");
533
577
  });
534
578
  await git.init({ fs: context.fs, dir, defaultBranch: branchOpt.value ?? "main" });
535
- return ok(`Initialized empty Git repository in ${posix3.join(dir, ".git")}/
579
+ return ok(`Initialized empty Git repository in ${posix4.join(dir, ".git")}/
536
580
  `);
537
581
  }
538
582
  async function handleStatus(context, args) {
@@ -685,7 +729,7 @@ async function handleDiff(context, args) {
685
729
  const changed = cachedFlag.enabled ? headStatus !== stageStatus : workdirStatus !== stageStatus;
686
730
  if (!changed) continue;
687
731
  const oldBytes = cachedFlag.enabled ? head && headStatus !== 0 ? (await git.readBlob({ fs: context.fs, dir, oid: head, filepath })).blob : empty : await readStage(filepath);
688
- const newBytes = cachedFlag.enabled ? await readStage(filepath) : workdirStatus === 0 ? empty : await context.command.fs.readFileBuffer(posix3.join(dir, filepath));
732
+ const newBytes = cachedFlag.enabled ? await readStage(filepath) : workdirStatus === 0 ? empty : await context.command.fs.readFileBuffer(posix4.join(dir, filepath));
689
733
  patches.push(patch(filepath, oldBytes, newBytes));
690
734
  }
691
735
  return ok(patches.join(""));
@@ -752,7 +796,7 @@ async function handleCheckout(context, args) {
752
796
  const oid = await git.resolveRef({ fs: context.fs, dir, ref });
753
797
  for (const filepath of filepaths) {
754
798
  const blob = await readCheckoutBlob(context, dir, oid, filepath);
755
- await context.command.fs.writeFile(posix3.join(dir, filepath), blob);
799
+ await context.command.fs.writeFile(posix4.join(dir, filepath), blob);
756
800
  }
757
801
  return ok();
758
802
  }
@@ -879,7 +923,7 @@ async function handleClone(context, args) {
879
923
  const [rawUrl, destination, ...rest] = noCheckoutFlag.rest;
880
924
  if (!rawUrl || rest.length) usage("clone requires a URL and optional directory");
881
925
  const url = validateRemoteUrl(rawUrl);
882
- const inferred = posix3.basename(new URL(url).pathname).replace(/\.git$/, "") || "repository";
926
+ const inferred = posix4.basename(new URL(url).pathname).replace(/\.git$/, "") || "repository";
883
927
  const dir = virtualPath(context.command, destination ?? inferred);
884
928
  const depth = positiveInteger(depthOpt.value, "--depth");
885
929
  await git.clone({
@@ -1053,7 +1097,11 @@ function canonicalizeWorkspaceRoot(workspaceRoot) {
1053
1097
  }
1054
1098
  function createWorkspaceFs(workspaceRoot, options) {
1055
1099
  const root = canonicalizeWorkspaceRoot(workspaceRoot);
1056
- const additionalMounts = validateSandboxMounts(options.mounts ?? []).map((mount) => ({
1100
+ const workspaceMountPoint = options.workspaceMountPoint ?? SANDBOX_WORKSPACE_PATH2;
1101
+ validateSandboxPath(workspaceMountPoint, "Workspace mount point");
1102
+ const additionalMounts = validateSandboxMounts(options.mounts ?? [], {
1103
+ reservedMountPoint: workspaceMountPoint
1104
+ }).map((mount) => ({
1057
1105
  mountPoint: mount.mountPoint,
1058
1106
  filesystem: new ReadWriteFs({ root: mount.hostPath, allowSymlinks: false })
1059
1107
  }));
@@ -1061,7 +1109,7 @@ function createWorkspaceFs(workspaceRoot, options) {
1061
1109
  base: new InMemoryFs(),
1062
1110
  mounts: [
1063
1111
  {
1064
- mountPoint: SANDBOX_WORKSPACE_PATH2,
1112
+ mountPoint: workspaceMountPoint,
1065
1113
  filesystem: new ReadWriteFs({ root, allowSymlinks: false })
1066
1114
  },
1067
1115
  ...additionalMounts
@@ -1069,13 +1117,19 @@ function createWorkspaceFs(workspaceRoot, options) {
1069
1117
  });
1070
1118
  }
1071
1119
  function createJustBash(workspaceRoot, options = {}) {
1120
+ const workspaceMountPoint = options.workspaceMountPoint ?? SANDBOX_WORKSPACE_PATH2;
1121
+ const sandboxCwd = options.sandboxCwd ?? workspaceMountPoint;
1122
+ validateSandboxPath(sandboxCwd, "Sandbox cwd");
1123
+ const configuredMountPoints = [workspaceMountPoint, ...(options.mounts ?? []).map((m) => m.mountPoint)];
1124
+ if (!configuredMountPoints.some((mountPoint) => sandboxPathContains(mountPoint, sandboxCwd))) {
1125
+ throw new CliError(
1126
+ `Sandbox cwd ${sandboxCwd} is not contained by the primary workspace or any configured mount.`
1127
+ );
1128
+ }
1072
1129
  return new Bash({
1073
1130
  fs: createWorkspaceFs(workspaceRoot, options),
1074
- cwd: SANDBOX_WORKSPACE_PATH2,
1075
- env: {
1076
- HOME: "/home/user",
1077
- TERM: "xterm-256color"
1078
- },
1131
+ cwd: sandboxCwd,
1132
+ env: { HOME: "/home/user", TERM: "xterm-256color" },
1079
1133
  python: true,
1080
1134
  javascript: true,
1081
1135
  customCommands: options.git === false ? [] : [createGitCommand(options.git)],
@@ -1172,7 +1226,11 @@ async function runExecCommand(cwd, command, options, signal) {
1172
1226
  const combinedSignal = combineSignals([signal, timeout.signal]);
1173
1227
  const timedOut = () => timeout.signal.aborted && !signal?.aborted;
1174
1228
  try {
1175
- const bash = createJustBash(cwd);
1229
+ const bash = createJustBash(cwd, {
1230
+ workspaceMountPoint: options.workspaceMountPoint,
1231
+ sandboxCwd: options.sandboxCwd,
1232
+ mounts: options.mounts
1233
+ });
1176
1234
  const result = await bash.exec(command, { env: options.env, signal: combinedSignal });
1177
1235
  return {
1178
1236
  ...result,
@@ -1471,12 +1529,14 @@ async function listLocalFiles(root) {
1471
1529
  var DaytonaSandbox = class {
1472
1530
  #sandbox;
1473
1531
  #workspacePath;
1532
+ #workingDirectory;
1474
1533
  #commandTimeoutMs;
1475
1534
  #signal;
1476
1535
  #disposed = false;
1477
1536
  constructor(sandbox, options) {
1478
1537
  this.#sandbox = sandbox;
1479
1538
  this.#workspacePath = options.workspacePath;
1539
+ this.#workingDirectory = options.workingDirectory ?? options.workspacePath;
1480
1540
  this.#commandTimeoutMs = options.commandTimeoutMs;
1481
1541
  this.#signal = options.signal;
1482
1542
  }
@@ -1493,7 +1553,7 @@ ${marker}
1493
1553
  const response = await abortable(
1494
1554
  this.#sandbox.process.executeCommand(
1495
1555
  wrapped,
1496
- this.#workspacePath,
1556
+ this.#workingDirectory,
1497
1557
  void 0,
1498
1558
  timeoutSeconds(this.#commandTimeoutMs)
1499
1559
  ),
@@ -1540,7 +1600,7 @@ ${marker}
1540
1600
  this.#disposed = true;
1541
1601
  }
1542
1602
  #resolvePath(path) {
1543
- const target = isAbsolute(path) ? resolve3(path) : resolve3(this.#workspacePath, path);
1603
+ const target = isAbsolute(path) ? resolve3(path) : resolve3(this.#workingDirectory, path);
1544
1604
  if (target !== this.#workspacePath && !target.startsWith(`${this.#workspacePath}/`)) {
1545
1605
  throw new Error(`Path is outside the Daytona workspace: ${path}`);
1546
1606
  }
@@ -1674,16 +1734,24 @@ var DaytonaExecutorSession = class {
1674
1734
  #disposed = false;
1675
1735
  constructor(remote, options, signal) {
1676
1736
  const workspacePath = options.remoteWorkspacePath ?? DEFAULT_REMOTE_WORKSPACE;
1737
+ const relativeCwd = options.relativeCwd ?? ".";
1738
+ const workingDirectory = resolve3(workspacePath, relativeCwd);
1677
1739
  this.#remote = remote;
1678
1740
  this.#synchronizer = new DaytonaWorkspaceSynchronizer(options.cwd, workspacePath, remote);
1679
1741
  this.sandbox = new DaytonaSandbox(remote, {
1680
1742
  workspacePath,
1743
+ workingDirectory,
1681
1744
  commandTimeoutMs: options.commandTimeoutMs,
1682
1745
  signal
1683
1746
  });
1684
1747
  this.context = {
1685
1748
  kind: "daytona",
1686
1749
  workspacePath,
1750
+ ...options.relativeCwd !== void 0 ? {
1751
+ workingDirectory,
1752
+ cwdPreserved: options.relativeCwd !== null,
1753
+ ...options.relativeCwd === null ? { cwdLimitation: "The selected sandbox cwd is on an additional mount; starting at the effective workspace root." } : {}
1754
+ } : {},
1687
1755
  isolated: true,
1688
1756
  networkAccess: "full"
1689
1757
  };
@@ -1783,7 +1851,7 @@ Terminus-2 spirit:
1783
1851
  - Summarize final findings and important command results clearly.
1784
1852
 
1785
1853
  Runtime context:
1786
- - Working directory: ${SANDBOX_WORKSPACE_PATH2}
1854
+ - Working directory: ${options.sandboxCwd ?? "/workspace"}
1787
1855
  - Current time: ${options.now.toISOString()}
1788
1856
  - Maximum model/tool steps: ${options.maxSteps}
1789
1857
  - Per-command timeout: ${options.commandTimeoutMs} ms
@@ -1806,17 +1874,20 @@ Tool policy:
1806
1874
  }
1807
1875
  function buildDelegatedSystemPrompt(options) {
1808
1876
  const executionBoundary = options.context.isolated ? "Commands run natively inside an isolated remote environment." : "Commands run natively with the current user's permissions and are not isolated.";
1877
+ const cwdNote = options.context.cwdLimitation ? `
1878
+ - Working-directory limitation: ${options.context.cwdLimitation}` : "";
1809
1879
  return `You are Ollie's delegated execution agent. Complete one bounded task using native workspace tools.
1810
1880
 
1811
1881
  Runtime context:
1812
1882
  - Workspace root: ${options.context.workspacePath}
1883
+ - Working directory: ${options.context.workingDirectory ?? options.context.workspacePath}${cwdNote}
1813
1884
  - Current time: ${options.now.toISOString()}
1814
1885
  - Maximum model/tool steps: ${options.maxSteps}
1815
1886
  - Per-command timeout: ${options.commandTimeoutMs} ms
1816
1887
  - ${executionBoundary}
1817
1888
 
1818
1889
  Operating rules:
1819
- - Start at the workspace root and use relative paths for workspace files.
1890
+ - Start at the configured working directory and use relative paths for workspace files.
1820
1891
  - Inspect the relevant state before making changes.
1821
1892
  - Keep changes minimal and directly related to the delegated task.
1822
1893
  - Run appropriate native checks or tests and report their actual results.
@@ -1838,8 +1909,12 @@ async function createWorkspaceTools(options) {
1838
1909
  }
1839
1910
  async function createTools(options) {
1840
1911
  return createWorkspaceTools({
1841
- sandbox: createJustBash(options.cwd, { mounts: options.mounts }),
1842
- destination: SANDBOX_WORKSPACE_PATH2,
1912
+ sandbox: createJustBash(options.cwd, {
1913
+ mounts: options.mounts,
1914
+ workspaceMountPoint: options.workspaceMountPoint,
1915
+ sandboxCwd: options.sandboxCwd
1916
+ }),
1917
+ destination: options.sandboxCwd ?? options.workspaceMountPoint ?? SANDBOX_WORKSPACE_PATH2,
1843
1918
  maxOutputBytes: options.maxOutputBytes,
1844
1919
  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."
1845
1920
  });
@@ -1864,9 +1939,9 @@ var ModelTaskDelegator = class {
1864
1939
  });
1865
1940
  const tools = await createWorkspaceTools({
1866
1941
  sandbox: session.sandbox,
1867
- destination: session.context.workspacePath,
1942
+ destination: session.context.workingDirectory ?? session.context.workspacePath,
1868
1943
  maxOutputBytes: this.options.maxOutputBytes,
1869
- extraInstructions: "Use the native workspace tools to complete the delegated task. Start at the workspace root, use relative paths, keep changes focused, and verify the result before reporting it."
1944
+ extraInstructions: "Use the native workspace tools to complete the delegated task. Use relative paths from the configured working directory, keep changes focused, and verify the result before reporting it."
1870
1945
  });
1871
1946
  const result = await generateText({
1872
1947
  model: this.options.model,
@@ -1923,7 +1998,7 @@ var LocalSandbox = class {
1923
1998
  #processes = /* @__PURE__ */ new Set();
1924
1999
  #disposed = false;
1925
2000
  constructor(options, signal) {
1926
- this.#cwd = resolve4(options.cwd);
2001
+ this.#cwd = resolve4(options.cwd, options.relativeCwd ?? ".");
1927
2002
  this.#commandTimeoutMs = options.commandTimeoutMs;
1928
2003
  this.#env = options.env ?? process.env;
1929
2004
  this.#signal = signal;
@@ -2048,10 +2123,16 @@ var LocalExecutorSession = class {
2048
2123
  #disposed = false;
2049
2124
  constructor(options, signal) {
2050
2125
  const workspacePath = resolve4(options.cwd);
2126
+ const workingDirectory = resolve4(workspacePath, options.relativeCwd ?? ".");
2051
2127
  this.sandbox = new LocalSandbox(options, signal);
2052
2128
  this.context = {
2053
2129
  kind: "local",
2054
2130
  workspacePath,
2131
+ ...options.relativeCwd !== void 0 ? {
2132
+ workingDirectory,
2133
+ cwdPreserved: options.relativeCwd !== null,
2134
+ ...options.relativeCwd === null ? { cwdLimitation: "The selected sandbox cwd is on an additional mount; starting at the effective workspace root." } : {}
2135
+ } : {},
2055
2136
  isolated: false,
2056
2137
  networkAccess: "full"
2057
2138
  };
@@ -2145,11 +2226,13 @@ function createExecutorProvider(options) {
2145
2226
  case "local":
2146
2227
  return new LocalExecutorProvider({
2147
2228
  cwd: options.cwd,
2229
+ relativeCwd: options.workspaceRelativeCwd,
2148
2230
  commandTimeoutMs: options.commandTimeoutMs
2149
2231
  });
2150
2232
  case "daytona":
2151
2233
  return new DaytonaExecutorProvider({
2152
2234
  cwd: options.cwd,
2235
+ relativeCwd: options.workspaceRelativeCwd,
2153
2236
  commandTimeoutMs: options.commandTimeoutMs,
2154
2237
  snapshot: process.env.OLLIE_DAYTONA_SNAPSHOT
2155
2238
  });
@@ -2164,6 +2247,8 @@ async function runOneShot(options) {
2164
2247
  });
2165
2248
  const workspaceTools = await createTools({
2166
2249
  cwd: options.cwd,
2250
+ workspaceMountPoint: options.workspaceMountPoint,
2251
+ sandboxCwd: options.sandboxCwd,
2167
2252
  mounts: options.mounts,
2168
2253
  maxOutputBytes: options.maxOutputBytes
2169
2254
  });
@@ -2191,6 +2276,7 @@ async function runOneShot(options) {
2191
2276
  now: /* @__PURE__ */ new Date(),
2192
2277
  maxSteps: options.maxSteps,
2193
2278
  commandTimeoutMs: options.commandTimeoutMs,
2279
+ sandboxCwd: options.sandboxCwd,
2194
2280
  executorInstructions: executorProvider?.systemPrompt
2195
2281
  }),
2196
2282
  prompt: options.prompt,
@@ -2242,7 +2328,7 @@ async function runOneShot(options) {
2242
2328
 
2243
2329
  // src/sandbox/runtime.ts
2244
2330
  import { once } from "events";
2245
- import { posix as posix4 } from "path";
2331
+ import { posix as posix5 } from "path";
2246
2332
  import { createInterface } from "readline";
2247
2333
  function isRecord(value) {
2248
2334
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -2280,7 +2366,7 @@ function requireString(params, name) {
2280
2366
  return value;
2281
2367
  }
2282
2368
  function validateVirtualPath(path) {
2283
- if (!posix4.isAbsolute(path) || path.includes("\\") || path.includes("\0") || posix4.normalize(path) !== path || path.split("/").some((component) => component === "." || component === "..")) {
2369
+ if (!posix5.isAbsolute(path) || path.includes("\\") || path.includes("\0") || posix5.normalize(path) !== path || path.split("/").some((component) => component === "." || component === "..")) {
2284
2370
  throw new Error("Path must be a normalized absolute virtual path");
2285
2371
  }
2286
2372
  return path;
@@ -2341,7 +2427,11 @@ async function runWorker(options = {}) {
2341
2427
  const workspaceRoot = options.cwd ?? process.cwd();
2342
2428
  const input = options.input ?? process.stdin;
2343
2429
  const output = options.output ?? process.stdout;
2344
- const bash = createJustBash(workspaceRoot, { mounts: options.mounts });
2430
+ const bash = createJustBash(workspaceRoot, {
2431
+ mounts: options.mounts,
2432
+ workspaceMountPoint: options.workspaceMountPoint,
2433
+ sandboxCwd: options.sandboxCwd
2434
+ });
2345
2435
  const lines = createInterface({ input, crlfDelay: Number.POSITIVE_INFINITY });
2346
2436
  const close = () => lines.close();
2347
2437
  if (options.signal?.aborted) {
@@ -2390,7 +2480,7 @@ function truncateText(input, maxBytes) {
2390
2480
 
2391
2481
  // src/cli-program.ts
2392
2482
  function addExecOptions(command, ndjsonDescription = "emit one JSON result record") {
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);
2483
+ return addMountOption(command.option("--cwd <host-path>", "host workspace mounted at /workspace").option("--sandbox-cwd <sandbox-path>", "initial working directory inside the sandbox")).option("--timeout <ms>", "command timeout in milliseconds").option("--max-output <bytes>", "maximum retained output").option("--ndjson", ndjsonDescription);
2394
2484
  }
2395
2485
  function collectOption(value, previous) {
2396
2486
  return [...previous ?? [], value];
@@ -2403,18 +2493,26 @@ function addMountOption(command) {
2403
2493
  );
2404
2494
  }
2405
2495
  function addAgentOptions(command) {
2406
- return addMountOption(addExecOptions(command, "emit newline-delimited JSON event records")).option("--provider <provider>", "model provider", "openai").option("--model <model>", "model id").option(
2496
+ return addExecOptions(command, "emit newline-delimited JSON event records").option("--provider <provider>", "model provider", "openai").option("--model <model>", "model id").option(
2407
2497
  "--executor <executor>",
2408
2498
  "delegated executor: none, local, or daytona (local is unisolated; daytona requires DAYTONA_API_KEY)",
2409
2499
  "none"
2410
2500
  ).option("--max-steps <number>", "maximum model/tool steps").option("--no-color", "disable color output");
2411
2501
  }
2502
+ function warnImplicitCwd(usedImplicitCwd) {
2503
+ if (usedImplicitCwd) {
2504
+ process.stderr.write(
2505
+ "Warning: omitting --cwd and --sandbox-cwd is deprecated; currently using --cwd .\n"
2506
+ );
2507
+ }
2508
+ }
2412
2509
  async function runAgent(promptParts, options) {
2413
2510
  const prompt = promptParts.join(" ").trim();
2414
2511
  if (!prompt) {
2415
2512
  throw new CliError('A prompt is required. Example: ollie agent "summarize this repository"');
2416
2513
  }
2417
2514
  const config = await resolveRunConfig(options);
2515
+ warnImplicitCwd(config.usedImplicitCwd);
2418
2516
  const renderer = createRenderer({ ndjson: config.ndjson, color: config.color });
2419
2517
  const controller = new AbortController();
2420
2518
  const onSigint = () => controller.abort();
@@ -2437,6 +2535,7 @@ async function runExec(commandParts, options) {
2437
2535
  throw new CliError('A command is required. Example: ollie "pwd && ls -la"');
2438
2536
  }
2439
2537
  const config = await resolveRunConfig(options);
2538
+ warnImplicitCwd(config.usedImplicitCwd);
2440
2539
  const controller = new AbortController();
2441
2540
  let signalExitCode;
2442
2541
  const onSigint = () => {
@@ -2455,7 +2554,10 @@ async function runExec(commandParts, options) {
2455
2554
  shellCommand,
2456
2555
  {
2457
2556
  timeoutMs: config.commandTimeoutMs,
2458
- env: process.env
2557
+ env: process.env,
2558
+ workspaceMountPoint: config.workspaceMountPoint,
2559
+ sandboxCwd: config.sandboxCwd,
2560
+ mounts: config.mounts
2459
2561
  },
2460
2562
  controller.signal
2461
2563
  );
@@ -2494,6 +2596,8 @@ async function runExec(commandParts, options) {
2494
2596
  }
2495
2597
  }
2496
2598
  async function runSandbox(options) {
2599
+ const config = await resolveRunConfig(options);
2600
+ warnImplicitCwd(config.usedImplicitCwd);
2497
2601
  const controller = new AbortController();
2498
2602
  let signalExitCode;
2499
2603
  const onSigint = () => {
@@ -2508,8 +2612,10 @@ async function runSandbox(options) {
2508
2612
  process.once("SIGTERM", onSigterm);
2509
2613
  try {
2510
2614
  await runWorker({
2511
- cwd: options.cwd,
2512
- mounts: parseSandboxMounts(options.mount),
2615
+ cwd: config.cwd,
2616
+ workspaceMountPoint: config.workspaceMountPoint,
2617
+ sandboxCwd: config.sandboxCwd,
2618
+ mounts: config.mounts,
2513
2619
  signal: controller.signal
2514
2620
  });
2515
2621
  if (signalExitCode !== void 0) {
@@ -2539,7 +2645,7 @@ function createProgram() {
2539
2645
  await runAgent(prompt, options);
2540
2646
  });
2541
2647
  const sandbox = addMountOption(
2542
- program.command("sandbox").description("run the persistent sandbox NDJSON command interface").option("--cwd <path>", "working directory")
2648
+ program.command("sandbox").description("run the persistent sandbox NDJSON command interface").option("--cwd <host-path>", "host workspace mounted at /workspace").option("--sandbox-cwd <sandbox-path>", "initial working directory inside the sandbox")
2543
2649
  );
2544
2650
  sandbox.action(async (options) => {
2545
2651
  await runSandbox(options);
@@ -2551,7 +2657,13 @@ function createProgram() {
2551
2657
  return program;
2552
2658
  }
2553
2659
  var RESERVED_SUBCOMMANDS = /* @__PURE__ */ new Set(["agent", "sandbox", "doctor"]);
2554
- var ROOT_OPTIONS_WITH_VALUES = /* @__PURE__ */ new Set(["--cwd", "--timeout", "--max-output"]);
2660
+ var ROOT_OPTIONS_WITH_VALUES = /* @__PURE__ */ new Set([
2661
+ "--cwd",
2662
+ "--sandbox-cwd",
2663
+ "--mount",
2664
+ "--timeout",
2665
+ "--max-output"
2666
+ ]);
2555
2667
  function rootBoundaryPrecedesCommand(args) {
2556
2668
  for (let index = 0; index < args.length; index += 1) {
2557
2669
  const token = args[index] ?? "";