@tanstack/ai-acp 0.2.2 → 0.2.4
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/esm/adapters/compatible.js +305 -300
- package/dist/esm/adapters/compatible.js.map +1 -1
- package/dist/esm/adapters/projection.js +87 -69
- package/dist/esm/adapters/projection.js.map +1 -1
- package/dist/esm/index.js +2 -25
- package/dist/esm/messages/prompt.js +30 -33
- package/dist/esm/messages/prompt.js.map +1 -1
- package/dist/esm/permissions.js +44 -37
- package/dist/esm/permissions.js.map +1 -1
- package/dist/esm/session/acp-client.js +118 -134
- package/dist/esm/session/acp-client.js.map +1 -1
- package/dist/esm/session/sandbox-server.js +93 -105
- package/dist/esm/session/sandbox-server.js.map +1 -1
- package/dist/esm/stream/queue.js +57 -53
- package/dist/esm/stream/queue.js.map +1 -1
- package/dist/esm/stream/translate.js +287 -309
- package/dist/esm/stream/translate.js.map +1 -1
- package/dist/esm/transport/resolve.js +21 -32
- package/dist/esm/transport/resolve.js.map +1 -1
- package/dist/esm/transport/stdio.js +40 -49
- package/dist/esm/transport/stdio.js.map +1 -1
- package/dist/esm/transport/websocket.js +121 -120
- package/dist/esm/transport/websocket.js.map +1 -1
- package/package.json +8 -8
- package/src/adapters/compatible.ts +52 -1
- package/src/transport/websocket.ts +4 -1
- package/dist/esm/index.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissions.js","sources":["../../src/permissions.ts"],"sourcesContent":["import { approvalId } from '@tanstack/ai-sandbox'\nimport { matchBridgedToolName } from './stream/translate'\nimport type {\n AcpPermissionMode,\n AcpPermissionOutcome,\n AcpPermissionRequest,\n PermissionHandler,\n} from './types/acp-types'\n\nexport type { AcpPermissionMode, PermissionHandler }\n\nconst EDIT_KINDS = new Set(['edit', 'move', 'delete'])\n\nfunction pickOption(\n request: AcpPermissionRequest,\n kinds: Array<string>,\n): AcpPermissionOutcome {\n for (const kind of kinds) {\n const option = request.options.find((candidate) => candidate.kind === kind)\n if (option) return { outcome: 'selected', optionId: option.optionId }\n }\n return { outcome: 'cancelled' }\n}\n\nexport function resolvePermission(\n request: AcpPermissionRequest,\n mode: AcpPermissionMode,\n bridgedToolNames: ReadonlySet<string> | undefined,\n): AcpPermissionOutcome {\n const allow = () => pickOption(request, ['allow_once', 'allow_always'])\n const reject = () => pickOption(request, ['reject_once', 'reject_always'])\n\n if (\n matchBridgedToolName(request.toolCall.title, bridgedToolNames) !== undefined\n ) {\n return allow()\n }\n if (mode === 'bypassPermissions') return allow()\n if (mode === 'acceptEdits' && EDIT_KINDS.has(request.toolCall.kind ?? '')) {\n return allow()\n }\n return reject()\n}\n\nexport function resolveInteractivePermission(\n request: AcpPermissionRequest,\n mode: AcpPermissionMode,\n bridgedToolNames: ReadonlySet<string> | undefined,\n approvals: ReadonlyMap<string, boolean> | undefined,\n provider: string,\n): { outcome: AcpPermissionOutcome; approvalId?: string; title?: string } {\n const allow = (): AcpPermissionOutcome =>\n pickOption(request, ['allow_once', 'allow_always'])\n const reject = (): AcpPermissionOutcome =>\n pickOption(request, ['reject_once', 'reject_always'])\n const title = request.toolCall.title ?? request.toolCall.toolCallId\n\n if (matchBridgedToolName(title, bridgedToolNames) !== undefined) {\n return { outcome: allow() }\n }\n if (mode === 'bypassPermissions') return { outcome: allow() }\n if (mode === 'acceptEdits' && EDIT_KINDS.has(request.toolCall.kind ?? '')) {\n return { outcome: allow() }\n }\n\n const id = approvalId({ provider, kind: 'tool', target: title })\n const granted = approvals?.get(id)\n if (granted === true) return { outcome: allow() }\n if (granted === false) return { outcome: reject() }\n return { outcome: reject(), approvalId: id, title }\n}\n"],"
|
|
1
|
+
{"version":3,"file":"permissions.js","names":[],"sources":["../../src/permissions.ts"],"sourcesContent":["import { approvalId } from '@tanstack/ai-sandbox'\nimport { matchBridgedToolName } from './stream/translate'\nimport type {\n AcpPermissionMode,\n AcpPermissionOutcome,\n AcpPermissionRequest,\n PermissionHandler,\n} from './types/acp-types'\n\nexport type { AcpPermissionMode, PermissionHandler }\n\nconst EDIT_KINDS = new Set(['edit', 'move', 'delete'])\n\nfunction pickOption(\n request: AcpPermissionRequest,\n kinds: Array<string>,\n): AcpPermissionOutcome {\n for (const kind of kinds) {\n const option = request.options.find((candidate) => candidate.kind === kind)\n if (option) return { outcome: 'selected', optionId: option.optionId }\n }\n return { outcome: 'cancelled' }\n}\n\nexport function resolvePermission(\n request: AcpPermissionRequest,\n mode: AcpPermissionMode,\n bridgedToolNames: ReadonlySet<string> | undefined,\n): AcpPermissionOutcome {\n const allow = () => pickOption(request, ['allow_once', 'allow_always'])\n const reject = () => pickOption(request, ['reject_once', 'reject_always'])\n\n if (\n matchBridgedToolName(request.toolCall.title, bridgedToolNames) !== undefined\n ) {\n return allow()\n }\n if (mode === 'bypassPermissions') return allow()\n if (mode === 'acceptEdits' && EDIT_KINDS.has(request.toolCall.kind ?? '')) {\n return allow()\n }\n return reject()\n}\n\nexport function resolveInteractivePermission(\n request: AcpPermissionRequest,\n mode: AcpPermissionMode,\n bridgedToolNames: ReadonlySet<string> | undefined,\n approvals: ReadonlyMap<string, boolean> | undefined,\n provider: string,\n): { outcome: AcpPermissionOutcome; approvalId?: string; title?: string } {\n const allow = (): AcpPermissionOutcome =>\n pickOption(request, ['allow_once', 'allow_always'])\n const reject = (): AcpPermissionOutcome =>\n pickOption(request, ['reject_once', 'reject_always'])\n const title = request.toolCall.title ?? request.toolCall.toolCallId\n\n if (matchBridgedToolName(title, bridgedToolNames) !== undefined) {\n return { outcome: allow() }\n }\n if (mode === 'bypassPermissions') return { outcome: allow() }\n if (mode === 'acceptEdits' && EDIT_KINDS.has(request.toolCall.kind ?? '')) {\n return { outcome: allow() }\n }\n\n const id = approvalId({ provider, kind: 'tool', target: title })\n const granted = approvals?.get(id)\n if (granted === true) return { outcome: allow() }\n if (granted === false) return { outcome: reject() }\n return { outcome: reject(), approvalId: id, title }\n}\n"],"mappings":";;;AAWA,IAAM,6BAAa,IAAI,IAAI;CAAC;CAAQ;CAAQ;AAAQ,CAAC;AAErD,SAAS,WACP,SACA,OACsB;CACtB,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,SAAS,QAAQ,QAAQ,MAAM,cAAc,UAAU,SAAS,IAAI;EAC1E,IAAI,QAAQ,OAAO;GAAE,SAAS;GAAY,UAAU,OAAO;EAAS;CACtE;CACA,OAAO,EAAE,SAAS,YAAY;AAChC;AAEA,SAAgB,kBACd,SACA,MACA,kBACsB;CACtB,MAAM,cAAc,WAAW,SAAS,CAAC,cAAc,cAAc,CAAC;CACtE,MAAM,eAAe,WAAW,SAAS,CAAC,eAAe,eAAe,CAAC;CAEzE,IACE,qBAAqB,QAAQ,SAAS,OAAO,gBAAgB,MAAM,KAAA,GAEnE,OAAO,MAAM;CAEf,IAAI,SAAS,qBAAqB,OAAO,MAAM;CAC/C,IAAI,SAAS,iBAAiB,WAAW,IAAI,QAAQ,SAAS,QAAQ,EAAE,GACtE,OAAO,MAAM;CAEf,OAAO,OAAO;AAChB;AAEA,SAAgB,6BACd,SACA,MACA,kBACA,WACA,UACwE;CACxE,MAAM,cACJ,WAAW,SAAS,CAAC,cAAc,cAAc,CAAC;CACpD,MAAM,eACJ,WAAW,SAAS,CAAC,eAAe,eAAe,CAAC;CACtD,MAAM,QAAQ,QAAQ,SAAS,SAAS,QAAQ,SAAS;CAEzD,IAAI,qBAAqB,OAAO,gBAAgB,MAAM,KAAA,GACpD,OAAO,EAAE,SAAS,MAAM,EAAE;CAE5B,IAAI,SAAS,qBAAqB,OAAO,EAAE,SAAS,MAAM,EAAE;CAC5D,IAAI,SAAS,iBAAiB,WAAW,IAAI,QAAQ,SAAS,QAAQ,EAAE,GACtE,OAAO,EAAE,SAAS,MAAM,EAAE;CAG5B,MAAM,KAAK,WAAW;EAAE;EAAU,MAAM;EAAQ,QAAQ;CAAM,CAAC;CAC/D,MAAM,UAAU,WAAW,IAAI,EAAE;CACjC,IAAI,YAAY,MAAM,OAAO,EAAE,SAAS,MAAM,EAAE;CAChD,IAAI,YAAY,OAAO,OAAO,EAAE,SAAS,OAAO,EAAE;CAClD,OAAO;EAAE,SAAS,OAAO;EAAG,YAAY;EAAI;CAAM;AACpD"}
|
|
@@ -1,140 +1,124 @@
|
|
|
1
|
-
import { ClientSideConnection, PROTOCOL_VERSION, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
2
1
|
import { spawnHandleToAcpTransport } from "../transport/stdio.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
import { ClientSideConnection, PROTOCOL_VERSION, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
3
|
+
//#region src/session/acp-client.ts
|
|
4
|
+
/**
|
|
5
|
+
* Identifies this client to the agent in the ACP `initialize` handshake. The
|
|
6
|
+
* `version` is informational (sent in `clientInfo`); keep it loosely in step
|
|
7
|
+
* with the package version. `clientInfo` is currently optional in the protocol
|
|
8
|
+
* but will become required, so we always send it.
|
|
9
|
+
* ponytail: hardcoded — clientInfo is diagnostic; a stale patch version is harmless.
|
|
10
|
+
*/
|
|
11
|
+
var CLIENT_INFO = {
|
|
12
|
+
name: "@tanstack/ai-acp",
|
|
13
|
+
title: "TanStack AI",
|
|
14
|
+
version: "0.1.0"
|
|
7
15
|
};
|
|
8
16
|
function streamFromTransport(transport) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
if (transport.kind === "stdio") {
|
|
18
|
+
const byteTransport = spawnHandleToAcpTransport(transport.process);
|
|
19
|
+
return {
|
|
20
|
+
stream: ndJsonStream(byteTransport.writable, byteTransport.readable),
|
|
21
|
+
teardown: () => byteTransport.kill(),
|
|
22
|
+
exited: byteTransport.exited,
|
|
23
|
+
stderrTail: byteTransport.stderrTail
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
stream: transport.stream,
|
|
28
|
+
teardown: transport.dispose,
|
|
29
|
+
exited: void 0,
|
|
30
|
+
stderrTail: transport.stderrTail ?? (() => "")
|
|
31
|
+
};
|
|
24
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Drive an ACP harness over stdio or a pre-connected JSON-RPC stream.
|
|
35
|
+
*/
|
|
25
36
|
async function startAcpSession(options) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
sessionId,
|
|
111
|
-
resumed,
|
|
112
|
-
prompt: async (text) => {
|
|
113
|
-
const response = await race(
|
|
114
|
-
connection.prompt({
|
|
115
|
-
sessionId,
|
|
116
|
-
prompt: [{ type: "text", text }]
|
|
117
|
-
})
|
|
118
|
-
);
|
|
119
|
-
return {
|
|
120
|
-
stopReason: response.stopReason,
|
|
121
|
-
...response.usage != null && { usage: response.usage }
|
|
122
|
-
};
|
|
123
|
-
},
|
|
124
|
-
cancel: () => connection.cancel({ sessionId }),
|
|
125
|
-
dispose: teardown
|
|
126
|
-
};
|
|
127
|
-
} catch (error) {
|
|
128
|
-
await teardown();
|
|
129
|
-
const tail = stderrTail().trim();
|
|
130
|
-
if (error instanceof Error && tail !== "" && !error.message.includes(tail)) {
|
|
131
|
-
throw new Error(`${error.message}
|
|
132
|
-
stderr: ${tail}`, { cause: error });
|
|
133
|
-
}
|
|
134
|
-
throw error;
|
|
135
|
-
}
|
|
37
|
+
const { stream, teardown, exited, stderrTail } = streamFromTransport(options.transport);
|
|
38
|
+
let replaying = false;
|
|
39
|
+
const client = {
|
|
40
|
+
requestPermission: async (params) => {
|
|
41
|
+
return { outcome: await options.onPermissionRequest(params) };
|
|
42
|
+
},
|
|
43
|
+
sessionUpdate: (params) => {
|
|
44
|
+
if (!replaying) options.onUpdate(params.update);
|
|
45
|
+
return Promise.resolve();
|
|
46
|
+
},
|
|
47
|
+
extNotification: (method, params) => {
|
|
48
|
+
options.onExtNotification?.(method, params);
|
|
49
|
+
return Promise.resolve();
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const race = (work) => exited !== void 0 ? Promise.race([work, exited]) : work;
|
|
53
|
+
try {
|
|
54
|
+
const connection = new ClientSideConnection(() => client, stream);
|
|
55
|
+
const initResult = await race(connection.initialize({
|
|
56
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
57
|
+
clientInfo: CLIENT_INFO,
|
|
58
|
+
clientCapabilities: { fs: {
|
|
59
|
+
readTextFile: false,
|
|
60
|
+
writeTextFile: false
|
|
61
|
+
} }
|
|
62
|
+
}));
|
|
63
|
+
if (typeof initResult.protocolVersion === "number" && initResult.protocolVersion > PROTOCOL_VERSION) throw new Error(`ACP agent negotiated protocol version ${initResult.protocolVersion}, but this client supports up to ${PROTOCOL_VERSION}. Update @tanstack/ai-acp.`);
|
|
64
|
+
if (options.authMethodId !== void 0) {
|
|
65
|
+
const available = initResult.authMethods ?? [];
|
|
66
|
+
if (!available.some((method) => method.id === options.authMethodId)) throw new Error(`Harness does not advertise the ACP auth method '${options.authMethodId}'. Available: ${available.map((method) => method.id).join(", ") || "(none)"}.`);
|
|
67
|
+
await race(connection.authenticate({ methodId: options.authMethodId }));
|
|
68
|
+
}
|
|
69
|
+
const mcpServers = (options.mcpServers ?? []).map((server) => ({
|
|
70
|
+
type: "http",
|
|
71
|
+
name: server.name,
|
|
72
|
+
url: server.url,
|
|
73
|
+
headers: server.headers ?? []
|
|
74
|
+
}));
|
|
75
|
+
let sessionId;
|
|
76
|
+
let resumed = false;
|
|
77
|
+
if (options.resumeSessionId !== void 0 && initResult.agentCapabilities?.loadSession === true) {
|
|
78
|
+
replaying = true;
|
|
79
|
+
try {
|
|
80
|
+
await race(connection.loadSession({
|
|
81
|
+
sessionId: options.resumeSessionId,
|
|
82
|
+
cwd: options.cwd,
|
|
83
|
+
mcpServers
|
|
84
|
+
}));
|
|
85
|
+
sessionId = options.resumeSessionId;
|
|
86
|
+
resumed = true;
|
|
87
|
+
} catch {} finally {
|
|
88
|
+
replaying = false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (sessionId === void 0) sessionId = (await race(connection.newSession({
|
|
92
|
+
cwd: options.cwd,
|
|
93
|
+
mcpServers
|
|
94
|
+
}))).sessionId;
|
|
95
|
+
return {
|
|
96
|
+
sessionId,
|
|
97
|
+
resumed,
|
|
98
|
+
prompt: async (text) => {
|
|
99
|
+
const response = await race(connection.prompt({
|
|
100
|
+
sessionId,
|
|
101
|
+
prompt: [{
|
|
102
|
+
type: "text",
|
|
103
|
+
text
|
|
104
|
+
}]
|
|
105
|
+
}));
|
|
106
|
+
return {
|
|
107
|
+
stopReason: response.stopReason,
|
|
108
|
+
...response.usage != null && { usage: response.usage }
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
cancel: () => connection.cancel({ sessionId }),
|
|
112
|
+
dispose: teardown
|
|
113
|
+
};
|
|
114
|
+
} catch (error) {
|
|
115
|
+
await teardown();
|
|
116
|
+
const tail = stderrTail().trim();
|
|
117
|
+
if (error instanceof Error && tail !== "" && !error.message.includes(tail)) throw new Error(`${error.message}\nstderr: ${tail}`, { cause: error });
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
136
120
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
//# sourceMappingURL=acp-client.js.map
|
|
121
|
+
//#endregion
|
|
122
|
+
export { startAcpSession };
|
|
123
|
+
|
|
124
|
+
//# sourceMappingURL=acp-client.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"acp-client.js","sources":["../../../src/session/acp-client.ts"],"sourcesContent":["import {\n ClientSideConnection,\n PROTOCOL_VERSION,\n ndJsonStream,\n} from '@agentclientprotocol/sdk'\nimport { spawnHandleToAcpTransport } from '../transport/stdio'\nimport type {\n Client,\n McpServer,\n RequestPermissionRequest,\n RequestPermissionResponse,\n SessionNotification,\n} from '@agentclientprotocol/sdk'\nimport type {\n AcpPermissionOutcome,\n AcpPermissionRequest,\n AcpSessionUpdate,\n AcpStopReason,\n AcpUsage,\n} from '../types/acp-types'\nimport type { AcpJsonRpcStream, AcpSessionTransport } from '../transport/types'\n\n/**\n * Identifies this client to the agent in the ACP `initialize` handshake. The\n * `version` is informational (sent in `clientInfo`); keep it loosely in step\n * with the package version. `clientInfo` is currently optional in the protocol\n * but will become required, so we always send it.\n * ponytail: hardcoded — clientInfo is diagnostic; a stale patch version is harmless.\n */\nconst CLIENT_INFO = {\n name: '@tanstack/ai-acp',\n title: 'TanStack AI',\n version: '0.1.0',\n} as const\n\nexport interface AcpSessionHandle {\n sessionId: string\n resumed: boolean\n prompt: (\n text: string,\n ) => Promise<{ stopReason: AcpStopReason; usage?: AcpUsage }>\n cancel: () => Promise<void>\n dispose: () => Promise<void>\n}\n\nexport interface StartAcpSessionOptions {\n transport: AcpSessionTransport\n cwd: string\n authMethodId?: string\n mcpServers?: Array<{\n name: string\n url: string\n headers?: Array<{ name: string; value: string }>\n }>\n resumeSessionId?: string\n onUpdate: (update: AcpSessionUpdate) => void\n onPermissionRequest: (\n request: AcpPermissionRequest,\n ) => Promise<AcpPermissionOutcome> | AcpPermissionOutcome\n /**\n * Harness-specific JSON-RPC notifications (e.g. Grok `_x.ai/session_notification`).\n * Return without throwing — unknown vendor extensions must not tear down the session.\n */\n onExtNotification?: (method: string, params: Record<string, unknown>) => void\n}\n\nfunction streamFromTransport(transport: AcpSessionTransport): {\n stream: AcpJsonRpcStream\n teardown: () => Promise<void>\n exited: Promise<never> | undefined\n stderrTail: () => string\n} {\n if (transport.kind === 'stdio') {\n const byteTransport = spawnHandleToAcpTransport(transport.process)\n return {\n stream: ndJsonStream(byteTransport.writable, byteTransport.readable),\n teardown: () => byteTransport.kill(),\n exited: byteTransport.exited,\n stderrTail: byteTransport.stderrTail,\n }\n }\n\n return {\n stream: transport.stream,\n teardown: transport.dispose,\n exited: undefined,\n stderrTail: transport.stderrTail ?? (() => ''),\n }\n}\n\n/**\n * Drive an ACP harness over stdio or a pre-connected JSON-RPC stream.\n */\nexport async function startAcpSession(\n options: StartAcpSessionOptions,\n): Promise<AcpSessionHandle> {\n const { stream, teardown, exited, stderrTail } = streamFromTransport(\n options.transport,\n )\n\n let replaying = false\n\n const client: Client = {\n requestPermission: async (\n params: RequestPermissionRequest,\n ): Promise<RequestPermissionResponse> => {\n const outcome = await options.onPermissionRequest(params)\n return { outcome }\n },\n sessionUpdate: (params: SessionNotification): Promise<void> => {\n if (!replaying) {\n options.onUpdate(params.update as AcpSessionUpdate)\n }\n return Promise.resolve()\n },\n extNotification: (method, params) => {\n options.onExtNotification?.(method, params)\n return Promise.resolve()\n },\n }\n\n const race = <T>(work: Promise<T>): Promise<T> =>\n exited !== undefined ? Promise.race([work, exited]) : work\n\n try {\n const connection = new ClientSideConnection(() => client, stream)\n\n const initResult = await race(\n connection.initialize({\n protocolVersion: PROTOCOL_VERSION,\n clientInfo: CLIENT_INFO,\n clientCapabilities: {\n // The agent runs inside the sandbox with direct filesystem + shell\n // access, so it never needs to delegate file/terminal I/O back to the\n // client. We advertise these as unsupported; per the ACP spec the\n // agent MUST then treat them as unavailable and not call them.\n fs: { readTextFile: false, writeTextFile: false },\n },\n }),\n )\n\n // Protocol-version negotiation: the agent echoes the version it will speak.\n // The spec says a client SHOULD close the connection if that version is one\n // it doesn't support. We only implement the current PROTOCOL_VERSION, so a\n // higher number means the agent needs a newer client than this one.\n if (\n typeof initResult.protocolVersion === 'number' &&\n initResult.protocolVersion > PROTOCOL_VERSION\n ) {\n throw new Error(\n `ACP agent negotiated protocol version ${initResult.protocolVersion}, ` +\n `but this client supports up to ${PROTOCOL_VERSION}. Update @tanstack/ai-acp.`,\n )\n }\n\n if (options.authMethodId !== undefined) {\n const available = initResult.authMethods ?? []\n if (!available.some((method) => method.id === options.authMethodId)) {\n throw new Error(\n `Harness does not advertise the ACP auth method '${options.authMethodId}'. Available: ${\n available.map((method) => method.id).join(', ') || '(none)'\n }.`,\n )\n }\n await race(connection.authenticate({ methodId: options.authMethodId }))\n }\n\n const mcpServers: Array<McpServer> = (options.mcpServers ?? []).map(\n (server) => ({\n type: 'http' as const,\n name: server.name,\n url: server.url,\n headers: server.headers ?? [],\n }),\n )\n\n let sessionId: string | undefined\n let resumed = false\n if (\n options.resumeSessionId !== undefined &&\n initResult.agentCapabilities?.loadSession === true\n ) {\n replaying = true\n try {\n await race(\n connection.loadSession({\n sessionId: options.resumeSessionId,\n cwd: options.cwd,\n mcpServers,\n }),\n )\n sessionId = options.resumeSessionId\n resumed = true\n } catch {\n // Session unknown — fall through to a fresh one.\n } finally {\n replaying = false\n }\n }\n\n if (sessionId === undefined) {\n const session = await race(\n connection.newSession({ cwd: options.cwd, mcpServers }),\n )\n sessionId = session.sessionId\n }\n\n return {\n sessionId,\n resumed,\n prompt: async (text: string) => {\n const response = await race(\n connection.prompt({\n sessionId,\n prompt: [{ type: 'text', text }],\n }),\n )\n return {\n stopReason: response.stopReason,\n ...(response.usage != null && { usage: response.usage }),\n }\n },\n cancel: () => connection.cancel({ sessionId }),\n dispose: teardown,\n }\n } catch (error) {\n await teardown()\n const tail = stderrTail().trim()\n if (\n error instanceof Error &&\n tail !== '' &&\n !error.message.includes(tail)\n ) {\n throw new Error(`${error.message}\\nstderr: ${tail}`, { cause: error })\n }\n throw error\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"acp-client.js","names":[],"sources":["../../../src/session/acp-client.ts"],"sourcesContent":["import {\n ClientSideConnection,\n PROTOCOL_VERSION,\n ndJsonStream,\n} from '@agentclientprotocol/sdk'\nimport { spawnHandleToAcpTransport } from '../transport/stdio'\nimport type {\n Client,\n McpServer,\n RequestPermissionRequest,\n RequestPermissionResponse,\n SessionNotification,\n} from '@agentclientprotocol/sdk'\nimport type {\n AcpPermissionOutcome,\n AcpPermissionRequest,\n AcpSessionUpdate,\n AcpStopReason,\n AcpUsage,\n} from '../types/acp-types'\nimport type { AcpJsonRpcStream, AcpSessionTransport } from '../transport/types'\n\n/**\n * Identifies this client to the agent in the ACP `initialize` handshake. The\n * `version` is informational (sent in `clientInfo`); keep it loosely in step\n * with the package version. `clientInfo` is currently optional in the protocol\n * but will become required, so we always send it.\n * ponytail: hardcoded — clientInfo is diagnostic; a stale patch version is harmless.\n */\nconst CLIENT_INFO = {\n name: '@tanstack/ai-acp',\n title: 'TanStack AI',\n version: '0.1.0',\n} as const\n\nexport interface AcpSessionHandle {\n sessionId: string\n resumed: boolean\n prompt: (\n text: string,\n ) => Promise<{ stopReason: AcpStopReason; usage?: AcpUsage }>\n cancel: () => Promise<void>\n dispose: () => Promise<void>\n}\n\nexport interface StartAcpSessionOptions {\n transport: AcpSessionTransport\n cwd: string\n authMethodId?: string\n mcpServers?: Array<{\n name: string\n url: string\n headers?: Array<{ name: string; value: string }>\n }>\n resumeSessionId?: string\n onUpdate: (update: AcpSessionUpdate) => void\n onPermissionRequest: (\n request: AcpPermissionRequest,\n ) => Promise<AcpPermissionOutcome> | AcpPermissionOutcome\n /**\n * Harness-specific JSON-RPC notifications (e.g. Grok `_x.ai/session_notification`).\n * Return without throwing — unknown vendor extensions must not tear down the session.\n */\n onExtNotification?: (method: string, params: Record<string, unknown>) => void\n}\n\nfunction streamFromTransport(transport: AcpSessionTransport): {\n stream: AcpJsonRpcStream\n teardown: () => Promise<void>\n exited: Promise<never> | undefined\n stderrTail: () => string\n} {\n if (transport.kind === 'stdio') {\n const byteTransport = spawnHandleToAcpTransport(transport.process)\n return {\n stream: ndJsonStream(byteTransport.writable, byteTransport.readable),\n teardown: () => byteTransport.kill(),\n exited: byteTransport.exited,\n stderrTail: byteTransport.stderrTail,\n }\n }\n\n return {\n stream: transport.stream,\n teardown: transport.dispose,\n exited: undefined,\n stderrTail: transport.stderrTail ?? (() => ''),\n }\n}\n\n/**\n * Drive an ACP harness over stdio or a pre-connected JSON-RPC stream.\n */\nexport async function startAcpSession(\n options: StartAcpSessionOptions,\n): Promise<AcpSessionHandle> {\n const { stream, teardown, exited, stderrTail } = streamFromTransport(\n options.transport,\n )\n\n let replaying = false\n\n const client: Client = {\n requestPermission: async (\n params: RequestPermissionRequest,\n ): Promise<RequestPermissionResponse> => {\n const outcome = await options.onPermissionRequest(params)\n return { outcome }\n },\n sessionUpdate: (params: SessionNotification): Promise<void> => {\n if (!replaying) {\n options.onUpdate(params.update as AcpSessionUpdate)\n }\n return Promise.resolve()\n },\n extNotification: (method, params) => {\n options.onExtNotification?.(method, params)\n return Promise.resolve()\n },\n }\n\n const race = <T>(work: Promise<T>): Promise<T> =>\n exited !== undefined ? Promise.race([work, exited]) : work\n\n try {\n const connection = new ClientSideConnection(() => client, stream)\n\n const initResult = await race(\n connection.initialize({\n protocolVersion: PROTOCOL_VERSION,\n clientInfo: CLIENT_INFO,\n clientCapabilities: {\n // The agent runs inside the sandbox with direct filesystem + shell\n // access, so it never needs to delegate file/terminal I/O back to the\n // client. We advertise these as unsupported; per the ACP spec the\n // agent MUST then treat them as unavailable and not call them.\n fs: { readTextFile: false, writeTextFile: false },\n },\n }),\n )\n\n // Protocol-version negotiation: the agent echoes the version it will speak.\n // The spec says a client SHOULD close the connection if that version is one\n // it doesn't support. We only implement the current PROTOCOL_VERSION, so a\n // higher number means the agent needs a newer client than this one.\n if (\n typeof initResult.protocolVersion === 'number' &&\n initResult.protocolVersion > PROTOCOL_VERSION\n ) {\n throw new Error(\n `ACP agent negotiated protocol version ${initResult.protocolVersion}, ` +\n `but this client supports up to ${PROTOCOL_VERSION}. Update @tanstack/ai-acp.`,\n )\n }\n\n if (options.authMethodId !== undefined) {\n const available = initResult.authMethods ?? []\n if (!available.some((method) => method.id === options.authMethodId)) {\n throw new Error(\n `Harness does not advertise the ACP auth method '${options.authMethodId}'. Available: ${\n available.map((method) => method.id).join(', ') || '(none)'\n }.`,\n )\n }\n await race(connection.authenticate({ methodId: options.authMethodId }))\n }\n\n const mcpServers: Array<McpServer> = (options.mcpServers ?? []).map(\n (server) => ({\n type: 'http' as const,\n name: server.name,\n url: server.url,\n headers: server.headers ?? [],\n }),\n )\n\n let sessionId: string | undefined\n let resumed = false\n if (\n options.resumeSessionId !== undefined &&\n initResult.agentCapabilities?.loadSession === true\n ) {\n replaying = true\n try {\n await race(\n connection.loadSession({\n sessionId: options.resumeSessionId,\n cwd: options.cwd,\n mcpServers,\n }),\n )\n sessionId = options.resumeSessionId\n resumed = true\n } catch {\n // Session unknown — fall through to a fresh one.\n } finally {\n replaying = false\n }\n }\n\n if (sessionId === undefined) {\n const session = await race(\n connection.newSession({ cwd: options.cwd, mcpServers }),\n )\n sessionId = session.sessionId\n }\n\n return {\n sessionId,\n resumed,\n prompt: async (text: string) => {\n const response = await race(\n connection.prompt({\n sessionId,\n prompt: [{ type: 'text', text }],\n }),\n )\n return {\n stopReason: response.stopReason,\n ...(response.usage != null && { usage: response.usage }),\n }\n },\n cancel: () => connection.cancel({ sessionId }),\n dispose: teardown,\n }\n } catch (error) {\n await teardown()\n const tail = stderrTail().trim()\n if (\n error instanceof Error &&\n tail !== '' &&\n !error.message.includes(tail)\n ) {\n throw new Error(`${error.message}\\nstderr: ${tail}`, { cause: error })\n }\n throw error\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,114 +1,102 @@
|
|
|
1
1
|
import { connectAcpWebSocket } from "../transport/websocket.js";
|
|
2
|
-
|
|
2
|
+
//#region src/session/sandbox-server.ts
|
|
3
|
+
var DEFAULT_READY_MARKER = "WebSocket URL:";
|
|
3
4
|
function waitForReady(proc, options) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
for await (const chunk of proc.stdout) {
|
|
62
|
-
stdout += chunk;
|
|
63
|
-
tryFinish();
|
|
64
|
-
if (state.settled) return;
|
|
65
|
-
}
|
|
66
|
-
} catch (error) {
|
|
67
|
-
settle(() => reject(error));
|
|
68
|
-
return;
|
|
69
|
-
} finally {
|
|
70
|
-
stdoutDone = true;
|
|
71
|
-
tryFinish();
|
|
72
|
-
}
|
|
73
|
-
})();
|
|
74
|
-
});
|
|
5
|
+
const readyMarker = options.readyMarker ?? DEFAULT_READY_MARKER;
|
|
6
|
+
const isReady = options.isReady ?? ((output) => output.includes(readyMarker));
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
let stdout = "";
|
|
9
|
+
let stderr = "";
|
|
10
|
+
const state = { settled: false };
|
|
11
|
+
const settle = (fn) => {
|
|
12
|
+
if (state.settled) return;
|
|
13
|
+
state.settled = true;
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
fn();
|
|
16
|
+
};
|
|
17
|
+
const diagnostics = () => [stdout, stderr].map((s) => s.trim()).filter(Boolean).join("\n").slice(-500);
|
|
18
|
+
const combined = () => stdout + stderr;
|
|
19
|
+
let stdoutDone = false;
|
|
20
|
+
let stderrDone = false;
|
|
21
|
+
const tryFinish = () => {
|
|
22
|
+
if (state.settled) return;
|
|
23
|
+
if (isReady(combined())) {
|
|
24
|
+
settle(() => resolve({
|
|
25
|
+
stdout,
|
|
26
|
+
stderr
|
|
27
|
+
}));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (stdoutDone && stderrDone) settle(() => reject(/* @__PURE__ */ new Error(`ACP server exited before becoming ready${diagnostics() ? `: ${diagnostics()}` : " (no output)"}`)));
|
|
31
|
+
};
|
|
32
|
+
const timer = setTimeout(() => settle(() => reject(/* @__PURE__ */ new Error(`ACP server did not become ready within ${options.timeoutMs ?? 3e4}ms${diagnostics() ? `: ${diagnostics()}` : ""}`))), options.timeoutMs ?? 3e4);
|
|
33
|
+
(async () => {
|
|
34
|
+
try {
|
|
35
|
+
for await (const chunk of proc.stderr) {
|
|
36
|
+
stderr += chunk;
|
|
37
|
+
tryFinish();
|
|
38
|
+
if (state.settled) return;
|
|
39
|
+
}
|
|
40
|
+
} catch {} finally {
|
|
41
|
+
stderrDone = true;
|
|
42
|
+
tryFinish();
|
|
43
|
+
}
|
|
44
|
+
})();
|
|
45
|
+
(async () => {
|
|
46
|
+
try {
|
|
47
|
+
for await (const chunk of proc.stdout) {
|
|
48
|
+
stdout += chunk;
|
|
49
|
+
tryFinish();
|
|
50
|
+
if (state.settled) return;
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
settle(() => reject(error));
|
|
54
|
+
return;
|
|
55
|
+
} finally {
|
|
56
|
+
stdoutDone = true;
|
|
57
|
+
tryFinish();
|
|
58
|
+
}
|
|
59
|
+
})();
|
|
60
|
+
});
|
|
75
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Boot a harness ACP WebSocket server inside a sandbox and expose its port via
|
|
64
|
+
* {@link SandboxHandle.ports.connect}. Mirrors the `opencode serve` pattern.
|
|
65
|
+
*/
|
|
76
66
|
async function startAcpServerInSandbox(sandbox, options) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
67
|
+
const proc = await sandbox.process.spawn(options.command, {
|
|
68
|
+
cwd: options.cwd,
|
|
69
|
+
...options.env ? { env: options.env } : {},
|
|
70
|
+
...options.signal ? { signal: options.signal } : {}
|
|
71
|
+
});
|
|
72
|
+
const { stdout } = await waitForReady(proc, options);
|
|
73
|
+
const channel = await sandbox.ports.connect(options.port);
|
|
74
|
+
const wsUrl = options.buildWsUrl({
|
|
75
|
+
channel,
|
|
76
|
+
port: options.port,
|
|
77
|
+
stdout
|
|
78
|
+
});
|
|
79
|
+
const headers = channel.headers ?? (channel.token ? { Authorization: `Bearer ${channel.token}` } : void 0);
|
|
80
|
+
return {
|
|
81
|
+
wsUrl,
|
|
82
|
+
channel,
|
|
83
|
+
connect: (signal) => connectAcpWebSocket(wsUrl, {
|
|
84
|
+
...headers ? { headers } : {},
|
|
85
|
+
...signal ? { signal } : {},
|
|
86
|
+
framing: options.framing ?? "frame"
|
|
87
|
+
}),
|
|
88
|
+
dispose: () => proc.kill()
|
|
89
|
+
};
|
|
100
90
|
}
|
|
91
|
+
/** Parse a `WebSocket URL: ws://…` line printed by `grok agent serve`. */
|
|
101
92
|
function parseWebSocketUrlFromServeOutput(output) {
|
|
102
|
-
|
|
103
|
-
return match?.[1];
|
|
93
|
+
return output.match(/WebSocket URL:\s*(ws\S+)/i)?.[1];
|
|
104
94
|
}
|
|
95
|
+
/** Build a Grok `agent serve` WebSocket URL from a sandbox channel + secret. */
|
|
105
96
|
function buildGrokServeWebSocketUrl(channelUrl, secret) {
|
|
106
|
-
|
|
107
|
-
return `${wsBase}/ws?server-key=${encodeURIComponent(secret)}`;
|
|
97
|
+
return `${channelUrl.replace(/^http/i, "ws").replace(/\/$/, "")}/ws?server-key=${encodeURIComponent(secret)}`;
|
|
108
98
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
};
|
|
114
|
-
//# sourceMappingURL=sandbox-server.js.map
|
|
99
|
+
//#endregion
|
|
100
|
+
export { buildGrokServeWebSocketUrl, parseWebSocketUrlFromServeOutput, startAcpServerInSandbox };
|
|
101
|
+
|
|
102
|
+
//# sourceMappingURL=sandbox-server.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sandbox-server.js","sources":["../../../src/session/sandbox-server.ts"],"sourcesContent":["import { connectAcpWebSocket } from '../transport/websocket'\nimport type {\n SandboxChannel,\n SandboxHandle,\n SpawnHandle,\n} from '@tanstack/ai-sandbox'\nimport type { AcpMessageFraming } from '../transport/types'\nimport type { AcpWebSocketConnection } from '../transport/websocket'\n\nexport interface AcpSandboxServer {\n /** WebSocket URL the orchestrator uses to reach the in-sandbox ACP server. */\n wsUrl: string\n /** Sandbox channel used to build {@link wsUrl} (auth headers, when issued). */\n channel: SandboxChannel\n /** Open the ACP JSON-RPC stream over WebSocket. */\n connect: (signal?: AbortSignal) => Promise<AcpWebSocketConnection>\n /** Stop the in-sandbox server process. */\n dispose: () => Promise<void>\n}\n\nexport interface StartAcpServerOptions {\n port: number\n cwd: string\n /** Full shell command that starts the harness ACP server inside the sandbox. */\n command: string\n /** Build the WebSocket URL once the server is ready and the port is exposed. */\n buildWsUrl: (input: {\n channel: SandboxChannel\n port: number\n stdout: string\n }) => string\n /**\n * Return true once {@link buildWsUrl} can be called. Receives accumulated\n * stdout from the server process.\n */\n isReady?: (stdout: string) => boolean\n /** Substring/regex marker used when {@link isReady} is omitted. */\n readyMarker?: string\n env?: Record<string, string>\n timeoutMs?: number\n signal?: AbortSignal\n framing?: AcpMessageFraming\n hostname?: string\n}\n\nconst DEFAULT_READY_MARKER = 'WebSocket URL:'\n\nfunction waitForReady(\n proc: SpawnHandle,\n options: Pick<StartAcpServerOptions, 'isReady' | 'readyMarker' | 'timeoutMs'>,\n): Promise<{ stdout: string; stderr: string }> {\n const readyMarker = options.readyMarker ?? DEFAULT_READY_MARKER\n const isReady =\n options.isReady ?? ((output: string) => output.includes(readyMarker))\n\n return new Promise((resolve, reject) => {\n let stdout = ''\n let stderr = ''\n const state = { settled: false }\n const settle = (fn: () => void): void => {\n if (state.settled) return\n state.settled = true\n clearTimeout(timer)\n fn()\n }\n const diagnostics = (): string =>\n [stdout, stderr]\n .map((s) => s.trim())\n .filter(Boolean)\n .join('\\n')\n .slice(-500)\n const combined = (): string => stdout + stderr\n let stdoutDone = false\n let stderrDone = false\n const tryFinish = (): void => {\n if (state.settled) return\n if (isReady(combined())) {\n settle(() => resolve({ stdout, stderr }))\n return\n }\n if (stdoutDone && stderrDone) {\n settle(() =>\n reject(\n new Error(\n `ACP server exited before becoming ready${\n diagnostics() ? `: ${diagnostics()}` : ' (no output)'\n }`,\n ),\n ),\n )\n }\n }\n\n const timer = setTimeout(\n () =>\n settle(() =>\n reject(\n new Error(\n `ACP server did not become ready within ${options.timeoutMs ?? 30_000}ms${\n diagnostics() ? `: ${diagnostics()}` : ''\n }`,\n ),\n ),\n ),\n options.timeoutMs ?? 30_000,\n )\n\n void (async () => {\n try {\n for await (const chunk of proc.stderr) {\n stderr += chunk\n tryFinish()\n if (state.settled) return\n }\n } catch {\n // non-fatal\n } finally {\n stderrDone = true\n tryFinish()\n }\n })()\n\n void (async () => {\n try {\n for await (const chunk of proc.stdout) {\n stdout += chunk\n tryFinish()\n if (state.settled) return\n }\n } catch (error) {\n settle(() => reject(error))\n return\n } finally {\n stdoutDone = true\n tryFinish()\n }\n })()\n })\n}\n\n/**\n * Boot a harness ACP WebSocket server inside a sandbox and expose its port via\n * {@link SandboxHandle.ports.connect}. Mirrors the `opencode serve` pattern.\n */\nexport async function startAcpServerInSandbox(\n sandbox: SandboxHandle,\n options: StartAcpServerOptions,\n): Promise<AcpSandboxServer> {\n const proc = await sandbox.process.spawn(options.command, {\n cwd: options.cwd,\n ...(options.env ? { env: options.env } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n })\n\n const { stdout } = await waitForReady(proc, options)\n const channel = await sandbox.ports.connect(options.port)\n const wsUrl = options.buildWsUrl({\n channel,\n port: options.port,\n stdout,\n })\n\n const headers =\n channel.headers ??\n (channel.token ? { Authorization: `Bearer ${channel.token}` } : undefined)\n\n return {\n wsUrl,\n channel,\n connect: (signal?: AbortSignal) =>\n connectAcpWebSocket(wsUrl, {\n ...(headers ? { headers } : {}),\n ...(signal ? { signal } : {}),\n framing: options.framing ?? 'frame',\n }),\n dispose: () => proc.kill(),\n }\n}\n\n/** Parse a `WebSocket URL: ws://…` line printed by `grok agent serve`. */\nexport function parseWebSocketUrlFromServeOutput(\n output: string,\n): string | undefined {\n const match = output.match(/WebSocket URL:\\s*(ws\\S+)/i)\n return match?.[1]\n}\n\n/** Build a Grok `agent serve` WebSocket URL from a sandbox channel + secret. */\nexport function buildGrokServeWebSocketUrl(\n channelUrl: string,\n secret: string,\n): string {\n const wsBase = channelUrl.replace(/^http/i, 'ws').replace(/\\/$/, '')\n return `${wsBase}/ws?server-key=${encodeURIComponent(secret)}`\n}\n"],"
|
|
1
|
+
{"version":3,"file":"sandbox-server.js","names":[],"sources":["../../../src/session/sandbox-server.ts"],"sourcesContent":["import { connectAcpWebSocket } from '../transport/websocket'\nimport type {\n SandboxChannel,\n SandboxHandle,\n SpawnHandle,\n} from '@tanstack/ai-sandbox'\nimport type { AcpMessageFraming } from '../transport/types'\nimport type { AcpWebSocketConnection } from '../transport/websocket'\n\nexport interface AcpSandboxServer {\n /** WebSocket URL the orchestrator uses to reach the in-sandbox ACP server. */\n wsUrl: string\n /** Sandbox channel used to build {@link wsUrl} (auth headers, when issued). */\n channel: SandboxChannel\n /** Open the ACP JSON-RPC stream over WebSocket. */\n connect: (signal?: AbortSignal) => Promise<AcpWebSocketConnection>\n /** Stop the in-sandbox server process. */\n dispose: () => Promise<void>\n}\n\nexport interface StartAcpServerOptions {\n port: number\n cwd: string\n /** Full shell command that starts the harness ACP server inside the sandbox. */\n command: string\n /** Build the WebSocket URL once the server is ready and the port is exposed. */\n buildWsUrl: (input: {\n channel: SandboxChannel\n port: number\n stdout: string\n }) => string\n /**\n * Return true once {@link buildWsUrl} can be called. Receives accumulated\n * stdout from the server process.\n */\n isReady?: (stdout: string) => boolean\n /** Substring/regex marker used when {@link isReady} is omitted. */\n readyMarker?: string\n env?: Record<string, string>\n timeoutMs?: number\n signal?: AbortSignal\n framing?: AcpMessageFraming\n hostname?: string\n}\n\nconst DEFAULT_READY_MARKER = 'WebSocket URL:'\n\nfunction waitForReady(\n proc: SpawnHandle,\n options: Pick<StartAcpServerOptions, 'isReady' | 'readyMarker' | 'timeoutMs'>,\n): Promise<{ stdout: string; stderr: string }> {\n const readyMarker = options.readyMarker ?? DEFAULT_READY_MARKER\n const isReady =\n options.isReady ?? ((output: string) => output.includes(readyMarker))\n\n return new Promise((resolve, reject) => {\n let stdout = ''\n let stderr = ''\n const state = { settled: false }\n const settle = (fn: () => void): void => {\n if (state.settled) return\n state.settled = true\n clearTimeout(timer)\n fn()\n }\n const diagnostics = (): string =>\n [stdout, stderr]\n .map((s) => s.trim())\n .filter(Boolean)\n .join('\\n')\n .slice(-500)\n const combined = (): string => stdout + stderr\n let stdoutDone = false\n let stderrDone = false\n const tryFinish = (): void => {\n if (state.settled) return\n if (isReady(combined())) {\n settle(() => resolve({ stdout, stderr }))\n return\n }\n if (stdoutDone && stderrDone) {\n settle(() =>\n reject(\n new Error(\n `ACP server exited before becoming ready${\n diagnostics() ? `: ${diagnostics()}` : ' (no output)'\n }`,\n ),\n ),\n )\n }\n }\n\n const timer = setTimeout(\n () =>\n settle(() =>\n reject(\n new Error(\n `ACP server did not become ready within ${options.timeoutMs ?? 30_000}ms${\n diagnostics() ? `: ${diagnostics()}` : ''\n }`,\n ),\n ),\n ),\n options.timeoutMs ?? 30_000,\n )\n\n void (async () => {\n try {\n for await (const chunk of proc.stderr) {\n stderr += chunk\n tryFinish()\n if (state.settled) return\n }\n } catch {\n // non-fatal\n } finally {\n stderrDone = true\n tryFinish()\n }\n })()\n\n void (async () => {\n try {\n for await (const chunk of proc.stdout) {\n stdout += chunk\n tryFinish()\n if (state.settled) return\n }\n } catch (error) {\n settle(() => reject(error))\n return\n } finally {\n stdoutDone = true\n tryFinish()\n }\n })()\n })\n}\n\n/**\n * Boot a harness ACP WebSocket server inside a sandbox and expose its port via\n * {@link SandboxHandle.ports.connect}. Mirrors the `opencode serve` pattern.\n */\nexport async function startAcpServerInSandbox(\n sandbox: SandboxHandle,\n options: StartAcpServerOptions,\n): Promise<AcpSandboxServer> {\n const proc = await sandbox.process.spawn(options.command, {\n cwd: options.cwd,\n ...(options.env ? { env: options.env } : {}),\n ...(options.signal ? { signal: options.signal } : {}),\n })\n\n const { stdout } = await waitForReady(proc, options)\n const channel = await sandbox.ports.connect(options.port)\n const wsUrl = options.buildWsUrl({\n channel,\n port: options.port,\n stdout,\n })\n\n const headers =\n channel.headers ??\n (channel.token ? { Authorization: `Bearer ${channel.token}` } : undefined)\n\n return {\n wsUrl,\n channel,\n connect: (signal?: AbortSignal) =>\n connectAcpWebSocket(wsUrl, {\n ...(headers ? { headers } : {}),\n ...(signal ? { signal } : {}),\n framing: options.framing ?? 'frame',\n }),\n dispose: () => proc.kill(),\n }\n}\n\n/** Parse a `WebSocket URL: ws://…` line printed by `grok agent serve`. */\nexport function parseWebSocketUrlFromServeOutput(\n output: string,\n): string | undefined {\n const match = output.match(/WebSocket URL:\\s*(ws\\S+)/i)\n return match?.[1]\n}\n\n/** Build a Grok `agent serve` WebSocket URL from a sandbox channel + secret. */\nexport function buildGrokServeWebSocketUrl(\n channelUrl: string,\n secret: string,\n): string {\n const wsBase = channelUrl.replace(/^http/i, 'ws').replace(/\\/$/, '')\n return `${wsBase}/ws?server-key=${encodeURIComponent(secret)}`\n}\n"],"mappings":"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"}
|