@evermore.work/adapter-utils 2026.509.0-canary.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/billing.d.ts +2 -0
- package/dist/billing.d.ts.map +1 -0
- package/dist/billing.js +16 -0
- package/dist/billing.js.map +1 -0
- package/dist/billing.test.d.ts +2 -0
- package/dist/billing.test.d.ts.map +1 -0
- package/dist/billing.test.js +14 -0
- package/dist/billing.test.js.map +1 -0
- package/dist/command-managed-runtime.d.ts +45 -0
- package/dist/command-managed-runtime.d.ts.map +1 -0
- package/dist/command-managed-runtime.js +164 -0
- package/dist/command-managed-runtime.js.map +1 -0
- package/dist/command-managed-runtime.test.d.ts +2 -0
- package/dist/command-managed-runtime.test.d.ts.map +1 -0
- package/dist/command-managed-runtime.test.js +102 -0
- package/dist/command-managed-runtime.test.js.map +1 -0
- package/dist/command-redaction.d.ts +3 -0
- package/dist/command-redaction.d.ts.map +1 -0
- package/dist/command-redaction.js +17 -0
- package/dist/command-redaction.js.map +1 -0
- package/dist/execution-target-sandbox.test.d.ts +2 -0
- package/dist/execution-target-sandbox.test.d.ts.map +1 -0
- package/dist/execution-target-sandbox.test.js +392 -0
- package/dist/execution-target-sandbox.test.js.map +1 -0
- package/dist/execution-target.d.ts +150 -0
- package/dist/execution-target.d.ts.map +1 -0
- package/dist/execution-target.js +791 -0
- package/dist/execution-target.js.map +1 -0
- package/dist/execution-target.test.d.ts +2 -0
- package/dist/execution-target.test.d.ts.map +1 -0
- package/dist/execution-target.test.js +314 -0
- package/dist/execution-target.test.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/log-redaction.d.ts +9 -0
- package/dist/log-redaction.d.ts.map +1 -0
- package/dist/log-redaction.js +88 -0
- package/dist/log-redaction.js.map +1 -0
- package/dist/remote-execution-env.d.ts +2 -0
- package/dist/remote-execution-env.d.ts.map +1 -0
- package/dist/remote-execution-env.js +46 -0
- package/dist/remote-execution-env.js.map +1 -0
- package/dist/remote-managed-runtime.d.ts +31 -0
- package/dist/remote-managed-runtime.d.ts.map +1 -0
- package/dist/remote-managed-runtime.js +81 -0
- package/dist/remote-managed-runtime.js.map +1 -0
- package/dist/sandbox-callback-bridge.d.ts +132 -0
- package/dist/sandbox-callback-bridge.d.ts.map +1 -0
- package/dist/sandbox-callback-bridge.js +925 -0
- package/dist/sandbox-callback-bridge.js.map +1 -0
- package/dist/sandbox-callback-bridge.test.d.ts +2 -0
- package/dist/sandbox-callback-bridge.test.d.ts.map +1 -0
- package/dist/sandbox-callback-bridge.test.js +719 -0
- package/dist/sandbox-callback-bridge.test.js.map +1 -0
- package/dist/sandbox-managed-runtime.d.ts +54 -0
- package/dist/sandbox-managed-runtime.d.ts.map +1 -0
- package/dist/sandbox-managed-runtime.js +234 -0
- package/dist/sandbox-managed-runtime.js.map +1 -0
- package/dist/sandbox-managed-runtime.test.d.ts +2 -0
- package/dist/sandbox-managed-runtime.test.d.ts.map +1 -0
- package/dist/sandbox-managed-runtime.test.js +118 -0
- package/dist/sandbox-managed-runtime.test.js.map +1 -0
- package/dist/sandbox-shell.d.ts +2 -0
- package/dist/sandbox-shell.d.ts.map +1 -0
- package/dist/sandbox-shell.js +4 -0
- package/dist/sandbox-shell.js.map +1 -0
- package/dist/server-utils.d.ts +253 -0
- package/dist/server-utils.d.ts.map +1 -0
- package/dist/server-utils.js +1522 -0
- package/dist/server-utils.js.map +1 -0
- package/dist/server-utils.test.d.ts +2 -0
- package/dist/server-utils.test.d.ts.map +1 -0
- package/dist/server-utils.test.js +685 -0
- package/dist/server-utils.test.js.map +1 -0
- package/dist/session-compaction.d.ts +25 -0
- package/dist/session-compaction.d.ts.map +1 -0
- package/dist/session-compaction.js +154 -0
- package/dist/session-compaction.js.map +1 -0
- package/dist/ssh-fixture.test.d.ts +2 -0
- package/dist/ssh-fixture.test.d.ts.map +1 -0
- package/dist/ssh-fixture.test.js +214 -0
- package/dist/ssh-fixture.test.js.map +1 -0
- package/dist/ssh.d.ts +111 -0
- package/dist/ssh.d.ts.map +1 -0
- package/dist/ssh.js +1098 -0
- package/dist/ssh.js.map +1 -0
- package/dist/types.d.ts +465 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +41 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
7
|
+
import { prepareCommandManagedRuntime } from "./command-managed-runtime.js";
|
|
8
|
+
import { authorizeSandboxCallbackBridgeRequestWithRoutes, createCommandManagedSandboxCallbackBridgeQueueClient, createFileSystemSandboxCallbackBridgeQueueClient, createSandboxCallbackBridgeAsset, createSandboxCallbackBridgeToken, sandboxCallbackBridgeDirectories, syncSandboxCallbackBridgeEntrypoint, startSandboxCallbackBridgeServer, startSandboxCallbackBridgeWorker, } from "./sandbox-callback-bridge.js";
|
|
9
|
+
const execFile = promisify(execFileCallback);
|
|
10
|
+
describe("sandbox callback bridge", () => {
|
|
11
|
+
const cleanupDirs = [];
|
|
12
|
+
const cleanupFns = [];
|
|
13
|
+
function createExecRunner() {
|
|
14
|
+
return {
|
|
15
|
+
execute: async (input) => {
|
|
16
|
+
const startedAt = new Date().toISOString();
|
|
17
|
+
const env = {
|
|
18
|
+
...process.env,
|
|
19
|
+
...input.env,
|
|
20
|
+
};
|
|
21
|
+
const command = input.command === "sh" ? "/bin/sh" : input.command === "bash" ? "/bin/bash" : input.command;
|
|
22
|
+
const args = [...(input.args ?? [])];
|
|
23
|
+
if (input.stdin != null &&
|
|
24
|
+
(input.command === "sh" || input.command === "bash") &&
|
|
25
|
+
args[0] === "-lc" &&
|
|
26
|
+
typeof args[1] === "string") {
|
|
27
|
+
env.EVERMORE_TEST_STDIN = input.stdin;
|
|
28
|
+
args[1] = `printf '%s' \"$EVERMORE_TEST_STDIN\" | (${args[1]})`;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const result = await execFile(command, args, {
|
|
32
|
+
cwd: input.cwd,
|
|
33
|
+
env,
|
|
34
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
35
|
+
timeout: input.timeoutMs,
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
exitCode: 0,
|
|
39
|
+
signal: null,
|
|
40
|
+
timedOut: false,
|
|
41
|
+
stdout: result.stdout,
|
|
42
|
+
stderr: result.stderr,
|
|
43
|
+
pid: null,
|
|
44
|
+
startedAt,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
const err = error;
|
|
49
|
+
return {
|
|
50
|
+
exitCode: typeof err.code === "number" ? err.code : null,
|
|
51
|
+
signal: err.signal ?? null,
|
|
52
|
+
timedOut: Boolean(err.killed && input.timeoutMs),
|
|
53
|
+
stdout: err.stdout ?? "",
|
|
54
|
+
stderr: err.stderr ?? "",
|
|
55
|
+
pid: null,
|
|
56
|
+
startedAt,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
async function waitForJsonFile(directory, timeoutMs = 2_000) {
|
|
63
|
+
const deadline = Date.now() + timeoutMs;
|
|
64
|
+
while (Date.now() < deadline) {
|
|
65
|
+
const entries = await readdir(directory).catch(() => []);
|
|
66
|
+
const match = entries.find((entry) => entry.endsWith(".json"));
|
|
67
|
+
if (match)
|
|
68
|
+
return match;
|
|
69
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`Timed out waiting for a JSON file in ${directory}.`);
|
|
72
|
+
}
|
|
73
|
+
afterEach(async () => {
|
|
74
|
+
while (cleanupFns.length > 0) {
|
|
75
|
+
const cleanup = cleanupFns.pop();
|
|
76
|
+
if (!cleanup)
|
|
77
|
+
continue;
|
|
78
|
+
await cleanup().catch(() => undefined);
|
|
79
|
+
}
|
|
80
|
+
while (cleanupDirs.length > 0) {
|
|
81
|
+
const dir = cleanupDirs.pop();
|
|
82
|
+
if (!dir)
|
|
83
|
+
continue;
|
|
84
|
+
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
it("round-trips localhost bridge requests over the sandbox queue without forwarding the bridge token", async () => {
|
|
88
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "evermore-bridge-runtime-"));
|
|
89
|
+
cleanupDirs.push(rootDir);
|
|
90
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
91
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
92
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
93
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
94
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "bridge test\n", "utf8");
|
|
95
|
+
const runner = createExecRunner();
|
|
96
|
+
const bridgeAsset = await createSandboxCallbackBridgeAsset();
|
|
97
|
+
cleanupFns.push(bridgeAsset.cleanup);
|
|
98
|
+
const prepared = await prepareCommandManagedRuntime({
|
|
99
|
+
runner,
|
|
100
|
+
spec: {
|
|
101
|
+
remoteCwd: remoteWorkspaceDir,
|
|
102
|
+
timeoutMs: 30_000,
|
|
103
|
+
},
|
|
104
|
+
adapterKey: "codex",
|
|
105
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
106
|
+
assets: [
|
|
107
|
+
{
|
|
108
|
+
key: "bridge",
|
|
109
|
+
localDir: bridgeAsset.localDir,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
const queueDir = path.posix.join(prepared.runtimeRootDir, "evermore-bridge");
|
|
114
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
115
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
116
|
+
const seenRequests = [];
|
|
117
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
118
|
+
client: createFileSystemSandboxCallbackBridgeQueueClient(),
|
|
119
|
+
queueDir,
|
|
120
|
+
authorizeRequest: async (request) => request.path === "/api/agents/me" ? null : `Route not allowed: ${request.method} ${request.path}`,
|
|
121
|
+
handleRequest: async (request) => {
|
|
122
|
+
seenRequests.push({
|
|
123
|
+
method: request.method,
|
|
124
|
+
path: request.path,
|
|
125
|
+
query: request.query,
|
|
126
|
+
headers: request.headers,
|
|
127
|
+
body: request.body,
|
|
128
|
+
});
|
|
129
|
+
return {
|
|
130
|
+
status: 200,
|
|
131
|
+
headers: {
|
|
132
|
+
"content-type": "application/json",
|
|
133
|
+
etag: '"bridge-rev-1"',
|
|
134
|
+
"last-modified": "Tue, 01 Apr 2025 00:00:00 GMT",
|
|
135
|
+
},
|
|
136
|
+
body: JSON.stringify({
|
|
137
|
+
ok: true,
|
|
138
|
+
method: request.method,
|
|
139
|
+
path: request.path,
|
|
140
|
+
}),
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
cleanupFns.push(async () => {
|
|
145
|
+
await worker.stop();
|
|
146
|
+
});
|
|
147
|
+
const bridge = await startSandboxCallbackBridgeServer({
|
|
148
|
+
runner,
|
|
149
|
+
remoteCwd: remoteWorkspaceDir,
|
|
150
|
+
assetRemoteDir: prepared.assetDirs.bridge,
|
|
151
|
+
queueDir,
|
|
152
|
+
bridgeToken,
|
|
153
|
+
timeoutMs: 30_000,
|
|
154
|
+
});
|
|
155
|
+
cleanupFns.push(async () => {
|
|
156
|
+
await bridge.stop();
|
|
157
|
+
});
|
|
158
|
+
const okResponse = await fetch(`${bridge.baseUrl}/api/agents/me?view=compact`, {
|
|
159
|
+
headers: {
|
|
160
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
161
|
+
accept: "application/json",
|
|
162
|
+
"if-none-match": '"client-cache-key"',
|
|
163
|
+
"x-evermore-run-id": "run-bridge-1",
|
|
164
|
+
"x-bridge-debug": "drop-me",
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
expect(okResponse.status).toBe(200);
|
|
168
|
+
expect(okResponse.headers.get("content-type")).toContain("application/json");
|
|
169
|
+
expect(okResponse.headers.get("etag")).toBe('"bridge-rev-1"');
|
|
170
|
+
expect(okResponse.headers.get("last-modified")).toBe("Tue, 01 Apr 2025 00:00:00 GMT");
|
|
171
|
+
await expect(okResponse.json()).resolves.toMatchObject({
|
|
172
|
+
ok: true,
|
|
173
|
+
method: "GET",
|
|
174
|
+
path: "/api/agents/me",
|
|
175
|
+
});
|
|
176
|
+
const deniedResponse = await fetch(`${bridge.baseUrl}/api/issues/issue-1`, {
|
|
177
|
+
method: "PATCH",
|
|
178
|
+
headers: {
|
|
179
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
180
|
+
"content-type": "application/json",
|
|
181
|
+
},
|
|
182
|
+
body: JSON.stringify({ status: "in_progress" }),
|
|
183
|
+
});
|
|
184
|
+
expect(deniedResponse.status).toBe(403);
|
|
185
|
+
await expect(deniedResponse.json()).resolves.toMatchObject({
|
|
186
|
+
error: "Route not allowed: PATCH /api/issues/issue-1",
|
|
187
|
+
});
|
|
188
|
+
const unauthorizedResponse = await fetch(`${bridge.baseUrl}/api/agents/me`, {
|
|
189
|
+
headers: {
|
|
190
|
+
authorization: "Bearer wrong-token",
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
expect(unauthorizedResponse.status).toBe(401);
|
|
194
|
+
await expect(unauthorizedResponse.json()).resolves.toMatchObject({
|
|
195
|
+
error: "Invalid bridge token.",
|
|
196
|
+
});
|
|
197
|
+
expect(seenRequests).toHaveLength(1);
|
|
198
|
+
expect(seenRequests[0]).toMatchObject({
|
|
199
|
+
method: "GET",
|
|
200
|
+
path: "/api/agents/me",
|
|
201
|
+
query: "?view=compact",
|
|
202
|
+
body: "",
|
|
203
|
+
headers: {
|
|
204
|
+
accept: "application/json",
|
|
205
|
+
"if-none-match": '"client-cache-key"',
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
expect(seenRequests[0]?.headers.authorization).toBeUndefined();
|
|
209
|
+
expect(seenRequests[0]?.headers["x-evermore-run-id"]).toBeUndefined();
|
|
210
|
+
});
|
|
211
|
+
it("denies non-allowlisted requests by default", async () => {
|
|
212
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "evermore-bridge-default-policy-"));
|
|
213
|
+
cleanupDirs.push(rootDir);
|
|
214
|
+
const queueDir = path.posix.join(rootDir, "queue");
|
|
215
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
216
|
+
let handled = 0;
|
|
217
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
218
|
+
client: createFileSystemSandboxCallbackBridgeQueueClient(),
|
|
219
|
+
queueDir,
|
|
220
|
+
handleRequest: async () => {
|
|
221
|
+
handled += 1;
|
|
222
|
+
return {
|
|
223
|
+
status: 200,
|
|
224
|
+
body: "should not happen",
|
|
225
|
+
};
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
await writeFile(path.posix.join(directories.requestsDir, "req-1.json"), `${JSON.stringify({
|
|
229
|
+
id: "req-1",
|
|
230
|
+
method: "DELETE",
|
|
231
|
+
path: "/api/secrets",
|
|
232
|
+
query: "",
|
|
233
|
+
headers: {},
|
|
234
|
+
body: "",
|
|
235
|
+
createdAt: new Date().toISOString(),
|
|
236
|
+
})}\n`, "utf8");
|
|
237
|
+
await worker.stop({ drainTimeoutMs: 1_000 });
|
|
238
|
+
const response = JSON.parse(await readFile(path.posix.join(directories.responsesDir, "req-1.json"), "utf8"));
|
|
239
|
+
expect(handled).toBe(0);
|
|
240
|
+
expect(response.status).toBe(403);
|
|
241
|
+
expect(JSON.parse(response.body)).toEqual({
|
|
242
|
+
error: "Route not allowed: DELETE /api/secrets",
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
it("drains already-queued requests on stop", async () => {
|
|
246
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "evermore-bridge-drain-"));
|
|
247
|
+
cleanupDirs.push(rootDir);
|
|
248
|
+
const queueDir = path.posix.join(rootDir, "queue");
|
|
249
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
250
|
+
const processed = [];
|
|
251
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
252
|
+
client: createFileSystemSandboxCallbackBridgeQueueClient(),
|
|
253
|
+
queueDir,
|
|
254
|
+
authorizeRequest: async () => null,
|
|
255
|
+
handleRequest: async (request) => {
|
|
256
|
+
processed.push(request.id);
|
|
257
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
258
|
+
return {
|
|
259
|
+
status: 200,
|
|
260
|
+
body: request.id,
|
|
261
|
+
};
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
await writeFile(path.posix.join(directories.requestsDir, "req-a.json"), `${JSON.stringify({
|
|
265
|
+
id: "req-a",
|
|
266
|
+
method: "GET",
|
|
267
|
+
path: "/api/agents/me",
|
|
268
|
+
query: "",
|
|
269
|
+
headers: {},
|
|
270
|
+
body: "",
|
|
271
|
+
createdAt: new Date().toISOString(),
|
|
272
|
+
})}\n`, "utf8");
|
|
273
|
+
await writeFile(path.posix.join(directories.requestsDir, "req-b.json"), `${JSON.stringify({
|
|
274
|
+
id: "req-b",
|
|
275
|
+
method: "GET",
|
|
276
|
+
path: "/api/agents/me",
|
|
277
|
+
query: "",
|
|
278
|
+
headers: {},
|
|
279
|
+
body: "",
|
|
280
|
+
createdAt: new Date().toISOString(),
|
|
281
|
+
})}\n`, "utf8");
|
|
282
|
+
await worker.stop({ drainTimeoutMs: 1_000 });
|
|
283
|
+
expect(processed).toEqual(["req-a", "req-b"]);
|
|
284
|
+
await expect(readFile(path.posix.join(directories.responsesDir, "req-a.json"), "utf8")).resolves.toContain("\"req-a\"");
|
|
285
|
+
await expect(readFile(path.posix.join(directories.responsesDir, "req-b.json"), "utf8")).resolves.toContain("\"req-b\"");
|
|
286
|
+
});
|
|
287
|
+
it("writes fast 503 responses for queued requests that miss the drain deadline", async () => {
|
|
288
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "evermore-bridge-drain-timeout-"));
|
|
289
|
+
cleanupDirs.push(rootDir);
|
|
290
|
+
const queueDir = path.posix.join(rootDir, "queue");
|
|
291
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
292
|
+
const processed = [];
|
|
293
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
294
|
+
client: createFileSystemSandboxCallbackBridgeQueueClient(),
|
|
295
|
+
queueDir,
|
|
296
|
+
authorizeRequest: async () => null,
|
|
297
|
+
handleRequest: async (request) => {
|
|
298
|
+
processed.push(request.id);
|
|
299
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
300
|
+
return {
|
|
301
|
+
status: 200,
|
|
302
|
+
body: request.id,
|
|
303
|
+
};
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
await writeFile(path.posix.join(directories.requestsDir, "req-a.json"), `${JSON.stringify({
|
|
307
|
+
id: "req-a",
|
|
308
|
+
method: "GET",
|
|
309
|
+
path: "/api/agents/me",
|
|
310
|
+
query: "",
|
|
311
|
+
headers: {},
|
|
312
|
+
body: "",
|
|
313
|
+
createdAt: new Date().toISOString(),
|
|
314
|
+
})}\n`, "utf8");
|
|
315
|
+
await writeFile(path.posix.join(directories.requestsDir, "req-b.json"), `${JSON.stringify({
|
|
316
|
+
id: "req-b",
|
|
317
|
+
method: "GET",
|
|
318
|
+
path: "/api/agents/me",
|
|
319
|
+
query: "",
|
|
320
|
+
headers: {},
|
|
321
|
+
body: "",
|
|
322
|
+
createdAt: new Date().toISOString(),
|
|
323
|
+
})}\n`, "utf8");
|
|
324
|
+
for (let attempt = 0; attempt < 50 && processed.length === 0; attempt += 1) {
|
|
325
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
326
|
+
}
|
|
327
|
+
await worker.stop({ drainTimeoutMs: 10 });
|
|
328
|
+
expect(processed).toEqual(["req-a"]);
|
|
329
|
+
await expect(readFile(path.posix.join(directories.responsesDir, "req-a.json"), "utf8")).resolves.toContain("\"req-a\"");
|
|
330
|
+
await expect(readFile(path.posix.join(directories.responsesDir, "req-b.json"), "utf8")).resolves.toContain("Bridge worker stopped before request could be handled.");
|
|
331
|
+
});
|
|
332
|
+
it("serializes remote response writes so stop does not recreate a late orphaned response", async () => {
|
|
333
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "evermore-bridge-response-lock-"));
|
|
334
|
+
cleanupDirs.push(rootDir);
|
|
335
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
336
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
337
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
338
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
339
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "bridge response lock test\n", "utf8");
|
|
340
|
+
const runner = createExecRunner();
|
|
341
|
+
const bridgeAsset = await createSandboxCallbackBridgeAsset();
|
|
342
|
+
cleanupFns.push(bridgeAsset.cleanup);
|
|
343
|
+
const prepared = await prepareCommandManagedRuntime({
|
|
344
|
+
runner,
|
|
345
|
+
spec: {
|
|
346
|
+
remoteCwd: remoteWorkspaceDir,
|
|
347
|
+
timeoutMs: 30_000,
|
|
348
|
+
},
|
|
349
|
+
adapterKey: "codex",
|
|
350
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
351
|
+
assets: [{ key: "bridge", localDir: bridgeAsset.localDir }],
|
|
352
|
+
});
|
|
353
|
+
const queueDir = path.posix.join(prepared.runtimeRootDir, "evermore-bridge");
|
|
354
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
355
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
356
|
+
const seenRequestIds = [];
|
|
357
|
+
const worker = await startSandboxCallbackBridgeWorker({
|
|
358
|
+
client: createCommandManagedSandboxCallbackBridgeQueueClient({
|
|
359
|
+
runner,
|
|
360
|
+
remoteCwd: remoteWorkspaceDir,
|
|
361
|
+
timeoutMs: 30_000,
|
|
362
|
+
}),
|
|
363
|
+
queueDir,
|
|
364
|
+
authorizeRequest: async () => null,
|
|
365
|
+
handleRequest: async (request) => {
|
|
366
|
+
seenRequestIds.push(request.id);
|
|
367
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
368
|
+
return {
|
|
369
|
+
status: 200,
|
|
370
|
+
headers: { "content-type": "application/json" },
|
|
371
|
+
body: JSON.stringify({ ok: true, id: request.id }),
|
|
372
|
+
};
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
cleanupFns.push(async () => {
|
|
376
|
+
await worker.stop();
|
|
377
|
+
});
|
|
378
|
+
const bridge = await startSandboxCallbackBridgeServer({
|
|
379
|
+
runner,
|
|
380
|
+
remoteCwd: remoteWorkspaceDir,
|
|
381
|
+
assetRemoteDir: prepared.assetDirs.bridge,
|
|
382
|
+
queueDir,
|
|
383
|
+
bridgeToken,
|
|
384
|
+
timeoutMs: 30_000,
|
|
385
|
+
});
|
|
386
|
+
cleanupFns.push(async () => {
|
|
387
|
+
await bridge.stop();
|
|
388
|
+
});
|
|
389
|
+
const responsePromise = fetch(`${bridge.baseUrl}/api/agents/me`, {
|
|
390
|
+
headers: {
|
|
391
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
392
|
+
},
|
|
393
|
+
});
|
|
394
|
+
for (let attempt = 0; attempt < 50 && seenRequestIds.length === 0; attempt += 1) {
|
|
395
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
396
|
+
}
|
|
397
|
+
expect(seenRequestIds).toHaveLength(1);
|
|
398
|
+
await worker.stop({ drainTimeoutMs: 10 });
|
|
399
|
+
const response = await responsePromise;
|
|
400
|
+
expect(response.status).toBe(503);
|
|
401
|
+
await expect(response.json()).resolves.toEqual({
|
|
402
|
+
error: "Bridge worker stopped before request could be handled.",
|
|
403
|
+
});
|
|
404
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
405
|
+
await expect(readdir(directories.responsesDir)).resolves.toEqual([]);
|
|
406
|
+
await expect(readdir(directories.responsesDir).then((entries) => entries.filter((entry) => entry.endsWith(".tmp") || entry.includes(".evermore-write.lock")))).resolves.toEqual([]);
|
|
407
|
+
});
|
|
408
|
+
it("rejects non-JSON request bodies and full queues at the bridge server", async () => {
|
|
409
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "evermore-bridge-server-guards-"));
|
|
410
|
+
cleanupDirs.push(rootDir);
|
|
411
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
412
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
413
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
414
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
415
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "bridge guard test\n", "utf8");
|
|
416
|
+
const runner = createExecRunner();
|
|
417
|
+
const bridgeAsset = await createSandboxCallbackBridgeAsset();
|
|
418
|
+
cleanupFns.push(bridgeAsset.cleanup);
|
|
419
|
+
const prepared = await prepareCommandManagedRuntime({
|
|
420
|
+
runner,
|
|
421
|
+
spec: {
|
|
422
|
+
remoteCwd: remoteWorkspaceDir,
|
|
423
|
+
timeoutMs: 30_000,
|
|
424
|
+
},
|
|
425
|
+
adapterKey: "codex",
|
|
426
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
427
|
+
assets: [{ key: "bridge", localDir: bridgeAsset.localDir }],
|
|
428
|
+
});
|
|
429
|
+
const queueDir = path.posix.join(prepared.runtimeRootDir, "evermore-bridge");
|
|
430
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
431
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
432
|
+
const bridge = await startSandboxCallbackBridgeServer({
|
|
433
|
+
runner,
|
|
434
|
+
remoteCwd: remoteWorkspaceDir,
|
|
435
|
+
assetRemoteDir: prepared.assetDirs.bridge,
|
|
436
|
+
queueDir,
|
|
437
|
+
bridgeToken,
|
|
438
|
+
timeoutMs: 30_000,
|
|
439
|
+
maxQueueDepth: 1,
|
|
440
|
+
});
|
|
441
|
+
cleanupFns.push(async () => {
|
|
442
|
+
await bridge.stop();
|
|
443
|
+
});
|
|
444
|
+
await writeFile(path.posix.join(directories.requestsDir, "existing.json"), `${JSON.stringify({
|
|
445
|
+
id: "existing",
|
|
446
|
+
method: "GET",
|
|
447
|
+
path: "/api/agents/me",
|
|
448
|
+
query: "",
|
|
449
|
+
headers: {},
|
|
450
|
+
body: "",
|
|
451
|
+
createdAt: new Date().toISOString(),
|
|
452
|
+
})}\n`, "utf8");
|
|
453
|
+
const queueFullResponse = await fetch(`${bridge.baseUrl}/api/agents/me`, {
|
|
454
|
+
headers: {
|
|
455
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
expect(queueFullResponse.status).toBe(503);
|
|
459
|
+
await expect(queueFullResponse.json()).resolves.toEqual({
|
|
460
|
+
error: "Bridge request queue is full.",
|
|
461
|
+
});
|
|
462
|
+
await rm(path.posix.join(directories.requestsDir, "existing.json"), { force: true });
|
|
463
|
+
const nonJsonResponse = await fetch(`${bridge.baseUrl}/api/issues/issue-1/comments`, {
|
|
464
|
+
method: "POST",
|
|
465
|
+
headers: {
|
|
466
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
467
|
+
"content-type": "text/plain",
|
|
468
|
+
},
|
|
469
|
+
body: "not json",
|
|
470
|
+
});
|
|
471
|
+
expect(nonJsonResponse.status).toBe(415);
|
|
472
|
+
await expect(nonJsonResponse.json()).resolves.toEqual({
|
|
473
|
+
error: "Bridge only accepts JSON request bodies.",
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
it("returns a 502 when the host response times out", async () => {
|
|
477
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "evermore-bridge-timeout-"));
|
|
478
|
+
cleanupDirs.push(rootDir);
|
|
479
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
480
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
481
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
482
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
483
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "bridge timeout test\n", "utf8");
|
|
484
|
+
const runner = createExecRunner();
|
|
485
|
+
const bridgeAsset = await createSandboxCallbackBridgeAsset();
|
|
486
|
+
cleanupFns.push(bridgeAsset.cleanup);
|
|
487
|
+
const prepared = await prepareCommandManagedRuntime({
|
|
488
|
+
runner,
|
|
489
|
+
spec: {
|
|
490
|
+
remoteCwd: remoteWorkspaceDir,
|
|
491
|
+
timeoutMs: 30_000,
|
|
492
|
+
},
|
|
493
|
+
adapterKey: "codex",
|
|
494
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
495
|
+
assets: [{ key: "bridge", localDir: bridgeAsset.localDir }],
|
|
496
|
+
});
|
|
497
|
+
const queueDir = path.posix.join(prepared.runtimeRootDir, "evermore-bridge");
|
|
498
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
499
|
+
const bridge = await startSandboxCallbackBridgeServer({
|
|
500
|
+
runner,
|
|
501
|
+
remoteCwd: remoteWorkspaceDir,
|
|
502
|
+
assetRemoteDir: prepared.assetDirs.bridge,
|
|
503
|
+
queueDir,
|
|
504
|
+
bridgeToken,
|
|
505
|
+
timeoutMs: 30_000,
|
|
506
|
+
pollIntervalMs: 10,
|
|
507
|
+
responseTimeoutMs: 75,
|
|
508
|
+
});
|
|
509
|
+
cleanupFns.push(async () => {
|
|
510
|
+
await bridge.stop();
|
|
511
|
+
});
|
|
512
|
+
const response = await fetch(`${bridge.baseUrl}/api/agents/me`, {
|
|
513
|
+
headers: {
|
|
514
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
expect(response.status).toBe(502);
|
|
518
|
+
await expect(response.json()).resolves.toEqual({
|
|
519
|
+
error: "Timed out waiting for host bridge response.",
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
it("returns a 502 for malformed host response files", async () => {
|
|
523
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "evermore-bridge-malformed-response-"));
|
|
524
|
+
cleanupDirs.push(rootDir);
|
|
525
|
+
const localWorkspaceDir = path.join(rootDir, "local-workspace");
|
|
526
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
527
|
+
await mkdir(localWorkspaceDir, { recursive: true });
|
|
528
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
529
|
+
await writeFile(path.join(localWorkspaceDir, "README.md"), "bridge malformed response test\n", "utf8");
|
|
530
|
+
const runner = createExecRunner();
|
|
531
|
+
const bridgeAsset = await createSandboxCallbackBridgeAsset();
|
|
532
|
+
cleanupFns.push(bridgeAsset.cleanup);
|
|
533
|
+
const prepared = await prepareCommandManagedRuntime({
|
|
534
|
+
runner,
|
|
535
|
+
spec: {
|
|
536
|
+
remoteCwd: remoteWorkspaceDir,
|
|
537
|
+
timeoutMs: 30_000,
|
|
538
|
+
},
|
|
539
|
+
adapterKey: "codex",
|
|
540
|
+
workspaceLocalDir: localWorkspaceDir,
|
|
541
|
+
assets: [{ key: "bridge", localDir: bridgeAsset.localDir }],
|
|
542
|
+
});
|
|
543
|
+
const queueDir = path.posix.join(prepared.runtimeRootDir, "evermore-bridge");
|
|
544
|
+
const directories = sandboxCallbackBridgeDirectories(queueDir);
|
|
545
|
+
const bridgeToken = createSandboxCallbackBridgeToken();
|
|
546
|
+
const bridge = await startSandboxCallbackBridgeServer({
|
|
547
|
+
runner,
|
|
548
|
+
remoteCwd: remoteWorkspaceDir,
|
|
549
|
+
assetRemoteDir: prepared.assetDirs.bridge,
|
|
550
|
+
queueDir,
|
|
551
|
+
bridgeToken,
|
|
552
|
+
timeoutMs: 30_000,
|
|
553
|
+
pollIntervalMs: 10,
|
|
554
|
+
responseTimeoutMs: 1_000,
|
|
555
|
+
});
|
|
556
|
+
cleanupFns.push(async () => {
|
|
557
|
+
await bridge.stop();
|
|
558
|
+
});
|
|
559
|
+
const responsePromise = fetch(`${bridge.baseUrl}/api/agents/me`, {
|
|
560
|
+
headers: {
|
|
561
|
+
authorization: `Bearer ${bridgeToken}`,
|
|
562
|
+
},
|
|
563
|
+
});
|
|
564
|
+
const requestFile = await waitForJsonFile(directories.requestsDir);
|
|
565
|
+
await writeFile(path.posix.join(directories.responsesDir, requestFile), '{"status":200,"headers":{"content-type":"application/json"},"body"', "utf8");
|
|
566
|
+
const response = await responsePromise;
|
|
567
|
+
expect(response.status).toBe(502);
|
|
568
|
+
await expect(response.json()).resolves.toMatchObject({
|
|
569
|
+
error: expect.stringMatching(/JSON|Unexpected|Unterminated/i),
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
it("reuses an already-uploaded bridge entrypoint when the remote file hash matches", async () => {
|
|
573
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "evermore-bridge-sync-"));
|
|
574
|
+
cleanupDirs.push(rootDir);
|
|
575
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
576
|
+
const remoteAssetDir = path.posix.join(remoteWorkspaceDir, ".evermore-runtime", "codex", "evermore-bridge", "server");
|
|
577
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
578
|
+
const bridgeAsset = await createSandboxCallbackBridgeAsset();
|
|
579
|
+
cleanupFns.push(bridgeAsset.cleanup);
|
|
580
|
+
const originalSource = await readFile(bridgeAsset.entrypoint, "utf8");
|
|
581
|
+
const expandedSource = `${originalSource}\n// bridge payload padding\n`;
|
|
582
|
+
await writeFile(bridgeAsset.entrypoint, expandedSource, "utf8");
|
|
583
|
+
const runner = createExecRunner();
|
|
584
|
+
const first = await syncSandboxCallbackBridgeEntrypoint({
|
|
585
|
+
runner,
|
|
586
|
+
remoteCwd: remoteWorkspaceDir,
|
|
587
|
+
assetRemoteDir: remoteAssetDir,
|
|
588
|
+
bridgeAsset,
|
|
589
|
+
timeoutMs: 30_000,
|
|
590
|
+
});
|
|
591
|
+
const second = await syncSandboxCallbackBridgeEntrypoint({
|
|
592
|
+
runner,
|
|
593
|
+
remoteCwd: remoteWorkspaceDir,
|
|
594
|
+
assetRemoteDir: remoteAssetDir,
|
|
595
|
+
bridgeAsset,
|
|
596
|
+
timeoutMs: 30_000,
|
|
597
|
+
});
|
|
598
|
+
expect(first.uploaded).toBe(true);
|
|
599
|
+
expect(second.uploaded).toBe(false);
|
|
600
|
+
await expect(readFile(path.posix.join(remoteAssetDir, "evermore-bridge-server.mjs"), "utf8")).resolves.toBe(expandedSource);
|
|
601
|
+
await expect(readdir(remoteAssetDir).then((entries) => entries.filter((entry) => entry.endsWith(".evermore-upload.b64") ||
|
|
602
|
+
entry.endsWith(".partial") ||
|
|
603
|
+
entry === ".evermore-bridge-upload.lock"))).resolves.toEqual([]);
|
|
604
|
+
});
|
|
605
|
+
it("rejects a corrupted bridge entrypoint upload without committing a torn remote file", async () => {
|
|
606
|
+
const rootDir = await mkdtemp(path.join(os.tmpdir(), "evermore-bridge-sync-corrupt-"));
|
|
607
|
+
cleanupDirs.push(rootDir);
|
|
608
|
+
const remoteWorkspaceDir = path.join(rootDir, "remote-workspace");
|
|
609
|
+
const remoteAssetDir = path.posix.join(remoteWorkspaceDir, ".evermore-runtime", "codex", "evermore-bridge", "server");
|
|
610
|
+
await mkdir(remoteWorkspaceDir, { recursive: true });
|
|
611
|
+
const bridgeAsset = await createSandboxCallbackBridgeAsset();
|
|
612
|
+
cleanupFns.push(bridgeAsset.cleanup);
|
|
613
|
+
const runner = {
|
|
614
|
+
execute: async (input) => await createExecRunner().execute({
|
|
615
|
+
...input,
|
|
616
|
+
stdin: input.stdin != null ? "" : input.stdin,
|
|
617
|
+
}),
|
|
618
|
+
};
|
|
619
|
+
await expect(syncSandboxCallbackBridgeEntrypoint({
|
|
620
|
+
runner,
|
|
621
|
+
remoteCwd: remoteWorkspaceDir,
|
|
622
|
+
assetRemoteDir: remoteAssetDir,
|
|
623
|
+
bridgeAsset,
|
|
624
|
+
timeoutMs: 30_000,
|
|
625
|
+
})).rejects.toThrow(/sha mismatch/i);
|
|
626
|
+
await expect(readFile(path.posix.join(remoteAssetDir, "evermore-bridge-server.mjs"), "utf8")).rejects.toThrow();
|
|
627
|
+
await expect(readdir(remoteAssetDir).then((entries) => entries.filter((entry) => entry.endsWith(".evermore-upload.b64") ||
|
|
628
|
+
entry.endsWith(".partial") ||
|
|
629
|
+
entry === ".evermore-bridge-upload.lock"))).resolves.toEqual([]);
|
|
630
|
+
});
|
|
631
|
+
it("permits the documented heartbeat surface and denies unrelated routes", () => {
|
|
632
|
+
const allowed = [
|
|
633
|
+
{ method: "GET", path: "/api/agents/me" },
|
|
634
|
+
{ method: "GET", path: "/api/agents/me/inbox-lite" },
|
|
635
|
+
{ method: "GET", path: "/api/agents/me/inbox/mine" },
|
|
636
|
+
{ method: "GET", path: "/api/agents/agent-1" },
|
|
637
|
+
{ method: "GET", path: "/api/agents/agent-1/skills" },
|
|
638
|
+
{ method: "POST", path: "/api/agents/agent-1/skills/sync" },
|
|
639
|
+
{ method: "PATCH", path: "/api/agents/agent-1/instructions-path" },
|
|
640
|
+
{ method: "GET", path: "/api/companies/co-1" },
|
|
641
|
+
{ method: "GET", path: "/api/companies/co-1/dashboard" },
|
|
642
|
+
{ method: "GET", path: "/api/companies/co-1/agents" },
|
|
643
|
+
{ method: "GET", path: "/api/companies/co-1/issues" },
|
|
644
|
+
{ method: "GET", path: "/api/companies/co-1/projects" },
|
|
645
|
+
{ method: "GET", path: "/api/companies/co-1/goals" },
|
|
646
|
+
{ method: "GET", path: "/api/companies/co-1/org" },
|
|
647
|
+
{ method: "GET", path: "/api/companies/co-1/approvals" },
|
|
648
|
+
{ method: "GET", path: "/api/companies/co-1/routines" },
|
|
649
|
+
{ method: "GET", path: "/api/companies/co-1/skills" },
|
|
650
|
+
{ method: "GET", path: "/api/projects/proj-1" },
|
|
651
|
+
{ method: "GET", path: "/api/goals/goal-1" },
|
|
652
|
+
{ method: "GET", path: "/api/issues/issue-1" },
|
|
653
|
+
{ method: "GET", path: "/api/issues/issue-1/heartbeat-context" },
|
|
654
|
+
{ method: "GET", path: "/api/issues/issue-1/comments" },
|
|
655
|
+
{ method: "GET", path: "/api/issues/issue-1/comments/c-1" },
|
|
656
|
+
{ method: "POST", path: "/api/issues/issue-1/comments" },
|
|
657
|
+
{ method: "GET", path: "/api/issues/issue-1/documents" },
|
|
658
|
+
{ method: "GET", path: "/api/issues/issue-1/documents/plan" },
|
|
659
|
+
{ method: "GET", path: "/api/issues/issue-1/documents/plan/revisions" },
|
|
660
|
+
{ method: "PUT", path: "/api/issues/issue-1/documents/plan" },
|
|
661
|
+
{ method: "POST", path: "/api/issues/issue-1/checkout" },
|
|
662
|
+
{ method: "POST", path: "/api/issues/issue-1/release" },
|
|
663
|
+
{ method: "PATCH", path: "/api/issues/issue-1" },
|
|
664
|
+
{ method: "GET", path: "/api/issues/issue-1/approvals" },
|
|
665
|
+
{ method: "GET", path: "/api/issues/issue-1/interactions" },
|
|
666
|
+
{ method: "GET", path: "/api/issues/issue-1/interactions/inter-1" },
|
|
667
|
+
{ method: "POST", path: "/api/issues/issue-1/interactions" },
|
|
668
|
+
{ method: "POST", path: "/api/issues/issue-1/interactions/inter-1/accept" },
|
|
669
|
+
{ method: "POST", path: "/api/issues/issue-1/interactions/inter-1/reject" },
|
|
670
|
+
{ method: "POST", path: "/api/issues/issue-1/interactions/inter-1/respond" },
|
|
671
|
+
{ method: "POST", path: "/api/companies/co-1/issues" },
|
|
672
|
+
{ method: "GET", path: "/api/approvals/ap-1" },
|
|
673
|
+
{ method: "GET", path: "/api/approvals/ap-1/issues" },
|
|
674
|
+
{ method: "GET", path: "/api/approvals/ap-1/comments" },
|
|
675
|
+
{ method: "POST", path: "/api/approvals/ap-1/comments" },
|
|
676
|
+
{ method: "POST", path: "/api/companies/co-1/approvals" },
|
|
677
|
+
{ method: "GET", path: "/api/execution-workspaces/ws-1" },
|
|
678
|
+
{ method: "POST", path: "/api/execution-workspaces/ws-1/runtime-services/start" },
|
|
679
|
+
{ method: "POST", path: "/api/execution-workspaces/ws-1/runtime-services/stop" },
|
|
680
|
+
{ method: "POST", path: "/api/execution-workspaces/ws-1/runtime-services/restart" },
|
|
681
|
+
{ method: "GET", path: "/api/routines/r-1" },
|
|
682
|
+
{ method: "GET", path: "/api/routines/r-1/runs" },
|
|
683
|
+
{ method: "POST", path: "/api/companies/co-1/routines" },
|
|
684
|
+
{ method: "PATCH", path: "/api/routines/r-1" },
|
|
685
|
+
{ method: "POST", path: "/api/routines/r-1/run" },
|
|
686
|
+
{ method: "POST", path: "/api/routines/r-1/triggers" },
|
|
687
|
+
{ method: "PATCH", path: "/api/routine-triggers/t-1" },
|
|
688
|
+
{ method: "DELETE", path: "/api/routine-triggers/t-1" },
|
|
689
|
+
];
|
|
690
|
+
for (const request of allowed) {
|
|
691
|
+
expect(authorizeSandboxCallbackBridgeRequestWithRoutes(request)).toBeNull();
|
|
692
|
+
}
|
|
693
|
+
const denied = [
|
|
694
|
+
{ method: "DELETE", path: "/api/secrets" },
|
|
695
|
+
// Pin the runtime-services regex to start/stop/restart only — anything
|
|
696
|
+
// else (delete, reset, wipe, etc.) must stay denied even if the API
|
|
697
|
+
// grows new actions later.
|
|
698
|
+
{ method: "POST", path: "/api/execution-workspaces/ws-1/runtime-services/delete" },
|
|
699
|
+
{ method: "POST", path: "/api/companies/co-1/agents" },
|
|
700
|
+
{ method: "POST", path: "/api/agents/agent-1/pause" },
|
|
701
|
+
{ method: "POST", path: "/api/agents/agent-1/terminate" },
|
|
702
|
+
{ method: "POST", path: "/api/agents/agent-1/keys" },
|
|
703
|
+
{ method: "POST", path: "/api/companies/co-1/exports" },
|
|
704
|
+
{ method: "POST", path: "/api/companies/co-1/imports/apply" },
|
|
705
|
+
{ method: "POST", path: "/api/companies/co-1/archive" },
|
|
706
|
+
{ method: "DELETE", path: "/api/issues/issue-1/documents/plan" },
|
|
707
|
+
{ method: "DELETE", path: "/api/issues/issue-1/approvals/ap-1" },
|
|
708
|
+
{ method: "POST", path: "/api/approvals/ap-1/approve" },
|
|
709
|
+
{ method: "POST", path: "/api/approvals/ap-1/reject" },
|
|
710
|
+
{ method: "POST", path: "/api/companies/co-1/logo" },
|
|
711
|
+
{ method: "GET", path: "/api/companies/co-1/secrets" },
|
|
712
|
+
{ method: "PATCH", path: "/api/secrets/secret-1" },
|
|
713
|
+
];
|
|
714
|
+
for (const request of denied) {
|
|
715
|
+
expect(authorizeSandboxCallbackBridgeRequestWithRoutes(request)).toBe(`Route not allowed: ${request.method} ${request.path}`);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
//# sourceMappingURL=sandbox-callback-bridge.test.js.map
|