@springbrand/agent-runtime 0.1.3-alpha.1 → 0.1.3-alpha.10
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/package.json +12 -3
- package/src/adapter/cloudflare/index.ts +56 -0
- package/src/adapter/cloudflare/resources/runtime-resources.ts +89 -0
- package/src/adapter/cloudflare/sandbox/adapter.ts +1513 -0
- package/src/adapter/cloudflare/sandbox/id.ts +23 -0
- package/src/adapter/cloudflare/sandbox/policy.ts +15 -0
- package/src/adapter/cloudflare/subagent/definition.ts +574 -0
- package/src/adapter/cloudflare/subagent/runner.ts +175 -0
- package/src/adapter/cloudflare/subagent/tools.ts +254 -0
- package/src/adapter/cloudflare/universal-agent/hooks.ts +35 -0
- package/src/adapter/cloudflare/universal-agent/preparation.ts +277 -0
- package/src/adapter/cloudflare/universal-agent/tools.ts +80 -0
- package/src/adapter/cloudflare/workspace/git-fs.ts +178 -0
- package/src/adapter/cloudflare/workspace/publisher.ts +31 -0
- package/src/adapter/cloudflare/workspace/scoped-workspace.ts +376 -0
- package/src/adapter/cloudflare/workspace/version-control.ts +374 -0
- package/src/agent-tool-runtime.ts +152 -0
- package/src/db/agent-tool.repo.ts +27 -0
- package/src/db/index.ts +33 -0
- package/src/db/interaction.repo.ts +185 -0
- package/src/db/schema.ts +25 -1
- package/src/db/submission.repo.ts +63 -1
- package/src/index.ts +57 -21
- package/src/kernel/approval-lifecycle.ts +41 -6
- package/src/kernel/bindings.ts +73 -9
- package/src/kernel/interaction-lifecycle.ts +395 -0
- package/src/kernel/public-contracts.ts +2 -0
- package/src/kernel/recoverable-chat-agent.ts +104 -6
- package/src/kernel/runtime-assembly-view.ts +37 -0
- package/src/kernel/runtime-assembly.ts +41 -0
- package/src/kernel/runtime-config.ts +4 -0
- package/src/kernel/runtime-load.ts +191 -0
- package/src/kernel/state.ts +12 -1
- package/src/kernel/submission-lifecycle.ts +33 -2
- package/src/layers/orchestration/temporary-agent/core.ts +12 -1
- package/src/layers/orchestration/temporary-agent/runner.ts +1 -2
- package/src/lib/mcp.ts +7 -3
- package/src/lib/prompt.ts +1 -1
- package/src/lib/telemetry-dev.ts +7 -4
- package/src/pi/assembly/context.ts +3 -3
- package/src/pi/assembly/extensions.ts +11 -22
- package/src/pi/assembly/snapshot.ts +6 -3
- package/src/pi/message/contract.ts +7 -0
- package/src/pi/message/conversion.ts +9 -1
- package/src/pi/runtime-adapter/assembly.ts +26 -31
- package/src/pi/runtime-adapter/execution.ts +198 -15
- package/src/pi/runtime-adapter/index.ts +24 -8
- package/src/pi/runtime-adapter/models.ts +382 -35
- package/src/pi/runtime-adapter/recovery.ts +188 -1
- package/src/pi/runtime-adapter/transcript.ts +61 -3
- package/src/pi/tool/ai-adapter.ts +58 -1
- package/src/pi/tool/base.ts +190 -12
- package/src/pi/tool/compiler.ts +34 -1
- package/src/pi/tool/core-host.ts +19 -24
- package/src/pi/tool/core.ts +30 -120
- package/src/pi/tool/gateway.ts +54 -0
- package/src/pi/tool/index.ts +2 -0
- package/src/pi/tool/mcp.ts +96 -68
- package/src/pi/tool/schedule.ts +41 -19
- package/src/pi/tool/skill.ts +126 -420
- package/src/pi/tool/subagent.ts +14 -2
- package/src/pi/tool/web-fetch.ts +281 -0
- package/src/pi/tool/web-search/api.ts +34 -18
- package/src/pi/tool/web-search/web-search.ts +0 -1
- package/src/pi/tool/workspace-revision.ts +64 -0
- package/src/pi/tool/workspace-sandbox.ts +105 -263
- package/src/pi/turn/index.ts +20 -0
- package/src/pi/turn/interaction.ts +181 -0
- package/src/pi/turn/tool-recovery.ts +244 -1
- package/src/runtime-agent-context.ts +112 -0
- package/src/runtime-agent.ts +568 -321
- package/src/{plugins.ts → runtime-assembler.ts} +372 -398
- package/src/runtime-definition.ts +175 -0
- package/src/runtime.ts +835 -204
- package/src/tool-registry.ts +143 -0
- package/src/workspace-versioning.ts +46 -0
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
WorkspaceAdminPort,
|
|
3
|
+
WorkspaceFileInfo,
|
|
4
|
+
WorkspacePort,
|
|
5
|
+
WorkspaceQuota,
|
|
6
|
+
WorkspaceUsage,
|
|
7
|
+
} from "../../../kernel/bindings";
|
|
8
|
+
import type {
|
|
9
|
+
WorkspaceConditionalWriteResult,
|
|
10
|
+
WorkspaceFileVersion,
|
|
11
|
+
} from "./publisher";
|
|
12
|
+
|
|
13
|
+
const USAGE_PAGE_SIZE = 256;
|
|
14
|
+
const UNCONFIGURED_QUOTA: WorkspaceQuota = Object.freeze({
|
|
15
|
+
maxFiles: null,
|
|
16
|
+
maxTotalBytes: null,
|
|
17
|
+
maxFileBytes: null,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function normalizePath(path: string): string {
|
|
21
|
+
const value = path.trim().replaceAll("\\", "/");
|
|
22
|
+
const absolute = value.startsWith("/") ? value : `/${value}`;
|
|
23
|
+
const parts = absolute.split("/").filter(Boolean);
|
|
24
|
+
if (parts.some((part) => part === ".." || part === ".")) {
|
|
25
|
+
throw new Error("workspace path traversal is not allowed");
|
|
26
|
+
}
|
|
27
|
+
return `/${parts.join("/")}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isReservedMemoryPath(path: string): boolean {
|
|
31
|
+
return path === "/shared/memories" || path.startsWith("/shared/memories/");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A Session sees the user's shared mount plus its own directory. All other
|
|
36
|
+
* paths are interpreted relative to the current Session directory; another
|
|
37
|
+
* Session can never be named through this port. The Inbox Host also uses this
|
|
38
|
+
* object to create and remove the Session tree, so callers never need to build
|
|
39
|
+
* the physical `/sessions/<id>` path themselves.
|
|
40
|
+
*/
|
|
41
|
+
export class ScopedWorkspace implements WorkspacePort, WorkspaceAdminPort {
|
|
42
|
+
readonly sessionRoot: string;
|
|
43
|
+
|
|
44
|
+
constructor(
|
|
45
|
+
private readonly parent: WorkspacePort,
|
|
46
|
+
readonly sessionId: string,
|
|
47
|
+
private readonly quota: WorkspaceQuota = UNCONFIGURED_QUOTA,
|
|
48
|
+
) {
|
|
49
|
+
if (!sessionId || sessionId.includes("/") || sessionId === "." || sessionId === "..") {
|
|
50
|
+
throw new Error("invalid session id");
|
|
51
|
+
}
|
|
52
|
+
this.sessionRoot = `/sessions/${sessionId}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async ensure(): Promise<void> {
|
|
56
|
+
await this.parent.mkdir("/shared", { recursive: true });
|
|
57
|
+
await this.parent.mkdir(this.sessionRoot, { recursive: true });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Count only this Session's private tree. User-level `/shared` belongs to the
|
|
62
|
+
* Inbox and must not be charged once per Chat.
|
|
63
|
+
*/
|
|
64
|
+
async getUsage(): Promise<WorkspaceUsage> {
|
|
65
|
+
if (!(await this.parent.exists(this.sessionRoot))) {
|
|
66
|
+
return { fileCount: 0, directoryCount: 0, totalBytes: 0 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let fileCount = 0;
|
|
70
|
+
let directoryCount = 0;
|
|
71
|
+
let totalBytes = 0;
|
|
72
|
+
const pending = [this.sessionRoot];
|
|
73
|
+
|
|
74
|
+
while (pending.length > 0) {
|
|
75
|
+
const directory = pending.pop()!;
|
|
76
|
+
let offset = 0;
|
|
77
|
+
while (true) {
|
|
78
|
+
const entries = await this.parent.readDir(directory, {
|
|
79
|
+
limit: USAGE_PAGE_SIZE,
|
|
80
|
+
offset,
|
|
81
|
+
});
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (entry.type === "directory") {
|
|
84
|
+
directoryCount += 1;
|
|
85
|
+
pending.push(entry.path);
|
|
86
|
+
} else {
|
|
87
|
+
fileCount += 1;
|
|
88
|
+
totalBytes += entry.size;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (entries.length < USAGE_PAGE_SIZE) break;
|
|
92
|
+
offset += entries.length;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { fileCount, directoryCount, totalBytes };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async getQuota(): Promise<WorkspaceQuota> {
|
|
100
|
+
return { ...this.quota };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Host-only lifecycle operation. It never removes the user-level `/shared` tree. */
|
|
104
|
+
async removeAll(): Promise<void> {
|
|
105
|
+
await this.parent.rm(this.sessionRoot, {
|
|
106
|
+
recursive: true,
|
|
107
|
+
force: true,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private physical(path: string): string {
|
|
112
|
+
const normalized = normalizePath(path);
|
|
113
|
+
if (normalized === "/shared" || normalized.startsWith("/shared/")) {
|
|
114
|
+
return normalized;
|
|
115
|
+
}
|
|
116
|
+
if (normalized === this.sessionRoot || normalized.startsWith(`${this.sessionRoot}/`)) {
|
|
117
|
+
return normalized;
|
|
118
|
+
}
|
|
119
|
+
if (normalized === "/sessions" || normalized.startsWith("/sessions/")) {
|
|
120
|
+
throw new Error("another Session workspace is not accessible");
|
|
121
|
+
}
|
|
122
|
+
return normalized === "/"
|
|
123
|
+
? this.sessionRoot
|
|
124
|
+
: `${this.sessionRoot}${normalized}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private writablePhysical(path: string): string {
|
|
128
|
+
const physical = this.physical(path);
|
|
129
|
+
if (isReservedMemoryPath(physical)) {
|
|
130
|
+
throw new Error("/shared/memories is read-only in Session workspaces");
|
|
131
|
+
}
|
|
132
|
+
return physical;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private async assertNoSymlinkComponents(path: string): Promise<void> {
|
|
136
|
+
let root: string;
|
|
137
|
+
if (path === "/shared" || path.startsWith("/shared/")) {
|
|
138
|
+
root = "/shared";
|
|
139
|
+
} else if (
|
|
140
|
+
path === this.sessionRoot ||
|
|
141
|
+
path.startsWith(`${this.sessionRoot}/`)
|
|
142
|
+
) {
|
|
143
|
+
root = this.sessionRoot;
|
|
144
|
+
} else {
|
|
145
|
+
throw new Error("another Session workspace is not accessible");
|
|
146
|
+
}
|
|
147
|
+
const relative = path.slice(root.length);
|
|
148
|
+
const parts = relative.split("/").filter(Boolean);
|
|
149
|
+
let current = root;
|
|
150
|
+
|
|
151
|
+
for (const part of ["", ...parts]) {
|
|
152
|
+
if (part) current = `${current}/${part}`;
|
|
153
|
+
const info = await this.parent.lstat(current);
|
|
154
|
+
if (!info) return;
|
|
155
|
+
if (info.type === "symlink") {
|
|
156
|
+
throw new Error(
|
|
157
|
+
"symbolic links are not accessible in scoped workspaces",
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private async guardedPhysical(
|
|
164
|
+
path: string,
|
|
165
|
+
writable = false,
|
|
166
|
+
): Promise<string> {
|
|
167
|
+
const physical = writable
|
|
168
|
+
? this.writablePhysical(path)
|
|
169
|
+
: this.physical(path);
|
|
170
|
+
await this.assertNoSymlinkComponents(physical);
|
|
171
|
+
return physical;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private virtual(path: string): string {
|
|
175
|
+
if (path === this.sessionRoot) return "/";
|
|
176
|
+
if (path.startsWith(`${this.sessionRoot}/`)) {
|
|
177
|
+
return path.slice(this.sessionRoot.length);
|
|
178
|
+
}
|
|
179
|
+
return path;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private project(info: WorkspaceFileInfo | null): WorkspaceFileInfo | null {
|
|
183
|
+
return info ? { ...info, path: this.virtual(info.path) } : null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async readFile(path: string) {
|
|
187
|
+
return this.parent.readFile(await this.guardedPhysical(path));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async readFileBytes(path: string) {
|
|
191
|
+
return this.parent.readFileBytes(await this.guardedPhysical(path));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async writeFile(path: string, content: string, mimeType?: string) {
|
|
195
|
+
return this.parent.writeFile(
|
|
196
|
+
await this.guardedPhysical(path, true),
|
|
197
|
+
content,
|
|
198
|
+
mimeType,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async writeFileBytes(
|
|
203
|
+
path: string,
|
|
204
|
+
data: Uint8Array | ArrayBuffer,
|
|
205
|
+
mimeType?: string,
|
|
206
|
+
) {
|
|
207
|
+
return this.parent.writeFileBytes(
|
|
208
|
+
await this.guardedPhysical(path, true),
|
|
209
|
+
data,
|
|
210
|
+
mimeType,
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async writeFileBytesIfUnchanged(
|
|
215
|
+
path: string,
|
|
216
|
+
data: Uint8Array,
|
|
217
|
+
mimeType: string,
|
|
218
|
+
expected: WorkspaceFileVersion | null,
|
|
219
|
+
): Promise<WorkspaceConditionalWriteResult> {
|
|
220
|
+
const physical = await this.guardedPhysical(path, true);
|
|
221
|
+
if (physical === "/shared" || physical.startsWith("/shared/")) {
|
|
222
|
+
throw new Error("/shared is read-only for Sandbox publishing");
|
|
223
|
+
}
|
|
224
|
+
const current = await this.parent.stat(physical);
|
|
225
|
+
const currentVersion =
|
|
226
|
+
current?.type === "file"
|
|
227
|
+
? { updatedAt: current.updatedAt, size: current.size }
|
|
228
|
+
: null;
|
|
229
|
+
const matches =
|
|
230
|
+
expected === null
|
|
231
|
+
? current === null
|
|
232
|
+
: current?.type === "file" &&
|
|
233
|
+
current.updatedAt === expected.updatedAt &&
|
|
234
|
+
current.size === expected.size;
|
|
235
|
+
if (!matches) {
|
|
236
|
+
return {
|
|
237
|
+
written: false,
|
|
238
|
+
reason: "conflict",
|
|
239
|
+
current: currentVersion,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
await this.parent.writeFileBytes(physical, data, mimeType);
|
|
243
|
+
const written = await this.parent.stat(physical);
|
|
244
|
+
if (!written || written.type !== "file") {
|
|
245
|
+
throw new Error("published Workspace file could not be verified");
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
written: true,
|
|
249
|
+
version: {
|
|
250
|
+
updatedAt: written.updatedAt,
|
|
251
|
+
size: written.size,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async appendFile(path: string, content: string, mimeType?: string) {
|
|
257
|
+
return this.parent.appendFile(
|
|
258
|
+
await this.guardedPhysical(path, true),
|
|
259
|
+
content,
|
|
260
|
+
mimeType,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async exists(path: string) {
|
|
265
|
+
return this.parent.exists(await this.guardedPhysical(path));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async stat(path: string) {
|
|
269
|
+
return this.project(
|
|
270
|
+
await this.parent.stat(await this.guardedPhysical(path)),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async lstat(path: string) {
|
|
275
|
+
return this.project(
|
|
276
|
+
await this.parent.lstat(await this.guardedPhysical(path)),
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async mkdir(path: string, opts?: { recursive?: boolean }) {
|
|
281
|
+
return this.parent.mkdir(await this.guardedPhysical(path, true), opts);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async readDir(dir = "/", opts?: { limit?: number; offset?: number }) {
|
|
285
|
+
const files = await this.parent.readDir(
|
|
286
|
+
await this.guardedPhysical(dir),
|
|
287
|
+
opts,
|
|
288
|
+
);
|
|
289
|
+
return files.map((file) => this.project(file)!);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async rm(path: string, opts?: { recursive?: boolean; force?: boolean }) {
|
|
293
|
+
const physical = await this.guardedPhysical(path, true);
|
|
294
|
+
if (physical === "/shared" || physical === this.sessionRoot) {
|
|
295
|
+
throw new Error("workspace mount roots cannot be removed");
|
|
296
|
+
}
|
|
297
|
+
return this.parent.rm(physical, opts);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async cp(src: string, dest: string, opts?: { recursive?: boolean }) {
|
|
301
|
+
return this.parent.cp(
|
|
302
|
+
await this.guardedPhysical(src),
|
|
303
|
+
await this.guardedPhysical(dest, true),
|
|
304
|
+
opts,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async mv(src: string, dest: string, opts?: { recursive?: boolean }) {
|
|
309
|
+
return this.parent.mv(
|
|
310
|
+
await this.guardedPhysical(src, true),
|
|
311
|
+
await this.guardedPhysical(dest, true),
|
|
312
|
+
opts,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async symlink(target: string, linkPath: string) {
|
|
317
|
+
/*
|
|
318
|
+
* A symlink stored under `/shared` could target this Session's physical
|
|
319
|
+
* directory and then be followed by another Session. Path normalization
|
|
320
|
+
* cannot guard a later filesystem dereference, so scoped runtimes do not
|
|
321
|
+
* create symlinks at all.
|
|
322
|
+
*/
|
|
323
|
+
void target;
|
|
324
|
+
void linkPath;
|
|
325
|
+
throw new Error("symbolic links are disabled in scoped workspaces");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async readlink(path: string) {
|
|
329
|
+
return this.virtual(
|
|
330
|
+
await this.parent.readlink(await this.guardedPhysical(path)),
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async glob(pattern: string) {
|
|
335
|
+
const files = await this.parent.glob(this.physical(pattern));
|
|
336
|
+
for (const file of files) {
|
|
337
|
+
await this.assertNoSymlinkComponents(file.path);
|
|
338
|
+
}
|
|
339
|
+
return files.map((file) => this.project(file)!);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Build a runtime capability object that contains data-plane methods only.
|
|
345
|
+
*
|
|
346
|
+
* A TypeScript annotation does not remove extra methods at runtime. Returning
|
|
347
|
+
* this explicit facade prevents Agent Runtime and Sandbox adapters from
|
|
348
|
+
* reaching ScopedWorkspace lifecycle/accounting methods through the same
|
|
349
|
+
* object reference.
|
|
350
|
+
*/
|
|
351
|
+
export function createWorkspacePortFacade(
|
|
352
|
+
workspace: WorkspacePort,
|
|
353
|
+
): WorkspacePort {
|
|
354
|
+
const facade: WorkspacePort = {
|
|
355
|
+
readFile: (path) => workspace.readFile(path),
|
|
356
|
+
readFileBytes: (path) => workspace.readFileBytes(path),
|
|
357
|
+
writeFile: (path, content, mimeType) =>
|
|
358
|
+
workspace.writeFile(path, content, mimeType),
|
|
359
|
+
writeFileBytes: (path, data, mimeType) =>
|
|
360
|
+
workspace.writeFileBytes(path, data, mimeType),
|
|
361
|
+
appendFile: (path, content, mimeType) =>
|
|
362
|
+
workspace.appendFile(path, content, mimeType),
|
|
363
|
+
exists: (path) => workspace.exists(path),
|
|
364
|
+
stat: (path) => workspace.stat(path),
|
|
365
|
+
lstat: (path) => workspace.lstat(path),
|
|
366
|
+
mkdir: (path, opts) => workspace.mkdir(path, opts),
|
|
367
|
+
readDir: (dir, opts) => workspace.readDir(dir, opts),
|
|
368
|
+
rm: (path, opts) => workspace.rm(path, opts),
|
|
369
|
+
cp: (src, dest, opts) => workspace.cp(src, dest, opts),
|
|
370
|
+
mv: (src, dest, opts) => workspace.mv(src, dest, opts),
|
|
371
|
+
symlink: (target, linkPath) => workspace.symlink(target, linkPath),
|
|
372
|
+
readlink: (path) => workspace.readlink(path),
|
|
373
|
+
glob: (pattern) => workspace.glob(pattern),
|
|
374
|
+
};
|
|
375
|
+
return Object.freeze(facade);
|
|
376
|
+
}
|