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