@akira-tl/forgerelay 0.8.4 → 0.8.6

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +10 -10
  3. package/dist/activity/audit-store.js +44 -6
  4. package/dist/activity/mcp-query-tools.js +53 -36
  5. package/dist/activity/query-service.js +41 -11
  6. package/dist/cli.js +16 -17
  7. package/dist/composite-activity.js +124 -33
  8. package/dist/config.js +8 -13
  9. package/dist/lsp/test-support/server-fixture.js +7 -7
  10. package/dist/process-sessions.js +2 -2
  11. package/dist/remote-auth.js +11 -1
  12. package/dist/remote-mcp-connection-pool.js +90 -0
  13. package/dist/remote-transport.js +28 -14
  14. package/dist/remote-workspace-relay.js +203 -45
  15. package/dist/server.js +134 -64
  16. package/dist/skills.js +1 -1
  17. package/dist/subagents/profiles.js +1 -2
  18. package/dist/subagents/providers/adapters/pi.js +2 -2
  19. package/dist/subagents/providers/availability.js +2 -2
  20. package/dist/subagents/providers/path.js +4 -4
  21. package/dist/ui/.vite/manifest.json +32 -32
  22. package/dist/ui/activity-panel-app.html +3 -3
  23. package/dist/ui/assets/{activity-panel-app-E1ju2dqI.js → activity-panel-app-CUAN6zyW.js} +1 -1
  24. package/dist/ui/assets/{heavy-payload-CeW-n9w5.js → heavy-payload-CgzrutLm.js} +1 -1
  25. package/dist/ui/assets/{review-payload-B9CO298v.js → review-payload-BrLbezbq.js} +1 -1
  26. package/dist/ui/assets/{scrollbar-C2twAENW.js → scrollbar-CbhpdW05.js} +1 -1
  27. package/dist/ui/assets/workspace-app-Bhj96tsR.js +1 -0
  28. package/dist/ui/assets/{workspace-app-CwbJnb_w.js → workspace-app-CxwJuZyS.js} +1 -1
  29. package/dist/ui/assets/{workspace-app-BztEvZIC.js → workspace-app-D6UR0AFl.js} +3 -3
  30. package/dist/ui/assets/workspace-app-ldjBmCJR.css +1 -0
  31. package/dist/ui/assets/workspace-lifecycle-app-Cqfhx9pV.js +1 -0
  32. package/dist/ui/workspace-app.html +4 -4
  33. package/dist/ui/workspace-lifecycle-app.html +4 -4
  34. package/dist/user-config.js +3 -19
  35. package/dist/workspace-presentation.js +69 -0
  36. package/docs/agent-profile-schema.md +4 -9
  37. package/docs/artifact-exchange.md +2 -1
  38. package/docs/chatgpt-coding-workflow.md +9 -9
  39. package/docs/configuration.md +20 -29
  40. package/docs/gotchas.md +10 -7
  41. package/docs/roadmap.md +6 -3
  42. package/docs/security.md +3 -2
  43. package/docs/setup.md +8 -5
  44. package/docs/versioning.md +1 -1
  45. package/package.json +6 -2
  46. package/scripts/debug/accept.mjs +7 -1
  47. package/scripts/debug/relay-accept.mjs +889 -0
  48. package/scripts/debug/runtime.mjs +0 -4
  49. package/scripts/debug/runtime.test.mjs +0 -2
  50. package/scripts/debug/traffic/run.sh +5 -0
  51. package/scripts/debug/traffic/traffic-audit.mjs +907 -0
  52. package/scripts/release/push-ready.mjs +124 -0
  53. package/scripts/release/push-ready.test.mjs +108 -0
  54. package/scripts/release/release-gate.test.mjs +1 -0
  55. package/scripts/release-proof.mjs +16 -1
  56. package/scripts/wiki/sync.mjs +246 -0
  57. package/dist/ui/assets/workspace-app-YnUST8IP.css +0 -1
  58. package/dist/ui/assets/workspace-app-rKuhdae8.js +0 -1
  59. package/dist/ui/assets/workspace-lifecycle-app-CEfMdudP.js +0 -1
@@ -0,0 +1,889 @@
1
+ import assert from "node:assert/strict";
2
+ import { spawn, spawnSync } from "node:child_process";
3
+ import { createHash, randomBytes, randomUUID } from "node:crypto";
4
+ import { once } from "node:events";
5
+ import { connect } from "node:net";
6
+ import {
7
+ existsSync,
8
+ mkdirSync,
9
+ readFileSync,
10
+ readdirSync,
11
+ rmSync,
12
+ writeFileSync,
13
+ } from "node:fs";
14
+ import { join, resolve } from "node:path";
15
+ import { setTimeout as delay } from "node:timers/promises";
16
+ import { debugRoot, repoRoot } from "./runtime.mjs";
17
+
18
+ const gatewayPort = 7677;
19
+ const executionPort = 7678;
20
+ const gatewayBaseUrl = `http://127.0.0.1:${gatewayPort}`;
21
+ const gatewayMcpUrl = `${gatewayBaseUrl}/mcp`;
22
+ const executionBaseUrl = `http://127.0.0.1:${executionPort}`;
23
+ const acceptanceRoot = resolve(debugRoot, "relay-acceptance");
24
+ const gatewayConfigDir = join(acceptanceRoot, "gateway", "config");
25
+ const gatewayStateDir = join(acceptanceRoot, "gateway", "state");
26
+ const gatewayWorktreeRoot = join(acceptanceRoot, "gateway", "worktrees");
27
+ const gatewayProjects = join(acceptanceRoot, "gateway-projects");
28
+ const gatewayLocalProject = join(gatewayProjects, "local");
29
+ const executionConfigDir = join(acceptanceRoot, "execution", "config");
30
+ const executionStateDir = join(acceptanceRoot, "execution", "state");
31
+ const executionWorktreeRoot = join(acceptanceRoot, "execution", "worktrees");
32
+ const executionProjects = join(acceptanceRoot, "execution-projects");
33
+ const executionCheckout = join(executionProjects, "checkout");
34
+ const executionWorktreeSource = join(executionProjects, "worktree-source");
35
+ const gatewayOwnerToken = randomBytes(32).toString("base64url");
36
+ const executionOwnerToken = randomBytes(32).toString("base64url");
37
+ const bootstrapSecret = "RELAY_ACCEPTANCE_BOOTSTRAP_SECRET";
38
+ const checkoutTaskBody = "RELAY_ACCEPTANCE_EXECUTION_TASK_BODY";
39
+ const worktreeTaskBody = "RELAY_ACCEPTANCE_WORKTREE_TASK_BODY";
40
+ const compositeMemberTaskBody = "RELAY_ACCEPTANCE_COMPOSITE_MEMBER_TASK_BODY";
41
+ const compositeTaskBody = "RELAY_ACCEPTANCE_COMPOSITE_OWN_TASK_BODY";
42
+
43
+ await assertPortsFree([gatewayPort, executionPort]);
44
+ rmSync(acceptanceRoot, { recursive: true, force: true });
45
+ mkdirSync(gatewayLocalProject, { recursive: true });
46
+ mkdirSync(executionCheckout, { recursive: true });
47
+ mkdirSync(gatewayConfigDir, { recursive: true });
48
+ mkdirSync(executionConfigDir, { recursive: true });
49
+ writeFileSync(join(gatewayLocalProject, "sentinel.txt"), "gateway-local-content\n");
50
+ writeFileSync(join(executionCheckout, "sentinel.txt"), "execution-remote-content\n");
51
+ writeFileSync(join(executionCheckout, "AGENTS.md"), `${bootstrapSecret}\n`);
52
+ setupGitProject(executionWorktreeSource);
53
+
54
+ writeAuthFile(gatewayConfigDir, gatewayOwnerToken, "relay-acceptance-gateway");
55
+ writeAuthFile(executionConfigDir, executionOwnerToken, "relay-acceptance-execution");
56
+
57
+ const executionEnv = instanceEnv({
58
+ port: executionPort,
59
+ baseUrl: executionBaseUrl,
60
+ configDir: executionConfigDir,
61
+ stateDir: executionStateDir,
62
+ worktreeRoot: executionWorktreeRoot,
63
+ allowedRoot: executionProjects,
64
+ ownerToken: executionOwnerToken,
65
+ });
66
+ const gatewayEnv = instanceEnv({
67
+ port: gatewayPort,
68
+ baseUrl: gatewayBaseUrl,
69
+ configDir: gatewayConfigDir,
70
+ stateDir: gatewayStateDir,
71
+ worktreeRoot: gatewayWorktreeRoot,
72
+ allowedRoot: gatewayProjects,
73
+ ownerToken: gatewayOwnerToken,
74
+ });
75
+
76
+ const execution = spawnServer(executionEnv, "Execution 7678");
77
+ let gateway;
78
+ let completed = false;
79
+
80
+ try {
81
+ await waitForHealth(execution, executionBaseUrl, "Execution 7678");
82
+ pass("execution health", executionBaseUrl);
83
+
84
+ const authenticated = runCli(
85
+ ["auth", `127.0.0.1:${executionPort}`, "--token", executionOwnerToken, "--alias", "execution"],
86
+ { ...cleanProductEnv(), FORGERELAY_CONFIG_DIR: gatewayConfigDir },
87
+ );
88
+ assert.equal(authenticated.status, 0, authenticated.stderr || authenticated.stdout);
89
+ pass("Gateway remote auth", "CLI auth route to isolated 7678 Execution");
90
+
91
+ const gatewayAuth = JSON.parse(readFileSync(join(gatewayConfigDir, "auth.json"), "utf8"));
92
+ const remoteRecord = gatewayAuth.remotes?.execution;
93
+ assert.ok(remoteRecord?.accessToken);
94
+ assert.ok(remoteRecord?.refreshToken);
95
+ assert.equal(remoteRecord?.target, executionBaseUrl);
96
+
97
+ gateway = spawnServer(gatewayEnv, "Gateway 7677");
98
+ await waitForHealth(gateway, gatewayBaseUrl, "Gateway 7677");
99
+ pass("gateway health", gatewayBaseUrl);
100
+
101
+ const oauth = authorizeHost(gatewayBaseUrl, gatewayMcpUrl, gatewayOwnerToken);
102
+ const sessionA = initializeSession(gatewayMcpUrl, oauth.accessToken, 1, "relay-acceptance-a");
103
+ const sessionB = initializeSession(gatewayMcpUrl, oauth.accessToken, 2, "relay-acceptance-b");
104
+ pass("Gateway OAuth/MCP", "two real HTTP MCP sessions initialized on 7677");
105
+
106
+ let requestId = 10;
107
+ const nextId = () => requestId++;
108
+ const conversationA = { "openai/session": "relay-acceptance-conversation-a" };
109
+ const conversationB = { "openai/session": "relay-acceptance-conversation-b" };
110
+
111
+ const openedA = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "open_workspace", {
112
+ path: executionCheckout,
113
+ relay: "execution",
114
+ context: "none",
115
+ }, conversationA);
116
+ assertToolOk(openedA, "open relayed checkout A");
117
+ const checkoutRelayId = openedA.structuredContent.workspaceId;
118
+ assert.match(checkoutRelayId, /^rws_/);
119
+
120
+ const openedB = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "open_workspace", {
121
+ path: executionCheckout,
122
+ relay: "execution",
123
+ context: "none",
124
+ }, conversationB);
125
+ assertToolOk(openedB, "open relayed checkout B");
126
+ assert.equal(openedB.structuredContent.workspaceId, checkoutRelayId);
127
+ pass("relay persistent identity", `two Gateway conversations reused ${checkoutRelayId}`);
128
+
129
+ const checkoutList = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "capability", {
130
+ workspaceId: checkoutRelayId,
131
+ name: "workspace.tasks",
132
+ action: "run",
133
+ arguments: { operation: "list.create", name: "Relay checkout acceptance" },
134
+ }, conversationA);
135
+ assertToolOk(checkoutList, "create relayed checkout Task List");
136
+ const checkoutListId = checkoutList.structuredContent.result.lists[0].id;
137
+ const checkoutTask = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "capability", {
138
+ workspaceId: checkoutRelayId,
139
+ name: "workspace.tasks",
140
+ action: "run",
141
+ arguments: {
142
+ operation: "task.create",
143
+ listId: checkoutListId,
144
+ subject: "Verify real Relay Task ownership",
145
+ content: checkoutTaskBody,
146
+ status: "in_progress",
147
+ },
148
+ }, conversationA);
149
+ assertToolOk(checkoutTask, "create relayed checkout Task");
150
+
151
+ const checkoutTaskFile = findTaskStateFile(executionStateDir, checkoutTaskBody);
152
+ assert.ok(checkoutTaskFile, "Execution Task state file was not found");
153
+ const checkoutExecutionWorkspaceId = checkoutTaskFile.split("/").at(-2);
154
+ assert.match(checkoutExecutionWorkspaceId, /^ws_[0-9a-f]{10}$/);
155
+ assertTaskBodyAbsentFromGateway(checkoutTaskBody);
156
+ pass("Execution-owned Task truth", `${checkoutExecutionWorkspaceId} owns the Task state; Gateway has no shadow copy`);
157
+
158
+ for (let index = 0; index < 2; index += 1) {
159
+ const read = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "read", {
160
+ workspaceId: checkoutRelayId,
161
+ path: "sentinel.txt",
162
+ }, conversationB);
163
+ assertToolOk(read, "pre-reminder relayed read");
164
+ assert.doesNotMatch(toolText(read), /Reminder: this Workspace has unfinished active Tasks/);
165
+ }
166
+ const reminderRead = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "read", {
167
+ workspaceId: checkoutRelayId,
168
+ path: "sentinel.txt",
169
+ }, conversationB);
170
+ assertToolOk(reminderRead, "relayed reminder read");
171
+ assert.match(toolText(reminderRead), /Reminder: this Workspace has unfinished active Tasks/);
172
+ assert.equal(toolText(reminderRead).includes(checkoutTaskBody), false);
173
+ pass("Relay Task reminder", "owning Execution emitted a body-free reminder after 3 semantic calls");
174
+
175
+ const closedCheckout = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "close_workspace", {
176
+ workspaceId: checkoutRelayId,
177
+ }, conversationA);
178
+ assertToolOk(closedCheckout, "close relayed checkout");
179
+ assert.equal(closedCheckout.structuredContent.action, "close");
180
+
181
+ const inspectedCheckout = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "open_workspace", {
182
+ action: "inspect",
183
+ workspaceId: checkoutRelayId,
184
+ }, conversationB);
185
+ assertToolOk(inspectedCheckout, "inspect closed relayed checkout");
186
+ assert.equal(inspectedCheckout.structuredContent.inspection.state, "closed");
187
+ assert.equal(inspectedCheckout.structuredContent.inspection.status, "closed");
188
+ assert.equal(inspectedCheckout.structuredContent.inspection.taskSummary.lists[0].unfinishedTaskCount, 1);
189
+ assertSafeRelayInspection(inspectedCheckout, {
190
+ executionWorkspaceId: checkoutExecutionWorkspaceId,
191
+ remoteRecord,
192
+ taskBodies: [checkoutTaskBody],
193
+ });
194
+
195
+ const reopenedCheckout = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "open_workspace", {
196
+ workspaceId: checkoutRelayId,
197
+ context: "none",
198
+ }, conversationB);
199
+ assertToolOk(reopenedCheckout, "reopen relayed checkout");
200
+ assert.equal(reopenedCheckout.structuredContent.workspaceId, checkoutRelayId);
201
+ const restoredCheckoutTasks = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "capability", {
202
+ workspaceId: checkoutRelayId,
203
+ name: "workspace.tasks",
204
+ action: "run",
205
+ arguments: { operation: "get", level: "headers", listId: checkoutListId },
206
+ }, conversationB);
207
+ assertToolOk(restoredCheckoutTasks, "restore relayed checkout Tasks");
208
+ assert.equal(restoredCheckoutTasks.structuredContent.result.lists[0].tasks[0].subject, "Verify real Relay Task ownership");
209
+ assert.equal("content" in restoredCheckoutTasks.structuredContent.result.lists[0].tasks[0], false);
210
+
211
+ const deletedCheckout = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "close_workspace", {
212
+ workspaceId: checkoutRelayId,
213
+ action: "delete",
214
+ }, conversationB);
215
+ assertToolOk(deletedCheckout, "delete relayed checkout");
216
+ assert.equal(deletedCheckout.structuredContent.action, "delete");
217
+ assert.equal(readFileSync(join(executionCheckout, "sentinel.txt"), "utf8"), "execution-remote-content\n");
218
+ assert.equal(existsSync(checkoutTaskFile), false);
219
+ const deletedCheckoutInspection = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "open_workspace", {
220
+ action: "inspect",
221
+ workspaceId: checkoutRelayId,
222
+ }, conversationA);
223
+ assert.equal(deletedCheckoutInspection.isError, true);
224
+
225
+ const freshCheckout = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "open_workspace", {
226
+ path: executionCheckout,
227
+ relay: "execution",
228
+ context: "none",
229
+ }, conversationA);
230
+ assertToolOk(freshCheckout, "recreate relayed checkout after delete");
231
+ const compositeRelayMemberId = freshCheckout.structuredContent.workspaceId;
232
+ assert.match(compositeRelayMemberId, /^rws_/);
233
+ assert.notEqual(compositeRelayMemberId, checkoutRelayId);
234
+ pass("relay close/reopen/delete", "close preserved route, reopen reused it, delete removed it");
235
+
236
+ const worktreeOpened = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "open_workspace", {
237
+ path: executionWorktreeSource,
238
+ relay: "execution",
239
+ mode: "worktree",
240
+ context: "none",
241
+ }, conversationA);
242
+ assertToolOk(worktreeOpened, "open relayed managed worktree");
243
+ const worktreeRelayId = worktreeOpened.structuredContent.workspaceId;
244
+ const firstWorktreeRoot = worktreeOpened.structuredContent.root;
245
+ assert.match(worktreeRelayId, /^rws_/);
246
+ assert.notEqual(firstWorktreeRoot, executionWorktreeSource);
247
+
248
+ const worktreeList = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "capability", {
249
+ workspaceId: worktreeRelayId,
250
+ name: "workspace.tasks",
251
+ action: "run",
252
+ arguments: { operation: "list.create", name: "Relay worktree acceptance" },
253
+ }, conversationA);
254
+ assertToolOk(worktreeList, "create relayed worktree Task List");
255
+ const worktreeListId = worktreeList.structuredContent.result.lists[0].id;
256
+ const worktreeTask = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "capability", {
257
+ workspaceId: worktreeRelayId,
258
+ name: "workspace.tasks",
259
+ action: "run",
260
+ arguments: {
261
+ operation: "task.create",
262
+ listId: worktreeListId,
263
+ subject: "Survive real managed finalize",
264
+ content: worktreeTaskBody,
265
+ status: "in_progress",
266
+ },
267
+ }, conversationA);
268
+ assertToolOk(worktreeTask, "create relayed worktree Task");
269
+ const worktreeTaskFile = findTaskStateFile(executionStateDir, worktreeTaskBody);
270
+ assert.ok(worktreeTaskFile, "Execution managed-worktree Task state file was not found");
271
+ const worktreeExecutionWorkspaceId = worktreeTaskFile.split("/").at(-2);
272
+ assertTaskBodyAbsentFromGateway(worktreeTaskBody);
273
+
274
+ const worktreeWrite = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "write", {
275
+ workspaceId: worktreeRelayId,
276
+ path: "relay-acceptance.txt",
277
+ content: "real managed Relay acceptance\n",
278
+ }, conversationA);
279
+ assertToolOk(worktreeWrite, "write relayed managed worktree");
280
+
281
+ const worktreeClosed = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "close_workspace", {
282
+ workspaceId: worktreeRelayId,
283
+ commitMessage: "TEST: (relay) finalize real managed worktree",
284
+ }, conversationA);
285
+ assertToolOk(worktreeClosed, "close/finalize relayed managed worktree");
286
+ assert.equal(existsSync(firstWorktreeRoot), false);
287
+ assert.equal(readFileSync(join(executionWorktreeSource, "relay-acceptance.txt"), "utf8"), "real managed Relay acceptance\n");
288
+ assert.equal(existsSync(worktreeTaskFile), true);
289
+
290
+ const inspectedWorktree = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "open_workspace", {
291
+ action: "inspect",
292
+ workspaceId: worktreeRelayId,
293
+ }, conversationB);
294
+ assertToolOk(inspectedWorktree, "inspect closed relayed managed worktree");
295
+ assert.equal(inspectedWorktree.structuredContent.inspection.state, "closed");
296
+ assert.equal(inspectedWorktree.structuredContent.inspection.mode, "worktree");
297
+ assert.equal(inspectedWorktree.structuredContent.inspection.managed, true);
298
+ assert.equal(inspectedWorktree.structuredContent.inspection.rootValid, false);
299
+ assertSafeRelayInspection(inspectedWorktree, {
300
+ executionWorkspaceId: worktreeExecutionWorkspaceId,
301
+ remoteRecord,
302
+ taskBodies: [worktreeTaskBody],
303
+ });
304
+
305
+ const worktreeReopened = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "open_workspace", {
306
+ workspaceId: worktreeRelayId,
307
+ context: "none",
308
+ }, conversationB);
309
+ assertToolOk(worktreeReopened, "reopen relayed managed worktree");
310
+ assert.equal(worktreeReopened.structuredContent.workspaceId, worktreeRelayId);
311
+ assert.notEqual(worktreeReopened.structuredContent.root, firstWorktreeRoot);
312
+ const worktreeRead = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "read", {
313
+ workspaceId: worktreeRelayId,
314
+ path: "relay-acceptance.txt",
315
+ }, conversationB);
316
+ assertToolOk(worktreeRead, "read reopened relayed managed worktree");
317
+ assert.match(toolText(worktreeRead), /real managed Relay acceptance/);
318
+ const restoredWorktreeTasks = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "capability", {
319
+ workspaceId: worktreeRelayId,
320
+ name: "workspace.tasks",
321
+ action: "run",
322
+ arguments: { operation: "get", level: "headers", listId: worktreeListId },
323
+ }, conversationB);
324
+ assertToolOk(restoredWorktreeTasks, "restore relayed worktree Tasks");
325
+ assert.equal(restoredWorktreeTasks.structuredContent.result.lists[0].tasks[0].subject, "Survive real managed finalize");
326
+
327
+ const worktreeDeleted = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "close_workspace", {
328
+ workspaceId: worktreeRelayId,
329
+ action: "delete",
330
+ commitMessage: "TEST: (relay) delete real managed worktree",
331
+ }, conversationB);
332
+ assertToolOk(worktreeDeleted, "delete relayed managed worktree");
333
+ assert.equal(existsSync(worktreeTaskFile), false);
334
+ pass("managed-worktree Relay parity", "finalize removed backing, reopen kept rws/Task identity, delete removed state");
335
+
336
+ const localOpened = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "open_workspace", {
337
+ path: gatewayLocalProject,
338
+ context: "none",
339
+ }, conversationA);
340
+ assertToolOk(localOpened, "open local Composite member");
341
+ const localWorkspaceId = localOpened.structuredContent.workspaceId;
342
+
343
+ const memberList = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "capability", {
344
+ workspaceId: compositeRelayMemberId,
345
+ name: "workspace.tasks",
346
+ action: "run",
347
+ arguments: { operation: "list.create", name: "Composite relay member acceptance" },
348
+ }, conversationA);
349
+ assertToolOk(memberList, "create Composite relay member Task List");
350
+ const memberListId = memberList.structuredContent.result.lists[0].id;
351
+ const memberTask = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "capability", {
352
+ workspaceId: compositeRelayMemberId,
353
+ name: "workspace.tasks",
354
+ action: "run",
355
+ arguments: {
356
+ operation: "task.create",
357
+ listId: memberListId,
358
+ subject: "Remote Composite member Task",
359
+ content: compositeMemberTaskBody,
360
+ status: "in_progress",
361
+ },
362
+ }, conversationA);
363
+ assertToolOk(memberTask, "create Composite relay member Task");
364
+ const memberTaskFile = findTaskStateFile(executionStateDir, compositeMemberTaskBody);
365
+ assert.ok(memberTaskFile, "Execution Composite-member Task state file was not found");
366
+ const memberExecutionWorkspaceId = memberTaskFile.split("/").at(-2);
367
+ assertTaskBodyAbsentFromGateway(compositeMemberTaskBody);
368
+
369
+ const compositeOpened = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "open_workspace", {
370
+ kind: "composite",
371
+ name: `relay-acceptance-${randomUUID().slice(0, 8)}`,
372
+ context: "none",
373
+ }, conversationA);
374
+ assertToolOk(compositeOpened, "open Composite Workspace");
375
+ const compositeId = compositeOpened.structuredContent.workspaceId;
376
+
377
+ for (const member of [
378
+ { name: "local", purpose: "Gateway local member", workspaceId: localWorkspaceId },
379
+ { name: "remote", purpose: "Execution Relay member", workspaceId: compositeRelayMemberId },
380
+ ]) {
381
+ const mounted = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "open_workspace", {
382
+ action: "member",
383
+ workspaceId: compositeId,
384
+ memberAction: "add",
385
+ member,
386
+ }, conversationA);
387
+ assertToolOk(mounted, `mount Composite member ${member.name}`);
388
+ }
389
+
390
+ const localRead = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "read", {
391
+ workspaceId: compositeId,
392
+ member: "local",
393
+ path: "sentinel.txt",
394
+ }, conversationA);
395
+ assertToolOk(localRead, "read local Composite member");
396
+ assert.match(toolText(localRead), /gateway-local-content/);
397
+
398
+ const remoteRead = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "read", {
399
+ workspaceId: compositeId,
400
+ member: "remote",
401
+ path: "sentinel.txt",
402
+ }, conversationA);
403
+ assertToolOk(remoteRead, "read relayed Composite member");
404
+ assert.match(toolText(remoteRead), /execution-remote-content/);
405
+ assert.doesNotMatch(toolText(remoteRead), /gateway-local-content/);
406
+
407
+ const compositeList = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "capability", {
408
+ workspaceId: compositeId,
409
+ name: "workspace.tasks",
410
+ action: "run",
411
+ arguments: { operation: "list.create", name: "Composite own acceptance" },
412
+ }, conversationA);
413
+ assertToolOk(compositeList, "create Composite-owned Task List");
414
+ const compositeListId = compositeList.structuredContent.result.lists[0].id;
415
+ const compositeTask = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "capability", {
416
+ workspaceId: compositeId,
417
+ name: "workspace.tasks",
418
+ action: "run",
419
+ arguments: {
420
+ operation: "task.create",
421
+ listId: compositeListId,
422
+ subject: "Composite own Task",
423
+ content: compositeTaskBody,
424
+ status: "in_progress",
425
+ },
426
+ }, conversationA);
427
+ assertToolOk(compositeTask, "create Composite-owned Task");
428
+ const compositeTaskFile = findTaskStateFile(gatewayStateDir, compositeTaskBody);
429
+ assert.ok(compositeTaskFile, "Gateway Composite-owned Task state file was not found");
430
+ assert.equal(compositeTaskFile.split("/").at(-2), compositeId);
431
+ assert.equal(readFileSync(memberTaskFile, "utf8").includes(compositeTaskBody), false);
432
+
433
+ const compositeInspection = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "open_workspace", {
434
+ action: "inspect",
435
+ workspaceId: compositeId,
436
+ }, conversationB);
437
+ assertToolOk(compositeInspection, "inspect Composite Workspace");
438
+ assert.equal(compositeInspection.structuredContent.inspection.taskSummary.lists[0].unfinishedTaskCount, 1);
439
+ assert.equal(JSON.stringify(compositeInspection).includes(compositeMemberTaskBody), false);
440
+
441
+ const memberInspection = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "open_workspace", {
442
+ action: "inspect",
443
+ workspaceId: compositeRelayMemberId,
444
+ }, conversationB);
445
+ assertToolOk(memberInspection, "inspect relayed Composite member");
446
+ assertSafeRelayInspection(memberInspection, {
447
+ executionWorkspaceId: memberExecutionWorkspaceId,
448
+ remoteRecord,
449
+ taskBodies: [compositeMemberTaskBody],
450
+ });
451
+
452
+ const closedMember = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "close_workspace", {
453
+ workspaceId: compositeRelayMemberId,
454
+ }, conversationB);
455
+ assertToolOk(closedMember, "close relayed Composite member");
456
+ const closedMemberRead = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "read", {
457
+ workspaceId: compositeId,
458
+ member: "remote",
459
+ path: "sentinel.txt",
460
+ }, conversationA);
461
+ assert.equal(closedMemberRead.isError, true);
462
+
463
+ const inspectedClosedMember = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "open_workspace", {
464
+ action: "inspect",
465
+ workspaceId: compositeRelayMemberId,
466
+ }, conversationB);
467
+ assertToolOk(inspectedClosedMember, "inspect closed relayed Composite member");
468
+ assert.equal(inspectedClosedMember.structuredContent.inspection.state, "closed");
469
+ assert.equal(inspectedClosedMember.structuredContent.inspection.taskSummary.lists[0].unfinishedTaskCount, 1);
470
+
471
+ const reopenedMember = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "open_workspace", {
472
+ workspaceId: compositeRelayMemberId,
473
+ context: "none",
474
+ }, conversationB);
475
+ assertToolOk(reopenedMember, "reopen relayed Composite member");
476
+ assert.equal(reopenedMember.structuredContent.workspaceId, compositeRelayMemberId);
477
+ const reopenedMemberRead = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "read", {
478
+ workspaceId: compositeId,
479
+ member: "remote",
480
+ path: "sentinel.txt",
481
+ }, conversationA);
482
+ assertToolOk(reopenedMemberRead, "read reopened relayed Composite member");
483
+ assert.match(toolText(reopenedMemberRead), /execution-remote-content/);
484
+ const restoredMemberTasks = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "capability", {
485
+ workspaceId: compositeRelayMemberId,
486
+ name: "workspace.tasks",
487
+ action: "run",
488
+ arguments: { operation: "get", level: "headers", listId: memberListId },
489
+ }, conversationB);
490
+ assertToolOk(restoredMemberTasks, "restore relayed Composite member Tasks");
491
+ assert.equal(restoredMemberTasks.structuredContent.result.lists[0].tasks[0].subject, "Remote Composite member Task");
492
+ assertTaskBodyAbsentFromGateway(compositeMemberTaskBody);
493
+ pass("Composite local + Relay member", "member execution/lifecycle/Task truth stayed with owning Workspace");
494
+
495
+ const deleteComposite = callTool(gatewayMcpUrl, oauth.accessToken, sessionA, nextId(), "close_workspace", {
496
+ workspaceId: compositeId,
497
+ action: "delete",
498
+ }, conversationA);
499
+ assertToolOk(deleteComposite, "delete Composite Workspace");
500
+ const deleteRemoteMember = callTool(gatewayMcpUrl, oauth.accessToken, sessionB, nextId(), "close_workspace", {
501
+ workspaceId: compositeRelayMemberId,
502
+ action: "delete",
503
+ }, conversationB);
504
+ assertToolOk(deleteRemoteMember, "delete relayed Composite member Workspace");
505
+
506
+ writeFileSync(join(acceptanceRoot, "result.json"), `${JSON.stringify({
507
+ passed: true,
508
+ gateway: gatewayBaseUrl,
509
+ execution: executionBaseUrl,
510
+ ports: [gatewayPort, executionPort],
511
+ verified: [
512
+ "persistent relay checkout identity across Gateway conversations",
513
+ "Execution-owned Task state and body-free reminders",
514
+ "safe relayed inspection projection",
515
+ "close/reopen/delete parity",
516
+ "managed-worktree finalize/reopen/delete parity",
517
+ "Composite local + relayed member lifecycle and Task ownership",
518
+ "no 7676 interaction",
519
+ ],
520
+ }, null, 2)}\n`);
521
+ completed = true;
522
+ } catch (error) {
523
+ console.error("\nForgeRelay 7677/7678 Relay acceptance failed.");
524
+ throw error;
525
+ } finally {
526
+ if (gateway) await stopServer(gateway);
527
+ await stopServer(execution);
528
+ }
529
+
530
+ if (completed) {
531
+ console.log("\nForgeRelay 7677/7678 Relay acceptance passed.");
532
+ console.log(`Artifacts: ${acceptanceRoot}`);
533
+ }
534
+
535
+ function cleanProductEnv() {
536
+ return Object.fromEntries(Object.entries(process.env).filter(([name]) =>
537
+ name !== "HOST"
538
+ && name !== "PORT"
539
+ && !name.startsWith("FORGERELAY_")
540
+ ));
541
+ }
542
+
543
+ function instanceEnv({ port, baseUrl, configDir, stateDir, worktreeRoot, allowedRoot, ownerToken }) {
544
+ return {
545
+ ...cleanProductEnv(),
546
+ HOST: "127.0.0.1",
547
+ PORT: String(port),
548
+ FORGERELAY_CONFIG_DIR: configDir,
549
+ FORGERELAY_PUBLIC_BASE_URL: baseUrl,
550
+ FORGERELAY_ALLOWED_ROOTS: allowedRoot,
551
+ FORGERELAY_STATE_DIR: stateDir,
552
+ FORGERELAY_WORKTREE_ROOT: worktreeRoot,
553
+ FORGERELAY_OAUTH_OWNER_TOKEN: ownerToken,
554
+ FORGERELAY_TOOL_MODE: "full",
555
+ FORGERELAY_WIDGETS: "off",
556
+ FORGERELAY_SKILLS: "0",
557
+ FORGERELAY_SUBAGENTS: "0",
558
+ FORGERELAY_ARTIFACTS: "0",
559
+ FORGERELAY_TASK_REMINDER_INTERVAL: "3",
560
+ FORGERELAY_LOG_LEVEL: process.env.FORGERELAY_LOG_LEVEL ?? "warn",
561
+ FORGERELAY_LOG_FORMAT: process.env.FORGERELAY_LOG_FORMAT ?? "pretty",
562
+ };
563
+ }
564
+
565
+ function writeAuthFile(configDir, ownerToken, instanceId) {
566
+ mkdirSync(configDir, { recursive: true });
567
+ writeFileSync(join(configDir, "auth.json"), `${JSON.stringify({ ownerToken, instanceId }, null, 2)}\n`, { mode: 0o600 });
568
+ }
569
+
570
+ function spawnServer(env, label) {
571
+ const child = spawn(process.execPath, ["--import", "tsx", "src/cli.ts", "serve"], {
572
+ cwd: repoRoot,
573
+ env,
574
+ stdio: ["ignore", "inherit", "inherit"],
575
+ });
576
+ child.once("exit", (code, signal) => {
577
+ if (code && code !== 0) console.error(`${label} exited with code ${code}${signal ? ` (${signal})` : ""}`);
578
+ });
579
+ return child;
580
+ }
581
+
582
+ function runCli(args, env) {
583
+ return spawnSync(process.execPath, ["--import", "tsx", "src/cli.ts", ...args], {
584
+ cwd: repoRoot,
585
+ env,
586
+ encoding: "utf8",
587
+ });
588
+ }
589
+
590
+ function authorizeHost(baseUrl, mcpUrl, ownerToken) {
591
+ const metadata = jsonRequest(`${baseUrl}/.well-known/oauth-authorization-server`);
592
+ assert.equal(metadata.status, 200);
593
+ const redirectUri = `${baseUrl}/debug/relay-callback`;
594
+ const registration = jsonRequest(metadata.json.registration_endpoint, {
595
+ method: "POST",
596
+ body: JSON.stringify({
597
+ client_name: "ForgeRelay 7677/7678 Relay acceptance",
598
+ redirect_uris: [redirectUri],
599
+ token_endpoint_auth_method: "none",
600
+ grant_types: ["authorization_code", "refresh_token"],
601
+ response_types: ["code"],
602
+ }),
603
+ headers: { "content-type": "application/json" },
604
+ });
605
+ assert.equal(registration.status, 201);
606
+
607
+ const verifier = randomBytes(32).toString("base64url");
608
+ const challenge = createHash("sha256").update(verifier).digest("base64url");
609
+ const authorization = curlRequest({
610
+ method: "POST",
611
+ url: metadata.json.authorization_endpoint,
612
+ headers: { "content-type": "application/x-www-form-urlencoded" },
613
+ body: new URLSearchParams({
614
+ response_type: "code",
615
+ client_id: registration.json.client_id,
616
+ redirect_uri: redirectUri,
617
+ code_challenge: challenge,
618
+ code_challenge_method: "S256",
619
+ scope: "devspace",
620
+ resource: mcpUrl,
621
+ state: "forgerelay-relay-acceptance",
622
+ owner_token: ownerToken,
623
+ }).toString(),
624
+ });
625
+ assert.equal(authorization.status, 302);
626
+ const redirect = new URL(authorization.headers.get("location"));
627
+ assert.equal(redirect.origin + redirect.pathname, redirectUri);
628
+ assert.equal(redirect.searchParams.get("state"), "forgerelay-relay-acceptance");
629
+ const code = redirect.searchParams.get("code");
630
+ assert.ok(code);
631
+
632
+ const token = jsonRequest(metadata.json.token_endpoint, {
633
+ method: "POST",
634
+ headers: { "content-type": "application/x-www-form-urlencoded" },
635
+ body: new URLSearchParams({
636
+ grant_type: "authorization_code",
637
+ client_id: registration.json.client_id,
638
+ code,
639
+ redirect_uri: redirectUri,
640
+ code_verifier: verifier,
641
+ resource: mcpUrl,
642
+ }).toString(),
643
+ });
644
+ assert.equal(token.status, 200);
645
+ assert.ok(token.json.access_token);
646
+ return { accessToken: token.json.access_token };
647
+ }
648
+
649
+ function initializeSession(mcpUrl, accessToken, id, clientName) {
650
+ const initialized = mcpRequest(mcpUrl, accessToken, undefined, {
651
+ jsonrpc: "2.0",
652
+ id,
653
+ method: "initialize",
654
+ params: {
655
+ protocolVersion: "2025-06-18",
656
+ capabilities: {},
657
+ clientInfo: { name: clientName, version: "1.0.0" },
658
+ },
659
+ });
660
+ const sessionId = initialized.response.headers.get("mcp-session-id");
661
+ assert.ok(sessionId);
662
+ const notification = curlRequest({
663
+ method: "POST",
664
+ url: mcpUrl,
665
+ headers: mcpHeaders(accessToken, sessionId),
666
+ body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }),
667
+ });
668
+ assert.equal(notification.status, 202);
669
+ return sessionId;
670
+ }
671
+
672
+ function callTool(mcpUrl, accessToken, sessionId, id, name, args, meta) {
673
+ const message = mcpRequest(mcpUrl, accessToken, sessionId, {
674
+ jsonrpc: "2.0",
675
+ id,
676
+ method: "tools/call",
677
+ params: {
678
+ name,
679
+ arguments: args,
680
+ ...(meta ? { _meta: meta } : {}),
681
+ },
682
+ }).message;
683
+ assert.equal(message.id, id);
684
+ assert.ok(message.result, `tool ${name} did not return a result`);
685
+ return message.result;
686
+ }
687
+
688
+ function mcpRequest(mcpUrl, accessToken, sessionId, request) {
689
+ const response = curlRequest({
690
+ method: "POST",
691
+ url: mcpUrl,
692
+ headers: mcpHeaders(accessToken, sessionId),
693
+ body: JSON.stringify(request),
694
+ });
695
+ assert.equal(response.status, 200, response.body);
696
+ return { response, message: parseMcpMessage(response.body, request.id) };
697
+ }
698
+
699
+ function mcpHeaders(accessToken, sessionId) {
700
+ return {
701
+ ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),
702
+ ...(sessionId ? { "mcp-session-id": sessionId } : {}),
703
+ "content-type": "application/json",
704
+ accept: "application/json, text/event-stream",
705
+ };
706
+ }
707
+
708
+ function parseMcpMessage(body, expectedId) {
709
+ const dataLines = body
710
+ .split(/\r?\n/)
711
+ .filter((line) => line.startsWith("data:"))
712
+ .map((line) => line.slice(5).trim())
713
+ .filter(Boolean);
714
+ const messages = dataLines.length > 0
715
+ ? dataLines.map((line) => JSON.parse(line))
716
+ : [JSON.parse(body)];
717
+ return messages.find((message) => message.id === expectedId) ?? messages[0];
718
+ }
719
+
720
+ function jsonRequest(url, options = {}) {
721
+ const response = curlRequest({
722
+ method: options.method ?? "GET",
723
+ url,
724
+ headers: options.headers,
725
+ body: options.body,
726
+ });
727
+ return { ...response, json: JSON.parse(response.body) };
728
+ }
729
+
730
+ function curlRequest({ method = "GET", url, headers = {}, body }) {
731
+ const marker = `__FORGERELAY_RELAY_ACCEPT_STATUS_${randomUUID()}__`;
732
+ const args = [
733
+ "--silent",
734
+ "--show-error",
735
+ "--max-time",
736
+ "20",
737
+ "--request",
738
+ method,
739
+ "--dump-header",
740
+ "-",
741
+ "--output",
742
+ "-",
743
+ "--write-out",
744
+ `\n${marker}%{http_code}`,
745
+ ];
746
+ for (const [name, value] of Object.entries(headers)) args.push("--header", `${name}: ${value}`);
747
+ if (body !== undefined) args.push("--data-binary", "@-");
748
+ args.push(url);
749
+ const result = spawnSync("curl", args, {
750
+ cwd: repoRoot,
751
+ input: body,
752
+ encoding: "utf8",
753
+ maxBuffer: 20 * 1024 * 1024,
754
+ });
755
+ if (result.status !== 0) {
756
+ throw new Error(`curl ${method} ${url} failed: ${result.stderr.trim() || `exit ${result.status}`}`);
757
+ }
758
+
759
+ const statusMarker = `\n${marker}`;
760
+ const markerIndex = result.stdout.lastIndexOf(statusMarker);
761
+ assert.notEqual(markerIndex, -1, `curl response did not contain status marker for ${url}`);
762
+ const rawResponse = result.stdout.slice(0, markerIndex);
763
+ const status = Number(result.stdout.slice(markerIndex + statusMarker.length).trim());
764
+ const separator = rawResponse.indexOf("\r\n\r\n") >= 0 ? "\r\n\r\n" : "\n\n";
765
+ const headerEnd = rawResponse.indexOf(separator);
766
+ assert.notEqual(headerEnd, -1, `curl response did not contain headers for ${url}`);
767
+ const headerBlock = rawResponse.slice(0, headerEnd);
768
+ const responseBody = rawResponse.slice(headerEnd + separator.length);
769
+ const responseHeaders = new Map();
770
+ for (const line of headerBlock.split(/\r?\n/).slice(1)) {
771
+ const colon = line.indexOf(":");
772
+ if (colon < 0) continue;
773
+ responseHeaders.set(line.slice(0, colon).trim().toLowerCase(), line.slice(colon + 1).trim());
774
+ }
775
+ return { status, headers: responseHeaders, body: responseBody };
776
+ }
777
+
778
+ async function waitForHealth(child, baseUrl, label) {
779
+ for (let attempt = 0; attempt < 80; attempt += 1) {
780
+ if (child.exitCode !== null) throw new Error(`${label} exited before health check: ${child.exitCode}`);
781
+ try {
782
+ const response = jsonRequest(`${baseUrl}/healthz`);
783
+ if (response.status === 200) return response.json;
784
+ } catch {
785
+ // Server is still starting.
786
+ }
787
+ await delay(100);
788
+ }
789
+ throw new Error(`${label} did not become healthy at ${baseUrl}`);
790
+ }
791
+
792
+ async function stopServer(child) {
793
+ if (!child || child.exitCode !== null) return;
794
+ child.kill("SIGTERM");
795
+ const exited = once(child, "exit");
796
+ await Promise.race([
797
+ exited,
798
+ delay(3000).then(() => {
799
+ if (child.exitCode === null) child.kill("SIGKILL");
800
+ }),
801
+ ]);
802
+ }
803
+
804
+ async function assertPortsFree(ports) {
805
+ for (const port of ports) {
806
+ const occupied = await new Promise((resolvePromise) => {
807
+ const socket = connect({ host: "127.0.0.1", port });
808
+ socket.once("connect", () => {
809
+ socket.destroy();
810
+ resolvePromise(true);
811
+ });
812
+ socket.once("error", () => resolvePromise(false));
813
+ });
814
+ assert.equal(occupied, false, `reserved debug port ${port} is already in use; refusing to touch the existing process`);
815
+ }
816
+ }
817
+
818
+ function setupGitProject(root) {
819
+ mkdirSync(root, { recursive: true });
820
+ writeFileSync(join(root, "README.md"), "Relay managed worktree acceptance\n");
821
+ runGit(root, ["init"]);
822
+ runGit(root, ["config", "user.email", "forgerelay-debug@example.com"]);
823
+ runGit(root, ["config", "user.name", "ForgeRelay Debug"]);
824
+ runGit(root, ["add", "."]);
825
+ runGit(root, ["commit", "-m", "Initial Relay acceptance commit"]);
826
+ }
827
+
828
+ function runGit(cwd, args) {
829
+ const result = spawnSync("git", args, { cwd, encoding: "utf8" });
830
+ if (result.status !== 0) {
831
+ throw new Error(`git ${args.join(" ")} failed: ${result.stderr.trim() || result.stdout.trim()}`);
832
+ }
833
+ }
834
+
835
+ function taskStateFiles(stateDir) {
836
+ const workspacesDir = join(stateDir, "workspaces");
837
+ if (!existsSync(workspacesDir)) return [];
838
+ const files = [];
839
+ for (const entry of readdirSync(workspacesDir, { withFileTypes: true })) {
840
+ if (!entry.isDirectory()) continue;
841
+ const candidate = join(workspacesDir, entry.name, "tasks.json");
842
+ if (existsSync(candidate)) files.push(candidate);
843
+ }
844
+ return files;
845
+ }
846
+
847
+ function findTaskStateFile(stateDir, needle) {
848
+ return taskStateFiles(stateDir).find((path) => readFileSync(path, "utf8").includes(needle));
849
+ }
850
+
851
+ function assertTaskBodyAbsentFromGateway(body) {
852
+ const leaked = taskStateFiles(gatewayStateDir).filter((path) => readFileSync(path, "utf8").includes(body));
853
+ assert.deepEqual(leaked, [], `Gateway stored an Execution-owned Task body in: ${leaked.join(", ")}`);
854
+ }
855
+
856
+ function assertSafeRelayInspection(result, { executionWorkspaceId, remoteRecord, taskBodies }) {
857
+ const json = JSON.stringify(result);
858
+ for (const forbidden of [
859
+ executionWorkspaceId,
860
+ executionBaseUrl,
861
+ `127.0.0.1:${executionPort}`,
862
+ executionOwnerToken,
863
+ remoteRecord.accessToken,
864
+ remoteRecord.refreshToken,
865
+ bootstrapSecret,
866
+ "remoteInstanceId",
867
+ "remoteWorkspaceId",
868
+ "sshRoute",
869
+ ...taskBodies,
870
+ ]) {
871
+ assert.equal(json.includes(forbidden), false, `Relay inspection leaked ${forbidden}`);
872
+ }
873
+ assert.doesNotMatch(json, /"ws_[0-9a-f]{10}"/);
874
+ }
875
+
876
+ function assertToolOk(result, label) {
877
+ assert.equal(result.isError, undefined, `${label}: ${toolText(result)}`);
878
+ }
879
+
880
+ function toolText(result) {
881
+ return (result.content ?? [])
882
+ .filter((entry) => entry.type === "text")
883
+ .map((entry) => entry.text)
884
+ .join("\n");
885
+ }
886
+
887
+ function pass(label, detail) {
888
+ console.log(`PASS ${label}: ${detail}`);
889
+ }