@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/README.md +3 -14
- package/dist/cli.js +197 -85
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.js +51 -39
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
35
|
+
function sandboxPathsOverlap(left, right) {
|
|
36
36
|
return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
|
|
37
37
|
}
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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) =>
|
|
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
|
|
119
|
-
const
|
|
120
|
-
const stats = await stat(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
480
|
+
if (await ctx.fs.exists(posix4.join(current, ".git"))) return current;
|
|
437
481
|
if (current === WORKSPACE) break;
|
|
438
|
-
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(
|
|
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 ${
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|
|
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:
|
|
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:
|
|
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.#
|
|
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.#
|
|
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: ${
|
|
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
|
|
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, {
|
|
1842
|
-
|
|
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.
|
|
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
|
|
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 (!
|
|
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, {
|
|
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
|
|
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:
|
|
2512
|
-
|
|
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([
|
|
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] ?? "";
|