runline 0.34.0 → 0.35.0
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/dist/core/engine.js +12 -8
- package/dist/credentials/registry.js +5 -0
- package/dist/credentials/types.d.ts +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plugin/schema.d.ts +16 -4
- package/dist/plugin/schema.js +28 -5
- package/dist/plugin/types.d.ts +8 -0
- package/dist/plugins/_shared/base64.js +30 -0
- package/dist/plugins/_shared/credentialAdapter.js +23 -3
- package/dist/plugins/_shared/credentials.js +29 -2
- package/dist/plugins/_shared/googleAuth.js +62 -1
- package/dist/plugins/_shared/googleCredentials.js +40 -7
- package/dist/plugins/_shared/localFile.js +40 -0
- package/dist/plugins/_shared/mediaFile.js +12 -12
- package/dist/plugins/_shared/microsoftAuth.js +19 -4
- package/dist/plugins/_shared/microsoftCredentials.js +12 -7
- package/dist/plugins/_shared/shiftCloud.js +14 -18
- package/dist/plugins/_shared/shiftUpload.js +14 -10
- package/dist/plugins/agentMail/src/credentials.js +35 -0
- package/dist/plugins/agentMail/src/drafts.js +137 -0
- package/dist/plugins/agentMail/src/inboxes.js +95 -0
- package/dist/plugins/agentMail/src/index.js +35 -0
- package/dist/plugins/agentMail/src/messages.js +154 -0
- package/dist/plugins/agentMail/src/schemas.js +156 -0
- package/dist/plugins/agentMail/src/shared.js +203 -0
- package/dist/plugins/agentMail/src/threads.js +75 -0
- package/dist/plugins/elevenlabs/src/shared.js +29 -43
- package/dist/plugins/fal/src/index.js +1 -1
- package/dist/plugins/gmail/src/index.js +22 -85
- package/dist/plugins/googleAppsScript/src/index.js +2 -1
- package/dist/plugins/googleCalendar/src/index.js +6 -39
- package/dist/plugins/googleContacts/src/index.js +2 -32
- package/dist/plugins/googleDocs/src/index.js +2 -33
- package/dist/plugins/googleDrive/src/index.js +19 -45
- package/dist/plugins/googleImage/src/index.js +1 -1
- package/dist/plugins/googleSheets/src/index.js +2 -34
- package/dist/plugins/googleSlides/src/index.js +2 -32
- package/dist/plugins/googleTasks/src/index.js +2 -32
- package/dist/plugins/highlevel/src/index.js +8 -2
- package/dist/plugins/microsoftCalendar/src/index.js +2 -7
- package/dist/plugins/microsoftFiles/src/index.js +7 -8
- package/dist/plugins/microsoftMail/src/index.js +2 -7
- package/dist/plugins/openai/src/index.js +1 -1
- package/dist/plugins/recraft/src/index.js +1 -1
- package/dist/plugins/replicate/src/index.js +1 -1
- package/dist/plugins/shiftObjects/src/objects.js +1 -1
- package/dist/plugins/shiftOcr/src/index.js +8 -3
- package/dist/plugins/shiftTranscription/src/transcription.js +1 -1
- package/dist/plugins/together/src/index.js +1 -1
- package/dist/plugins/xai/src/index.js +1 -1
- package/package.json +2 -1
- package/dist/tests/helpers/engine-harness.d.ts +0 -1
- package/dist/tests/helpers/engine-harness.js +0 -236
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { graphRequest, microsoftCredential,
|
|
1
|
+
import { graphRequest, microsoftCredential, microsoftOAuth, userBase, } from "../../_shared/microsoftAuth.js";
|
|
2
2
|
const NAME = "microsoftCalendar";
|
|
3
3
|
const SCOPES = ["https://graph.microsoft.com/Calendars.Read"];
|
|
4
4
|
export default function microsoftCalendar(rl) {
|
|
@@ -42,12 +42,7 @@ export default function microsoftCalendar(rl) {
|
|
|
42
42
|
description: "App-only only: target user UPN",
|
|
43
43
|
},
|
|
44
44
|
});
|
|
45
|
-
rl.setOAuth(
|
|
46
|
-
authUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
47
|
-
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
48
|
-
scopes: [...SCOPES, "offline_access"],
|
|
49
|
-
setupHelp: microsoftSetupHelp("Calendars.Read"),
|
|
50
|
-
});
|
|
45
|
+
rl.setOAuth(microsoftOAuth(SCOPES, "Calendars.Read"));
|
|
51
46
|
rl.registerAction("calendar.list", {
|
|
52
47
|
access: "read",
|
|
53
48
|
description: "List calendar events in a date range. Returns [{id,subject,start,end,location,organizer,attendees}].",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { standardBase64 } from "../../_shared/base64.js";
|
|
1
2
|
import { jsonAnswer, pathSegment, pathSegments, requestFailed, } from "../../_shared/credentials.js";
|
|
2
|
-
import { graphRequest, graphResponse, microsoftCredential, microsoftDownload, microsoftDriveBase,
|
|
3
|
+
import { graphRequest, graphResponse, microsoftCredential, microsoftDownload, microsoftDriveBase, microsoftOAuth, } from "../../_shared/microsoftAuth.js";
|
|
3
4
|
const NAME = "microsoftFiles";
|
|
4
5
|
const SCOPES = [
|
|
5
6
|
"https://graph.microsoft.com/Files.ReadWrite.All",
|
|
@@ -65,12 +66,7 @@ export default function microsoftFiles(rl) {
|
|
|
65
66
|
description: "Optional explicit drive id (overrides site/user)",
|
|
66
67
|
},
|
|
67
68
|
});
|
|
68
|
-
rl.setOAuth(
|
|
69
|
-
authUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
70
|
-
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
71
|
-
scopes: [...SCOPES, "offline_access"],
|
|
72
|
-
setupHelp: microsoftSetupHelp("Files.ReadWrite.All, Sites.ReadWrite.All"),
|
|
73
|
-
});
|
|
69
|
+
rl.setOAuth(microsoftOAuth(SCOPES, "Files.ReadWrite.All, Sites.ReadWrite.All"));
|
|
74
70
|
rl.registerAction("files.search", {
|
|
75
71
|
access: "read",
|
|
76
72
|
description: "Search the drive for files/folders by text. Returns [{id,name,size,lastModifiedDateTime,webUrl,folder?}].",
|
|
@@ -150,7 +146,10 @@ export default function microsoftFiles(rl) {
|
|
|
150
146
|
},
|
|
151
147
|
async execute(input, ctx) {
|
|
152
148
|
const { path, base64 } = input;
|
|
153
|
-
const
|
|
149
|
+
const content = standardBase64(base64);
|
|
150
|
+
if (content === undefined)
|
|
151
|
+
throw new Error(`${NAME}: base64 is not valid base64`);
|
|
152
|
+
const bytes = Buffer.from(content, "base64");
|
|
154
153
|
const res = await binaryFetch(ctx, "PUT", `${driveBase(ctx)}/root:/${pathSegments(path)}:/content`, bytes);
|
|
155
154
|
if (!res.ok)
|
|
156
155
|
throw requestFailed(NAME, res.status);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { graphRequest, microsoftCredential,
|
|
1
|
+
import { graphRequest, microsoftCredential, microsoftOAuth, userBase, } from "../../_shared/microsoftAuth.js";
|
|
2
2
|
const NAME = "microsoftMail";
|
|
3
3
|
const SCOPES = [
|
|
4
4
|
"https://graph.microsoft.com/Mail.Send",
|
|
@@ -60,12 +60,7 @@ export default function microsoftMail(rl) {
|
|
|
60
60
|
description: "App-only only: target mailbox UPN (e.g. agent@contoso.com)",
|
|
61
61
|
},
|
|
62
62
|
});
|
|
63
|
-
rl.setOAuth(
|
|
64
|
-
authUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
65
|
-
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
66
|
-
scopes: [...SCOPES, "offline_access"],
|
|
67
|
-
setupHelp: microsoftSetupHelp("Mail.Send, Mail.ReadWrite, Mail.Read"),
|
|
68
|
-
});
|
|
63
|
+
rl.setOAuth(microsoftOAuth(SCOPES, "Mail.Send, Mail.ReadWrite, Mail.Read"));
|
|
69
64
|
rl.registerAction("mail.send", {
|
|
70
65
|
access: "write",
|
|
71
66
|
description: "Send an email as the connected mailbox. Returns {success}. Get user approval before sending external mail.",
|
|
@@ -218,7 +218,7 @@ export default function openai(rl) {
|
|
|
218
218
|
if (p.quality)
|
|
219
219
|
form.append("quality", p.quality);
|
|
220
220
|
for (const path of paths) {
|
|
221
|
-
const img = readImageInput(path, "openai");
|
|
221
|
+
const img = await readImageInput(path, "openai");
|
|
222
222
|
form.append("image[]", new Blob([img.bytes], { type: img.mimeType }), img.fileName);
|
|
223
223
|
}
|
|
224
224
|
const { body: formBytes, contentType } = await multipartBody(form);
|
|
@@ -154,7 +154,7 @@ export default function recraft(rl) {
|
|
|
154
154
|
if (typeof p.prompt !== "string" || p.prompt.length === 0) {
|
|
155
155
|
throw new Error("recraft: prompt is required");
|
|
156
156
|
}
|
|
157
|
-
const img = readImageInput(p.imagePath, "recraft");
|
|
157
|
+
const img = await readImageInput(p.imagePath, "recraft");
|
|
158
158
|
const form = new FormData();
|
|
159
159
|
form.append("image", new Blob([img.bytes], { type: img.mimeType }), img.fileName);
|
|
160
160
|
form.append("prompt", p.prompt);
|
|
@@ -224,7 +224,7 @@ export default function replicate(rl) {
|
|
|
224
224
|
if (typeof p.prompt !== "string" || p.prompt.length === 0) {
|
|
225
225
|
throw new Error("replicate: prompt is required");
|
|
226
226
|
}
|
|
227
|
-
const img = readImageInput(p.imagePath, "replicate");
|
|
227
|
+
const img = await readImageInput(p.imagePath, "replicate");
|
|
228
228
|
const model = p.model ?? "black-forest-labs/flux-kontext-pro";
|
|
229
229
|
const imageKey = p.imageInputKey ?? "input_image";
|
|
230
230
|
const { images, failures } = await runPrediction({
|
|
@@ -84,7 +84,7 @@ export function registerObjectActions(rl) {
|
|
|
84
84
|
metadata: fields.metadata,
|
|
85
85
|
}),
|
|
86
86
|
});
|
|
87
|
-
await putThroughGrant(created.upload, fields.path, contentType,
|
|
87
|
+
await putThroughGrant(created.upload, fields.path, contentType, "Object upload");
|
|
88
88
|
return request(ctx, `${OBJECTS_BASE}/objects/${pathSegment(created.object.id)}/complete`, { method: "POST" });
|
|
89
89
|
},
|
|
90
90
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { readFile } from "node:fs/promises";
|
|
2
1
|
import { basename, extname } from "node:path";
|
|
3
2
|
import * as t from "typebox";
|
|
3
|
+
import { readLocalFile } from "../../_shared/localFile.js";
|
|
4
4
|
import { pathSegment, STRICT_OBJECT, } from "../../_shared/shiftCloud.js";
|
|
5
5
|
import { putThroughGrant, SIGNED_UPLOAD_MAX_BYTES, statUploadFile, } from "../../_shared/shiftUpload.js";
|
|
6
6
|
import { request, shiftOcrCredential } from "./credentials.js";
|
|
@@ -44,7 +44,12 @@ async function documentFromPath(ctx, path) {
|
|
|
44
44
|
const mediaType = (imageType ?? documentType);
|
|
45
45
|
const sizeBytes = await statUploadFile(path, SIGNED_UPLOAD_MAX_BYTES);
|
|
46
46
|
if (sizeBytes <= MAX_INLINE_BYTES) {
|
|
47
|
-
const
|
|
47
|
+
const bytes = await readLocalFile(path, {
|
|
48
|
+
subject: "File",
|
|
49
|
+
maxBytes: MAX_INLINE_BYTES,
|
|
50
|
+
oversize: "File grew past the inline limit while reading; retry",
|
|
51
|
+
});
|
|
52
|
+
const url = `data:${mediaType};base64,${bytes.toString("base64")}`;
|
|
48
53
|
return imageType
|
|
49
54
|
? { type: "image", url }
|
|
50
55
|
: { type: "document", url, name: basename(path) };
|
|
@@ -59,7 +64,7 @@ async function documentFromPath(ctx, path) {
|
|
|
59
64
|
filename: basename(path),
|
|
60
65
|
}),
|
|
61
66
|
});
|
|
62
|
-
await putThroughGrant(created.upload, path, mediaType
|
|
67
|
+
await putThroughGrant(created.upload, path, mediaType);
|
|
63
68
|
await request(ctx, `/v1/services/objects/objects/${pathSegment(created.object.id)}/complete`, { method: "POST" });
|
|
64
69
|
return { type: "object", objectId: created.object.id };
|
|
65
70
|
}
|
|
@@ -48,7 +48,7 @@ export function registerTranscriptionActions(rl) {
|
|
|
48
48
|
sizeBytes,
|
|
49
49
|
}),
|
|
50
50
|
});
|
|
51
|
-
await putThroughGrant(created.upload, fields.path, contentType,
|
|
51
|
+
await putThroughGrant(created.upload, fields.path, contentType, "Media upload");
|
|
52
52
|
await request(ctx, `/v1/services/transcription/assets/${pathSegment(created.asset.id)}/complete`, { method: "POST" });
|
|
53
53
|
const job = await request(ctx, "/v1/services/transcription/jobs", {
|
|
54
54
|
method: "POST",
|
|
@@ -139,7 +139,7 @@ export default function together(rl) {
|
|
|
139
139
|
if (typeof p.prompt !== "string" || p.prompt.length === 0) {
|
|
140
140
|
throw new Error("together: prompt is required");
|
|
141
141
|
}
|
|
142
|
-
const img = readImageInput(p.imagePath, "together");
|
|
142
|
+
const img = await readImageInput(p.imagePath, "together");
|
|
143
143
|
const model = p.model ?? "black-forest-labs/FLUX.1-kontext-pro";
|
|
144
144
|
const data = await generate(ctx, {
|
|
145
145
|
model,
|
|
@@ -119,7 +119,7 @@ export default function xai(rl) {
|
|
|
119
119
|
if (typeof p.prompt !== "string" || p.prompt.length === 0) {
|
|
120
120
|
throw new Error("xai: prompt is required");
|
|
121
121
|
}
|
|
122
|
-
const img = readImageInput(p.imagePath, "xai");
|
|
122
|
+
const img = await readImageInput(p.imagePath, "xai");
|
|
123
123
|
const model = p.model ?? EDIT_MODEL;
|
|
124
124
|
const data = (await credentialJson(ctx, xaiCredential, "xai", {
|
|
125
125
|
target: "api",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "runline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "Code mode for agents \u2014 turn any API or command into a callable action",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"prepublishOnly": "bun run build",
|
|
33
33
|
"dev": "bun run src/main.ts",
|
|
34
34
|
"test": "bun test src/tests",
|
|
35
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.test.json",
|
|
35
36
|
"format": "bunx @biomejs/biome format --write src/",
|
|
36
37
|
"lint": "bunx @biomejs/biome lint src/",
|
|
37
38
|
"check": "bunx @biomejs/biome check src/"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,236 +0,0 @@
|
|
|
1
|
-
// Test harness for engine robustness scenarios (large payloads, OOM,
|
|
2
|
-
// timeouts, crashes). Runs one named scenario in this process and prints a
|
|
3
|
-
// JSON report to stdout. Tests spawn this file so that a hard process
|
|
4
|
-
// abort or exit kills the harness, not the test runner.
|
|
5
|
-
//
|
|
6
|
-
// Usage: bun run engine-harness.ts <scenario>
|
|
7
|
-
import { createHash } from "node:crypto";
|
|
8
|
-
import { DEFAULT_CONFIG } from "../../config/types.js";
|
|
9
|
-
import { ExecutionEngine } from "../../core/engine.js";
|
|
10
|
-
import { createPluginAPI } from "../../plugin/api.js";
|
|
11
|
-
import { PluginRegistry } from "../../plugin/registry.js";
|
|
12
|
-
const BLOB_MB = 8;
|
|
13
|
-
// ~8MB of raw bytes → ~10.7MB base64
|
|
14
|
-
const base64 = Buffer.alloc(BLOB_MB * 1024 * 1024, 0xab).toString("base64");
|
|
15
|
-
const base64Sha = createHash("sha256").update(base64).digest("hex");
|
|
16
|
-
// Captures what the host-side actions actually received, so tests can
|
|
17
|
-
// verify that data crossing the execution boundary arrives byte-identical.
|
|
18
|
-
const received = {};
|
|
19
|
-
function makeFilesPlugin() {
|
|
20
|
-
const { api, resolve } = createPluginAPI("test");
|
|
21
|
-
api.setName("files");
|
|
22
|
-
api.setVersion("0.0.0");
|
|
23
|
-
api.registerAction("getAttachment", {
|
|
24
|
-
description: "Returns a large base64 payload",
|
|
25
|
-
async execute() {
|
|
26
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
27
|
-
return { filename: "msa.pdf", data: base64 };
|
|
28
|
-
},
|
|
29
|
-
});
|
|
30
|
-
api.registerAction("upload", {
|
|
31
|
-
description: "Accepts a large base64 payload",
|
|
32
|
-
async execute(input) {
|
|
33
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
34
|
-
const { data } = input;
|
|
35
|
-
received.uploadSha =
|
|
36
|
-
typeof data === "string"
|
|
37
|
-
? createHash("sha256").update(data).digest("hex")
|
|
38
|
-
: `not-a-string:${typeof data}`;
|
|
39
|
-
received.uploadBytes = typeof data === "string" ? data.length : -1;
|
|
40
|
-
return { id: "file_123", bytes: received.uploadBytes };
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
api.registerAction("send", {
|
|
44
|
-
description: "Accepts a large attachment",
|
|
45
|
-
async execute(input) {
|
|
46
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
47
|
-
const { attachment } = input;
|
|
48
|
-
received.sendBytes =
|
|
49
|
-
typeof attachment === "string" ? attachment.length : -1;
|
|
50
|
-
return { messageId: "msg_456", bytes: received.sendBytes };
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
api.registerAction("append", {
|
|
54
|
-
description: "Small side effect",
|
|
55
|
-
async execute() {
|
|
56
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
57
|
-
return { updatedRows: 1 };
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
api.registerAction("slow", {
|
|
61
|
-
description: "Sleeps for the given ms, then returns",
|
|
62
|
-
async execute(input) {
|
|
63
|
-
const { ms } = input;
|
|
64
|
-
await new Promise((r) => setTimeout(r, ms));
|
|
65
|
-
return { waited: ms };
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
api.registerAction("circular", {
|
|
69
|
-
description: "Returns a non-JSON-serializable (circular) value",
|
|
70
|
-
async execute() {
|
|
71
|
-
const obj = { name: "loop" };
|
|
72
|
-
obj.self = obj;
|
|
73
|
-
return obj;
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
return resolve();
|
|
77
|
-
}
|
|
78
|
-
function makeEngine(memoryMb = 64, timeoutMs = 30_000) {
|
|
79
|
-
const registry = new PluginRegistry();
|
|
80
|
-
registry.register(makeFilesPlugin());
|
|
81
|
-
return new ExecutionEngine(registry, {
|
|
82
|
-
...DEFAULT_CONFIG,
|
|
83
|
-
timeoutMs,
|
|
84
|
-
memoryLimitBytes: memoryMb * 1024 * 1024,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
const scenarios = {
|
|
88
|
-
// The agent's original failure: multi-step chain with a large payload,
|
|
89
|
-
// default 64MB memory limit. Must complete cleanly.
|
|
90
|
-
async "chain-default"() {
|
|
91
|
-
const engine = makeEngine(64);
|
|
92
|
-
const out = await engine.execute(`
|
|
93
|
-
const att = await files.getAttachment({ messageId: "m1" });
|
|
94
|
-
const up = await files.upload({ name: att.filename, data: att.data });
|
|
95
|
-
const sent = await files.send({ to: "x@y.z", attachment: att.data });
|
|
96
|
-
const row = await files.append({ values: [["Triple-A MSA", "May"]] });
|
|
97
|
-
return { up, sent, row };
|
|
98
|
-
`);
|
|
99
|
-
return { error: out.error ?? null, result: out.result, received };
|
|
100
|
-
},
|
|
101
|
-
// Integrity: the bytes the upload action receives must be byte-identical
|
|
102
|
-
// to what getAttachment produced.
|
|
103
|
-
async integrity() {
|
|
104
|
-
const engine = makeEngine(64);
|
|
105
|
-
const out = await engine.execute(`
|
|
106
|
-
const att = await files.getAttachment({ messageId: "m1" });
|
|
107
|
-
await files.upload({ name: att.filename, data: att.data });
|
|
108
|
-
return "done";
|
|
109
|
-
`);
|
|
110
|
-
return {
|
|
111
|
-
error: out.error ?? null,
|
|
112
|
-
expectedSha: base64Sha,
|
|
113
|
-
expectedBytes: base64.length,
|
|
114
|
-
received,
|
|
115
|
-
};
|
|
116
|
-
},
|
|
117
|
-
// Ergonomics: inside the sandbox a large payload is a plain string the
|
|
118
|
-
// agent can measure, slice, and pass around — no tokens, no proxies.
|
|
119
|
-
async "string-surface"() {
|
|
120
|
-
const engine = makeEngine(64);
|
|
121
|
-
const out = await engine.execute(`
|
|
122
|
-
const att = await files.getAttachment({ messageId: "m1" });
|
|
123
|
-
const d = att.data;
|
|
124
|
-
return { type: typeof d, bytes: d.length, head: d.slice(0, 8) };
|
|
125
|
-
`);
|
|
126
|
-
return {
|
|
127
|
-
error: out.error ?? null,
|
|
128
|
-
result: out.result,
|
|
129
|
-
expectedBytes: base64.length,
|
|
130
|
-
expectedHead: base64.slice(0, 8),
|
|
131
|
-
};
|
|
132
|
-
},
|
|
133
|
-
// A large value in the final return reaches the host caller intact.
|
|
134
|
-
async "final-result-large"() {
|
|
135
|
-
const engine = makeEngine(64);
|
|
136
|
-
const out = await engine.execute(`
|
|
137
|
-
const att = await files.getAttachment({ messageId: "m1" });
|
|
138
|
-
return { data: att.data };
|
|
139
|
-
`);
|
|
140
|
-
const data = out.result?.data;
|
|
141
|
-
return {
|
|
142
|
-
error: out.error ?? null,
|
|
143
|
-
resultBytes: typeof data === "string" ? data.length : -1,
|
|
144
|
-
resultSha: typeof data === "string"
|
|
145
|
-
? createHash("sha256").update(data).digest("hex")
|
|
146
|
-
: null,
|
|
147
|
-
expectedSha: base64Sha,
|
|
148
|
-
expectedBytes: base64.length,
|
|
149
|
-
};
|
|
150
|
-
},
|
|
151
|
-
// An action still in flight when the run times out must not crash the
|
|
152
|
-
// host when its result arrives after the worker is gone — no unhandled
|
|
153
|
-
// rejections, no uncaught exceptions, clean timeout error.
|
|
154
|
-
async "timeout-inflight-action"() {
|
|
155
|
-
const unhandled = [];
|
|
156
|
-
process.on("unhandledRejection", (e) => unhandled.push(String(e)));
|
|
157
|
-
process.on("uncaughtException", (e) => unhandled.push(String(e)));
|
|
158
|
-
const engine = makeEngine(64, 150);
|
|
159
|
-
const out = await engine.execute(`
|
|
160
|
-
await files.slow({ ms: 1000 });
|
|
161
|
-
return "unreachable";
|
|
162
|
-
`);
|
|
163
|
-
// let the in-flight action resolve against the dead worker
|
|
164
|
-
await new Promise((r) => setTimeout(r, 1200));
|
|
165
|
-
return { error: out.error ?? null, unhandled };
|
|
166
|
-
},
|
|
167
|
-
// An action returning a non-JSON-serializable value must surface as a
|
|
168
|
-
// clean per-call error inside the sandbox — catchable by agent code —
|
|
169
|
-
// not a hang or a crash.
|
|
170
|
-
async "circular-result"() {
|
|
171
|
-
const engine = makeEngine(64);
|
|
172
|
-
const out = await engine.execute(`
|
|
173
|
-
try {
|
|
174
|
-
await files.circular({});
|
|
175
|
-
return { caught: false };
|
|
176
|
-
} catch (e) {
|
|
177
|
-
return { caught: true, message: e.message };
|
|
178
|
-
}
|
|
179
|
-
`);
|
|
180
|
-
return { error: out.error ?? null, result: out.result };
|
|
181
|
-
},
|
|
182
|
-
// Two executes on the same engine running concurrently must not
|
|
183
|
-
// interfere — separate workers, separate logs, correct results.
|
|
184
|
-
async concurrent() {
|
|
185
|
-
const engine = makeEngine(64);
|
|
186
|
-
const [a, b] = await Promise.all([
|
|
187
|
-
engine.execute(`console.log("run-a"); const r = await files.slow({ ms: 50 }); return { tag: "a", waited: r.waited };`),
|
|
188
|
-
engine.execute(`console.log("run-b"); const r = await files.slow({ ms: 30 }); return { tag: "b", waited: r.waited };`),
|
|
189
|
-
]);
|
|
190
|
-
return {
|
|
191
|
-
aError: a.error ?? null,
|
|
192
|
-
bError: b.error ?? null,
|
|
193
|
-
aResult: a.result,
|
|
194
|
-
bResult: b.result,
|
|
195
|
-
aLogs: a.logs,
|
|
196
|
-
bLogs: b.logs,
|
|
197
|
-
};
|
|
198
|
-
},
|
|
199
|
-
// Agent code killing its own worker (it has the full runtime, so it can)
|
|
200
|
-
// must fail soft with a descriptive error and leave the engine usable.
|
|
201
|
-
async "worker-suicide"() {
|
|
202
|
-
const engine = makeEngine(64);
|
|
203
|
-
const out = await engine.execute(`process.exit(7);`);
|
|
204
|
-
const after = await engine.execute("return 1 + 1");
|
|
205
|
-
return {
|
|
206
|
-
error: out.error ?? null,
|
|
207
|
-
afterError: after.error ?? null,
|
|
208
|
-
afterResult: after.result,
|
|
209
|
-
};
|
|
210
|
-
},
|
|
211
|
-
// Sandbox code that genuinely exhausts the memory limit must fail soft:
|
|
212
|
-
// a clean error returned from execute(), no process abort, and the engine
|
|
213
|
-
// must remain usable for a subsequent run.
|
|
214
|
-
async "sandbox-oom"() {
|
|
215
|
-
const engine = makeEngine(32);
|
|
216
|
-
const out = await engine.execute(`
|
|
217
|
-
const hog = [];
|
|
218
|
-
while (true) hog.push(new Array(1e6).fill(1));
|
|
219
|
-
`);
|
|
220
|
-
const after = await engine.execute("return 1 + 1");
|
|
221
|
-
return {
|
|
222
|
-
error: out.error ?? null,
|
|
223
|
-
afterError: after.error ?? null,
|
|
224
|
-
afterResult: after.result,
|
|
225
|
-
};
|
|
226
|
-
},
|
|
227
|
-
};
|
|
228
|
-
const name = process.argv[2];
|
|
229
|
-
const scenario = scenarios[name];
|
|
230
|
-
if (!scenario) {
|
|
231
|
-
console.error(`Unknown scenario: ${name}`);
|
|
232
|
-
process.exit(2);
|
|
233
|
-
}
|
|
234
|
-
const report = await scenario();
|
|
235
|
-
console.log(JSON.stringify(report));
|
|
236
|
-
process.exit(0);
|