@sweny-ai/core 0.1.149 → 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/builtin-workflows.d.ts +45 -0
- package/dist/cli/builtin-workflows.js +129 -0
- package/dist/cli/check.d.ts +2 -8
- package/dist/cli/check.js +2 -34
- package/dist/cli/config.js +2 -0
- package/dist/cli/main.js +49 -13
- package/dist/cli/packs.d.ts +9 -5
- package/dist/cli/packs.js +100 -50
- package/dist/executor.js +15 -3
- package/dist/harness/auth.d.ts +38 -0
- package/dist/harness/auth.js +93 -0
- package/dist/harness/claude-code.d.ts +8 -0
- package/dist/harness/claude-code.js +8 -1
- package/dist/harness/codex.d.ts +10 -1
- package/dist/harness/codex.js +17 -3
- package/dist/harness/pi.js +14 -3
- package/dist/harness/policy.js +6 -0
- package/dist/harness/tool-bridge/server.d.ts +20 -0
- package/dist/harness/tool-bridge/server.js +27 -10
- package/dist/harness/tool-bridge/shim.d.ts +41 -4
- package/dist/harness/tool-bridge/shim.js +118 -17
- package/dist/harness/types.d.ts +6 -0
- package/dist/index.d.ts +1 -1
- package/dist/node-policy.js +1 -0
- package/dist/safe-outputs.d.ts +17 -5
- package/dist/safe-outputs.js +85 -11
- package/dist/schema.d.ts +80 -0
- package/dist/schema.js +23 -0
- package/dist/types.d.ts +13 -0
- package/dist/workflows/browser.js +2 -2
- package/dist/workflows/implement.yml +61 -19
- package/dist/workflows/triage.yml +171 -139
- package/package.json +1 -1
package/dist/harness/codex.js
CHANGED
|
@@ -37,6 +37,7 @@ import { parseToolResultContent, summarizeToolError, tryParseJSON } from "./pars
|
|
|
37
37
|
import { startToolBridge } from "./tool-bridge/server.js";
|
|
38
38
|
import { defaultSandboxWrapper, prepareAgentSpawn, wrappersFrom, } from "./sandbox-wrapper.js";
|
|
39
39
|
import { TOKEN_ENV } from "./tool-bridge/protocol.js";
|
|
40
|
+
import { codexAuth } from "./auth.js";
|
|
40
41
|
export { CODEX_CAPABILITIES };
|
|
41
42
|
/** Oldest Codex CLI whose flags this adapter uses (`--ignore-user-config`, `--ignore-rules`). */
|
|
42
43
|
export const MIN_CODEX_VERSION = "0.159.0";
|
|
@@ -288,6 +289,7 @@ export class CodexHarness {
|
|
|
288
289
|
killGraceMs;
|
|
289
290
|
preflightResult;
|
|
290
291
|
version = "unknown";
|
|
292
|
+
authProbe;
|
|
291
293
|
constructor(opts = {}) {
|
|
292
294
|
this.model = opts.model;
|
|
293
295
|
this.maxTurns = opts.maxTurns ?? 20;
|
|
@@ -305,12 +307,24 @@ export class CodexHarness {
|
|
|
305
307
|
this.codexCommand = opts.codexCommand ?? { command: process.env.SWENY_CODEX_PATH || "codex", args: [] };
|
|
306
308
|
this.toolBridgeShim = opts.toolBridgeShim;
|
|
307
309
|
this.killGraceMs = opts.killGraceMs ?? 2000;
|
|
310
|
+
this.authProbe = opts.authProbe ?? (() => codexAuth(process.env));
|
|
308
311
|
}
|
|
309
312
|
info() {
|
|
310
313
|
return { id: this.id, version: this.version };
|
|
311
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* Before any node runs (#339): the Codex CLI is installed and new enough,
|
|
317
|
+
* and Codex can authenticate (an API key, an access token, or `codex login`).
|
|
318
|
+
*/
|
|
319
|
+
async preflight() {
|
|
320
|
+
const cli = await this.checkCli();
|
|
321
|
+
if (!cli.ok)
|
|
322
|
+
return cli;
|
|
323
|
+
const auth = this.authProbe();
|
|
324
|
+
return auth.ok ? cli : { ok: false, reason: auth.reason };
|
|
325
|
+
}
|
|
312
326
|
/** `codex --version`, checked against {@link MIN_CODEX_VERSION}. Cached per instance. */
|
|
313
|
-
|
|
327
|
+
checkCli() {
|
|
314
328
|
this.preflightResult ??= new Promise((resolve) => {
|
|
315
329
|
const { command, args } = this.codexCommand;
|
|
316
330
|
execFile(command, [...args, "--version"], { timeout: 30_000 }, (err, stdout) => {
|
|
@@ -723,7 +737,7 @@ export class CodexHarness {
|
|
|
723
737
|
};
|
|
724
738
|
if (gate.refuse)
|
|
725
739
|
return refused(gate.refuse);
|
|
726
|
-
const pre = await this.
|
|
740
|
+
const pre = await this.checkCli();
|
|
727
741
|
if (!pre.ok) {
|
|
728
742
|
this.logger.error(pre.reason);
|
|
729
743
|
return tag({ status: "failed", data: { error: pre.reason }, toolCalls: [] });
|
|
@@ -922,7 +936,7 @@ export class CodexHarness {
|
|
|
922
936
|
: `codex.ask: ${why}. Returning empty string.`);
|
|
923
937
|
return null;
|
|
924
938
|
};
|
|
925
|
-
const pre = await this.
|
|
939
|
+
const pre = await this.checkCli();
|
|
926
940
|
if (!pre.ok)
|
|
927
941
|
return failClosed(pre.reason);
|
|
928
942
|
const args = [
|
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
|
}
|
package/dist/harness/policy.js
CHANGED
|
@@ -74,6 +74,12 @@ export function policyGate(caps, policy, wrappers = {}) {
|
|
|
74
74
|
unenforced.push(`disallowed_tools [${policy.nativeDeny.join(", ")}]: harness has no tools by these names`);
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
+
// Exclusive MCP (`permissions.strict`): only the servers sweny passes may
|
|
78
|
+
// load, never the user's own config. A harness that cannot exclude them
|
|
79
|
+
// cannot honor it, so strict refuses the node.
|
|
80
|
+
if (policy.exclusiveMcp && caps.mcp.exclusive === "none") {
|
|
81
|
+
unenforced.push("exclusive MCP: harness cannot keep the user's own MCP servers out of the run");
|
|
82
|
+
}
|
|
77
83
|
if (policy.egress.length > 0 && !caps.sandbox.network && !wrappers.egress) {
|
|
78
84
|
unenforced.push("egress allowlist: harness has no network sandbox and no egress wrapper is active");
|
|
79
85
|
}
|
|
@@ -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");
|
package/dist/harness/types.d.ts
CHANGED
|
@@ -55,6 +55,12 @@ export interface NodePolicy {
|
|
|
55
55
|
egress: string[];
|
|
56
56
|
/** Refuse instead of degrade when an opinion cannot be enforced. */
|
|
57
57
|
strict: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Only the MCP servers sweny passes may load, never the user's own config
|
|
60
|
+
* (`permissions.strict`, #365). A harness whose `mcp.exclusive` is `none`
|
|
61
|
+
* cannot honor it: reported in `degraded`, refused under `strict`.
|
|
62
|
+
*/
|
|
63
|
+
exclusiveMcp?: boolean;
|
|
58
64
|
/**
|
|
59
65
|
* Turn budget for the run. Set by adapters whose `turnLimit` is not native,
|
|
60
66
|
* so the gate reports how the budget is kept (watchdog) or that it is not.
|
package/dist/index.d.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* })
|
|
22
22
|
* ```
|
|
23
23
|
*/
|
|
24
|
-
export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, Source, ResolvedSource, SourceKind, SourceResolutionMap, WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, NodeAccess, NodePermissions, NodePermissionsSpec, SafeOutputType, SafeOutputDeclaration, SafeOutputsPolicy, SafeOutputReceipt, AuthorAssociation, } from "./types.js";
|
|
24
|
+
export type { Skill, SkillCategory, SkillDefinition, Tool, ToolContext, ConfigField, JSONSchema, Workflow, Node, Edge, NodeResult, ToolCall, ExecutionEvent, Observer, Claude, Logger, TraceStep, TraceEdge, ExecutionTrace, ExecutionResult, Source, ResolvedSource, SourceKind, SourceResolutionMap, WorkflowInputs, WorkflowInputField, WorkflowInputType, InputValidationError, InputValidationResult, Evaluator, EvaluatorRule, EvalResult, EvaluatorKind, NodeRequires, NodeRetry, NodeToolFilter, OutputMatch, NodeSources, EvalPolicy, RequiresOnFail, NodeOnFail, McpTransport, WorkflowType, SkillHarnessKey, NodeAccess, NodePermissions, NodePermissionsSpec, SafeOutputType, SafeOutputDeclaration, SafeOutputsPolicy, SafeOutputReceipt, SafeOutputPin, AuthorAssociation, } from "./types.js";
|
|
25
25
|
export { WORKFLOW_INPUT_TYPES } from "./types.js";
|
|
26
26
|
export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRANSPORTS, SKILL_CATEGORIES, SKILL_HARNESSES, SKILL_ID_PATTERN, SKILL_ID_MAX_LENGTH, isValidSkillId, NODE_ACCESS, TOOL_CLASSES, SAFE_OUTPUT_TYPES, SAFE_OUTPUT_APPLIERS, AUTHOR_ASSOCIATIONS, } from "./types.js";
|
|
27
27
|
export { consoleLogger } from "./types.js";
|
package/dist/node-policy.js
CHANGED
|
@@ -38,5 +38,6 @@ export function buildNodePolicy(opts) {
|
|
|
38
38
|
...(opts.disallowedTools && opts.disallowedTools.length > 0 ? { nativeDeny: opts.disallowedTools } : {}),
|
|
39
39
|
egress: opts.egress ?? [],
|
|
40
40
|
strict: opts.permissions.strict,
|
|
41
|
+
...(opts.permissions.strict ? { exclusiveMcp: true } : {}),
|
|
41
42
|
};
|
|
42
43
|
}
|
package/dist/safe-outputs.d.ts
CHANGED
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
* the write, so this holds on every harness.
|
|
10
10
|
*
|
|
11
11
|
* Order of checks per intent (all deterministic): declared type, workflow
|
|
12
|
-
* ceiling, actor trust, expiry, skill, target pin, shape and size,
|
|
13
|
-
* title prefix, dedupe, node cap, run cap. An optional model screen runs last
|
|
12
|
+
* ceiling, actor trust, expiry, skill, target pin, issue pin, shape and size,
|
|
13
|
+
* labels, title prefix, dedupe, node cap, run cap. An optional model screen runs last
|
|
14
14
|
* over the intents that passed and can only veto them.
|
|
15
15
|
*/
|
|
16
|
-
import type { Logger, SafeOutputDeclaration, SafeOutputReceipt, SafeOutputsPolicy, SafeOutputType, Skill, Tool } from "./types.js";
|
|
16
|
+
import type { Logger, SafeOutputDeclaration, SafeOutputPin, SafeOutputReceipt, SafeOutputsPolicy, SafeOutputType, Skill, Tool } from "./types.js";
|
|
17
17
|
/** Name of the intent-recording tool a node with `outputs` receives. */
|
|
18
18
|
export declare const EMIT_OUTPUT_TOOL = "emit_output";
|
|
19
19
|
/** Longest title the write stage accepts (after the prefix). */
|
|
@@ -43,7 +43,7 @@ export interface SafeOutputIntent {
|
|
|
43
43
|
*/
|
|
44
44
|
export declare function createEmitOutputTool(declarations: SafeOutputDeclaration[], buffer: SafeOutputIntent[], now?: () => number): Tool;
|
|
45
45
|
/** Instruction block that tells the agent how writes work at this node. */
|
|
46
|
-
export declare function safeOutputsInstruction(declarations: SafeOutputDeclaration[]): string;
|
|
46
|
+
export declare function safeOutputsInstruction(declarations: SafeOutputDeclaration[], input?: Record<string, unknown>): string;
|
|
47
47
|
export interface ActorInfo {
|
|
48
48
|
/** GitHub login of whoever triggered the run. */
|
|
49
49
|
login?: string;
|
|
@@ -54,9 +54,19 @@ export interface ActorInfo {
|
|
|
54
54
|
export declare function resolveActor(env: Record<string, string | undefined>, override?: ActorInfo): ActorInfo;
|
|
55
55
|
/** True when no trust list is declared, or the actor matches one. Unknown actor = untrusted. */
|
|
56
56
|
export declare function actorTrusted(policy: SafeOutputsPolicy | undefined, actor: ActorInfo): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* The issue or PR a declaration pins, as a string. `{ input: name }` reads the
|
|
59
|
+
* run input; an absent or empty value is `undefined`, which refuses the write.
|
|
60
|
+
*/
|
|
61
|
+
export declare function resolvePin(pin: SafeOutputPin, input?: Record<string, unknown>): string | undefined;
|
|
57
62
|
/** `30m` → 1_800_000. Undefined for anything the schema would reject. */
|
|
58
63
|
export declare function parseDuration(s: string | undefined): number | undefined;
|
|
59
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* The skill that applies a declaration: its `via`, else the first node skill
|
|
66
|
+
* that can and is configured, else the first node skill that can, else any
|
|
67
|
+
* configured skill that can. A node listing `[linear, github]` therefore files
|
|
68
|
+
* on GitHub when only GitHub has credentials.
|
|
69
|
+
*/
|
|
60
70
|
export declare function resolveOutputSkill(decl: SafeOutputDeclaration, nodeSkills: string[], skills: Map<string, Skill>): string | undefined;
|
|
61
71
|
/** Declarations whose applying skill is not configured (no credentials loaded). */
|
|
62
72
|
export declare function unresolvedOutputs(declarations: SafeOutputDeclaration[], nodeSkills: string[], skills: Map<string, Skill>): SafeOutputType[];
|
|
@@ -85,6 +95,8 @@ export interface ApplySafeOutputsOptions {
|
|
|
85
95
|
staged: boolean;
|
|
86
96
|
state: WriteStageState;
|
|
87
97
|
logger: Logger;
|
|
98
|
+
/** The run input, for `number: { input }` pins. */
|
|
99
|
+
input?: Record<string, unknown>;
|
|
88
100
|
/**
|
|
89
101
|
* Model screen (only called when `policy.screen` is on and at least one write
|
|
90
102
|
* passed every deterministic check). Anything but `ALLOW` is a veto.
|