@tanstack/ai-sandbox 0.1.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/README.md +182 -0
- package/dist/esm/agents-file.d.ts +36 -0
- package/dist/esm/agents-file.js +44 -0
- package/dist/esm/agents-file.js.map +1 -0
- package/dist/esm/approvals.d.ts +38 -0
- package/dist/esm/approvals.js +36 -0
- package/dist/esm/approvals.js.map +1 -0
- package/dist/esm/bootstrap.d.ts +17 -0
- package/dist/esm/bootstrap.js +124 -0
- package/dist/esm/bootstrap.js.map +1 -0
- package/dist/esm/bridge-events.d.ts +21 -0
- package/dist/esm/bridge-events.js +76 -0
- package/dist/esm/bridge-events.js.map +1 -0
- package/dist/esm/capabilities.d.ts +26 -0
- package/dist/esm/capabilities.js +29 -0
- package/dist/esm/capabilities.js.map +1 -0
- package/dist/esm/contracts.d.ts +211 -0
- package/dist/esm/errors.d.ts +16 -0
- package/dist/esm/errors.js +25 -0
- package/dist/esm/errors.js.map +1 -0
- package/dist/esm/git-exec.d.ts +2 -0
- package/dist/esm/git-exec.js +68 -0
- package/dist/esm/git-exec.js.map +1 -0
- package/dist/esm/harness-cwd.d.ts +2 -0
- package/dist/esm/harness-cwd.js +24 -0
- package/dist/esm/harness-cwd.js.map +1 -0
- package/dist/esm/index.d.ts +39 -0
- package/dist/esm/index.js +103 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/key.d.ts +20 -0
- package/dist/esm/key.js +41 -0
- package/dist/esm/key.js.map +1 -0
- package/dist/esm/middleware.d.ts +5 -0
- package/dist/esm/middleware.js +140 -0
- package/dist/esm/middleware.js.map +1 -0
- package/dist/esm/ngrok.d.ts +16 -0
- package/dist/esm/ngrok.js +54 -0
- package/dist/esm/ngrok.js.map +1 -0
- package/dist/esm/policy.d.ts +47 -0
- package/dist/esm/policy.js +44 -0
- package/dist/esm/policy.js.map +1 -0
- package/dist/esm/projection.d.ts +31 -0
- package/dist/esm/projection.js +9 -0
- package/dist/esm/projection.js.map +1 -0
- package/dist/esm/remote-tools.d.ts +48 -0
- package/dist/esm/remote-tools.js +76 -0
- package/dist/esm/remote-tools.js.map +1 -0
- package/dist/esm/run-log.d.ts +81 -0
- package/dist/esm/run-log.js +107 -0
- package/dist/esm/run-log.js.map +1 -0
- package/dist/esm/run.d.ts +58 -0
- package/dist/esm/run.js +89 -0
- package/dist/esm/run.js.map +1 -0
- package/dist/esm/runner.d.ts +21 -0
- package/dist/esm/runner.js +54 -0
- package/dist/esm/runner.js.map +1 -0
- package/dist/esm/sandbox.d.ts +79 -0
- package/dist/esm/sandbox.js +125 -0
- package/dist/esm/sandbox.js.map +1 -0
- package/dist/esm/secrets.d.ts +37 -0
- package/dist/esm/secrets.js +59 -0
- package/dist/esm/secrets.js.map +1 -0
- package/dist/esm/setup-plan.d.ts +13 -0
- package/dist/esm/setup-plan.js +16 -0
- package/dist/esm/setup-plan.js.map +1 -0
- package/dist/esm/shell.d.ts +45 -0
- package/dist/esm/shell.js +164 -0
- package/dist/esm/shell.js.map +1 -0
- package/dist/esm/store.d.ts +53 -0
- package/dist/esm/store.js +34 -0
- package/dist/esm/store.js.map +1 -0
- package/dist/esm/tool-bridge.d.ts +130 -0
- package/dist/esm/tool-bridge.js +197 -0
- package/dist/esm/tool-bridge.js.map +1 -0
- package/dist/esm/watch.d.ts +36 -0
- package/dist/esm/watch.js +144 -0
- package/dist/esm/watch.js.map +1 -0
- package/dist/esm/workspace.d.ts +128 -0
- package/dist/esm/workspace.js +42 -0
- package/dist/esm/workspace.js.map +1 -0
- package/package.json +72 -0
- package/skills/ai-sandbox/SKILL.md +366 -0
- package/src/agents-file.ts +101 -0
- package/src/approvals.ts +96 -0
- package/src/bootstrap.ts +196 -0
- package/src/bridge-events.ts +112 -0
- package/src/capabilities.ts +47 -0
- package/src/contracts.ts +236 -0
- package/src/errors.ts +31 -0
- package/src/git-exec.ts +114 -0
- package/src/harness-cwd.ts +38 -0
- package/src/index.ts +222 -0
- package/src/key.ts +70 -0
- package/src/middleware.ts +233 -0
- package/src/ngrok.ts +85 -0
- package/src/policy.ts +111 -0
- package/src/projection.ts +46 -0
- package/src/remote-tools.ts +180 -0
- package/src/run-log.ts +224 -0
- package/src/run.ts +167 -0
- package/src/runner.ts +99 -0
- package/src/sandbox.ts +259 -0
- package/src/secrets.ts +101 -0
- package/src/setup-plan.ts +25 -0
- package/src/shell.ts +288 -0
- package/src/store.ts +83 -0
- package/src/tool-bridge.ts +399 -0
- package/src/watch.ts +256 -0
- package/src/workspace.ts +151 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { AnyTool } from '@tanstack/ai';
|
|
2
|
+
/**
|
|
3
|
+
* Name of the bridged MCP server. The agent sees tools as
|
|
4
|
+
* `mcp__tanstack__<tool>`; each adapter's stream translator strips this prefix
|
|
5
|
+
* so tool-call events match the names the application registered.
|
|
6
|
+
*/
|
|
7
|
+
export declare const BRIDGED_MCP_SERVER_NAME = "tanstack";
|
|
8
|
+
/** Hostname the sandbox uses to reach the bridge endpoint, per provider. */
|
|
9
|
+
export declare function hostForSandbox(provider: string): string;
|
|
10
|
+
/** Result of a permission decision returned to the harness's prompt tool. */
|
|
11
|
+
export interface PermissionToolResult {
|
|
12
|
+
behavior: 'allow' | 'deny';
|
|
13
|
+
message?: string;
|
|
14
|
+
updatedInput?: unknown;
|
|
15
|
+
}
|
|
16
|
+
export interface BridgePermission {
|
|
17
|
+
toolName: string;
|
|
18
|
+
resolve: (input: {
|
|
19
|
+
tool_name?: string;
|
|
20
|
+
input?: unknown;
|
|
21
|
+
}) => PermissionToolResult | Promise<PermissionToolResult>;
|
|
22
|
+
}
|
|
23
|
+
export interface ToolBridgeCoreOptions {
|
|
24
|
+
/** Runtime context forwarded to each tool's `execute()`. */
|
|
25
|
+
context?: unknown;
|
|
26
|
+
/** Abort signal forwarded to each tool's `execute()`. */
|
|
27
|
+
signal?: AbortSignal;
|
|
28
|
+
/**
|
|
29
|
+
* Forwarded to each tool's `execute()` so a bridged tool can stream progress /
|
|
30
|
+
* custom events back to the client mid-execution (e.g. code mode's
|
|
31
|
+
* `code_mode:console` logs). Without it those events are silently dropped — the
|
|
32
|
+
* bridge runs out-of-band from the main tool executor, so the executor's own
|
|
33
|
+
* `emitCustomEvent` never reaches a bridged tool. The harness adapter supplies
|
|
34
|
+
* one that injects a CUSTOM chunk into its live output stream.
|
|
35
|
+
*/
|
|
36
|
+
emitCustomEvent?: (eventName: string, value: Record<string, unknown>) => void;
|
|
37
|
+
/**
|
|
38
|
+
* Optional permission-prompt tool (e.g. for Claude Code's
|
|
39
|
+
* `--permission-prompt-tool`). When set, the bridge exposes an extra MCP tool
|
|
40
|
+
* `<name>` whose handler returns the orchestrator's allow/deny decision.
|
|
41
|
+
*/
|
|
42
|
+
permission?: BridgePermission;
|
|
43
|
+
}
|
|
44
|
+
/** An MCP tool descriptor as advertised to the in-sandbox agent. */
|
|
45
|
+
export interface ToolDescriptor {
|
|
46
|
+
name: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
inputSchema: {
|
|
49
|
+
type: 'object';
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/** MCP `tools/call` result shape. */
|
|
54
|
+
export interface ToolCallResult {
|
|
55
|
+
content: Array<{
|
|
56
|
+
type: 'text';
|
|
57
|
+
text: string;
|
|
58
|
+
}>;
|
|
59
|
+
isError?: boolean;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Transport-agnostic bridge logic: list tools, and dispatch a tool/permission
|
|
63
|
+
* call. No sockets, no auth — a transport ({@link startHostToolBridge} or a
|
|
64
|
+
* `fetch` handler) wraps this and owns I/O + the bearer check.
|
|
65
|
+
*/
|
|
66
|
+
export interface ToolBridgeCore {
|
|
67
|
+
listTools: () => Array<ToolDescriptor>;
|
|
68
|
+
callTool: (name: string, args: unknown) => Promise<ToolCallResult>;
|
|
69
|
+
}
|
|
70
|
+
/** Build the transport-agnostic bridge core for the given tools. */
|
|
71
|
+
export declare function createToolBridgeCore(tools: Array<AnyTool>, options?: ToolBridgeCoreOptions): ToolBridgeCore;
|
|
72
|
+
/**
|
|
73
|
+
* Minimal JSON-RPC dispatcher over a {@link ToolBridgeCore}, so a `fetch`-based
|
|
74
|
+
* transport (Worker / Durable Object) can serve MCP `initialize` / `tools/list`
|
|
75
|
+
* / `tools/call` without the node-specific HTTP transport. Returns the JSON-RPC
|
|
76
|
+
* response object, or `null` for a notification (no `id`).
|
|
77
|
+
*/
|
|
78
|
+
export declare function handleBridgeJsonRpc(core: ToolBridgeCore, message: unknown): Promise<unknown>;
|
|
79
|
+
/**
|
|
80
|
+
* Constant-time check of an `Authorization: Bearer <token>` header against the
|
|
81
|
+
* expected token. Length mismatch returns false early (token length is not
|
|
82
|
+
* secret); equal-length comparison is timing-safe.
|
|
83
|
+
*/
|
|
84
|
+
export declare function timingSafeBearerEqual(header: string | undefined, token: string): boolean;
|
|
85
|
+
export interface HostToolBridge {
|
|
86
|
+
/** MCP server name; tools appear to the agent as `mcp__<name>__<tool>`. */
|
|
87
|
+
name: string;
|
|
88
|
+
/** URL the SANDBOX uses to reach this bridge. */
|
|
89
|
+
url: string;
|
|
90
|
+
/** Per-run bearer token gating the endpoint. */
|
|
91
|
+
token: string;
|
|
92
|
+
close: () => Promise<void>;
|
|
93
|
+
}
|
|
94
|
+
export interface StartBridgeOptions extends ToolBridgeCoreOptions {
|
|
95
|
+
/** Hostname the sandbox uses to reach the host (e.g. `host.docker.internal`). */
|
|
96
|
+
hostForSandbox: string;
|
|
97
|
+
/**
|
|
98
|
+
* Address to bind the listener to. Defaults to `127.0.0.1` (loopback) and is
|
|
99
|
+
* widened to `0.0.0.0` only when the sandbox reaches the host via
|
|
100
|
+
* `host.docker.internal` (a container can't reach the host's loopback).
|
|
101
|
+
*/
|
|
102
|
+
bindAddress?: string;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Start the `node:http` MCP tool-proxy bridge for the given tools. For a
|
|
106
|
+
* long-running host (laptop / CI / Docker orchestrator). Serverless/edge
|
|
107
|
+
* orchestrators serve {@link createToolBridgeCore} from their own `fetch`
|
|
108
|
+
* handler instead.
|
|
109
|
+
*/
|
|
110
|
+
export declare function startHostToolBridge(tools: Array<AnyTool>, options: StartBridgeOptions): Promise<HostToolBridge>;
|
|
111
|
+
/** A provisioned, reachable bridge endpoint (same shape as {@link HostToolBridge}). */
|
|
112
|
+
export type ProvisionedBridge = HostToolBridge;
|
|
113
|
+
export interface ToolBridgeProvisionOptions extends ToolBridgeCoreOptions {
|
|
114
|
+
/** Sandbox provider name, to derive how the sandbox reaches the bridge. */
|
|
115
|
+
provider: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Stands up the tool-bridge endpoint for a run. The seam that makes the bridge
|
|
119
|
+
* portable across runtimes: a harness adapter asks its capability context for a
|
|
120
|
+
* provisioner and uses {@link nodeHttpBridgeProvisioner} as the default (host /
|
|
121
|
+
* Docker). A serverless/edge orchestrator PROVIDES its own — e.g. a Durable
|
|
122
|
+
* Object that mounts {@link createToolBridgeCore} / {@link handleBridgeJsonRpc}
|
|
123
|
+
* on its `fetch` handler and returns a sandbox-reachable URL — so no raw TCP
|
|
124
|
+
* listener is needed.
|
|
125
|
+
*/
|
|
126
|
+
export interface ToolBridgeProvisioner {
|
|
127
|
+
provision: (tools: Array<AnyTool>, options: ToolBridgeProvisionOptions) => Promise<ProvisionedBridge>;
|
|
128
|
+
}
|
|
129
|
+
/** Default provisioner: a `node:http` listener on the host. */
|
|
130
|
+
export declare const nodeHttpBridgeProvisioner: ToolBridgeProvisioner;
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
6
|
+
const BRIDGED_MCP_SERVER_NAME = "tanstack";
|
|
7
|
+
function hostForSandbox(provider) {
|
|
8
|
+
return provider === "docker" ? "host.docker.internal" : "127.0.0.1";
|
|
9
|
+
}
|
|
10
|
+
function toObjectSchema(schema) {
|
|
11
|
+
if (schema !== null && typeof schema === "object" && "type" in schema && schema.type === "object") {
|
|
12
|
+
return { ...schema, type: "object" };
|
|
13
|
+
}
|
|
14
|
+
return { type: "object", properties: {} };
|
|
15
|
+
}
|
|
16
|
+
function createToolBridgeCore(tools, options = {}) {
|
|
17
|
+
const toolsByName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
18
|
+
const permission = options.permission;
|
|
19
|
+
const permissionDescriptor = permission ? {
|
|
20
|
+
name: permission.toolName,
|
|
21
|
+
description: 'Permission prompt: returns {behavior:"allow"|"deny"} for a requested action.',
|
|
22
|
+
inputSchema: { type: "object", properties: {} }
|
|
23
|
+
} : void 0;
|
|
24
|
+
return {
|
|
25
|
+
listTools() {
|
|
26
|
+
return [
|
|
27
|
+
...tools.map((tool) => ({
|
|
28
|
+
name: tool.name,
|
|
29
|
+
description: tool.description,
|
|
30
|
+
inputSchema: toObjectSchema(tool.inputSchema)
|
|
31
|
+
})),
|
|
32
|
+
...permissionDescriptor ? [permissionDescriptor] : []
|
|
33
|
+
];
|
|
34
|
+
},
|
|
35
|
+
async callTool(name, args) {
|
|
36
|
+
if (permission && name === permission.toolName) {
|
|
37
|
+
const result = await permission.resolve(args ?? {});
|
|
38
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
39
|
+
}
|
|
40
|
+
const tool = toolsByName.get(name);
|
|
41
|
+
if (!tool?.execute) throw new Error(`Unknown tool: ${name}`);
|
|
42
|
+
try {
|
|
43
|
+
const result = await tool.execute(args ?? {}, {
|
|
44
|
+
context: options.context,
|
|
45
|
+
abortSignal: options.signal,
|
|
46
|
+
// No-op default so tools that always call it (e.g. code mode) don't
|
|
47
|
+
// crash when the transport didn't wire a sink.
|
|
48
|
+
emitCustomEvent: options.emitCustomEvent ?? (() => {
|
|
49
|
+
})
|
|
50
|
+
});
|
|
51
|
+
const text = typeof result === "string" ? result : JSON.stringify(result);
|
|
52
|
+
return { content: [{ type: "text", text }] };
|
|
53
|
+
} catch (error) {
|
|
54
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
55
|
+
return {
|
|
56
|
+
isError: true,
|
|
57
|
+
content: [
|
|
58
|
+
{ type: "text", text: `Tool execution failed: ${message}` }
|
|
59
|
+
]
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
async function handleBridgeJsonRpc(core, message) {
|
|
66
|
+
if (message === null || typeof message !== "object") {
|
|
67
|
+
return {
|
|
68
|
+
jsonrpc: "2.0",
|
|
69
|
+
id: null,
|
|
70
|
+
error: { code: -32600, message: "Invalid Request" }
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const rpc = message;
|
|
74
|
+
const id = rpc.id ?? null;
|
|
75
|
+
const respond = (result) => ({ jsonrpc: "2.0", id, result });
|
|
76
|
+
switch (rpc.method) {
|
|
77
|
+
case "initialize":
|
|
78
|
+
return respond({
|
|
79
|
+
protocolVersion: "2024-11-05",
|
|
80
|
+
capabilities: { tools: {} },
|
|
81
|
+
serverInfo: { name: BRIDGED_MCP_SERVER_NAME, version: "1.0.0" }
|
|
82
|
+
});
|
|
83
|
+
case "notifications/initialized":
|
|
84
|
+
return null;
|
|
85
|
+
case "tools/list":
|
|
86
|
+
return respond({ tools: core.listTools() });
|
|
87
|
+
case "tools/call": {
|
|
88
|
+
const params = rpc.params ?? {};
|
|
89
|
+
if (typeof params.name !== "string") {
|
|
90
|
+
return {
|
|
91
|
+
jsonrpc: "2.0",
|
|
92
|
+
id,
|
|
93
|
+
error: { code: -32602, message: "Invalid params: name" }
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
return respond(await core.callTool(params.name, params.arguments ?? {}));
|
|
97
|
+
}
|
|
98
|
+
default:
|
|
99
|
+
return {
|
|
100
|
+
jsonrpc: "2.0",
|
|
101
|
+
id,
|
|
102
|
+
error: { code: -32601, message: "Method not found" }
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function timingSafeBearerEqual(header, token) {
|
|
107
|
+
if (header === void 0) return false;
|
|
108
|
+
const a = Buffer.from(header);
|
|
109
|
+
const b = Buffer.from(`Bearer ${token}`);
|
|
110
|
+
if (a.length !== b.length) return false;
|
|
111
|
+
return timingSafeEqual(a, b);
|
|
112
|
+
}
|
|
113
|
+
function buildMcpServer(core) {
|
|
114
|
+
const server = new McpServer(
|
|
115
|
+
{ name: BRIDGED_MCP_SERVER_NAME, version: "1.0.0" },
|
|
116
|
+
{ capabilities: { tools: {} } }
|
|
117
|
+
);
|
|
118
|
+
server.server.setRequestHandler(ListToolsRequestSchema, () => ({
|
|
119
|
+
tools: core.listTools()
|
|
120
|
+
}));
|
|
121
|
+
server.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
122
|
+
const result = await core.callTool(
|
|
123
|
+
request.params.name,
|
|
124
|
+
request.params.arguments ?? {}
|
|
125
|
+
);
|
|
126
|
+
return {
|
|
127
|
+
content: result.content,
|
|
128
|
+
...result.isError ? { isError: true } : {}
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
return server;
|
|
132
|
+
}
|
|
133
|
+
async function startHostToolBridge(tools, options) {
|
|
134
|
+
const token = randomBytes(24).toString("hex");
|
|
135
|
+
const core = createToolBridgeCore(tools, options);
|
|
136
|
+
const bindAddress = options.bindAddress ?? (options.hostForSandbox === "host.docker.internal" ? "0.0.0.0" : "127.0.0.1");
|
|
137
|
+
const httpServer = createServer((req, res) => {
|
|
138
|
+
void (async () => {
|
|
139
|
+
if (!timingSafeBearerEqual(req.headers["authorization"], token)) {
|
|
140
|
+
res.writeHead(401).end("unauthorized");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const server = buildMcpServer(core);
|
|
144
|
+
const transport = new StreamableHTTPServerTransport({
|
|
145
|
+
sessionIdGenerator: void 0
|
|
146
|
+
});
|
|
147
|
+
res.on("close", () => {
|
|
148
|
+
void transport.close();
|
|
149
|
+
void server.close();
|
|
150
|
+
});
|
|
151
|
+
await server.connect(transport);
|
|
152
|
+
let body = "";
|
|
153
|
+
for await (const chunk of req) body += chunk;
|
|
154
|
+
let parsed;
|
|
155
|
+
try {
|
|
156
|
+
parsed = body ? JSON.parse(body) : void 0;
|
|
157
|
+
} catch {
|
|
158
|
+
if (!res.headersSent) res.writeHead(400).end("invalid JSON body");
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
await transport.handleRequest(req, res, parsed);
|
|
162
|
+
})().catch((error) => {
|
|
163
|
+
console.error("[tool-bridge] request handler failed:", error);
|
|
164
|
+
if (!res.headersSent) res.writeHead(500).end("bridge error");
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
await new Promise(
|
|
168
|
+
(resolve) => httpServer.listen(0, bindAddress, resolve)
|
|
169
|
+
);
|
|
170
|
+
const port = httpServer.address().port;
|
|
171
|
+
const url = `http://${options.hostForSandbox}:${port}/mcp`;
|
|
172
|
+
return {
|
|
173
|
+
name: BRIDGED_MCP_SERVER_NAME,
|
|
174
|
+
url,
|
|
175
|
+
token,
|
|
176
|
+
close: () => new Promise((resolve) => httpServer.close(() => resolve()))
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const nodeHttpBridgeProvisioner = {
|
|
180
|
+
provision(tools, options) {
|
|
181
|
+
const { provider, ...core } = options;
|
|
182
|
+
return startHostToolBridge(tools, {
|
|
183
|
+
hostForSandbox: hostForSandbox(provider),
|
|
184
|
+
...core
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
export {
|
|
189
|
+
BRIDGED_MCP_SERVER_NAME,
|
|
190
|
+
createToolBridgeCore,
|
|
191
|
+
handleBridgeJsonRpc,
|
|
192
|
+
hostForSandbox,
|
|
193
|
+
nodeHttpBridgeProvisioner,
|
|
194
|
+
startHostToolBridge,
|
|
195
|
+
timingSafeBearerEqual
|
|
196
|
+
};
|
|
197
|
+
//# sourceMappingURL=tool-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tool-bridge.js","sources":["../../src/tool-bridge.ts"],"sourcesContent":["/**\n * MCP tool-proxy bridge, shared by all harness adapters.\n *\n * Exposes chat()-provided server tools to an in-sandbox agent as an MCP server.\n * The agent (inside the sandbox) calls `mcp__tanstack__<tool>`; the call is\n * proxied OUT to a bridge endpoint, where the tool's `execute()` runs in the\n * orchestrator process (with its closures / DB / secrets), and the result is\n * returned into the sandbox.\n *\n * The bridge is split into a transport-agnostic CORE and a TRANSPORT:\n * - {@link createToolBridgeCore} owns tool dispatch + the permission resolver\n * (no I/O). It is what makes the bridge portable.\n * - {@link startHostToolBridge} is the `node:http` transport for a long-running\n * host (laptop / CI / Docker orchestrator). It binds loopback unless the\n * sandbox must reach it via `host.docker.internal`, and authenticates with a\n * constant-time bearer check.\n * - A serverless/edge orchestrator (e.g. a Durable Object) instead serves the\n * SAME core from its own `fetch` handler — no raw TCP listener — see\n * {@link handleBridgeJsonRpc} and the Cloudflare example.\n */\nimport { createServer } from 'node:http'\nimport { randomBytes, timingSafeEqual } from 'node:crypto'\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n} from '@modelcontextprotocol/sdk/types.js'\nimport type { AddressInfo } from 'node:net'\nimport type { AnyTool } from '@tanstack/ai'\n\n/**\n * Name of the bridged MCP server. The agent sees tools as\n * `mcp__tanstack__<tool>`; each adapter's stream translator strips this prefix\n * so tool-call events match the names the application registered.\n */\nexport const BRIDGED_MCP_SERVER_NAME = 'tanstack'\n\n/** Hostname the sandbox uses to reach the bridge endpoint, per provider. */\nexport function hostForSandbox(provider: string): string {\n return provider === 'docker' ? 'host.docker.internal' : '127.0.0.1'\n}\n\n/** Result of a permission decision returned to the harness's prompt tool. */\nexport interface PermissionToolResult {\n behavior: 'allow' | 'deny'\n message?: string\n updatedInput?: unknown\n}\n\nexport interface BridgePermission {\n toolName: string\n resolve: (input: {\n tool_name?: string\n input?: unknown\n }) => PermissionToolResult | Promise<PermissionToolResult>\n}\n\nexport interface ToolBridgeCoreOptions {\n /** Runtime context forwarded to each tool's `execute()`. */\n context?: unknown\n /** Abort signal forwarded to each tool's `execute()`. */\n signal?: AbortSignal\n /**\n * Forwarded to each tool's `execute()` so a bridged tool can stream progress /\n * custom events back to the client mid-execution (e.g. code mode's\n * `code_mode:console` logs). Without it those events are silently dropped — the\n * bridge runs out-of-band from the main tool executor, so the executor's own\n * `emitCustomEvent` never reaches a bridged tool. The harness adapter supplies\n * one that injects a CUSTOM chunk into its live output stream.\n */\n emitCustomEvent?: (eventName: string, value: Record<string, unknown>) => void\n /**\n * Optional permission-prompt tool (e.g. for Claude Code's\n * `--permission-prompt-tool`). When set, the bridge exposes an extra MCP tool\n * `<name>` whose handler returns the orchestrator's allow/deny decision.\n */\n permission?: BridgePermission\n}\n\n/** An MCP tool descriptor as advertised to the in-sandbox agent. */\nexport interface ToolDescriptor {\n name: string\n description?: string\n inputSchema: { type: 'object'; [key: string]: unknown }\n}\n\n/**\n * Coerce a tool's `inputSchema` into the object-schema shape MCP advertises,\n * substituting an empty object schema when it isn't already a JSON-schema object\n * (project rule: a guard, not an `as` cast).\n */\nfunction toObjectSchema(schema: unknown): {\n type: 'object'\n [key: string]: unknown\n} {\n if (\n schema !== null &&\n typeof schema === 'object' &&\n 'type' in schema &&\n schema.type === 'object'\n ) {\n return { ...schema, type: 'object' }\n }\n return { type: 'object', properties: {} }\n}\n\n/** MCP `tools/call` result shape. */\nexport interface ToolCallResult {\n content: Array<{ type: 'text'; text: string }>\n isError?: boolean\n}\n\n/**\n * Transport-agnostic bridge logic: list tools, and dispatch a tool/permission\n * call. No sockets, no auth — a transport ({@link startHostToolBridge} or a\n * `fetch` handler) wraps this and owns I/O + the bearer check.\n */\nexport interface ToolBridgeCore {\n listTools: () => Array<ToolDescriptor>\n callTool: (name: string, args: unknown) => Promise<ToolCallResult>\n}\n\n/** Build the transport-agnostic bridge core for the given tools. */\nexport function createToolBridgeCore(\n tools: Array<AnyTool>,\n options: ToolBridgeCoreOptions = {},\n): ToolBridgeCore {\n const toolsByName = new Map(tools.map((tool) => [tool.name, tool]))\n const permission = options.permission\n\n const permissionDescriptor: ToolDescriptor | undefined = permission\n ? {\n name: permission.toolName,\n description:\n 'Permission prompt: returns {behavior:\"allow\"|\"deny\"} for a requested action.',\n inputSchema: { type: 'object', properties: {} },\n }\n : undefined\n\n return {\n listTools() {\n return [\n ...tools.map((tool) => ({\n name: tool.name,\n description: tool.description,\n inputSchema: toObjectSchema(tool.inputSchema),\n })),\n ...(permissionDescriptor ? [permissionDescriptor] : []),\n ]\n },\n\n async callTool(name, args) {\n if (permission && name === permission.toolName) {\n const result = await permission.resolve(args ?? {})\n return { content: [{ type: 'text', text: JSON.stringify(result) }] }\n }\n const tool = toolsByName.get(name)\n if (!tool?.execute) throw new Error(`Unknown tool: ${name}`)\n try {\n const result: unknown = await tool.execute(args ?? {}, {\n context: options.context,\n abortSignal: options.signal,\n // No-op default so tools that always call it (e.g. code mode) don't\n // crash when the transport didn't wire a sink.\n emitCustomEvent: options.emitCustomEvent ?? (() => {}),\n })\n const text =\n typeof result === 'string' ? result : JSON.stringify(result)\n return { content: [{ type: 'text', text }] }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n return {\n isError: true,\n content: [\n { type: 'text', text: `Tool execution failed: ${message}` },\n ],\n }\n }\n },\n }\n}\n\n/**\n * Minimal JSON-RPC dispatcher over a {@link ToolBridgeCore}, so a `fetch`-based\n * transport (Worker / Durable Object) can serve MCP `initialize` / `tools/list`\n * / `tools/call` without the node-specific HTTP transport. Returns the JSON-RPC\n * response object, or `null` for a notification (no `id`).\n */\nexport async function handleBridgeJsonRpc(\n core: ToolBridgeCore,\n message: unknown,\n): Promise<unknown> {\n if (message === null || typeof message !== 'object') {\n return {\n jsonrpc: '2.0',\n id: null,\n error: { code: -32600, message: 'Invalid Request' },\n }\n }\n const rpc = message as { id?: unknown; method?: unknown; params?: unknown }\n const id = rpc.id ?? null\n const respond = (result: unknown): unknown => ({ jsonrpc: '2.0', id, result })\n switch (rpc.method) {\n case 'initialize':\n return respond({\n protocolVersion: '2024-11-05',\n capabilities: { tools: {} },\n serverInfo: { name: BRIDGED_MCP_SERVER_NAME, version: '1.0.0' },\n })\n case 'notifications/initialized':\n return null\n case 'tools/list':\n return respond({ tools: core.listTools() })\n case 'tools/call': {\n const params = (rpc.params ?? {}) as {\n name?: unknown\n arguments?: unknown\n }\n if (typeof params.name !== 'string') {\n return {\n jsonrpc: '2.0',\n id,\n error: { code: -32602, message: 'Invalid params: name' },\n }\n }\n return respond(await core.callTool(params.name, params.arguments ?? {}))\n }\n default:\n return {\n jsonrpc: '2.0',\n id,\n error: { code: -32601, message: 'Method not found' },\n }\n }\n}\n\n/**\n * Constant-time check of an `Authorization: Bearer <token>` header against the\n * expected token. Length mismatch returns false early (token length is not\n * secret); equal-length comparison is timing-safe.\n */\nexport function timingSafeBearerEqual(\n header: string | undefined,\n token: string,\n): boolean {\n if (header === undefined) return false\n const a = Buffer.from(header)\n const b = Buffer.from(`Bearer ${token}`)\n if (a.length !== b.length) return false\n return timingSafeEqual(a, b)\n}\n\nexport interface HostToolBridge {\n /** MCP server name; tools appear to the agent as `mcp__<name>__<tool>`. */\n name: string\n /** URL the SANDBOX uses to reach this bridge. */\n url: string\n /** Per-run bearer token gating the endpoint. */\n token: string\n close: () => Promise<void>\n}\n\nexport interface StartBridgeOptions extends ToolBridgeCoreOptions {\n /** Hostname the sandbox uses to reach the host (e.g. `host.docker.internal`). */\n hostForSandbox: string\n /**\n * Address to bind the listener to. Defaults to `127.0.0.1` (loopback) and is\n * widened to `0.0.0.0` only when the sandbox reaches the host via\n * `host.docker.internal` (a container can't reach the host's loopback).\n */\n bindAddress?: string\n}\n\nfunction buildMcpServer(core: ToolBridgeCore): McpServer {\n const server = new McpServer(\n { name: BRIDGED_MCP_SERVER_NAME, version: '1.0.0' },\n { capabilities: { tools: {} } },\n )\n server.server.setRequestHandler(ListToolsRequestSchema, () => ({\n tools: core.listTools(),\n }))\n server.server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const result = await core.callTool(\n request.params.name,\n request.params.arguments ?? {},\n )\n return {\n content: result.content,\n ...(result.isError ? { isError: true } : {}),\n }\n })\n return server\n}\n\n/**\n * Start the `node:http` MCP tool-proxy bridge for the given tools. For a\n * long-running host (laptop / CI / Docker orchestrator). Serverless/edge\n * orchestrators serve {@link createToolBridgeCore} from their own `fetch`\n * handler instead.\n */\nexport async function startHostToolBridge(\n tools: Array<AnyTool>,\n options: StartBridgeOptions,\n): Promise<HostToolBridge> {\n const token = randomBytes(24).toString('hex')\n const core = createToolBridgeCore(tools, options)\n // Loopback by default; widen to all interfaces only for the Docker bridge,\n // which a container reaches via host.docker.internal (host gateway).\n const bindAddress =\n options.bindAddress ??\n (options.hostForSandbox === 'host.docker.internal'\n ? '0.0.0.0'\n : '127.0.0.1')\n\n const httpServer = createServer((req, res) => {\n void (async () => {\n if (!timingSafeBearerEqual(req.headers['authorization'], token)) {\n res.writeHead(401).end('unauthorized')\n return\n }\n const server = buildMcpServer(core)\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n })\n res.on('close', () => {\n void transport.close()\n void server.close()\n })\n await server.connect(transport)\n\n let body = ''\n for await (const chunk of req) body += chunk\n let parsed: unknown\n try {\n parsed = body ? JSON.parse(body) : undefined\n } catch {\n // Malformed agent request → 400, distinct from an internal 500.\n if (!res.headersSent) res.writeHead(400).end('invalid JSON body')\n return\n }\n await transport.handleRequest(req, res, parsed)\n })().catch((error: unknown) => {\n // Log the underlying fault — on the host/Docker path there is no run-log\n // capturing it, so swallowing it leaves an operator with nothing.\n console.error('[tool-bridge] request handler failed:', error)\n if (!res.headersSent) res.writeHead(500).end('bridge error')\n })\n })\n\n await new Promise<void>((resolve) =>\n httpServer.listen(0, bindAddress, resolve),\n )\n const port = (httpServer.address() as AddressInfo).port\n const url = `http://${options.hostForSandbox}:${port}/mcp`\n\n return {\n name: BRIDGED_MCP_SERVER_NAME,\n url,\n token,\n close: () =>\n new Promise<void>((resolve) => httpServer.close(() => resolve())),\n }\n}\n\n/** A provisioned, reachable bridge endpoint (same shape as {@link HostToolBridge}). */\nexport type ProvisionedBridge = HostToolBridge\n\nexport interface ToolBridgeProvisionOptions extends ToolBridgeCoreOptions {\n /** Sandbox provider name, to derive how the sandbox reaches the bridge. */\n provider: string\n}\n\n/**\n * Stands up the tool-bridge endpoint for a run. The seam that makes the bridge\n * portable across runtimes: a harness adapter asks its capability context for a\n * provisioner and uses {@link nodeHttpBridgeProvisioner} as the default (host /\n * Docker). A serverless/edge orchestrator PROVIDES its own — e.g. a Durable\n * Object that mounts {@link createToolBridgeCore} / {@link handleBridgeJsonRpc}\n * on its `fetch` handler and returns a sandbox-reachable URL — so no raw TCP\n * listener is needed.\n */\nexport interface ToolBridgeProvisioner {\n provision: (\n tools: Array<AnyTool>,\n options: ToolBridgeProvisionOptions,\n ) => Promise<ProvisionedBridge>\n}\n\n/** Default provisioner: a `node:http` listener on the host. */\nexport const nodeHttpBridgeProvisioner: ToolBridgeProvisioner = {\n provision(tools, options) {\n const { provider, ...core } = options\n return startHostToolBridge(tools, {\n hostForSandbox: hostForSandbox(provider),\n ...core,\n })\n },\n}\n"],"names":[],"mappings":";;;;;AAoCO,MAAM,0BAA0B;AAGhC,SAAS,eAAe,UAA0B;AACvD,SAAO,aAAa,WAAW,yBAAyB;AAC1D;AAmDA,SAAS,eAAe,QAGtB;AACA,MACE,WAAW,QACX,OAAO,WAAW,YAClB,UAAU,UACV,OAAO,SAAS,UAChB;AACA,WAAO,EAAE,GAAG,QAAQ,MAAM,SAAA;AAAA,EAC5B;AACA,SAAO,EAAE,MAAM,UAAU,YAAY,CAAA,EAAC;AACxC;AAmBO,SAAS,qBACd,OACA,UAAiC,IACjB;AAChB,QAAM,cAAc,IAAI,IAAI,MAAM,IAAI,CAAC,SAAS,CAAC,KAAK,MAAM,IAAI,CAAC,CAAC;AAClE,QAAM,aAAa,QAAQ;AAE3B,QAAM,uBAAmD,aACrD;AAAA,IACE,MAAM,WAAW;AAAA,IACjB,aACE;AAAA,IACF,aAAa,EAAE,MAAM,UAAU,YAAY,CAAA,EAAC;AAAA,EAAE,IAEhD;AAEJ,SAAO;AAAA,IACL,YAAY;AACV,aAAO;AAAA,QACL,GAAG,MAAM,IAAI,CAAC,UAAU;AAAA,UACtB,MAAM,KAAK;AAAA,UACX,aAAa,KAAK;AAAA,UAClB,aAAa,eAAe,KAAK,WAAW;AAAA,QAAA,EAC5C;AAAA,QACF,GAAI,uBAAuB,CAAC,oBAAoB,IAAI,CAAA;AAAA,MAAC;AAAA,IAEzD;AAAA,IAEA,MAAM,SAAS,MAAM,MAAM;AACzB,UAAI,cAAc,SAAS,WAAW,UAAU;AAC9C,cAAM,SAAS,MAAM,WAAW,QAAQ,QAAQ,CAAA,CAAE;AAClD,eAAO,EAAE,SAAS,CAAC,EAAE,MAAM,QAAQ,MAAM,KAAK,UAAU,MAAM,EAAA,CAAG,EAAA;AAAA,MACnE;AACA,YAAM,OAAO,YAAY,IAAI,IAAI;AACjC,UAAI,CAAC,MAAM,QAAS,OAAM,IAAI,MAAM,iBAAiB,IAAI,EAAE;AAC3D,UAAI;AACF,cAAM,SAAkB,MAAM,KAAK,QAAQ,QAAQ,CAAA,GAAI;AAAA,UACrD,SAAS,QAAQ;AAAA,UACjB,aAAa,QAAQ;AAAA;AAAA;AAAA,UAGrB,iBAAiB,QAAQ,oBAAoB,MAAM;AAAA,UAAC;AAAA,QAAA,CACrD;AACD,cAAM,OACJ,OAAO,WAAW,WAAW,SAAS,KAAK,UAAU,MAAM;AAC7D,eAAO,EAAE,SAAS,CAAC,EAAE,MAAM,QAAQ,KAAA,CAAM,EAAA;AAAA,MAC3C,SAAS,OAAO;AACd,cAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,eAAO;AAAA,UACL,SAAS;AAAA,UACT,SAAS;AAAA,YACP,EAAE,MAAM,QAAQ,MAAM,0BAA0B,OAAO,GAAA;AAAA,UAAG;AAAA,QAC5D;AAAA,MAEJ;AAAA,IACF;AAAA,EAAA;AAEJ;AAQA,eAAsB,oBACpB,MACA,SACkB;AAClB,MAAI,YAAY,QAAQ,OAAO,YAAY,UAAU;AACnD,WAAO;AAAA,MACL,SAAS;AAAA,MACT,IAAI;AAAA,MACJ,OAAO,EAAE,MAAM,QAAQ,SAAS,kBAAA;AAAA,IAAkB;AAAA,EAEtD;AACA,QAAM,MAAM;AACZ,QAAM,KAAK,IAAI,MAAM;AACrB,QAAM,UAAU,CAAC,YAA8B,EAAE,SAAS,OAAO,IAAI;AACrE,UAAQ,IAAI,QAAA;AAAA,IACV,KAAK;AACH,aAAO,QAAQ;AAAA,QACb,iBAAiB;AAAA,QACjB,cAAc,EAAE,OAAO,GAAC;AAAA,QACxB,YAAY,EAAE,MAAM,yBAAyB,SAAS,QAAA;AAAA,MAAQ,CAC/D;AAAA,IACH,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,QAAQ,EAAE,OAAO,KAAK,UAAA,GAAa;AAAA,IAC5C,KAAK,cAAc;AACjB,YAAM,SAAU,IAAI,UAAU,CAAA;AAI9B,UAAI,OAAO,OAAO,SAAS,UAAU;AACnC,eAAO;AAAA,UACL,SAAS;AAAA,UACT;AAAA,UACA,OAAO,EAAE,MAAM,QAAQ,SAAS,uBAAA;AAAA,QAAuB;AAAA,MAE3D;AACA,aAAO,QAAQ,MAAM,KAAK,SAAS,OAAO,MAAM,OAAO,aAAa,CAAA,CAAE,CAAC;AAAA,IACzE;AAAA,IACA;AACE,aAAO;AAAA,QACL,SAAS;AAAA,QACT;AAAA,QACA,OAAO,EAAE,MAAM,QAAQ,SAAS,mBAAA;AAAA,MAAmB;AAAA,EACrD;AAEN;AAOO,SAAS,sBACd,QACA,OACS;AACT,MAAI,WAAW,OAAW,QAAO;AACjC,QAAM,IAAI,OAAO,KAAK,MAAM;AAC5B,QAAM,IAAI,OAAO,KAAK,UAAU,KAAK,EAAE;AACvC,MAAI,EAAE,WAAW,EAAE,OAAQ,QAAO;AAClC,SAAO,gBAAgB,GAAG,CAAC;AAC7B;AAuBA,SAAS,eAAe,MAAiC;AACvD,QAAM,SAAS,IAAI;AAAA,IACjB,EAAE,MAAM,yBAAyB,SAAS,QAAA;AAAA,IAC1C,EAAE,cAAc,EAAE,OAAO,KAAG;AAAA,EAAE;AAEhC,SAAO,OAAO,kBAAkB,wBAAwB,OAAO;AAAA,IAC7D,OAAO,KAAK,UAAA;AAAA,EAAU,EACtB;AACF,SAAO,OAAO,kBAAkB,uBAAuB,OAAO,YAAY;AACxE,UAAM,SAAS,MAAM,KAAK;AAAA,MACxB,QAAQ,OAAO;AAAA,MACf,QAAQ,OAAO,aAAa,CAAA;AAAA,IAAC;AAE/B,WAAO;AAAA,MACL,SAAS,OAAO;AAAA,MAChB,GAAI,OAAO,UAAU,EAAE,SAAS,KAAA,IAAS,CAAA;AAAA,IAAC;AAAA,EAE9C,CAAC;AACD,SAAO;AACT;AAQA,eAAsB,oBACpB,OACA,SACyB;AACzB,QAAM,QAAQ,YAAY,EAAE,EAAE,SAAS,KAAK;AAC5C,QAAM,OAAO,qBAAqB,OAAO,OAAO;AAGhD,QAAM,cACJ,QAAQ,gBACP,QAAQ,mBAAmB,yBACxB,YACA;AAEN,QAAM,aAAa,aAAa,CAAC,KAAK,QAAQ;AAC5C,UAAM,YAAY;AAChB,UAAI,CAAC,sBAAsB,IAAI,QAAQ,eAAe,GAAG,KAAK,GAAG;AAC/D,YAAI,UAAU,GAAG,EAAE,IAAI,cAAc;AACrC;AAAA,MACF;AACA,YAAM,SAAS,eAAe,IAAI;AAClC,YAAM,YAAY,IAAI,8BAA8B;AAAA,QAClD,oBAAoB;AAAA,MAAA,CACrB;AACD,UAAI,GAAG,SAAS,MAAM;AACpB,aAAK,UAAU,MAAA;AACf,aAAK,OAAO,MAAA;AAAA,MACd,CAAC;AACD,YAAM,OAAO,QAAQ,SAAS;AAE9B,UAAI,OAAO;AACX,uBAAiB,SAAS,IAAK,SAAQ;AACvC,UAAI;AACJ,UAAI;AACF,iBAAS,OAAO,KAAK,MAAM,IAAI,IAAI;AAAA,MACrC,QAAQ;AAEN,YAAI,CAAC,IAAI,YAAa,KAAI,UAAU,GAAG,EAAE,IAAI,mBAAmB;AAChE;AAAA,MACF;AACA,YAAM,UAAU,cAAc,KAAK,KAAK,MAAM;AAAA,IAChD,GAAA,EAAK,MAAM,CAAC,UAAmB;AAG7B,cAAQ,MAAM,yCAAyC,KAAK;AAC5D,UAAI,CAAC,IAAI,YAAa,KAAI,UAAU,GAAG,EAAE,IAAI,cAAc;AAAA,IAC7D,CAAC;AAAA,EACH,CAAC;AAED,QAAM,IAAI;AAAA,IAAc,CAAC,YACvB,WAAW,OAAO,GAAG,aAAa,OAAO;AAAA,EAAA;AAE3C,QAAM,OAAQ,WAAW,QAAA,EAA0B;AACnD,QAAM,MAAM,UAAU,QAAQ,cAAc,IAAI,IAAI;AAEpD,SAAO;AAAA,IACL,MAAM;AAAA,IACN;AAAA,IACA;AAAA,IACA,OAAO,MACL,IAAI,QAAc,CAAC,YAAY,WAAW,MAAM,MAAM,SAAS,CAAC;AAAA,EAAA;AAEtE;AA2BO,MAAM,4BAAmD;AAAA,EAC9D,UAAU,OAAO,SAAS;AACxB,UAAM,EAAE,UAAU,GAAG,KAAA,IAAS;AAC9B,WAAO,oBAAoB,OAAO;AAAA,MAChC,gBAAgB,eAAe,QAAQ;AAAA,MACvC,GAAG;AAAA,IAAA,CACJ;AAAA,EACH;AACF;"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { SandboxHandle } from './contracts.js';
|
|
2
|
+
import { SandboxFileEvent } from '@tanstack/ai';
|
|
3
|
+
export type { SandboxFileEvent } from '@tanstack/ai';
|
|
4
|
+
/** @deprecated alias retained for the low-level watch API. */
|
|
5
|
+
export type FileEvent = SandboxFileEvent;
|
|
6
|
+
export type FileEventType = SandboxFileEvent['type'];
|
|
7
|
+
export interface WatchOptions {
|
|
8
|
+
/** Called for every observed file event. */
|
|
9
|
+
onEvent: (event: SandboxFileEvent) => void;
|
|
10
|
+
/** Workspace root to watch. Defaults to `/workspace`. */
|
|
11
|
+
root?: string;
|
|
12
|
+
/** Poll interval for the exec-poll fallback, in ms. Defaults to 700. */
|
|
13
|
+
intervalMs?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Directory-name fragments to ignore (a path containing `/<entry>/` is
|
|
16
|
+
* skipped). Defaults to `['.git', 'node_modules']`.
|
|
17
|
+
*/
|
|
18
|
+
ignore?: Array<string>;
|
|
19
|
+
/** Stop watching when this signal aborts. */
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
}
|
|
22
|
+
export interface SandboxWatchHandle {
|
|
23
|
+
/** Stop the watcher and release its resources. */
|
|
24
|
+
stop: () => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Diff two file snapshots (`Map<path, signature>`, signature = `mtime\tsize`).
|
|
28
|
+
* Pure — the heart of the exec-poll path, unit-tested in isolation.
|
|
29
|
+
*/
|
|
30
|
+
export declare function diffSnapshots(prev: Map<string, string>, next: Map<string, string>, timestamp: number): Array<SandboxFileEvent>;
|
|
31
|
+
/**
|
|
32
|
+
* Start watching a sandbox workspace for file events. Picks the native
|
|
33
|
+
* `fs.watch` fast-path when the provider advertises it, otherwise polls via
|
|
34
|
+
* `find`. Returns a handle whose `stop()` tears everything down.
|
|
35
|
+
*/
|
|
36
|
+
export declare function watchWorkspace(handle: SandboxHandle, options: WatchOptions): Promise<SandboxWatchHandle>;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { DEFAULT_WORKSPACE_ROOT } from "./bootstrap.js";
|
|
2
|
+
const DEFAULT_INTERVAL_MS = 700;
|
|
3
|
+
const DEFAULT_IGNORE = [".git", "node_modules"];
|
|
4
|
+
function q(value) {
|
|
5
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
6
|
+
}
|
|
7
|
+
function diffSnapshots(prev, next, timestamp) {
|
|
8
|
+
const events = [];
|
|
9
|
+
for (const [path, sig] of next) {
|
|
10
|
+
const before = prev.get(path);
|
|
11
|
+
if (before === void 0) events.push({ type: "create", path, timestamp });
|
|
12
|
+
else if (before !== sig) events.push({ type: "change", path, timestamp });
|
|
13
|
+
}
|
|
14
|
+
for (const path of prev.keys()) {
|
|
15
|
+
if (!next.has(path)) events.push({ type: "delete", path, timestamp });
|
|
16
|
+
}
|
|
17
|
+
return events;
|
|
18
|
+
}
|
|
19
|
+
function buildFindCommand(root, ignore) {
|
|
20
|
+
const prunes = ignore.map((entry) => `-not -path ${q(`*/${entry}/*`)}`).join(" ");
|
|
21
|
+
return `find ${q(root)} -type f ${prunes} -printf '%T@\\t%s\\t%p\\n'`;
|
|
22
|
+
}
|
|
23
|
+
function parseFindOutput(stdout) {
|
|
24
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
25
|
+
for (const line of stdout.split("\n")) {
|
|
26
|
+
if (line === "") continue;
|
|
27
|
+
const firstTab = line.indexOf(" ");
|
|
28
|
+
const secondTab = line.indexOf(" ", firstTab + 1);
|
|
29
|
+
if (firstTab === -1 || secondTab === -1) continue;
|
|
30
|
+
const mtime = line.slice(0, firstTab);
|
|
31
|
+
const size = line.slice(firstTab + 1, secondTab);
|
|
32
|
+
const path = line.slice(secondTab + 1);
|
|
33
|
+
snapshot.set(path, `${mtime} ${size}`);
|
|
34
|
+
}
|
|
35
|
+
return snapshot;
|
|
36
|
+
}
|
|
37
|
+
function isIgnored(path, ignore) {
|
|
38
|
+
return ignore.some((entry) => path.includes(`/${entry}/`));
|
|
39
|
+
}
|
|
40
|
+
async function watchWorkspace(handle, options) {
|
|
41
|
+
const root = options.root ?? DEFAULT_WORKSPACE_ROOT;
|
|
42
|
+
const ignore = options.ignore ?? DEFAULT_IGNORE;
|
|
43
|
+
const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS;
|
|
44
|
+
if (options.signal?.aborted) return { stop: () => Promise.resolve() };
|
|
45
|
+
if (handle.fs.watch) {
|
|
46
|
+
return startNativeWatch(handle, { ...options, root, ignore });
|
|
47
|
+
}
|
|
48
|
+
return startPollWatch(handle, { ...options, root, ignore, intervalMs });
|
|
49
|
+
}
|
|
50
|
+
async function startNativeWatch(handle, options) {
|
|
51
|
+
const { onEvent, root, ignore } = options;
|
|
52
|
+
const watch = handle.fs.watch;
|
|
53
|
+
if (!watch) throw new Error("native watch is unavailable on this provider");
|
|
54
|
+
const known = await collectPaths(handle, root, ignore);
|
|
55
|
+
const subscription = await watch(root, (raw) => {
|
|
56
|
+
const path = raw.path;
|
|
57
|
+
if (isIgnored(path, ignore)) return;
|
|
58
|
+
void (async () => {
|
|
59
|
+
const exists = await handle.fs.exists(path);
|
|
60
|
+
const timestamp = Date.now();
|
|
61
|
+
if (!exists) {
|
|
62
|
+
if (known.delete(path)) onEvent({ type: "delete", path, timestamp });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
if (known.has(path)) onEvent({ type: "change", path, timestamp });
|
|
66
|
+
else {
|
|
67
|
+
known.add(path);
|
|
68
|
+
onEvent({ type: "create", path, timestamp });
|
|
69
|
+
}
|
|
70
|
+
})().catch(() => void 0);
|
|
71
|
+
});
|
|
72
|
+
const onAbort = () => void subscription.stop().catch(() => void 0);
|
|
73
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
74
|
+
if (options.signal?.aborted) void subscription.stop().catch(() => void 0);
|
|
75
|
+
return {
|
|
76
|
+
stop: async () => {
|
|
77
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
78
|
+
await subscription.stop();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
async function startPollWatch(handle, options) {
|
|
83
|
+
const { onEvent, root, ignore, intervalMs } = options;
|
|
84
|
+
const command = buildFindCommand(root, ignore);
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
const snapshot = async () => {
|
|
87
|
+
const result = await handle.process.exec(command, {
|
|
88
|
+
cwd: root,
|
|
89
|
+
signal: controller.signal
|
|
90
|
+
});
|
|
91
|
+
return result.exitCode === 0 ? parseFindOutput(result.stdout) : /* @__PURE__ */ new Map();
|
|
92
|
+
};
|
|
93
|
+
let previous = await snapshot();
|
|
94
|
+
const state = { running: true };
|
|
95
|
+
const tick = async () => {
|
|
96
|
+
if (!state.running) return;
|
|
97
|
+
try {
|
|
98
|
+
const next = await snapshot();
|
|
99
|
+
for (const event of diffSnapshots(previous, next, Date.now())) {
|
|
100
|
+
onEvent(event);
|
|
101
|
+
}
|
|
102
|
+
previous = next;
|
|
103
|
+
} catch {
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
const timer = setInterval(() => void tick(), intervalMs);
|
|
107
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
108
|
+
const stop = () => {
|
|
109
|
+
if (state.running) {
|
|
110
|
+
state.running = false;
|
|
111
|
+
clearInterval(timer);
|
|
112
|
+
controller.abort();
|
|
113
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
114
|
+
}
|
|
115
|
+
return Promise.resolve();
|
|
116
|
+
};
|
|
117
|
+
const onAbort = () => void stop();
|
|
118
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
119
|
+
if (options.signal?.aborted) void stop();
|
|
120
|
+
return { stop };
|
|
121
|
+
}
|
|
122
|
+
async function collectPaths(handle, root, ignore) {
|
|
123
|
+
const files = /* @__PURE__ */ new Set();
|
|
124
|
+
const walk = async (dir) => {
|
|
125
|
+
let entries;
|
|
126
|
+
try {
|
|
127
|
+
entries = await handle.fs.list(dir);
|
|
128
|
+
} catch {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
for (const entry of entries) {
|
|
132
|
+
if (ignore.includes(entry.name)) continue;
|
|
133
|
+
if (entry.type === "dir") await walk(entry.path);
|
|
134
|
+
else files.add(entry.path);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
await walk(root);
|
|
138
|
+
return files;
|
|
139
|
+
}
|
|
140
|
+
export {
|
|
141
|
+
diffSnapshots,
|
|
142
|
+
watchWorkspace
|
|
143
|
+
};
|
|
144
|
+
//# sourceMappingURL=watch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch.js","sources":["../../src/watch.ts"],"sourcesContent":["/**\n * Sandbox file-event hooks — observe create / change / delete of files inside a\n * sandbox (e.g. as an in-sandbox agent edits the workspace).\n *\n * Provider-agnostic: coded against the {@link SandboxHandle} contract only.\n * Two mechanisms, auto-selected:\n *\n * - **Native** — when a provider implements the optional `fs.watch` seam\n * (local-process does, via Node `fs.watch`), OS events drive the feed with low\n * latency.\n * - **Exec-poll** — otherwise (Docker, Cloudflare, any exec-only provider), a\n * single `find … -printf` snapshot of `mtime\\tsize\\tpath` is taken every\n * `intervalMs` and diffed. Works on any Linux container with GNU findutils\n * (true for `node:*` / debian images) with no extra deps or image changes.\n *\n * The feed intentionally rides only the portable surface, so the same\n * `watchWorkspace` call behaves identically across providers.\n */\nimport { DEFAULT_WORKSPACE_ROOT } from './bootstrap'\nimport type { SandboxHandle } from './contracts'\nimport type { SandboxFileEvent } from '@tanstack/ai'\n\nexport type { SandboxFileEvent } from '@tanstack/ai'\n/** @deprecated alias retained for the low-level watch API. */\nexport type FileEvent = SandboxFileEvent\nexport type FileEventType = SandboxFileEvent['type']\n\nexport interface WatchOptions {\n /** Called for every observed file event. */\n onEvent: (event: SandboxFileEvent) => void\n /** Workspace root to watch. Defaults to `/workspace`. */\n root?: string\n /** Poll interval for the exec-poll fallback, in ms. Defaults to 700. */\n intervalMs?: number\n /**\n * Directory-name fragments to ignore (a path containing `/<entry>/` is\n * skipped). Defaults to `['.git', 'node_modules']`.\n */\n ignore?: Array<string>\n /** Stop watching when this signal aborts. */\n signal?: AbortSignal\n}\n\nexport interface SandboxWatchHandle {\n /** Stop the watcher and release its resources. */\n stop: () => Promise<void>\n}\n\nconst DEFAULT_INTERVAL_MS = 700\nconst DEFAULT_IGNORE = ['.git', 'node_modules']\n\n/** POSIX single-quote escape for embedding values in a shell command. */\nfunction q(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`\n}\n\n/**\n * Diff two file snapshots (`Map<path, signature>`, signature = `mtime\\tsize`).\n * Pure — the heart of the exec-poll path, unit-tested in isolation.\n */\nexport function diffSnapshots(\n prev: Map<string, string>,\n next: Map<string, string>,\n timestamp: number,\n): Array<SandboxFileEvent> {\n const events: Array<SandboxFileEvent> = []\n for (const [path, sig] of next) {\n const before = prev.get(path)\n if (before === undefined) events.push({ type: 'create', path, timestamp })\n else if (before !== sig) events.push({ type: 'change', path, timestamp })\n }\n for (const path of prev.keys()) {\n if (!next.has(path)) events.push({ type: 'delete', path, timestamp })\n }\n return events\n}\n\n/** Build the `find` command that prints `mtime\\tsize\\tpath` for every file. */\nfunction buildFindCommand(root: string, ignore: Array<string>): string {\n const prunes = ignore\n .map((entry) => `-not -path ${q(`*/${entry}/*`)}`)\n .join(' ')\n return `find ${q(root)} -type f ${prunes} -printf '%T@\\\\t%s\\\\t%p\\\\n'`\n}\n\n/** Parse `find -printf` output into a `Map<path, signature>`. */\nfunction parseFindOutput(stdout: string): Map<string, string> {\n const snapshot = new Map<string, string>()\n for (const line of stdout.split('\\n')) {\n if (line === '') continue\n const firstTab = line.indexOf('\\t')\n const secondTab = line.indexOf('\\t', firstTab + 1)\n if (firstTab === -1 || secondTab === -1) continue\n const mtime = line.slice(0, firstTab)\n const size = line.slice(firstTab + 1, secondTab)\n const path = line.slice(secondTab + 1)\n snapshot.set(path, `${mtime}\\t${size}`)\n }\n return snapshot\n}\n\n/** Whether a path should be ignored (contains a `/<entry>/` fragment). */\nfunction isIgnored(path: string, ignore: Array<string>): boolean {\n return ignore.some((entry) => path.includes(`/${entry}/`))\n}\n\n/**\n * Start watching a sandbox workspace for file events. Picks the native\n * `fs.watch` fast-path when the provider advertises it, otherwise polls via\n * `find`. Returns a handle whose `stop()` tears everything down.\n */\nexport async function watchWorkspace(\n handle: SandboxHandle,\n options: WatchOptions,\n): Promise<SandboxWatchHandle> {\n const root = options.root ?? DEFAULT_WORKSPACE_ROOT\n const ignore = options.ignore ?? DEFAULT_IGNORE\n const intervalMs = options.intervalMs ?? DEFAULT_INTERVAL_MS\n\n // Already aborted before we start — don't begin any async work.\n if (options.signal?.aborted) return { stop: () => Promise.resolve() }\n\n if (handle.fs.watch) {\n return startNativeWatch(handle, { ...options, root, ignore })\n }\n return startPollWatch(handle, { ...options, root, ignore, intervalMs })\n}\n\n/** Native fs.watch path: OS events, disambiguated against a known-path set. */\nasync function startNativeWatch(\n handle: SandboxHandle,\n options: WatchOptions & { root: string; ignore: Array<string> },\n): Promise<SandboxWatchHandle> {\n const { onEvent, root, ignore } = options\n const watch = handle.fs.watch\n if (!watch) throw new Error('native watch is unavailable on this provider')\n // Seed the set of existing files so the first event per path is classified\n // correctly (create vs change).\n const known = await collectPaths(handle, root, ignore)\n\n const subscription = await watch(root, (raw) => {\n const path = raw.path\n if (isIgnored(path, ignore)) return\n void (async () => {\n const exists = await handle.fs.exists(path)\n const timestamp = Date.now()\n if (!exists) {\n if (known.delete(path)) onEvent({ type: 'delete', path, timestamp })\n return\n }\n if (known.has(path)) onEvent({ type: 'change', path, timestamp })\n else {\n known.add(path)\n onEvent({ type: 'create', path, timestamp })\n }\n })().catch(() => undefined)\n })\n\n const onAbort = (): void => void subscription.stop().catch(() => undefined)\n options.signal?.addEventListener('abort', onAbort, { once: true })\n // The signal may have aborted during the awaits above (the once-listener\n // would have missed it) — tear down now if so.\n if (options.signal?.aborted) void subscription.stop().catch(() => undefined)\n\n return {\n stop: async () => {\n options.signal?.removeEventListener('abort', onAbort)\n await subscription.stop()\n },\n }\n}\n\n/** Exec-poll path: snapshot `find -printf` on an interval and diff. */\nasync function startPollWatch(\n handle: SandboxHandle,\n options: WatchOptions & {\n root: string\n ignore: Array<string>\n intervalMs: number\n },\n): Promise<SandboxWatchHandle> {\n const { onEvent, root, ignore, intervalMs } = options\n const command = buildFindCommand(root, ignore)\n const controller = new AbortController()\n\n const snapshot = async (): Promise<Map<string, string>> => {\n const result = await handle.process.exec(command, {\n cwd: root,\n signal: controller.signal,\n })\n return result.exitCode === 0\n ? parseFindOutput(result.stdout)\n : new Map<string, string>()\n }\n\n let previous = await snapshot()\n const state = { running: true }\n\n const tick = async (): Promise<void> => {\n if (!state.running) return\n try {\n const next = await snapshot()\n for (const event of diffSnapshots(previous, next, Date.now())) {\n onEvent(event)\n }\n previous = next\n } catch {\n // transient exec failure (e.g. mid-teardown) — try again next tick\n }\n }\n\n const timer = setInterval(() => void tick(), intervalMs)\n // Don't keep the event loop alive on the watcher alone.\n if (typeof timer.unref === 'function') timer.unref()\n\n const stop = (): Promise<void> => {\n if (state.running) {\n state.running = false\n clearInterval(timer)\n controller.abort()\n options.signal?.removeEventListener('abort', onAbort)\n }\n return Promise.resolve()\n }\n const onAbort = (): void => void stop()\n options.signal?.addEventListener('abort', onAbort, { once: true })\n // The signal may have aborted during the initial `await snapshot()` above\n // (the once-listener would have missed it) — tear down now if so.\n if (options.signal?.aborted) void stop()\n\n return { stop }\n}\n\n/** Recursively collect file paths under `root`, honoring `ignore`. */\nasync function collectPaths(\n handle: SandboxHandle,\n root: string,\n ignore: Array<string>,\n): Promise<Set<string>> {\n const files = new Set<string>()\n const walk = async (dir: string): Promise<void> => {\n let entries: Awaited<ReturnType<SandboxHandle['fs']['list']>>\n try {\n entries = await handle.fs.list(dir)\n } catch {\n return\n }\n for (const entry of entries) {\n if (ignore.includes(entry.name)) continue\n if (entry.type === 'dir') await walk(entry.path)\n else files.add(entry.path)\n }\n }\n await walk(root)\n return files\n}\n"],"names":[],"mappings":"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"}
|