zeitlich 0.2.15 → 0.2.16
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 +50 -0
- package/dist/adapters/sandbox/daytona/index.cjs +52 -23
- package/dist/adapters/sandbox/daytona/index.cjs.map +1 -1
- package/dist/adapters/sandbox/daytona/index.d.cts +10 -2
- package/dist/adapters/sandbox/daytona/index.d.ts +10 -2
- package/dist/adapters/sandbox/daytona/index.js +52 -23
- package/dist/adapters/sandbox/daytona/index.js.map +1 -1
- package/dist/adapters/sandbox/inmemory/index.cjs +21 -16
- package/dist/adapters/sandbox/inmemory/index.cjs.map +1 -1
- package/dist/adapters/sandbox/inmemory/index.d.cts +1 -1
- package/dist/adapters/sandbox/inmemory/index.d.ts +1 -1
- package/dist/adapters/sandbox/inmemory/index.js +21 -16
- package/dist/adapters/sandbox/inmemory/index.js.map +1 -1
- package/dist/adapters/sandbox/virtual/index.cjs +38 -38
- package/dist/adapters/sandbox/virtual/index.cjs.map +1 -1
- package/dist/adapters/sandbox/virtual/index.d.cts +6 -6
- package/dist/adapters/sandbox/virtual/index.d.ts +6 -6
- package/dist/adapters/sandbox/virtual/index.js +37 -37
- package/dist/adapters/sandbox/virtual/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +2 -2
- package/dist/adapters/thread/google-genai/index.d.ts +2 -2
- package/dist/adapters/thread/langchain/index.d.cts +2 -2
- package/dist/adapters/thread/langchain/index.d.ts +2 -2
- package/dist/index.cjs +2 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/{types-CDubRtad.d.cts → types-BMRzfELQ.d.cts} +2 -0
- package/dist/{types-CDubRtad.d.ts → types-BMRzfELQ.d.ts} +2 -0
- package/dist/{types-CwwgQ_9H.d.ts → types-BSOte_8s.d.ts} +6 -2
- package/dist/{types-BVP87m_W.d.cts → types-DCi2qXjN.d.cts} +6 -2
- package/dist/{types-Dje1TdH6.d.cts → types-Drli9aCK.d.cts} +1 -1
- package/dist/{types-BWvIYK28.d.ts → types-XPtivmSJ.d.ts} +1 -1
- package/dist/workflow.cjs +2 -3
- package/dist/workflow.cjs.map +1 -1
- package/dist/workflow.d.cts +6 -6
- package/dist/workflow.d.ts +6 -6
- package/dist/workflow.js +2 -3
- package/dist/workflow.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/sandbox/daytona/filesystem.ts +43 -19
- package/src/adapters/sandbox/daytona/index.ts +16 -3
- package/src/adapters/sandbox/daytona/types.ts +4 -0
- package/src/adapters/sandbox/inmemory/index.ts +22 -16
- package/src/adapters/sandbox/virtual/filesystem.ts +29 -31
- package/src/adapters/sandbox/virtual/index.ts +5 -3
- package/src/adapters/sandbox/virtual/provider.ts +5 -2
- package/src/adapters/sandbox/virtual/types.ts +3 -0
- package/src/adapters/sandbox/virtual/with-virtual-sandbox.ts +4 -3
- package/src/lib/sandbox/tree.integration.test.ts +153 -0
- package/src/lib/sandbox/types.ts +2 -0
- package/src/lib/session/session-edge-cases.integration.test.ts +962 -0
- package/src/lib/session/session.integration.test.ts +852 -0
- package/src/lib/session/session.ts +5 -4
- package/src/lib/skills/skills.integration.test.ts +308 -0
- package/src/lib/state/manager.integration.test.ts +342 -0
- package/src/lib/subagent/subagent.integration.test.ts +467 -0
- package/src/lib/thread/id.test.ts +50 -0
- package/src/lib/tool-router/auto-append-sandbox.integration.test.ts +344 -0
- package/src/lib/tool-router/router-edge-cases.integration.test.ts +623 -0
- package/src/lib/tool-router/router.integration.test.ts +699 -0
- package/src/lib/types.test.ts +29 -0
package/package.json
CHANGED
|
@@ -15,46 +15,62 @@ import { posix } from "node:path";
|
|
|
15
15
|
* (e.g. `appendFile`, `cp`) are composed from primitives.
|
|
16
16
|
*/
|
|
17
17
|
export class DaytonaSandboxFileSystem implements SandboxFileSystem {
|
|
18
|
-
|
|
18
|
+
readonly workspaceBase: string;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
private sandbox: DaytonaSdkSandbox,
|
|
22
|
+
workspaceBase = "/home/daytona",
|
|
23
|
+
) {
|
|
24
|
+
this.workspaceBase = posix.resolve("/", workspaceBase);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private normalisePath(path: string): string {
|
|
28
|
+
return posix.resolve(this.workspaceBase, path);
|
|
29
|
+
}
|
|
19
30
|
|
|
20
31
|
async readFile(path: string): Promise<string> {
|
|
21
|
-
const
|
|
32
|
+
const norm = this.normalisePath(path);
|
|
33
|
+
const buf = await this.sandbox.fs.downloadFile(norm);
|
|
22
34
|
return buf.toString("utf-8");
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
async readFileBuffer(path: string): Promise<Uint8Array> {
|
|
26
|
-
const
|
|
38
|
+
const norm = this.normalisePath(path);
|
|
39
|
+
const buf = await this.sandbox.fs.downloadFile(norm);
|
|
27
40
|
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
async writeFile(path: string, content: string | Uint8Array): Promise<void> {
|
|
44
|
+
const norm = this.normalisePath(path);
|
|
31
45
|
const buf =
|
|
32
46
|
typeof content === "string"
|
|
33
47
|
? Buffer.from(content, "utf-8")
|
|
34
48
|
: Buffer.from(content);
|
|
35
|
-
await this.sandbox.fs.uploadFile(buf,
|
|
49
|
+
await this.sandbox.fs.uploadFile(buf, norm);
|
|
36
50
|
}
|
|
37
51
|
|
|
38
52
|
async appendFile(
|
|
39
53
|
path: string,
|
|
40
54
|
content: string | Uint8Array,
|
|
41
55
|
): Promise<void> {
|
|
56
|
+
const norm = this.normalisePath(path);
|
|
42
57
|
let existing: Buffer;
|
|
43
58
|
try {
|
|
44
|
-
existing = await this.sandbox.fs.downloadFile(
|
|
59
|
+
existing = await this.sandbox.fs.downloadFile(norm);
|
|
45
60
|
} catch {
|
|
46
|
-
return this.writeFile(
|
|
61
|
+
return this.writeFile(norm, content);
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
const addition =
|
|
50
65
|
typeof content === "string" ? Buffer.from(content, "utf-8") : content;
|
|
51
66
|
const merged = Buffer.concat([existing, Buffer.from(addition)]);
|
|
52
|
-
await this.sandbox.fs.uploadFile(merged,
|
|
67
|
+
await this.sandbox.fs.uploadFile(merged, norm);
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
async exists(path: string): Promise<boolean> {
|
|
71
|
+
const norm = this.normalisePath(path);
|
|
56
72
|
try {
|
|
57
|
-
await this.sandbox.fs.getFileDetails(
|
|
73
|
+
await this.sandbox.fs.getFileDetails(norm);
|
|
58
74
|
return true;
|
|
59
75
|
} catch {
|
|
60
76
|
return false;
|
|
@@ -62,7 +78,8 @@ export class DaytonaSandboxFileSystem implements SandboxFileSystem {
|
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
async stat(path: string): Promise<FileStat> {
|
|
65
|
-
const
|
|
81
|
+
const norm = this.normalisePath(path);
|
|
82
|
+
const info = await this.sandbox.fs.getFileDetails(norm);
|
|
66
83
|
return {
|
|
67
84
|
isFile: !info.isDir,
|
|
68
85
|
isDirectory: info.isDir,
|
|
@@ -76,16 +93,19 @@ export class DaytonaSandboxFileSystem implements SandboxFileSystem {
|
|
|
76
93
|
path: string,
|
|
77
94
|
_options?: { recursive?: boolean },
|
|
78
95
|
): Promise<void> {
|
|
79
|
-
|
|
96
|
+
const norm = this.normalisePath(path);
|
|
97
|
+
await this.sandbox.fs.createFolder(norm, "755");
|
|
80
98
|
}
|
|
81
99
|
|
|
82
100
|
async readdir(path: string): Promise<string[]> {
|
|
83
|
-
const
|
|
101
|
+
const norm = this.normalisePath(path);
|
|
102
|
+
const entries = await this.sandbox.fs.listFiles(norm);
|
|
84
103
|
return entries.map((e) => e.name);
|
|
85
104
|
}
|
|
86
105
|
|
|
87
106
|
async readdirWithFileTypes(path: string): Promise<DirentEntry[]> {
|
|
88
|
-
const
|
|
107
|
+
const norm = this.normalisePath(path);
|
|
108
|
+
const entries = await this.sandbox.fs.listFiles(norm);
|
|
89
109
|
return entries.map((e) => ({
|
|
90
110
|
name: e.name,
|
|
91
111
|
isFile: !e.isDir,
|
|
@@ -98,8 +118,9 @@ export class DaytonaSandboxFileSystem implements SandboxFileSystem {
|
|
|
98
118
|
path: string,
|
|
99
119
|
options?: { recursive?: boolean; force?: boolean },
|
|
100
120
|
): Promise<void> {
|
|
121
|
+
const norm = this.normalisePath(path);
|
|
101
122
|
try {
|
|
102
|
-
await this.sandbox.fs.deleteFile(
|
|
123
|
+
await this.sandbox.fs.deleteFile(norm, options?.recursive);
|
|
103
124
|
} catch (err) {
|
|
104
125
|
if (!options?.force) throw err;
|
|
105
126
|
}
|
|
@@ -110,19 +131,23 @@ export class DaytonaSandboxFileSystem implements SandboxFileSystem {
|
|
|
110
131
|
dest: string,
|
|
111
132
|
options?: { recursive?: boolean },
|
|
112
133
|
): Promise<void> {
|
|
113
|
-
const
|
|
134
|
+
const normSrc = this.normalisePath(src);
|
|
135
|
+
const normDest = this.normalisePath(dest);
|
|
136
|
+
const info = await this.sandbox.fs.getFileDetails(normSrc);
|
|
114
137
|
if (info.isDir) {
|
|
115
138
|
if (!options?.recursive) {
|
|
116
139
|
throw new Error(`EISDIR: is a directory (use recursive): ${src}`);
|
|
117
140
|
}
|
|
118
|
-
await this.sandbox.process.executeCommand(`cp -r "${
|
|
141
|
+
await this.sandbox.process.executeCommand(`cp -r "${normSrc}" "${normDest}"`);
|
|
119
142
|
} else {
|
|
120
|
-
await this.sandbox.process.executeCommand(`cp "${
|
|
143
|
+
await this.sandbox.process.executeCommand(`cp "${normSrc}" "${normDest}"`);
|
|
121
144
|
}
|
|
122
145
|
}
|
|
123
146
|
|
|
124
147
|
async mv(src: string, dest: string): Promise<void> {
|
|
125
|
-
|
|
148
|
+
const normSrc = this.normalisePath(src);
|
|
149
|
+
const normDest = this.normalisePath(dest);
|
|
150
|
+
await this.sandbox.fs.moveFiles(normSrc, normDest);
|
|
126
151
|
}
|
|
127
152
|
|
|
128
153
|
async readlink(_path: string): Promise<string> {
|
|
@@ -130,7 +155,6 @@ export class DaytonaSandboxFileSystem implements SandboxFileSystem {
|
|
|
130
155
|
}
|
|
131
156
|
|
|
132
157
|
resolvePath(base: string, path: string): string {
|
|
133
|
-
|
|
134
|
-
return posix.resolve(base, path);
|
|
158
|
+
return posix.resolve(this.normalisePath(base), path);
|
|
135
159
|
}
|
|
136
160
|
}
|
|
@@ -38,8 +38,9 @@ class DaytonaSandboxImpl implements Sandbox {
|
|
|
38
38
|
constructor(
|
|
39
39
|
readonly id: string,
|
|
40
40
|
private sdkSandbox: DaytonaSdkSandbox,
|
|
41
|
+
workspaceBase = "/home/daytona",
|
|
41
42
|
) {
|
|
42
|
-
this.fs = new DaytonaSandboxFileSystem(sdkSandbox);
|
|
43
|
+
this.fs = new DaytonaSandboxFileSystem(sdkSandbox, workspaceBase);
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
async exec(command: string, options?: ExecOptions): Promise<ExecResult> {
|
|
@@ -77,9 +78,12 @@ export class DaytonaSandboxProvider
|
|
|
77
78
|
};
|
|
78
79
|
|
|
79
80
|
private client: Daytona;
|
|
81
|
+
private readonly defaultWorkspaceBase: string;
|
|
82
|
+
private workspaceBaseById = new Map<string, string>();
|
|
80
83
|
|
|
81
84
|
constructor(config?: DaytonaSandboxConfig) {
|
|
82
85
|
this.client = new Daytona(config);
|
|
86
|
+
this.defaultWorkspaceBase = config?.workspaceBase ?? "/home/daytona";
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
async create(
|
|
@@ -98,7 +102,13 @@ export class DaytonaSandboxProvider
|
|
|
98
102
|
{ timeout: options?.timeout ?? 60 },
|
|
99
103
|
);
|
|
100
104
|
|
|
101
|
-
const
|
|
105
|
+
const workspaceBase = options?.workspaceBase ?? this.defaultWorkspaceBase;
|
|
106
|
+
this.workspaceBaseById.set(sdkSandbox.id, workspaceBase);
|
|
107
|
+
const sandbox = new DaytonaSandboxImpl(
|
|
108
|
+
sdkSandbox.id,
|
|
109
|
+
sdkSandbox,
|
|
110
|
+
workspaceBase,
|
|
111
|
+
);
|
|
102
112
|
|
|
103
113
|
if (options?.initialFiles) {
|
|
104
114
|
for (const [path, content] of Object.entries(options.initialFiles)) {
|
|
@@ -112,7 +122,9 @@ export class DaytonaSandboxProvider
|
|
|
112
122
|
async get(sandboxId: string): Promise<DaytonaSandbox> {
|
|
113
123
|
try {
|
|
114
124
|
const sdkSandbox = await this.client.get(sandboxId);
|
|
115
|
-
|
|
125
|
+
const workspaceBase =
|
|
126
|
+
this.workspaceBaseById.get(sandboxId) ?? this.defaultWorkspaceBase;
|
|
127
|
+
return new DaytonaSandboxImpl(sdkSandbox.id, sdkSandbox, workspaceBase);
|
|
116
128
|
} catch {
|
|
117
129
|
throw new SandboxNotFoundError(sandboxId);
|
|
118
130
|
}
|
|
@@ -122,6 +134,7 @@ export class DaytonaSandboxProvider
|
|
|
122
134
|
try {
|
|
123
135
|
const sdkSandbox = await this.client.get(sandboxId);
|
|
124
136
|
await this.client.delete(sdkSandbox);
|
|
137
|
+
this.workspaceBaseById.delete(sandboxId);
|
|
125
138
|
} catch {
|
|
126
139
|
// Already gone
|
|
127
140
|
}
|
|
@@ -10,6 +10,8 @@ export interface DaytonaSandboxConfig {
|
|
|
10
10
|
apiKey?: string;
|
|
11
11
|
apiUrl?: string;
|
|
12
12
|
target?: string;
|
|
13
|
+
/** Default base path for resolving relative filesystem paths. */
|
|
14
|
+
workspaceBase?: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export interface DaytonaSandboxCreateOptions extends SandboxCreateOptions {
|
|
@@ -31,4 +33,6 @@ export interface DaytonaSandboxCreateOptions extends SandboxCreateOptions {
|
|
|
31
33
|
labels?: Record<string, string>;
|
|
32
34
|
/** Timeout in seconds for sandbox creation. Default 60. */
|
|
33
35
|
timeout?: number;
|
|
36
|
+
/** Base path for resolving relative filesystem paths in this sandbox. */
|
|
37
|
+
workspaceBase?: string;
|
|
34
38
|
}
|
|
@@ -26,14 +26,18 @@ import { getShortId } from "../../../lib/thread/id";
|
|
|
26
26
|
// ============================================================================
|
|
27
27
|
|
|
28
28
|
function toSandboxFs(fs: IFileSystem): SandboxFileSystem {
|
|
29
|
+
const workspaceBase = "/";
|
|
30
|
+
const normalisePath = (path: string): string => fs.resolvePath(workspaceBase, path);
|
|
31
|
+
|
|
29
32
|
return {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
workspaceBase,
|
|
34
|
+
readFile: (path) => fs.readFile(normalisePath(path)),
|
|
35
|
+
readFileBuffer: (path) => fs.readFileBuffer(normalisePath(path)),
|
|
36
|
+
writeFile: (path, content) => fs.writeFile(normalisePath(path), content),
|
|
37
|
+
appendFile: (path, content) => fs.appendFile(normalisePath(path), content),
|
|
38
|
+
exists: (path) => fs.exists(normalisePath(path)),
|
|
35
39
|
stat: async (path): Promise<FileStat> => {
|
|
36
|
-
const s = await fs.stat(path);
|
|
40
|
+
const s = await fs.stat(normalisePath(path));
|
|
37
41
|
return {
|
|
38
42
|
isFile: s.isFile,
|
|
39
43
|
isDirectory: s.isDirectory,
|
|
@@ -42,14 +46,16 @@ function toSandboxFs(fs: IFileSystem): SandboxFileSystem {
|
|
|
42
46
|
mtime: s.mtime,
|
|
43
47
|
};
|
|
44
48
|
},
|
|
45
|
-
mkdir: (path, opts) => fs.mkdir(path, opts),
|
|
46
|
-
readdir: (path) => fs.readdir(path),
|
|
49
|
+
mkdir: (path, opts) => fs.mkdir(normalisePath(path), opts),
|
|
50
|
+
readdir: (path) => fs.readdir(normalisePath(path)),
|
|
47
51
|
readdirWithFileTypes: async (path): Promise<DirentEntry[]> => {
|
|
52
|
+
const dirPath = normalisePath(path);
|
|
48
53
|
if (!fs.readdirWithFileTypes) {
|
|
49
|
-
const names = await fs.readdir(
|
|
54
|
+
const names = await fs.readdir(dirPath);
|
|
50
55
|
return Promise.all(
|
|
51
56
|
names.map(async (name) => {
|
|
52
|
-
const
|
|
57
|
+
const childPath = fs.resolvePath(dirPath, name);
|
|
58
|
+
const s = await fs.stat(childPath);
|
|
53
59
|
return {
|
|
54
60
|
name,
|
|
55
61
|
isFile: s.isFile,
|
|
@@ -59,13 +65,13 @@ function toSandboxFs(fs: IFileSystem): SandboxFileSystem {
|
|
|
59
65
|
})
|
|
60
66
|
);
|
|
61
67
|
}
|
|
62
|
-
return fs.readdirWithFileTypes(
|
|
68
|
+
return fs.readdirWithFileTypes(dirPath);
|
|
63
69
|
},
|
|
64
|
-
rm: (path, opts) => fs.rm(path, opts),
|
|
65
|
-
cp: (src, dest, opts) => fs.cp(src, dest, opts),
|
|
66
|
-
mv: (src, dest) => fs.mv(src, dest),
|
|
67
|
-
readlink: (path) => fs.readlink(path),
|
|
68
|
-
resolvePath: (base, p) => fs.resolvePath(base, p),
|
|
70
|
+
rm: (path, opts) => fs.rm(normalisePath(path), opts),
|
|
71
|
+
cp: (src, dest, opts) => fs.cp(normalisePath(src), normalisePath(dest), opts),
|
|
72
|
+
mv: (src, dest) => fs.mv(normalisePath(src), normalisePath(dest)),
|
|
73
|
+
readlink: (path) => fs.readlink(normalisePath(path)),
|
|
74
|
+
resolvePath: (base, p) => fs.resolvePath(normalisePath(base), p),
|
|
69
75
|
};
|
|
70
76
|
}
|
|
71
77
|
|
|
@@ -4,6 +4,7 @@ import type {
|
|
|
4
4
|
FileStat,
|
|
5
5
|
} from "../../../lib/sandbox/types";
|
|
6
6
|
import { SandboxNotSupportedError } from "../../../lib/sandbox/types";
|
|
7
|
+
import { posix } from "node:path";
|
|
7
8
|
import type {
|
|
8
9
|
FileEntry,
|
|
9
10
|
FileEntryMetadata,
|
|
@@ -11,15 +12,9 @@ import type {
|
|
|
11
12
|
TreeMutation,
|
|
12
13
|
} from "./types";
|
|
13
14
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*/
|
|
18
|
-
function normalisePath(p: string): string {
|
|
19
|
-
if (!p.startsWith("/")) p = "/" + p;
|
|
20
|
-
p = p.replace(/\/+/g, "/");
|
|
21
|
-
if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
|
|
22
|
-
return p;
|
|
15
|
+
/** Normalize a path against the provided workspace base. */
|
|
16
|
+
function normalisePath(p: string, workspaceBase = "/"): string {
|
|
17
|
+
return posix.resolve(workspaceBase, p);
|
|
23
18
|
}
|
|
24
19
|
|
|
25
20
|
/** Return the parent directory of a normalised path ("/a/b" → "/a"). */
|
|
@@ -32,11 +27,14 @@ function parentDir(p: string): string {
|
|
|
32
27
|
* Collect the set of implicit directory paths from a flat file list.
|
|
33
28
|
* E.g. "/a/b/c.ts" contributes "/a/b", "/a", "/".
|
|
34
29
|
*/
|
|
35
|
-
function inferDirectories(
|
|
30
|
+
function inferDirectories(
|
|
31
|
+
entries: { path: string }[],
|
|
32
|
+
workspaceBase: string,
|
|
33
|
+
): Set<string> {
|
|
36
34
|
const dirs = new Set<string>();
|
|
37
35
|
dirs.add("/");
|
|
38
36
|
for (const entry of entries) {
|
|
39
|
-
let dir = parentDir(normalisePath(entry.path));
|
|
37
|
+
let dir = parentDir(normalisePath(entry.path, workspaceBase));
|
|
40
38
|
while (dir !== "/" && !dirs.has(dir)) {
|
|
41
39
|
dirs.add(dir);
|
|
42
40
|
dir = parentDir(dir);
|
|
@@ -58,6 +56,7 @@ export class VirtualSandboxFileSystem<
|
|
|
58
56
|
TMeta = FileEntryMetadata,
|
|
59
57
|
> implements SandboxFileSystem
|
|
60
58
|
{
|
|
59
|
+
readonly workspaceBase: string;
|
|
61
60
|
private entries: Map<string, FileEntry<TMeta>>;
|
|
62
61
|
private directories: Set<string>;
|
|
63
62
|
private mutations: TreeMutation<TMeta>[] = [];
|
|
@@ -66,11 +65,13 @@ export class VirtualSandboxFileSystem<
|
|
|
66
65
|
tree: FileEntry<TMeta>[],
|
|
67
66
|
private resolver: FileResolver<TCtx, TMeta>,
|
|
68
67
|
private ctx: TCtx,
|
|
68
|
+
workspaceBase = "/",
|
|
69
69
|
) {
|
|
70
|
+
this.workspaceBase = normalisePath(workspaceBase);
|
|
70
71
|
this.entries = new Map(
|
|
71
|
-
tree.map((e) => [normalisePath(e.path), e]),
|
|
72
|
+
tree.map((e) => [normalisePath(e.path, this.workspaceBase), e]),
|
|
72
73
|
);
|
|
73
|
-
this.directories = inferDirectories(tree);
|
|
74
|
+
this.directories = inferDirectories(tree, this.workspaceBase);
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
/** Return all mutations accumulated during this invocation. */
|
|
@@ -80,7 +81,7 @@ export class VirtualSandboxFileSystem<
|
|
|
80
81
|
|
|
81
82
|
/** Look up a file entry by virtual path. */
|
|
82
83
|
getEntry(path: string): FileEntry<TMeta> | undefined {
|
|
83
|
-
return this.entries.get(normalisePath(path));
|
|
84
|
+
return this.entries.get(normalisePath(path, this.workspaceBase));
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
// --------------------------------------------------------------------------
|
|
@@ -88,13 +89,13 @@ export class VirtualSandboxFileSystem<
|
|
|
88
89
|
// --------------------------------------------------------------------------
|
|
89
90
|
|
|
90
91
|
async readFile(path: string): Promise<string> {
|
|
91
|
-
const entry = this.entries.get(normalisePath(path));
|
|
92
|
+
const entry = this.entries.get(normalisePath(path, this.workspaceBase));
|
|
92
93
|
if (!entry) throw new Error(`ENOENT: no such file: ${path}`);
|
|
93
94
|
return this.resolver.readFile(entry.id, this.ctx);
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
async readFileBuffer(path: string): Promise<Uint8Array> {
|
|
97
|
-
const entry = this.entries.get(normalisePath(path));
|
|
98
|
+
const entry = this.entries.get(normalisePath(path, this.workspaceBase));
|
|
98
99
|
if (!entry) throw new Error(`ENOENT: no such file: ${path}`);
|
|
99
100
|
return this.resolver.readFileBuffer(entry.id, this.ctx);
|
|
100
101
|
}
|
|
@@ -104,12 +105,12 @@ export class VirtualSandboxFileSystem<
|
|
|
104
105
|
// --------------------------------------------------------------------------
|
|
105
106
|
|
|
106
107
|
async exists(path: string): Promise<boolean> {
|
|
107
|
-
const norm = normalisePath(path);
|
|
108
|
+
const norm = normalisePath(path, this.workspaceBase);
|
|
108
109
|
return this.entries.has(norm) || this.directories.has(norm);
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
async stat(path: string): Promise<FileStat> {
|
|
112
|
-
const norm = normalisePath(path);
|
|
113
|
+
const norm = normalisePath(path, this.workspaceBase);
|
|
113
114
|
const entry = this.entries.get(norm);
|
|
114
115
|
if (entry) {
|
|
115
116
|
return {
|
|
@@ -133,7 +134,7 @@ export class VirtualSandboxFileSystem<
|
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
async readdir(path: string): Promise<string[]> {
|
|
136
|
-
const norm = normalisePath(path);
|
|
137
|
+
const norm = normalisePath(path, this.workspaceBase);
|
|
137
138
|
if (!this.directories.has(norm)) {
|
|
138
139
|
throw new Error(`ENOENT: no such directory: ${path}`);
|
|
139
140
|
}
|
|
@@ -159,7 +160,7 @@ export class VirtualSandboxFileSystem<
|
|
|
159
160
|
|
|
160
161
|
async readdirWithFileTypes(path: string): Promise<DirentEntry[]> {
|
|
161
162
|
const names = await this.readdir(path);
|
|
162
|
-
const norm = normalisePath(path);
|
|
163
|
+
const norm = normalisePath(path, this.workspaceBase);
|
|
163
164
|
const prefix = norm === "/" ? "/" : norm + "/";
|
|
164
165
|
|
|
165
166
|
return names.map((name) => {
|
|
@@ -175,7 +176,7 @@ export class VirtualSandboxFileSystem<
|
|
|
175
176
|
// --------------------------------------------------------------------------
|
|
176
177
|
|
|
177
178
|
async writeFile(path: string, content: string | Uint8Array): Promise<void> {
|
|
178
|
-
const norm = normalisePath(path);
|
|
179
|
+
const norm = normalisePath(path, this.workspaceBase);
|
|
179
180
|
const existing = this.entries.get(norm);
|
|
180
181
|
|
|
181
182
|
if (existing) {
|
|
@@ -201,7 +202,7 @@ export class VirtualSandboxFileSystem<
|
|
|
201
202
|
}
|
|
202
203
|
|
|
203
204
|
async appendFile(path: string, content: string | Uint8Array): Promise<void> {
|
|
204
|
-
const norm = normalisePath(path);
|
|
205
|
+
const norm = normalisePath(path, this.workspaceBase);
|
|
205
206
|
const existing = this.entries.get(norm);
|
|
206
207
|
|
|
207
208
|
if (!existing) {
|
|
@@ -226,7 +227,7 @@ export class VirtualSandboxFileSystem<
|
|
|
226
227
|
}
|
|
227
228
|
|
|
228
229
|
async mkdir(_path: string, _options?: { recursive?: boolean }): Promise<void> {
|
|
229
|
-
const norm = normalisePath(_path);
|
|
230
|
+
const norm = normalisePath(_path, this.workspaceBase);
|
|
230
231
|
if (this.directories.has(norm)) return;
|
|
231
232
|
|
|
232
233
|
if (_options?.recursive) {
|
|
@@ -245,7 +246,7 @@ export class VirtualSandboxFileSystem<
|
|
|
245
246
|
path: string,
|
|
246
247
|
options?: { recursive?: boolean; force?: boolean },
|
|
247
248
|
): Promise<void> {
|
|
248
|
-
const norm = normalisePath(path);
|
|
249
|
+
const norm = normalisePath(path, this.workspaceBase);
|
|
249
250
|
const entry = this.entries.get(norm);
|
|
250
251
|
|
|
251
252
|
if (entry) {
|
|
@@ -284,8 +285,8 @@ export class VirtualSandboxFileSystem<
|
|
|
284
285
|
dest: string,
|
|
285
286
|
_options?: { recursive?: boolean },
|
|
286
287
|
): Promise<void> {
|
|
287
|
-
const normSrc = normalisePath(src);
|
|
288
|
-
const normDest = normalisePath(dest);
|
|
288
|
+
const normSrc = normalisePath(src, this.workspaceBase);
|
|
289
|
+
const normDest = normalisePath(dest, this.workspaceBase);
|
|
289
290
|
|
|
290
291
|
const entry = this.entries.get(normSrc);
|
|
291
292
|
if (entry) {
|
|
@@ -325,10 +326,7 @@ export class VirtualSandboxFileSystem<
|
|
|
325
326
|
}
|
|
326
327
|
|
|
327
328
|
resolvePath(base: string, path: string): string {
|
|
328
|
-
|
|
329
|
-
const combined =
|
|
330
|
-
base.endsWith("/") ? base + path : base + "/" + path;
|
|
331
|
-
return normalisePath(combined);
|
|
329
|
+
return posix.resolve(normalisePath(base, this.workspaceBase), path);
|
|
332
330
|
}
|
|
333
331
|
|
|
334
332
|
// --------------------------------------------------------------------------
|
|
@@ -336,7 +334,7 @@ export class VirtualSandboxFileSystem<
|
|
|
336
334
|
// --------------------------------------------------------------------------
|
|
337
335
|
|
|
338
336
|
private addParentDirectories(filePath: string): void {
|
|
339
|
-
let dir = parentDir(normalisePath(filePath));
|
|
337
|
+
let dir = parentDir(normalisePath(filePath, this.workspaceBase));
|
|
340
338
|
while (!this.directories.has(dir)) {
|
|
341
339
|
this.directories.add(dir);
|
|
342
340
|
dir = parentDir(dir);
|
|
@@ -34,9 +34,10 @@ class VirtualSandboxImpl<
|
|
|
34
34
|
readonly id: string,
|
|
35
35
|
tree: FileEntry<TMeta>[],
|
|
36
36
|
resolver: FileResolver<TCtx, TMeta>,
|
|
37
|
-
ctx: TCtx
|
|
37
|
+
ctx: TCtx,
|
|
38
|
+
workspaceBase = "/",
|
|
38
39
|
) {
|
|
39
|
-
this.fs = new VirtualSandboxFileSystem(tree, resolver, ctx);
|
|
40
|
+
this.fs = new VirtualSandboxFileSystem(tree, resolver, ctx, workspaceBase);
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
async exec(_command: string, _options?: ExecOptions): Promise<ExecResult> {
|
|
@@ -67,8 +68,9 @@ export function createVirtualSandbox<
|
|
|
67
68
|
tree: FileEntry<TMeta>[],
|
|
68
69
|
resolver: FileResolver<TCtx, TMeta>,
|
|
69
70
|
ctx: TCtx,
|
|
71
|
+
workspaceBase = "/",
|
|
70
72
|
): VirtualSandbox<TCtx, TMeta> {
|
|
71
|
-
return new VirtualSandboxImpl(id, tree, resolver, ctx);
|
|
73
|
+
return new VirtualSandboxImpl(id, tree, resolver, ctx, workspaceBase);
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
// Re-exports for convenience
|
|
@@ -16,7 +16,7 @@ import type {
|
|
|
16
16
|
* Stateless {@link SandboxProvider} backed by a {@link FileResolver}.
|
|
17
17
|
*
|
|
18
18
|
* The provider holds **no internal state**. All sandbox state (sandboxId,
|
|
19
|
-
* fileTree, resolverContext) is returned as a `stateUpdate` from
|
|
19
|
+
* fileTree, resolverContext, workspaceBase) is returned as a `stateUpdate` from
|
|
20
20
|
* {@link create} and merged into the workflow's `AgentState` by the session.
|
|
21
21
|
* {@link withVirtualSandbox} reads this state back on every tool invocation.
|
|
22
22
|
*
|
|
@@ -59,12 +59,14 @@ export class VirtualSandboxProvider<
|
|
|
59
59
|
const fileTree = await this.resolver.resolveEntries(
|
|
60
60
|
options.resolverContext
|
|
61
61
|
);
|
|
62
|
+
const workspaceBase = options.workspaceBase ?? "/";
|
|
62
63
|
|
|
63
64
|
const sandbox = createVirtualSandbox(
|
|
64
65
|
sandboxId,
|
|
65
66
|
fileTree,
|
|
66
67
|
this.resolver,
|
|
67
|
-
options.resolverContext
|
|
68
|
+
options.resolverContext,
|
|
69
|
+
workspaceBase,
|
|
68
70
|
);
|
|
69
71
|
|
|
70
72
|
return {
|
|
@@ -73,6 +75,7 @@ export class VirtualSandboxProvider<
|
|
|
73
75
|
sandboxId,
|
|
74
76
|
fileTree,
|
|
75
77
|
resolverContext: options.resolverContext,
|
|
78
|
+
workspaceBase,
|
|
76
79
|
},
|
|
77
80
|
};
|
|
78
81
|
}
|
|
@@ -79,6 +79,8 @@ export interface VirtualSandboxCreateOptions<
|
|
|
79
79
|
TCtx,
|
|
80
80
|
> extends SandboxCreateOptions {
|
|
81
81
|
resolverContext: TCtx;
|
|
82
|
+
/** Base path for resolving relative filesystem paths (default "/"). */
|
|
83
|
+
workspaceBase?: string;
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
// ============================================================================
|
|
@@ -97,6 +99,7 @@ export interface VirtualSandboxState<
|
|
|
97
99
|
sandboxId: string;
|
|
98
100
|
fileTree: FileEntry<TMeta>[];
|
|
99
101
|
resolverContext: TCtx;
|
|
102
|
+
workspaceBase?: string;
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
// ============================================================================
|
|
@@ -15,7 +15,7 @@ import { createVirtualSandbox } from "./index";
|
|
|
15
15
|
* the parent workflow for the current file tree and resolver context.
|
|
16
16
|
*
|
|
17
17
|
* On each invocation the wrapper:
|
|
18
|
-
* 1. Queries the workflow's `AgentState` for `fileTree` and `
|
|
18
|
+
* 1. Queries the workflow's `AgentState` for `fileTree`, `resolverContext`, and `workspaceBase`
|
|
19
19
|
* 2. Creates an ephemeral {@link VirtualSandbox} from tree + provider's resolver
|
|
20
20
|
* 3. Runs the inner handler
|
|
21
21
|
* 4. Returns the handler's result together with any {@link TreeMutation}s
|
|
@@ -63,7 +63,7 @@ export function withVirtualSandbox<
|
|
|
63
63
|
const state =
|
|
64
64
|
await queryParentWorkflowState<VirtualSandboxState<TCtx, TMeta>>(client);
|
|
65
65
|
|
|
66
|
-
const { sandboxId, fileTree, resolverContext } = state;
|
|
66
|
+
const { sandboxId, fileTree, resolverContext, workspaceBase } = state;
|
|
67
67
|
if (!fileTree || !sandboxId) {
|
|
68
68
|
return {
|
|
69
69
|
toolResponse: `Error: No fileTree/sandboxId in agent state. The ${context.toolName} tool requires a virtual sandbox.`,
|
|
@@ -75,7 +75,8 @@ export function withVirtualSandbox<
|
|
|
75
75
|
sandboxId,
|
|
76
76
|
fileTree,
|
|
77
77
|
provider.resolver,
|
|
78
|
-
resolverContext
|
|
78
|
+
resolverContext,
|
|
79
|
+
workspaceBase ?? "/",
|
|
79
80
|
);
|
|
80
81
|
const response = await handler(args, { ...context, sandbox });
|
|
81
82
|
const mutations = sandbox.fs.getMutations();
|