@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 +14 -3
- package/dist/cli.js +124 -41
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +90 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
436
|
+
if (await ctx.fs.exists(posix3.join(current, ".git"))) return current;
|
|
374
437
|
if (current === WORKSPACE) break;
|
|
375
|
-
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(
|
|
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 ${
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
1046
|
+
var SANDBOX_WORKSPACE_PATH2 = "/workspace";
|
|
984
1047
|
function canonicalizeWorkspaceRoot(workspaceRoot) {
|
|
985
|
-
const canonicalRoot =
|
|
986
|
-
if (!
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
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) ?
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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: ${
|
|
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:
|
|
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
|
|
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 =
|
|
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 :
|
|
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 =
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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) => {
|