@sweny-ai/core 0.1.150 → 0.1.151
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/cli/main.js
CHANGED
|
@@ -1313,11 +1313,12 @@ workflowCmd
|
|
|
1313
1313
|
program
|
|
1314
1314
|
.command("tool-bridge", { hidden: true })
|
|
1315
1315
|
.description("Internal: stdio MCP shim for sweny skill tools")
|
|
1316
|
-
.
|
|
1316
|
+
.option("--socket <path>", "Per-run bridge socket")
|
|
1317
|
+
.option("--connect <host:port>", "Per-run bridge TCP endpoint (inside the process sandbox, via its proxy)")
|
|
1317
1318
|
.option("--token <token>", "Per-run token (default: SWENY_TOOL_BRIDGE_TOKEN)")
|
|
1318
1319
|
.action(async (options) => {
|
|
1319
1320
|
const { runToolBridgeShim } = await import("../harness/tool-bridge/shim.js");
|
|
1320
|
-
await runToolBridgeShim({ socket: options.socket, token: options.token, version });
|
|
1321
|
+
await runToolBridgeShim({ socket: options.socket, connect: options.connect, token: options.token, version });
|
|
1321
1322
|
});
|
|
1322
1323
|
// ── sweny upgrade / update ────────────────────────────────────────────
|
|
1323
1324
|
// Self-update the globally-installed @sweny-ai/core. Mirrors the UX of
|
package/dist/harness/pi.js
CHANGED
|
@@ -771,15 +771,24 @@ export class PiHarness {
|
|
|
771
771
|
tools: req.tools,
|
|
772
772
|
context: this.defaultContext,
|
|
773
773
|
logger: this.logger,
|
|
774
|
+
// Wrapped, the shim cannot open the unix socket (srt blocks AF_UNIX on
|
|
775
|
+
// Linux); it tunnels to a loopback port through the sandbox proxy (#439).
|
|
776
|
+
tcp: wrapper !== undefined,
|
|
774
777
|
...(this.toolBridgeShim ? { shimCommand: this.toolBridgeShim } : {}),
|
|
775
778
|
});
|
|
776
779
|
const shim = bridge.mcpServer;
|
|
777
780
|
// The token reaches the shim by reference (`${VAR}`), never in a file or argv.
|
|
778
781
|
env[TOKEN_ENV] = bridge.token;
|
|
782
|
+
const ref = (name) => "${" + name + "}";
|
|
779
783
|
entries[PI_BRIDGE_SERVER] = {
|
|
780
784
|
command: shim.command,
|
|
781
785
|
args: shim.args ?? [],
|
|
782
|
-
env: {
|
|
786
|
+
env: {
|
|
787
|
+
SWENY_NO_UPDATE_CHECK: "1",
|
|
788
|
+
[TOKEN_ENV]: ref(TOKEN_ENV),
|
|
789
|
+
// The proxy srt sets for pi, which pi's MCP client does not pass on by itself.
|
|
790
|
+
...(bridge.tcp ? { HTTP_PROXY: ref("HTTP_PROXY"), http_proxy: ref("http_proxy") } : {}),
|
|
791
|
+
},
|
|
783
792
|
exposure: "direct",
|
|
784
793
|
timeout: 600,
|
|
785
794
|
};
|
|
@@ -802,8 +811,10 @@ export class PiHarness {
|
|
|
802
811
|
toolArgs.push("--exclude-tools", [...excluded].join(","));
|
|
803
812
|
}
|
|
804
813
|
const args = [...this.baseArgs(model, Object.keys(entries).length > 0), ...toolArgs];
|
|
805
|
-
const
|
|
806
|
-
|
|
814
|
+
const backendHosts = piBackendHosts(process.env, model);
|
|
815
|
+
// The bridge's loopback endpoint (wrapped runs only): the one extra host the shim needs.
|
|
816
|
+
const harnessEgress = [...backendHosts, ...(bridge?.egress ?? [])];
|
|
817
|
+
if (mode !== "off" && backendHosts.length === 0 && !this.egressWarned) {
|
|
807
818
|
this.egressWarned = true;
|
|
808
819
|
this.logger.warn("pi runs inside the sandbox wrapper with no known model API host; add it with SWENY_SANDBOX_ALLOWED_DOMAINS.");
|
|
809
820
|
}
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* Security properties (this is an injection boundary):
|
|
10
10
|
* - The socket lives in a fresh `mkdtemp` directory (mode 0700) and is itself
|
|
11
11
|
* chmod 0600, so other users cannot connect.
|
|
12
|
+
* - With `tcp` (only for agents inside the process sandbox wrapper, #439) the
|
|
13
|
+
* same handler also listens on 127.0.0.1:<random port>. Other local users
|
|
14
|
+
* can open that port, so there the token is the only gate; the sandboxed
|
|
15
|
+
* agent reaches it through srt's proxy only because the adapter allowlists
|
|
16
|
+
* exactly that host:port for the run.
|
|
12
17
|
* - A random 256-bit token is required on every frame. A wrong or missing
|
|
13
18
|
* token gets an `unauthorized` error and the connection is dropped.
|
|
14
19
|
* - Only the tools passed in are listed or callable. The executor has already
|
|
@@ -37,12 +42,27 @@ export interface ToolBridgeOptions {
|
|
|
37
42
|
};
|
|
38
43
|
/** Parent directory for the private socket dir. Default: `os.tmpdir()`. */
|
|
39
44
|
tmpDir?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Also serve on a loopback TCP port, and point the shim there (#439). For an
|
|
47
|
+
* agent inside the process sandbox wrapper: srt blocks `socket(AF_UNIX)` on
|
|
48
|
+
* Linux, so the shim cannot reach the unix socket, but it can reach an
|
|
49
|
+
* allowlisted host through srt's egress proxy. The adapter adds
|
|
50
|
+
* {@link ToolBridge.egress} to the node's egress; nothing else is opened.
|
|
51
|
+
*/
|
|
52
|
+
tcp?: boolean;
|
|
40
53
|
}
|
|
41
54
|
export interface ToolBridge {
|
|
42
55
|
/** Absolute path of the unix socket. */
|
|
43
56
|
readonly socketPath: string;
|
|
44
57
|
/** Per-run token. Never log it. */
|
|
45
58
|
readonly token: string;
|
|
59
|
+
/** Loopback TCP endpoint, when started with `tcp: true`. */
|
|
60
|
+
readonly tcp?: {
|
|
61
|
+
host: string;
|
|
62
|
+
port: number;
|
|
63
|
+
};
|
|
64
|
+
/** Hosts a sandboxed shim must reach (`127.0.0.1:<port>` with `tcp`, else none). */
|
|
65
|
+
readonly egress: string[];
|
|
46
66
|
/** stdio MCP server config a harness uses to start the shim. The token rides in `env`, not argv. */
|
|
47
67
|
readonly mcpServer: McpServerConfig;
|
|
48
68
|
/** Stop serving, drop connections, remove the socket and its directory. Idempotent. */
|
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
* Security properties (this is an injection boundary):
|
|
10
10
|
* - The socket lives in a fresh `mkdtemp` directory (mode 0700) and is itself
|
|
11
11
|
* chmod 0600, so other users cannot connect.
|
|
12
|
+
* - With `tcp` (only for agents inside the process sandbox wrapper, #439) the
|
|
13
|
+
* same handler also listens on 127.0.0.1:<random port>. Other local users
|
|
14
|
+
* can open that port, so there the token is the only gate; the sandboxed
|
|
15
|
+
* agent reaches it through srt's proxy only because the adapter allowlists
|
|
16
|
+
* exactly that host:port for the run.
|
|
12
17
|
* - A random 256-bit token is required on every frame. A wrong or missing
|
|
13
18
|
* token gets an `unauthorized` error and the connection is dropped.
|
|
14
19
|
* - Only the tools passed in are listed or callable. The executor has already
|
|
@@ -98,7 +103,7 @@ export async function startToolBridge(opts) {
|
|
|
98
103
|
liveDirs.add(dir);
|
|
99
104
|
installCrashHooks();
|
|
100
105
|
const sockets = new Set();
|
|
101
|
-
const
|
|
106
|
+
const onConnection = (sock) => {
|
|
102
107
|
sockets.add(sock);
|
|
103
108
|
sock.on("close", () => sockets.delete(sock));
|
|
104
109
|
sock.on("error", () => sock.destroy());
|
|
@@ -143,19 +148,29 @@ export async function startToolBridge(opts) {
|
|
|
143
148
|
sock.destroy();
|
|
144
149
|
});
|
|
145
150
|
sock.on("data", read);
|
|
151
|
+
};
|
|
152
|
+
const server = net.createServer(onConnection);
|
|
153
|
+
// Same handler and token check; only started for sandboxed agents.
|
|
154
|
+
const tcpServer = opts.tcp ? net.createServer(onConnection) : undefined;
|
|
155
|
+
const listen = (s, options) => new Promise((resolve, reject) => {
|
|
156
|
+
s.once("error", reject);
|
|
157
|
+
s.listen(options, () => {
|
|
158
|
+
s.removeListener("error", reject);
|
|
159
|
+
resolve();
|
|
160
|
+
});
|
|
146
161
|
});
|
|
162
|
+
let tcp;
|
|
147
163
|
try {
|
|
148
|
-
await
|
|
149
|
-
server.once("error", reject);
|
|
150
|
-
server.listen(socketPath, () => {
|
|
151
|
-
server.removeListener("error", reject);
|
|
152
|
-
resolve();
|
|
153
|
-
});
|
|
154
|
-
});
|
|
164
|
+
await listen(server, { path: socketPath });
|
|
155
165
|
fs.chmodSync(socketPath, 0o600);
|
|
166
|
+
if (tcpServer) {
|
|
167
|
+
await listen(tcpServer, { port: 0, host: "127.0.0.1" });
|
|
168
|
+
tcp = { host: "127.0.0.1", port: tcpServer.address().port };
|
|
169
|
+
}
|
|
156
170
|
}
|
|
157
171
|
catch (err) {
|
|
158
172
|
server.close();
|
|
173
|
+
tcpServer?.close();
|
|
159
174
|
liveDirs.delete(dir);
|
|
160
175
|
removeDirSync(dir);
|
|
161
176
|
uninstallCrashHooksIfIdle();
|
|
@@ -167,10 +182,12 @@ export async function startToolBridge(opts) {
|
|
|
167
182
|
return {
|
|
168
183
|
socketPath,
|
|
169
184
|
token,
|
|
185
|
+
tcp,
|
|
186
|
+
egress: tcp ? [`${tcp.host}:${tcp.port}`] : [],
|
|
170
187
|
mcpServer: {
|
|
171
188
|
type: "stdio",
|
|
172
189
|
command: shim.command,
|
|
173
|
-
args: [...shim.args, "--socket", socketPath],
|
|
190
|
+
args: [...shim.args, ...(tcp ? ["--connect", `${tcp.host}:${tcp.port}`] : ["--socket", socketPath])],
|
|
174
191
|
// No update-check nudge in the shim: its stdio belongs to MCP.
|
|
175
192
|
env: { [TOKEN_ENV]: token, SWENY_NO_UPDATE_CHECK: "1" },
|
|
176
193
|
},
|
|
@@ -180,7 +197,7 @@ export async function startToolBridge(opts) {
|
|
|
180
197
|
closed = true;
|
|
181
198
|
for (const s of sockets)
|
|
182
199
|
s.destroy();
|
|
183
|
-
await new Promise((resolve) =>
|
|
200
|
+
await Promise.all([server, tcpServer].map((s) => (s ? new Promise((resolve) => s.close(() => resolve())) : undefined)));
|
|
184
201
|
liveDirs.delete(dir);
|
|
185
202
|
removeDirSync(dir);
|
|
186
203
|
uninstallCrashHooksIfIdle();
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SwenyToolBridge shim (#414): `sweny tool-bridge --socket <path
|
|
2
|
+
* SwenyToolBridge shim (#414): `sweny tool-bridge --socket <path>`, or
|
|
3
|
+
* `--connect 127.0.0.1:<port>` inside the process sandbox (#439), where it
|
|
4
|
+
* tunnels through the sandbox's HTTP proxy.
|
|
3
5
|
*
|
|
4
6
|
* A stdio MCP server a harness starts as a child process. It holds no tools
|
|
5
7
|
* of its own: `tools/list` and `tools/call` are forwarded over the per-run
|
|
@@ -9,17 +11,49 @@
|
|
|
9
11
|
* `--token` or, by default, the `SWENY_TOOL_BRIDGE_TOKEN` env var the server
|
|
10
12
|
* puts in the MCP server config, so it never shows up in `ps` output.
|
|
11
13
|
*/
|
|
14
|
+
import * as net from "node:net";
|
|
12
15
|
import { type BridgeMethod, type BridgeResponse } from "./protocol.js";
|
|
16
|
+
/** Where the bridge listens: the per-run unix socket, or its loopback TCP port (#439). */
|
|
17
|
+
export type BridgeEndpoint = {
|
|
18
|
+
socketPath: string;
|
|
19
|
+
} | {
|
|
20
|
+
host: string;
|
|
21
|
+
port: number;
|
|
22
|
+
};
|
|
23
|
+
/** Parse `--connect host:port` (IPv4 literal or `localhost`, or `[v6]:port`). */
|
|
24
|
+
export declare function parseConnectTarget(value: string): {
|
|
25
|
+
host: string;
|
|
26
|
+
port: number;
|
|
27
|
+
} | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* The HTTP proxy a sandboxed shim must tunnel through, from its env. srt sets
|
|
30
|
+
* HTTP_PROXY / HTTPS_PROXY (with its per-run credentials) for everything it
|
|
31
|
+
* wraps; inside the sandbox nothing else reaches the host. NO_PROXY is
|
|
32
|
+
* ignored on purpose: srt lists 127.0.0.1 there, but inside the sandbox's own
|
|
33
|
+
* network namespace 127.0.0.1 is not the host.
|
|
34
|
+
*/
|
|
35
|
+
export declare function bridgeProxyFromEnv(env: NodeJS.ProcessEnv): URL | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Open a byte stream to the bridge. A TCP endpoint goes through an HTTP
|
|
38
|
+
* CONNECT tunnel when the env names a proxy (the sandboxed case), else
|
|
39
|
+
* straight to the port. `leftover` holds any bytes read past the proxy's
|
|
40
|
+
* response header.
|
|
41
|
+
*/
|
|
42
|
+
export declare function openBridgeStream(endpoint: BridgeEndpoint, env?: NodeJS.ProcessEnv): Promise<{
|
|
43
|
+
sock: net.Socket;
|
|
44
|
+
leftover: Buffer;
|
|
45
|
+
}>;
|
|
13
46
|
/** Socket client that multiplexes requests by id over one connection. */
|
|
14
47
|
export declare class BridgeClient {
|
|
15
|
-
private readonly socketPath;
|
|
16
48
|
private readonly token;
|
|
49
|
+
private readonly env;
|
|
17
50
|
private sock;
|
|
18
51
|
private connecting;
|
|
19
52
|
private nextId;
|
|
20
53
|
private pending;
|
|
21
54
|
private closedError;
|
|
22
|
-
|
|
55
|
+
private readonly endpoint;
|
|
56
|
+
constructor(endpoint: string | BridgeEndpoint, token: string, env?: NodeJS.ProcessEnv);
|
|
23
57
|
private connect;
|
|
24
58
|
private failAll;
|
|
25
59
|
request(method: BridgeMethod, params?: {
|
|
@@ -29,7 +63,10 @@ export declare class BridgeClient {
|
|
|
29
63
|
close(): void;
|
|
30
64
|
}
|
|
31
65
|
export interface ShimOptions {
|
|
32
|
-
socket
|
|
66
|
+
/** The per-run unix socket. */
|
|
67
|
+
socket?: string;
|
|
68
|
+
/** Or the bridge's loopback TCP endpoint, `host:port` (sandboxed agents, #439). */
|
|
69
|
+
connect?: string;
|
|
33
70
|
/** Falls back to `SWENY_TOOL_BRIDGE_TOKEN`. */
|
|
34
71
|
token?: string;
|
|
35
72
|
version?: string;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SwenyToolBridge shim (#414): `sweny tool-bridge --socket <path
|
|
2
|
+
* SwenyToolBridge shim (#414): `sweny tool-bridge --socket <path>`, or
|
|
3
|
+
* `--connect 127.0.0.1:<port>` inside the process sandbox (#439), where it
|
|
4
|
+
* tunnels through the sandbox's HTTP proxy.
|
|
3
5
|
*
|
|
4
6
|
* A stdio MCP server a harness starts as a child process. It holds no tools
|
|
5
7
|
* of its own: `tools/list` and `tools/call` are forwarded over the per-run
|
|
@@ -14,18 +16,116 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
14
16
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
17
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
16
18
|
import { BRIDGE_PROTOCOL_VERSION, TOKEN_ENV, createFrameReader, encodeFrame, } from "./protocol.js";
|
|
19
|
+
/** Parse `--connect host:port` (IPv4 literal or `localhost`, or `[v6]:port`). */
|
|
20
|
+
export function parseConnectTarget(value) {
|
|
21
|
+
const m = /^(?:\[([0-9a-fA-F:.]+)\]|([A-Za-z0-9.-]+)):([1-9][0-9]{0,4})$/.exec(value);
|
|
22
|
+
if (!m)
|
|
23
|
+
return undefined;
|
|
24
|
+
const port = Number(m[3]);
|
|
25
|
+
return port <= 65535 ? { host: m[1] ?? m[2], port } : undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The HTTP proxy a sandboxed shim must tunnel through, from its env. srt sets
|
|
29
|
+
* HTTP_PROXY / HTTPS_PROXY (with its per-run credentials) for everything it
|
|
30
|
+
* wraps; inside the sandbox nothing else reaches the host. NO_PROXY is
|
|
31
|
+
* ignored on purpose: srt lists 127.0.0.1 there, but inside the sandbox's own
|
|
32
|
+
* network namespace 127.0.0.1 is not the host.
|
|
33
|
+
*/
|
|
34
|
+
export function bridgeProxyFromEnv(env) {
|
|
35
|
+
for (const k of ["HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy", "ALL_PROXY", "all_proxy"]) {
|
|
36
|
+
const v = env[k];
|
|
37
|
+
if (!v)
|
|
38
|
+
continue;
|
|
39
|
+
try {
|
|
40
|
+
const u = new URL(v);
|
|
41
|
+
if (u.protocol === "http:")
|
|
42
|
+
return u;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// not a URL: try the next variable
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const MAX_PROXY_RESPONSE_BYTES = 16 * 1024;
|
|
51
|
+
/**
|
|
52
|
+
* Open a byte stream to the bridge. A TCP endpoint goes through an HTTP
|
|
53
|
+
* CONNECT tunnel when the env names a proxy (the sandboxed case), else
|
|
54
|
+
* straight to the port. `leftover` holds any bytes read past the proxy's
|
|
55
|
+
* response header.
|
|
56
|
+
*/
|
|
57
|
+
export function openBridgeStream(endpoint, env = process.env) {
|
|
58
|
+
const direct = (sock) => new Promise((resolve, reject) => {
|
|
59
|
+
sock.once("error", reject);
|
|
60
|
+
sock.once("connect", () => {
|
|
61
|
+
sock.removeListener("error", reject);
|
|
62
|
+
resolve({ sock, leftover: Buffer.alloc(0) });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
if ("socketPath" in endpoint)
|
|
66
|
+
return direct(net.createConnection(endpoint.socketPath));
|
|
67
|
+
const proxy = bridgeProxyFromEnv(env);
|
|
68
|
+
if (!proxy)
|
|
69
|
+
return direct(net.createConnection(endpoint.port, endpoint.host));
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
const target = endpoint.host.includes(":")
|
|
72
|
+
? `[${endpoint.host}]:${endpoint.port}`
|
|
73
|
+
: `${endpoint.host}:${endpoint.port}`;
|
|
74
|
+
const sock = net.createConnection(Number(proxy.port || 80), proxy.hostname.replace(/^\[|\]$/g, ""));
|
|
75
|
+
let buf = Buffer.alloc(0);
|
|
76
|
+
let done = false;
|
|
77
|
+
const fail = (err) => {
|
|
78
|
+
if (done)
|
|
79
|
+
return;
|
|
80
|
+
done = true;
|
|
81
|
+
sock.destroy();
|
|
82
|
+
reject(err);
|
|
83
|
+
};
|
|
84
|
+
const onData = (chunk) => {
|
|
85
|
+
buf = Buffer.concat([buf, chunk]);
|
|
86
|
+
const end = buf.indexOf("\r\n\r\n");
|
|
87
|
+
if (end === -1) {
|
|
88
|
+
if (buf.length > MAX_PROXY_RESPONSE_BYTES)
|
|
89
|
+
fail(new Error("tool bridge: malformed proxy response"));
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const status = buf.subarray(0, buf.indexOf("\r\n")).toString("latin1");
|
|
93
|
+
if (!/^HTTP\/1\.[01] 200(\s|$)/.test(status)) {
|
|
94
|
+
fail(new Error(`tool bridge: the sandbox proxy refused ${target} (${status})`));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
done = true;
|
|
98
|
+
sock.removeListener("data", onData);
|
|
99
|
+
sock.removeListener("error", fail);
|
|
100
|
+
sock.removeListener("close", onClose);
|
|
101
|
+
resolve({ sock, leftover: buf.subarray(end + 4) });
|
|
102
|
+
};
|
|
103
|
+
const onClose = () => fail(new Error("tool bridge: the sandbox proxy closed the connection"));
|
|
104
|
+
sock.on("data", onData);
|
|
105
|
+
sock.once("error", fail);
|
|
106
|
+
sock.once("close", onClose);
|
|
107
|
+
sock.once("connect", () => {
|
|
108
|
+
const user = decodeURIComponent(proxy.username);
|
|
109
|
+
const pass = decodeURIComponent(proxy.password);
|
|
110
|
+
const auth = user || pass ? `Proxy-Authorization: Basic ${Buffer.from(`${user}:${pass}`).toString("base64")}\r\n` : "";
|
|
111
|
+
sock.write(`CONNECT ${target} HTTP/1.1\r\nHost: ${target}\r\n${auth}\r\n`);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
17
115
|
/** Socket client that multiplexes requests by id over one connection. */
|
|
18
116
|
export class BridgeClient {
|
|
19
|
-
socketPath;
|
|
20
117
|
token;
|
|
118
|
+
env;
|
|
21
119
|
sock;
|
|
22
120
|
connecting;
|
|
23
121
|
nextId = 1;
|
|
24
122
|
pending = new Map();
|
|
25
123
|
closedError;
|
|
26
|
-
|
|
27
|
-
|
|
124
|
+
endpoint;
|
|
125
|
+
constructor(endpoint, token, env = process.env) {
|
|
28
126
|
this.token = token;
|
|
127
|
+
this.env = env;
|
|
128
|
+
this.endpoint = typeof endpoint === "string" ? { socketPath: endpoint } : endpoint;
|
|
29
129
|
}
|
|
30
130
|
connect() {
|
|
31
131
|
// Once the connection is gone (bad token, server closed) every later call fails fast.
|
|
@@ -33,8 +133,7 @@ export class BridgeClient {
|
|
|
33
133
|
return Promise.reject(this.closedError);
|
|
34
134
|
if (this.sock)
|
|
35
135
|
return Promise.resolve(this.sock);
|
|
36
|
-
this.connecting ??=
|
|
37
|
-
const sock = net.createConnection(this.socketPath);
|
|
136
|
+
this.connecting ??= openBridgeStream(this.endpoint, this.env).then(({ sock, leftover }) => {
|
|
38
137
|
const read = createFrameReader((value) => {
|
|
39
138
|
const res = value;
|
|
40
139
|
if (!res || typeof res.id !== "number")
|
|
@@ -50,15 +149,15 @@ export class BridgeClient {
|
|
|
50
149
|
}
|
|
51
150
|
}, () => this.failAll(new Error("tool bridge: response frame too large")));
|
|
52
151
|
sock.on("data", read);
|
|
53
|
-
sock.once("
|
|
54
|
-
this.sock = sock;
|
|
55
|
-
resolve(sock);
|
|
56
|
-
});
|
|
57
|
-
sock.once("error", (err) => {
|
|
58
|
-
reject(err);
|
|
59
|
-
this.failAll(err);
|
|
60
|
-
});
|
|
152
|
+
sock.once("error", (err) => this.failAll(err));
|
|
61
153
|
sock.once("close", () => this.failAll(new Error("tool bridge: connection closed")));
|
|
154
|
+
this.sock = sock;
|
|
155
|
+
if (leftover.length > 0)
|
|
156
|
+
read(leftover);
|
|
157
|
+
return sock;
|
|
158
|
+
}, (err) => {
|
|
159
|
+
this.failAll(err);
|
|
160
|
+
throw err;
|
|
62
161
|
});
|
|
63
162
|
return this.connecting;
|
|
64
163
|
}
|
|
@@ -89,13 +188,15 @@ export class BridgeClient {
|
|
|
89
188
|
/** Run the stdio MCP shim until stdin closes or the bridge goes away. */
|
|
90
189
|
export async function runToolBridgeShim(opts) {
|
|
91
190
|
const token = opts.token ?? process.env[TOKEN_ENV];
|
|
92
|
-
|
|
93
|
-
|
|
191
|
+
const tcp = opts.connect ? parseConnectTarget(opts.connect) : undefined;
|
|
192
|
+
const endpoint = opts.socket ? { socketPath: opts.socket } : tcp;
|
|
193
|
+
if (!endpoint || (opts.socket && opts.connect) || (opts.connect && !tcp) || !token) {
|
|
194
|
+
process.stderr.write(`sweny tool-bridge: one of --socket <path> or --connect <host:port>, and a token (--token or ${TOKEN_ENV}), are required\n`);
|
|
94
195
|
process.exit(2);
|
|
95
196
|
}
|
|
96
197
|
// Keep the token out of anything the shim might spawn or report.
|
|
97
198
|
delete process.env[TOKEN_ENV];
|
|
98
|
-
const client = new BridgeClient(
|
|
199
|
+
const client = new BridgeClient(endpoint, token);
|
|
99
200
|
const server = new Server({ name: "sweny-core", version: opts.version ?? "0.0.0" }, { capabilities: { tools: {} } });
|
|
100
201
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
101
202
|
const res = await client.request("tools/list");
|