@sweny-ai/core 0.1.148 → 0.1.149
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/config.js +6 -3
- package/dist/harness/acp-rpc.d.ts +75 -0
- package/dist/harness/acp-rpc.js +204 -0
- package/dist/harness/acp.d.ts +189 -0
- package/dist/harness/acp.js +942 -0
- package/dist/harness/agents.d.ts +5 -1
- package/dist/harness/agents.js +8 -1
- package/dist/harness/capabilities.d.ts +30 -0
- package/dist/harness/capabilities.js +41 -0
- package/dist/harness/index.d.ts +7 -3
- package/dist/harness/index.js +7 -2
- package/dist/harness/parse.d.ts +12 -0
- package/dist/harness/parse.js +1 -1
- package/package.json +1 -1
package/dist/cli/config.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { execSync } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import { hasCodexLogin } from "../agent-env.js";
|
|
4
|
-
import { unsupportedAgentError } from "../harness/agents.js";
|
|
4
|
+
import { parseAcpAgent, unsupportedAgentError } from "../harness/agents.js";
|
|
5
5
|
export function registerTriageCommand(program) {
|
|
6
6
|
// NOTE: File-eligible options omit Commander defaults so parseCliInputs() can
|
|
7
7
|
// distinguish "user passed flag" from "not passed" and fall through to .sweny.yml.
|
|
8
8
|
return program
|
|
9
9
|
.command("triage")
|
|
10
10
|
.description("Run the SWEny triage workflow")
|
|
11
|
-
.option("--agent <provider>", "Coding agent: claude (default), codex, or
|
|
11
|
+
.option("--agent <provider>", "Coding agent: claude (default), codex, pi (experimental), or acp:<command> for any ACP agent (experimental)")
|
|
12
12
|
.option("--coding-agent-provider <provider>", "Coding agent provider (alias for --agent)")
|
|
13
13
|
.option("--observability-provider <provider>", "Observability provider (default: none)")
|
|
14
14
|
.option("--issue-tracker-provider <provider>", "Issue tracker provider (default: github-issues)")
|
|
@@ -243,6 +243,9 @@ export function validateInputs(config) {
|
|
|
243
243
|
// there is no single credential to check here; pi reports its own auth error.
|
|
244
244
|
break;
|
|
245
245
|
default:
|
|
246
|
+
// An ACP agent (`acp:<command>`, #416) brings its own auth; it says so itself when it is missing.
|
|
247
|
+
if (parseAcpAgent(config.codingAgentProvider) !== undefined)
|
|
248
|
+
break;
|
|
246
249
|
// Honest --agent (#330): only agents with an adapter are accepted.
|
|
247
250
|
// Anything else would silently run a different agent under that name.
|
|
248
251
|
errors.push(unsupportedAgentError(config.codingAgentProvider));
|
|
@@ -662,7 +665,7 @@ export function registerImplementCommand(program) {
|
|
|
662
665
|
return program
|
|
663
666
|
.command("implement <issueId>")
|
|
664
667
|
.description("Implement a fix for a specific issue and open a PR")
|
|
665
|
-
.option("--agent <provider>", "Coding agent: claude (default), codex, or
|
|
668
|
+
.option("--agent <provider>", "Coding agent: claude (default), codex, pi (experimental), or acp:<command> for any ACP agent (experimental)")
|
|
666
669
|
.option("--coding-agent-provider <provider>", "Coding agent provider (alias for --agent)")
|
|
667
670
|
.option("--issue-tracker-provider <provider>", "Issue tracker (linear|jira|github-issues|file)")
|
|
668
671
|
.option("--source-control-provider <provider>", "Source control (github|gitlab|file)")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newline-delimited JSON-RPC 2.0 over a child process's stdio: the ACP stdio
|
|
3
|
+
* transport (agentclientprotocol.com/protocol/v1/transports, schema v1.24.1).
|
|
4
|
+
*
|
|
5
|
+
* - The client launches the agent as a subprocess and writes one JSON-RPC
|
|
6
|
+
* message per line to its stdin; the agent answers on stdout.
|
|
7
|
+
* - Messages MUST NOT contain embedded newlines (`JSON.stringify` never emits
|
|
8
|
+
* one).
|
|
9
|
+
* - The agent MAY log on stderr. It MUST NOT write anything but ACP messages
|
|
10
|
+
* to stdout; real agents sometimes do, so a stdout line that is not JSON is
|
|
11
|
+
* logged and skipped instead of ending the run.
|
|
12
|
+
*
|
|
13
|
+
* Hand-rolled on purpose: the wire is four message shapes, so sweny takes no
|
|
14
|
+
* SDK dependency for it, and the contract fake speaks the raw wire, which means
|
|
15
|
+
* the suite checks the protocol itself and not an SDK's reading of it.
|
|
16
|
+
*
|
|
17
|
+
* No agent SDK imports.
|
|
18
|
+
*/
|
|
19
|
+
import type { ChildProcess } from "node:child_process";
|
|
20
|
+
/** JSON-RPC / ACP error codes (schema `ErrorCode`). */
|
|
21
|
+
export declare const RPC_ERROR: {
|
|
22
|
+
readonly parse: -32700;
|
|
23
|
+
readonly invalidRequest: -32600;
|
|
24
|
+
readonly methodNotFound: -32601;
|
|
25
|
+
readonly invalidParams: -32602;
|
|
26
|
+
readonly internal: -32603;
|
|
27
|
+
readonly requestCancelled: -32800;
|
|
28
|
+
readonly authRequired: -32000;
|
|
29
|
+
readonly resourceNotFound: -32002;
|
|
30
|
+
};
|
|
31
|
+
/** An error response from the peer (or a local failure reaching it). */
|
|
32
|
+
export declare class RpcError extends Error {
|
|
33
|
+
readonly code: number;
|
|
34
|
+
readonly data?: unknown | undefined;
|
|
35
|
+
constructor(message: string, code: number, data?: unknown | undefined);
|
|
36
|
+
}
|
|
37
|
+
export interface RpcHandlers {
|
|
38
|
+
/** A request from the peer. Resolve with the `result`, or throw {@link RpcError} for an error response. */
|
|
39
|
+
onRequest(method: string, params: unknown): Promise<unknown>;
|
|
40
|
+
/** A notification from the peer. */
|
|
41
|
+
onNotification(method: string, params: unknown): void;
|
|
42
|
+
/** A stdout line that was not a JSON-RPC message (debug only). */
|
|
43
|
+
onJunk?(line: string): void;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* One JSON-RPC connection. Requests from us are matched to responses by id;
|
|
47
|
+
* requests from the peer are answered through {@link RpcHandlers.onRequest}.
|
|
48
|
+
* When the peer goes away every pending request rejects.
|
|
49
|
+
*/
|
|
50
|
+
export declare class RpcPeer {
|
|
51
|
+
private readonly child;
|
|
52
|
+
private readonly handlers;
|
|
53
|
+
private nextId;
|
|
54
|
+
private pending;
|
|
55
|
+
private buf;
|
|
56
|
+
private ended;
|
|
57
|
+
constructor(child: Pick<ChildProcess, "stdin" | "stdout">, handlers: RpcHandlers);
|
|
58
|
+
private onData;
|
|
59
|
+
private onLine;
|
|
60
|
+
private answer;
|
|
61
|
+
private write;
|
|
62
|
+
/** Send a request and wait for its response. */
|
|
63
|
+
request<T = unknown>(method: string, params: unknown): Promise<T>;
|
|
64
|
+
/** Send a notification (no response). */
|
|
65
|
+
notify(method: string, params: unknown): void;
|
|
66
|
+
/** The peer is gone: reject everything still waiting. Idempotent. */
|
|
67
|
+
end(reason: string): void;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Split a command line into argv: whitespace separates words, single and
|
|
71
|
+
* double quotes group, backslash escapes the next character outside single
|
|
72
|
+
* quotes. No expansion of any kind (no globbing, no variables, no shell), so
|
|
73
|
+
* the string after `acp:` can never run anything but the program it names.
|
|
74
|
+
*/
|
|
75
|
+
export declare function splitCommandLine(input: string): string[];
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Newline-delimited JSON-RPC 2.0 over a child process's stdio: the ACP stdio
|
|
3
|
+
* transport (agentclientprotocol.com/protocol/v1/transports, schema v1.24.1).
|
|
4
|
+
*
|
|
5
|
+
* - The client launches the agent as a subprocess and writes one JSON-RPC
|
|
6
|
+
* message per line to its stdin; the agent answers on stdout.
|
|
7
|
+
* - Messages MUST NOT contain embedded newlines (`JSON.stringify` never emits
|
|
8
|
+
* one).
|
|
9
|
+
* - The agent MAY log on stderr. It MUST NOT write anything but ACP messages
|
|
10
|
+
* to stdout; real agents sometimes do, so a stdout line that is not JSON is
|
|
11
|
+
* logged and skipped instead of ending the run.
|
|
12
|
+
*
|
|
13
|
+
* Hand-rolled on purpose: the wire is four message shapes, so sweny takes no
|
|
14
|
+
* SDK dependency for it, and the contract fake speaks the raw wire, which means
|
|
15
|
+
* the suite checks the protocol itself and not an SDK's reading of it.
|
|
16
|
+
*
|
|
17
|
+
* No agent SDK imports.
|
|
18
|
+
*/
|
|
19
|
+
/** JSON-RPC / ACP error codes (schema `ErrorCode`). */
|
|
20
|
+
export const RPC_ERROR = {
|
|
21
|
+
parse: -32700,
|
|
22
|
+
invalidRequest: -32600,
|
|
23
|
+
methodNotFound: -32601,
|
|
24
|
+
invalidParams: -32602,
|
|
25
|
+
internal: -32603,
|
|
26
|
+
requestCancelled: -32800,
|
|
27
|
+
authRequired: -32000,
|
|
28
|
+
resourceNotFound: -32002,
|
|
29
|
+
};
|
|
30
|
+
/** An error response from the peer (or a local failure reaching it). */
|
|
31
|
+
export class RpcError extends Error {
|
|
32
|
+
code;
|
|
33
|
+
data;
|
|
34
|
+
constructor(message, code, data) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.code = code;
|
|
37
|
+
this.data = data;
|
|
38
|
+
this.name = "RpcError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One JSON-RPC connection. Requests from us are matched to responses by id;
|
|
43
|
+
* requests from the peer are answered through {@link RpcHandlers.onRequest}.
|
|
44
|
+
* When the peer goes away every pending request rejects.
|
|
45
|
+
*/
|
|
46
|
+
export class RpcPeer {
|
|
47
|
+
child;
|
|
48
|
+
handlers;
|
|
49
|
+
nextId = 1;
|
|
50
|
+
pending = new Map();
|
|
51
|
+
buf = "";
|
|
52
|
+
ended;
|
|
53
|
+
constructor(child, handlers) {
|
|
54
|
+
this.child = child;
|
|
55
|
+
this.handlers = handlers;
|
|
56
|
+
child.stdout?.setEncoding("utf8");
|
|
57
|
+
child.stdout?.on("data", (chunk) => this.onData(chunk));
|
|
58
|
+
child.stdin?.on("error", () => {
|
|
59
|
+
// The agent exited before reading; the exit handling reports why.
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
onData(chunk) {
|
|
63
|
+
this.buf += chunk;
|
|
64
|
+
let nl;
|
|
65
|
+
while ((nl = this.buf.indexOf("\n")) !== -1) {
|
|
66
|
+
const line = this.buf.slice(0, nl);
|
|
67
|
+
this.buf = this.buf.slice(nl + 1);
|
|
68
|
+
this.onLine(line);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
onLine(raw) {
|
|
72
|
+
const line = raw.trim();
|
|
73
|
+
if (!line)
|
|
74
|
+
return;
|
|
75
|
+
let msg;
|
|
76
|
+
try {
|
|
77
|
+
const parsed = JSON.parse(line);
|
|
78
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
79
|
+
throw new Error("not an object");
|
|
80
|
+
msg = parsed;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
this.handlers.onJunk?.(line.length > 200 ? line.slice(0, 199) + "..." : line);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const id = msg.id;
|
|
87
|
+
const hasId = typeof id === "number" || typeof id === "string";
|
|
88
|
+
if (typeof msg.method === "string") {
|
|
89
|
+
if (hasId)
|
|
90
|
+
void this.answer(id, msg.method, msg.params);
|
|
91
|
+
else
|
|
92
|
+
this.handlers.onNotification(msg.method, msg.params);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!hasId)
|
|
96
|
+
return;
|
|
97
|
+
const p = this.pending.get(id);
|
|
98
|
+
if (!p)
|
|
99
|
+
return;
|
|
100
|
+
this.pending.delete(id);
|
|
101
|
+
const err = msg.error;
|
|
102
|
+
if (err && typeof err === "object") {
|
|
103
|
+
p.reject(new RpcError(typeof err.message === "string" ? err.message : `${p.method} failed`, typeof err.code === "number" ? err.code : RPC_ERROR.internal, err.data));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
p.resolve(msg.result);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async answer(id, method, params) {
|
|
110
|
+
try {
|
|
111
|
+
const result = await this.handlers.onRequest(method, params);
|
|
112
|
+
this.write({ jsonrpc: "2.0", id, result: result ?? null });
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
const e = err instanceof RpcError
|
|
116
|
+
? err
|
|
117
|
+
: new RpcError(err instanceof Error ? err.message : String(err), RPC_ERROR.internal);
|
|
118
|
+
this.write({ jsonrpc: "2.0", id, error: { code: e.code, message: e.message } });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
write(msg) {
|
|
122
|
+
const stdin = this.child.stdin;
|
|
123
|
+
if (!stdin || stdin.destroyed || !stdin.writable)
|
|
124
|
+
return;
|
|
125
|
+
stdin.write(JSON.stringify(msg) + "\n");
|
|
126
|
+
}
|
|
127
|
+
/** Send a request and wait for its response. */
|
|
128
|
+
request(method, params) {
|
|
129
|
+
if (this.ended !== undefined)
|
|
130
|
+
return Promise.reject(new RpcError(this.ended, RPC_ERROR.internal));
|
|
131
|
+
const id = this.nextId++;
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
133
|
+
this.pending.set(id, { resolve: (v) => resolve(v), reject, method });
|
|
134
|
+
this.write({ jsonrpc: "2.0", id, method, params });
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
/** Send a notification (no response). */
|
|
138
|
+
notify(method, params) {
|
|
139
|
+
if (this.ended !== undefined)
|
|
140
|
+
return;
|
|
141
|
+
this.write({ jsonrpc: "2.0", method, params });
|
|
142
|
+
}
|
|
143
|
+
/** The peer is gone: reject everything still waiting. Idempotent. */
|
|
144
|
+
end(reason) {
|
|
145
|
+
if (this.ended !== undefined)
|
|
146
|
+
return;
|
|
147
|
+
this.ended = reason;
|
|
148
|
+
// A final line without a newline is complete only if it parses.
|
|
149
|
+
if (this.buf.trim())
|
|
150
|
+
this.onLine(this.buf);
|
|
151
|
+
this.buf = "";
|
|
152
|
+
const pending = [...this.pending.values()];
|
|
153
|
+
this.pending.clear();
|
|
154
|
+
for (const p of pending)
|
|
155
|
+
p.reject(new RpcError(reason, RPC_ERROR.internal));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Split a command line into argv: whitespace separates words, single and
|
|
160
|
+
* double quotes group, backslash escapes the next character outside single
|
|
161
|
+
* quotes. No expansion of any kind (no globbing, no variables, no shell), so
|
|
162
|
+
* the string after `acp:` can never run anything but the program it names.
|
|
163
|
+
*/
|
|
164
|
+
export function splitCommandLine(input) {
|
|
165
|
+
const out = [];
|
|
166
|
+
let cur = "";
|
|
167
|
+
let started = false;
|
|
168
|
+
let quote = null;
|
|
169
|
+
for (let i = 0; i < input.length; i++) {
|
|
170
|
+
const ch = input[i];
|
|
171
|
+
if (quote) {
|
|
172
|
+
if (ch === quote)
|
|
173
|
+
quote = null;
|
|
174
|
+
else if (ch === "\\" && quote === '"' && i + 1 < input.length)
|
|
175
|
+
cur += input[++i];
|
|
176
|
+
else
|
|
177
|
+
cur += ch;
|
|
178
|
+
}
|
|
179
|
+
else if (ch === '"' || ch === "'") {
|
|
180
|
+
quote = ch;
|
|
181
|
+
started = true;
|
|
182
|
+
}
|
|
183
|
+
else if (ch === "\\" && i + 1 < input.length) {
|
|
184
|
+
cur += input[++i];
|
|
185
|
+
started = true;
|
|
186
|
+
}
|
|
187
|
+
else if (/\s/.test(ch)) {
|
|
188
|
+
if (started) {
|
|
189
|
+
out.push(cur);
|
|
190
|
+
cur = "";
|
|
191
|
+
started = false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
cur += ch;
|
|
196
|
+
started = true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (quote)
|
|
200
|
+
throw new Error(`unterminated ${quote} quote in "${input}"`);
|
|
201
|
+
if (started)
|
|
202
|
+
out.push(cur);
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACP harness (#416): runs a node on any Agent Client Protocol agent
|
|
3
|
+
* (`--agent acp:<command>`, for example `acp:opencode acp`,
|
|
4
|
+
* `acp:hermes acp`, `acp:goose acp`, `acp:gemini --experimental-acp`).
|
|
5
|
+
*
|
|
6
|
+
* ACP is the long-tail adapter. It is JSON-RPC 2.0 over the agent's stdio and
|
|
7
|
+
* carries a prompt and a stream of updates; it does not carry sweny's
|
|
8
|
+
* opinions. Read from agentclientprotocol/agent-client-protocol at tag
|
|
9
|
+
* schema-v1.24.1 (`schema/v1/schema.json`, `docs/protocol/v1/*.mdx`):
|
|
10
|
+
* - transports: newline-delimited JSON over stdio (`transports.mdx`).
|
|
11
|
+
* - `initialize` negotiates `protocolVersion` (1) and capabilities
|
|
12
|
+
* (`initialization.mdx`); `InitializeRequest` / `InitializeResponse`.
|
|
13
|
+
* - `session/new {cwd, mcpServers}` returns `{sessionId}`; every agent MUST
|
|
14
|
+
* support stdio MCP servers, http and sse are optional
|
|
15
|
+
* (`session-setup.mdx`, `McpServerStdio`, `McpServerHttp`).
|
|
16
|
+
* - `session/prompt {sessionId, prompt: ContentBlock[]}` streams
|
|
17
|
+
* `session/update` notifications (`agent_message_chunk`, `tool_call`,
|
|
18
|
+
* `tool_call_update`, `usage_update`, ...) and ends with
|
|
19
|
+
* `{stopReason}`; `PromptResponse` has no other field (`prompt-turn.mdx`).
|
|
20
|
+
* - `session/cancel` (notification) ends a turn with stopReason `cancelled`
|
|
21
|
+
* (`prompt-turn.mdx`, Cancellation); pending permission requests get the
|
|
22
|
+
* `cancelled` outcome.
|
|
23
|
+
* - `session/request_permission {toolCall, options}` is how an agent MAY ask
|
|
24
|
+
* before a tool runs; the client picks an option or answers `cancelled`
|
|
25
|
+
* (`tool-calls.mdx`, `RequestPermissionResponse`).
|
|
26
|
+
* - `fs/read_text_file` and `fs/write_text_file` are client methods an agent
|
|
27
|
+
* may use only when the client advertised them (`file-system.mdx`).
|
|
28
|
+
*
|
|
29
|
+
* Every sweny opinion is enforced by sweny here, refused or reported as
|
|
30
|
+
* `degraded` by `policyGate` (see ACP_CAPABILITIES for the honest list):
|
|
31
|
+
* - Structured output: the schema rides in the prompt; sweny parses the last
|
|
32
|
+
* JSON object, checks it and asks once more before giving up.
|
|
33
|
+
* - Tool deny and read-only: best effort through `session/request_permission`
|
|
34
|
+
* (denied classes and every non-read tool under `readOnly` are rejected),
|
|
35
|
+
* plus a refusal of every `fs/write_text_file`. The protocol has no deny list
|
|
36
|
+
* and an agent that does not ask is not stopped, so `policyGate` still
|
|
37
|
+
* reports deny and read-only as unenforced; strict refuses the node unless
|
|
38
|
+
* the process wrapper provides the read-only mount.
|
|
39
|
+
* - Sandbox and egress: none in the protocol. The whole agent process runs
|
|
40
|
+
* inside the sandbox wrapper (sandbox-wrapper.ts); strict refuses without it.
|
|
41
|
+
* - Env: sweny spawns the agent with the scoped env (agent-env.ts).
|
|
42
|
+
* - Skill tools: passed in `session/new` as the stdio MCP server of the tool
|
|
43
|
+
* bridge (#414), so the executor still observes every call.
|
|
44
|
+
* - Turn limit: a watchdog counts tool calls and cancels past the budget.
|
|
45
|
+
* - Timeout: `session/cancel`, a grace period, then the process is killed.
|
|
46
|
+
*/
|
|
47
|
+
import type { Logger, McpServerConfig, ToolContext } from "../types.js";
|
|
48
|
+
import { type SandboxMode } from "../agent-env.js";
|
|
49
|
+
import type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, HarnessId, HarnessInfo, HarnessRunRequest, HarnessRunResult, ToolClass } from "./types.js";
|
|
50
|
+
import { ACP_CAPABILITIES } from "./capabilities.js";
|
|
51
|
+
import { type HarnessPolicyMode } from "./policy.js";
|
|
52
|
+
import { type SandboxWrapper } from "./sandbox-wrapper.js";
|
|
53
|
+
export { ACP_CAPABILITIES };
|
|
54
|
+
/** The ACP major version sweny speaks (`InitializeRequest.protocolVersion`). */
|
|
55
|
+
export declare const ACP_PROTOCOL_VERSION = 1;
|
|
56
|
+
export interface AcpHarnessOptions {
|
|
57
|
+
/**
|
|
58
|
+
* The agent to run: a command line (`"opencode acp"`, the part after `acp:`)
|
|
59
|
+
* or an explicit `{command, args}`. Never run through a shell.
|
|
60
|
+
*/
|
|
61
|
+
acpCommand?: string | {
|
|
62
|
+
command: string;
|
|
63
|
+
args: string[];
|
|
64
|
+
};
|
|
65
|
+
/** Tool-call budget the watchdog enforces when a node sets no `max_turns` (default: 20). */
|
|
66
|
+
maxTurns?: number;
|
|
67
|
+
/** Working directory (default: process.cwd()). */
|
|
68
|
+
cwd?: string;
|
|
69
|
+
logger?: Logger;
|
|
70
|
+
/** Default tool context for standalone usage (not via the executor). */
|
|
71
|
+
defaultContext?: ToolContext;
|
|
72
|
+
/** Catalog defaults, overridden by per-run skill servers and explicit mcpServers. */
|
|
73
|
+
defaultMcpServers?: Record<string, McpServerConfig>;
|
|
74
|
+
/** Explicit external MCP servers, overriding defaults and per-run skill servers. */
|
|
75
|
+
mcpServers?: Record<string, McpServerConfig>;
|
|
76
|
+
/** Extra env var names passed through (see agent-env.ts). Default: `SWENY_ENV_PASSTHROUGH`. */
|
|
77
|
+
envPassthrough?: string[];
|
|
78
|
+
/** The agent's own auth env var names, always passed through when scoping (none are assumed). */
|
|
79
|
+
authVars?: string[];
|
|
80
|
+
/** Scope the agent env to the allowlist. Default: `SWENY_ENV_SCOPE`, else on in CI. */
|
|
81
|
+
envScope?: boolean;
|
|
82
|
+
/** Process sandbox mode. Default: `SWENY_SANDBOX`, else `auto` in CI and `off` locally. */
|
|
83
|
+
sandbox?: SandboxMode;
|
|
84
|
+
/** The host's process sandbox wrapper (#360 step 2). `undefined` = detect srt once per process; `null` = none (test seam). */
|
|
85
|
+
sandboxWrapper?: SandboxWrapper | null;
|
|
86
|
+
/** `strict` refuses a node with unenforceable opinions. Default: {@link resolveHarnessPolicy}. */
|
|
87
|
+
policy?: HarnessPolicyMode;
|
|
88
|
+
/** Hosts the agent itself needs (its model API), allowed through the sandbox wrapper. */
|
|
89
|
+
egress?: string[];
|
|
90
|
+
/** Shim command override for the tool bridge (test seam). */
|
|
91
|
+
toolBridgeShim?: {
|
|
92
|
+
command: string;
|
|
93
|
+
args: string[];
|
|
94
|
+
};
|
|
95
|
+
/** Grace period between SIGTERM and SIGKILL, and for the agent to exit on its own (default: 2000 ms). */
|
|
96
|
+
killGraceMs?: number;
|
|
97
|
+
/** How long the agent gets to answer `session/cancel` before it is killed (default: 5000 ms). */
|
|
98
|
+
cancelGraceMs?: number;
|
|
99
|
+
/** How long `initialize` and `session/new` may take (default: 60000 ms). */
|
|
100
|
+
startupTimeoutMs?: number;
|
|
101
|
+
}
|
|
102
|
+
/** Portable tool classes an ACP tool call can be, from its `kind` (schema `ToolKind`) and name. */
|
|
103
|
+
export declare function classifyAcpTool(kind: string | null | undefined, name: string | null | undefined): {
|
|
104
|
+
classes: ToolClass[];
|
|
105
|
+
readOnlySafe: boolean;
|
|
106
|
+
};
|
|
107
|
+
/** Does this tool call name one of sweny's skill tools (they reach the agent as MCP tools of the tool bridge)? */
|
|
108
|
+
export declare function isSkillToolCall(names: readonly (string | null | undefined)[], skillTools: readonly string[]): boolean;
|
|
109
|
+
interface PermissionOption {
|
|
110
|
+
optionId: string;
|
|
111
|
+
kind: string;
|
|
112
|
+
}
|
|
113
|
+
/** The permission outcome for an option list: the first option of a wanted kind, else `cancelled`. */
|
|
114
|
+
export declare function pickPermissionOutcome(options: readonly PermissionOption[], allow: boolean): {
|
|
115
|
+
outcome: "selected";
|
|
116
|
+
optionId: string;
|
|
117
|
+
} | {
|
|
118
|
+
outcome: "cancelled";
|
|
119
|
+
};
|
|
120
|
+
/** A loosely typed JSON object, for reading what the agent sends. */
|
|
121
|
+
type Obj = Record<string, any>;
|
|
122
|
+
/** `McpServerConfig` map to the ACP `mcpServers` array (schema `McpServer`). Returns what the agent cannot load too. */
|
|
123
|
+
export declare function toAcpMcpServers(servers: Record<string, McpServerConfig>, agentHttp: boolean): {
|
|
124
|
+
list: Obj[];
|
|
125
|
+
unsupported: string[];
|
|
126
|
+
};
|
|
127
|
+
export declare class AcpHarness implements AgentHarness {
|
|
128
|
+
readonly id: HarnessId;
|
|
129
|
+
readonly capabilities: HarnessCapabilities;
|
|
130
|
+
/** Empty on purpose: judges use the agent's own default model. */
|
|
131
|
+
readonly defaultJudgeModel = "";
|
|
132
|
+
readonly logger: Logger;
|
|
133
|
+
private agent;
|
|
134
|
+
private maxTurns;
|
|
135
|
+
private cwd;
|
|
136
|
+
private defaultContext;
|
|
137
|
+
private mcpServers;
|
|
138
|
+
private defaultMcpServers;
|
|
139
|
+
private envPassthrough;
|
|
140
|
+
private authVars;
|
|
141
|
+
private envScope;
|
|
142
|
+
private sandboxMode;
|
|
143
|
+
private sandboxWrapper;
|
|
144
|
+
private policyMode;
|
|
145
|
+
private egress;
|
|
146
|
+
private toolBridgeShim;
|
|
147
|
+
private killGraceMs;
|
|
148
|
+
private cancelGraceMs;
|
|
149
|
+
private startupTimeoutMs;
|
|
150
|
+
private agentVersion;
|
|
151
|
+
constructor(opts?: AcpHarnessOptions);
|
|
152
|
+
info(): HarnessInfo;
|
|
153
|
+
/**
|
|
154
|
+
* Is the agent command there? Deliberately does not start it: it would run
|
|
155
|
+
* outside the sandbox wrapper. The version comes from `initialize` on the
|
|
156
|
+
* first run (`agentInfo.version`).
|
|
157
|
+
*/
|
|
158
|
+
preflight(): Promise<{
|
|
159
|
+
ok: true;
|
|
160
|
+
version: string;
|
|
161
|
+
} | {
|
|
162
|
+
ok: false;
|
|
163
|
+
reason: string;
|
|
164
|
+
}>;
|
|
165
|
+
/** Env for the agent process: scoped like Claude Code's, with only the auth vars the operator named. */
|
|
166
|
+
private buildEnv;
|
|
167
|
+
/** The node policy, with the harness policy mode and the watchdog budget applied. */
|
|
168
|
+
private compilePolicy;
|
|
169
|
+
/**
|
|
170
|
+
* Talk to one agent process from `initialize` to the last `session/prompt`
|
|
171
|
+
* response, then stop it. Never throws: every failure is on the result.
|
|
172
|
+
*/
|
|
173
|
+
private converse;
|
|
174
|
+
/**
|
|
175
|
+
* Run a node. The gate runs first: in strict mode a node whose opinions an
|
|
176
|
+
* ACP agent cannot honor is refused before the agent starts. The agent
|
|
177
|
+
* process runs inside the host's sandbox wrapper (#360 step 2) when the
|
|
178
|
+
* sandbox mode asks for one and the host has one.
|
|
179
|
+
*/
|
|
180
|
+
run(req: HarnessRunRequest): Promise<HarnessRunResult>;
|
|
181
|
+
/** Map one conversation onto a NodeResult with Claude Code's semantics. */
|
|
182
|
+
private toResult;
|
|
183
|
+
/**
|
|
184
|
+
* One completion: a fresh session with no MCP servers, every permission
|
|
185
|
+
* request rejected, and any tool call over budget. null when it failed
|
|
186
|
+
* (callers fail closed).
|
|
187
|
+
*/
|
|
188
|
+
complete(req: HarnessCompleteRequest): Promise<string | null>;
|
|
189
|
+
}
|