@sweny-ai/core 0.1.147 → 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.
@@ -4,8 +4,12 @@
4
4
  * runs one agent under another's name. No runtime imports (the CLI config and
5
5
  * the harness factory share it).
6
6
  */
7
- export declare const SUPPORTED_AGENTS: readonly ["claude", "codex"];
7
+ export declare const SUPPORTED_AGENTS: readonly ["claude", "codex", "pi"];
8
8
  export type SupportedAgent = (typeof SUPPORTED_AGENTS)[number];
9
- export declare function isSupportedAgent(id: string): id is SupportedAgent;
9
+ /** `acp:<command>` (#416): any Agent Client Protocol agent, started with `<command>`. */
10
+ export type AcpAgentId = `acp:${string}`;
11
+ /** The command of an `acp:<command>` agent id (`"opencode acp"` for `acp:opencode acp`), or undefined when it is not one or the command is empty. */
12
+ export declare function parseAcpAgent(id: string): string | undefined;
13
+ export declare function isSupportedAgent(id: string): id is SupportedAgent | AcpAgentId;
10
14
  /** The error for an agent id with no adapter. */
11
15
  export declare function unsupportedAgentError(id: string): string;
@@ -4,12 +4,19 @@
4
4
  * runs one agent under another's name. No runtime imports (the CLI config and
5
5
  * the harness factory share it).
6
6
  */
7
- export const SUPPORTED_AGENTS = ["claude", "codex"];
7
+ export const SUPPORTED_AGENTS = ["claude", "codex", "pi"];
8
+ /** The command of an `acp:<command>` agent id (`"opencode acp"` for `acp:opencode acp`), or undefined when it is not one or the command is empty. */
9
+ export function parseAcpAgent(id) {
10
+ if (!id.startsWith("acp:"))
11
+ return undefined;
12
+ const command = id.slice("acp:".length).trim();
13
+ return command === "" ? undefined : command;
14
+ }
8
15
  export function isSupportedAgent(id) {
9
- return SUPPORTED_AGENTS.includes(id);
16
+ return SUPPORTED_AGENTS.includes(id) || parseAcpAgent(id) !== undefined;
10
17
  }
11
18
  /** The error for an agent id with no adapter. */
12
19
  export function unsupportedAgentError(id) {
13
20
  return (`Unsupported coding agent "${id}": supported agents are "claude" (headless Claude Code) ` +
14
- `and "codex" (Codex CLI). Remove --agent / coding-agent-provider or set it to one of them.`);
21
+ `and "codex" (Codex CLI), or "pi" (pi coding agent, experimental). Remove --agent / coding-agent-provider or set it to one of them.`);
15
22
  }
@@ -28,3 +28,61 @@ export declare const CLAUDE_CODE_CAPABILITIES: HarnessCapabilities;
28
28
  * - usage tokens only: `turn.completed.usage`; Codex reports no cost.
29
29
  */
30
30
  export declare const CODEX_CAPABILITIES: HarnessCapabilities;
31
+ /**
32
+ * pi (`pi --mode rpc`), declared from the pi coding-agent docs and source at
33
+ * badlogic/pi-mono v0.99.2 (`packages/coding-agent/docs/{cli,rpc,rpc-commands,json,mcp,
34
+ * security,message-types}.md`, `src/core/agent-session.ts`, `src/extensions/mcp/`)
35
+ * and probed by the contract suite against the scripted fake:
36
+ * - structuredOutput prompt: pi has no output-schema flag; sweny prompts for
37
+ * the JSON, parses and validates it.
38
+ * - toolTrace full: `tool_execution_start|end` events carry `toolCallId`,
39
+ * `toolName`, `args`, `isError` for built-in and MCP tools alike.
40
+ * - builtinDeny shell-only, plus write, edit and subagent: `--exclude-tools` and
41
+ * `--tools` take tool names (`bash`, `write`, `edit`), and `-ne` leaves no
42
+ * extension, so no subagent tool exists. pi has no built-in net tool, so `net`
43
+ * is not deniable as a class (`bash` can still curl): it degrades or, in
44
+ * strict, refuses.
45
+ * - mcp home-isolation: MCP is a built-in extension that reads `mcp.json` from
46
+ * `$PI_CODING_AGENT_DIR`. sweny points that at a scratch dir holding only the
47
+ * generated `mcp.json`, and passes `-na` so a project `.pi/mcp.json` never loads.
48
+ * - sandbox none: "no built-in sandbox" (docs/security.md). The process wrapper
49
+ * (sandbox-wrapper.ts) is the only containment; strict refuses without it.
50
+ * - readOnly tool-allowlist: `--tools read,grep,find,ls` plus sweny's own MCP
51
+ * tools; no shell, write or edit tool exists in the run.
52
+ * - turnLimit watchdog: pi has no turn limit flag; sweny counts
53
+ * `tool_execution_start` events and aborts.
54
+ * - usage tokens and cost: `get_session_stats` returns tokens and cost for the
55
+ * session. Read once at the end, so `live` is false.
56
+ * - cancel rpc: the `abort` command, then SIGTERM and SIGKILL.
57
+ */
58
+ export declare const PI_CAPABILITIES: HarnessCapabilities;
59
+ /**
60
+ * Any ACP agent (`--agent acp:<command>`, #416), declared from the Agent
61
+ * Client Protocol schema v1.24.1 (agentclientprotocol/agent-client-protocol,
62
+ * `schema/v1/schema.json`, docs under `docs/protocol/v1/`) and probed by the
63
+ * contract suite against a scripted fake agent. ACP is the long-tail adapter:
64
+ * the protocol carries a prompt and a stream of updates, not sweny's opinions.
65
+ * - structuredOutput prompt: `PromptResponse` has only `stopReason`, so there
66
+ * is no schema channel. sweny asks for the JSON in the prompt, parses it,
67
+ * checks it and retries once.
68
+ * - toolTrace full: `tool_call` and `tool_call_update` session updates carry an
69
+ * id, a kind, a status and (optionally) raw input and content.
70
+ * - builtinDeny none: the protocol has no tool allow or deny list. The only
71
+ * hook is `session/request_permission`, which fires only when the agent
72
+ * chooses to ask. sweny answers it by policy (best effort), but cannot
73
+ * promise the agent asks, so nothing is declared.
74
+ * - mcp inject, exclusive none: `session/new` takes `mcpServers`, but whether
75
+ * the agent also loads its own MCP config is agent-defined.
76
+ * - sandbox none: nothing in the protocol. The process wrapper (srt) is the
77
+ * only containment, see sandbox-wrapper.ts.
78
+ * - readOnly none: modes are agent-defined. `policyGate` counts read-only as
79
+ * enforced only with the wrapper's read-only mount.
80
+ * - turnLimit watchdog: no turn limit in the protocol; sweny counts tool calls.
81
+ * - usage costUsd only: `usage_update {used, size, cost?}` is the stable
82
+ * usage. `used` and `size` are context window occupancy, not billed tokens,
83
+ * so they are not mapped; `cost` (cumulative, ISO 4217 currency) is, when it
84
+ * is USD. Per-turn token usage is behind an unstable flag.
85
+ * - cancel rpc: `session/cancel`, then kill.
86
+ * - resume false: sweny starts a fresh session per node.
87
+ */
88
+ export declare const ACP_CAPABILITIES: HarnessCapabilities;
@@ -50,3 +50,84 @@ export const CODEX_CAPABILITIES = {
50
50
  cancel: "kill",
51
51
  resume: false,
52
52
  };
53
+ /**
54
+ * pi (`pi --mode rpc`), declared from the pi coding-agent docs and source at
55
+ * badlogic/pi-mono v0.99.2 (`packages/coding-agent/docs/{cli,rpc,rpc-commands,json,mcp,
56
+ * security,message-types}.md`, `src/core/agent-session.ts`, `src/extensions/mcp/`)
57
+ * and probed by the contract suite against the scripted fake:
58
+ * - structuredOutput prompt: pi has no output-schema flag; sweny prompts for
59
+ * the JSON, parses and validates it.
60
+ * - toolTrace full: `tool_execution_start|end` events carry `toolCallId`,
61
+ * `toolName`, `args`, `isError` for built-in and MCP tools alike.
62
+ * - builtinDeny shell-only, plus write, edit and subagent: `--exclude-tools` and
63
+ * `--tools` take tool names (`bash`, `write`, `edit`), and `-ne` leaves no
64
+ * extension, so no subagent tool exists. pi has no built-in net tool, so `net`
65
+ * is not deniable as a class (`bash` can still curl): it degrades or, in
66
+ * strict, refuses.
67
+ * - mcp home-isolation: MCP is a built-in extension that reads `mcp.json` from
68
+ * `$PI_CODING_AGENT_DIR`. sweny points that at a scratch dir holding only the
69
+ * generated `mcp.json`, and passes `-na` so a project `.pi/mcp.json` never loads.
70
+ * - sandbox none: "no built-in sandbox" (docs/security.md). The process wrapper
71
+ * (sandbox-wrapper.ts) is the only containment; strict refuses without it.
72
+ * - readOnly tool-allowlist: `--tools read,grep,find,ls` plus sweny's own MCP
73
+ * tools; no shell, write or edit tool exists in the run.
74
+ * - turnLimit watchdog: pi has no turn limit flag; sweny counts
75
+ * `tool_execution_start` events and aborts.
76
+ * - usage tokens and cost: `get_session_stats` returns tokens and cost for the
77
+ * session. Read once at the end, so `live` is false.
78
+ * - cancel rpc: the `abort` command, then SIGTERM and SIGKILL.
79
+ */
80
+ export const PI_CAPABILITIES = {
81
+ structuredOutput: "prompt",
82
+ toolTrace: "full",
83
+ builtinDeny: "shell-only",
84
+ denyClasses: ["shell", "write", "edit", "subagent"],
85
+ mcp: { inject: true, exclusive: "home-isolation" },
86
+ sandbox: { fs: false, network: false },
87
+ readOnly: "tool-allowlist",
88
+ turnLimit: "watchdog",
89
+ usage: { tokens: true, costUsd: true, live: false },
90
+ cancel: "rpc",
91
+ resume: false,
92
+ };
93
+ /**
94
+ * Any ACP agent (`--agent acp:<command>`, #416), declared from the Agent
95
+ * Client Protocol schema v1.24.1 (agentclientprotocol/agent-client-protocol,
96
+ * `schema/v1/schema.json`, docs under `docs/protocol/v1/`) and probed by the
97
+ * contract suite against a scripted fake agent. ACP is the long-tail adapter:
98
+ * the protocol carries a prompt and a stream of updates, not sweny's opinions.
99
+ * - structuredOutput prompt: `PromptResponse` has only `stopReason`, so there
100
+ * is no schema channel. sweny asks for the JSON in the prompt, parses it,
101
+ * checks it and retries once.
102
+ * - toolTrace full: `tool_call` and `tool_call_update` session updates carry an
103
+ * id, a kind, a status and (optionally) raw input and content.
104
+ * - builtinDeny none: the protocol has no tool allow or deny list. The only
105
+ * hook is `session/request_permission`, which fires only when the agent
106
+ * chooses to ask. sweny answers it by policy (best effort), but cannot
107
+ * promise the agent asks, so nothing is declared.
108
+ * - mcp inject, exclusive none: `session/new` takes `mcpServers`, but whether
109
+ * the agent also loads its own MCP config is agent-defined.
110
+ * - sandbox none: nothing in the protocol. The process wrapper (srt) is the
111
+ * only containment, see sandbox-wrapper.ts.
112
+ * - readOnly none: modes are agent-defined. `policyGate` counts read-only as
113
+ * enforced only with the wrapper's read-only mount.
114
+ * - turnLimit watchdog: no turn limit in the protocol; sweny counts tool calls.
115
+ * - usage costUsd only: `usage_update {used, size, cost?}` is the stable
116
+ * usage. `used` and `size` are context window occupancy, not billed tokens,
117
+ * so they are not mapped; `cost` (cumulative, ISO 4217 currency) is, when it
118
+ * is USD. Per-turn token usage is behind an unstable flag.
119
+ * - cancel rpc: `session/cancel`, then kill.
120
+ * - resume false: sweny starts a fresh session per node.
121
+ */
122
+ export const ACP_CAPABILITIES = {
123
+ structuredOutput: "prompt",
124
+ toolTrace: "full",
125
+ builtinDeny: "none",
126
+ mcp: { inject: true, exclusive: "none" },
127
+ sandbox: { fs: false, network: false },
128
+ readOnly: "none",
129
+ turnLimit: "watchdog",
130
+ usage: { tokens: false, costUsd: true, live: false },
131
+ cancel: "rpc",
132
+ resume: false,
133
+ };
@@ -4,6 +4,8 @@
4
4
  */
5
5
  import { ClaudeCodeHarness, type ClaudeCodeHarnessOptions } from "./claude-code.js";
6
6
  import { CodexHarness, type CodexHarnessOptions } from "./codex.js";
7
+ import { AcpHarness, type AcpHarnessOptions } from "./acp.js";
8
+ import { PiHarness, type PiHarnessOptions } from "./pi.js";
7
9
  export { claudeCompat, asClaude } from "./compat.js";
8
10
  export type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, HarnessId, HarnessInfo, HarnessRunRequest, HarnessRunResult, NodePolicy, PolicyGateResult, PolicyWrappers, ToolClass, } from "./types.js";
9
11
  export { policyGate, nativeDenyClasses, resolveHarnessPolicy, isToolClass, TOOL_CLASSES } from "./policy.js";
@@ -13,10 +15,14 @@ export { ClaudeCodeHarness, CLAUDE_CODE_CAPABILITIES } from "./claude-code.js";
13
15
  export type { ClaudeCodeHarnessOptions } from "./claude-code.js";
14
16
  export { CodexHarness, CODEX_CAPABILITIES, MIN_CODEX_VERSION } from "./codex.js";
15
17
  export type { CodexHarnessOptions } from "./codex.js";
18
+ export { AcpHarness, ACP_CAPABILITIES, ACP_PROTOCOL_VERSION } from "./acp.js";
19
+ export type { AcpHarnessOptions } from "./acp.js";
20
+ export { PiHarness, PI_CAPABILITIES, MIN_PI_VERSION } from "./pi.js";
21
+ export type { PiHarnessOptions } from "./pi.js";
16
22
  export { startToolBridge } from "./tool-bridge/server.js";
17
23
  export type { ToolBridge, ToolBridgeOptions } from "./tool-bridge/server.js";
18
- export { SUPPORTED_AGENTS, isSupportedAgent, unsupportedAgentError } from "./agents.js";
19
- export type { SupportedAgent } from "./agents.js";
24
+ export { SUPPORTED_AGENTS, isSupportedAgent, parseAcpAgent, unsupportedAgentError } from "./agents.js";
25
+ export type { AcpAgentId, SupportedAgent } from "./agents.js";
20
26
  /**
21
27
  * Build the harness for an agent id. `claude` is the historical `--agent` /
22
28
  * `coding-agent-provider` value and means the same as `claude-code`.
@@ -24,4 +30,6 @@ export type { SupportedAgent } from "./agents.js";
24
30
  */
25
31
  export declare function createHarness(id: "claude" | "claude-code", opts?: ClaudeCodeHarnessOptions): ClaudeCodeHarness;
26
32
  export declare function createHarness(id: "codex", opts?: CodexHarnessOptions): CodexHarness;
27
- export declare function createHarness(id: string, opts?: ClaudeCodeHarnessOptions & CodexHarnessOptions): ClaudeCodeHarness | CodexHarness;
33
+ export declare function createHarness(id: "pi", opts?: PiHarnessOptions): PiHarness;
34
+ export declare function createHarness(id: `acp:${string}`, opts?: AcpHarnessOptions): AcpHarness;
35
+ export declare function createHarness(id: string, opts?: ClaudeCodeHarnessOptions & CodexHarnessOptions & PiHarnessOptions & AcpHarnessOptions): ClaudeCodeHarness | CodexHarness | PiHarness | AcpHarness;
@@ -4,21 +4,30 @@
4
4
  */
5
5
  import { ClaudeCodeHarness } from "./claude-code.js";
6
6
  import { CodexHarness } from "./codex.js";
7
- import { unsupportedAgentError } from "./agents.js";
7
+ import { AcpHarness } from "./acp.js";
8
+ import { PiHarness } from "./pi.js";
9
+ import { parseAcpAgent, unsupportedAgentError } from "./agents.js";
8
10
  export { claudeCompat, asClaude } from "./compat.js";
9
11
  export { policyGate, nativeDenyClasses, resolveHarnessPolicy, isToolClass, TOOL_CLASSES } from "./policy.js";
10
12
  export { ask, evaluate, buildAskPrompt, buildEvaluatePrompt } from "./prompts.js";
11
13
  export { ClaudeCodeHarness, CLAUDE_CODE_CAPABILITIES } from "./claude-code.js";
12
14
  export { CodexHarness, CODEX_CAPABILITIES, MIN_CODEX_VERSION } from "./codex.js";
15
+ export { AcpHarness, ACP_CAPABILITIES, ACP_PROTOCOL_VERSION } from "./acp.js";
16
+ export { PiHarness, PI_CAPABILITIES, MIN_PI_VERSION } from "./pi.js";
13
17
  export { startToolBridge } from "./tool-bridge/server.js";
14
- export { SUPPORTED_AGENTS, isSupportedAgent, unsupportedAgentError } from "./agents.js";
18
+ export { SUPPORTED_AGENTS, isSupportedAgent, parseAcpAgent, unsupportedAgentError } from "./agents.js";
15
19
  export function createHarness(id, opts = {}) {
20
+ // `acp:<command>` (#416): any ACP agent. An empty command is an error, not a fallback.
21
+ if (id.startsWith("acp:"))
22
+ return new AcpHarness({ ...opts, acpCommand: parseAcpAgent(id) ?? "" });
16
23
  switch (id) {
17
24
  case "claude":
18
25
  case "claude-code":
19
26
  return new ClaudeCodeHarness(opts);
20
27
  case "codex":
21
28
  return new CodexHarness(opts);
29
+ case "pi":
30
+ return new PiHarness(opts);
22
31
  default:
23
32
  throw new Error(unsupportedAgentError(id));
24
33
  }
@@ -47,3 +47,15 @@ export declare function summarizeToolError(parsed: unknown): string;
47
47
  * non-conforming model answer is at least attributable.
48
48
  */
49
49
  export declare function tryParseJSON(text: string, outputSchema?: JSONSchema, logger?: Pick<Logger, "warn">, label?: string): Record<string, unknown>;
50
+ /**
51
+ * Lightweight structural check of a parsed object against a JSON Schema.
52
+ *
53
+ * Deliberately shallow: it verifies `required` keys are present and that the
54
+ * top-level `type` and each declared property `type` match. It does NOT do
55
+ * full JSON Schema validation (no `$ref`, `oneOf`, formats, nested arrays).
56
+ * The contract here is "flag an obviously non-conforming object" so a mismatch
57
+ * is logged rather than silently accepted; it is not a validation gate.
58
+ *
59
+ * Returns a list of human-readable problems; empty means no detected mismatch.
60
+ */
61
+ export declare function schemaMismatches(value: unknown, schema: JSONSchema): string[];
@@ -195,7 +195,7 @@ export function tryParseJSON(text, outputSchema, logger, label = "Claude") {
195
195
  *
196
196
  * Returns a list of human-readable problems; empty means no detected mismatch.
197
197
  */
198
- function schemaMismatches(value, schema) {
198
+ export function schemaMismatches(value, schema) {
199
199
  const problems = [];
200
200
  const s = schema;
201
201
  if (s.type === "object" || s.properties || s.required) {
@@ -0,0 +1,202 @@
1
+ /**
2
+ * pi harness (#415): runs a node on the pi coding agent (bring any model)
3
+ * through `pi --mode rpc`.
4
+ *
5
+ * Interfaces used, all read from badlogic/pi-mono at v0.99.2
6
+ * (`packages/coding-agent`, now also published as earendil-works/pi):
7
+ * - `docs/cli.md`: `--mode rpc`, `--no-session`, `--tools` (allowlist over
8
+ * built-in, extension and MCP tools), `--exclude-tools`, `--no-tools`,
9
+ * `--no-extensions` with explicit `-e builtin:mcp`, `--no-context-files`,
10
+ * `--no-skills`, `--no-prompt-templates`, `--no-themes`, `--no-approve`,
11
+ * `--model`, `--append-system-prompt`.
12
+ * - `docs/rpc.md`, `docs/rpc-commands.md`: JSONL framing on LF only, the
13
+ * `prompt`, `abort` and `get_session_stats` commands, `agent_settled`.
14
+ * - `docs/json.md`, `docs/message-types.md`: `message_end` (assistant message
15
+ * with `stopReason` and `errorMessage`), `tool_execution_start|end`,
16
+ * `auto_retry_end`.
17
+ * - `docs/mcp.md`, `src/extensions/mcp/`: `mcp.json` in `$PI_CODING_AGENT_DIR`,
18
+ * `exposure: "direct"` so tools are declared to the model, `${VAR}` in
19
+ * `env`, tool names `mcp__<server>__<tool>`.
20
+ * - `docs/security.md`: "no built-in sandbox", so the whole process runs
21
+ * inside the sweny process wrapper (sandbox-wrapper.ts) and strict refuses
22
+ * without it.
23
+ * - `src/core/agent-session.ts` `_refreshToolRegistry`: a `--tools` allowlist
24
+ * filters every registered tool, MCP tools included, by exact name.
25
+ *
26
+ * Every sweny opinion in the pi column of the enforcement matrix is enforced
27
+ * natively, kept by sweny (wrapper, tool bridge, prompt + parse + validate,
28
+ * watchdog), reported in `degraded`, or refused under strict policy by
29
+ * `policyGate`. Nothing is dropped silently.
30
+ */
31
+ import type { Logger, McpServerConfig, NodeUsage, ToolContext, Tool } from "../types.js";
32
+ import { type SandboxMode } from "../agent-env.js";
33
+ import type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, HarnessInfo, HarnessRunRequest, HarnessRunResult, ToolClass } from "./types.js";
34
+ import { PI_CAPABILITIES } from "./capabilities.js";
35
+ import { type HarnessPolicyMode } from "./policy.js";
36
+ import { type SandboxWrapper } from "./sandbox-wrapper.js";
37
+ export { PI_CAPABILITIES };
38
+ /**
39
+ * Oldest pi this adapter is written against. The docs and source were read at
40
+ * v0.99.2; older releases are unverified, so they are refused.
41
+ */
42
+ export declare const MIN_PI_VERSION = "0.99.2";
43
+ /** Name of the MCP server that carries sweny's skill tools (the tool bridge). */
44
+ export declare const PI_BRIDGE_SERVER = "sweny-core";
45
+ /**
46
+ * Hosts pi itself needs through the sandbox wrapper: the API host of every
47
+ * provider whose key is in the env, and of the provider named in `--model`
48
+ * (`provider/id`). Other providers (custom endpoints in `models.json`, cloud
49
+ * providers) are added with `SWENY_SANDBOX_ALLOWED_DOMAINS`.
50
+ */
51
+ export declare function piBackendHosts(env?: Record<string, string | undefined>, model?: string): string[];
52
+ export interface PiHarnessOptions {
53
+ /** Model passed to `--model`. Free text (`provider/id`, `id:thinking`); sweny has no model opinion. */
54
+ model?: string;
55
+ /** Tool-call budget the watchdog enforces when a node sets no `max_turns` (default: 20). */
56
+ maxTurns?: number;
57
+ /** Working directory (default: process.cwd()). */
58
+ cwd?: string;
59
+ logger?: Logger;
60
+ /** Default tool context for standalone usage (not via the executor). */
61
+ defaultContext?: ToolContext;
62
+ /** Catalog defaults, overridden by per-run skill servers and explicit mcpServers. */
63
+ defaultMcpServers?: Record<string, McpServerConfig>;
64
+ /** Explicit external MCP servers, overriding defaults and per-run skill servers. */
65
+ mcpServers?: Record<string, McpServerConfig>;
66
+ /** Extra env var names passed through (see agent-env.ts). Default: `SWENY_ENV_PASSTHROUGH`. */
67
+ envPassthrough?: string[];
68
+ /** Scope the pi env to the allowlist. Default: `SWENY_ENV_SCOPE`, else on in CI. */
69
+ envScope?: boolean;
70
+ /**
71
+ * Process sandbox mode (`SWENY_SANDBOX`): `strict` refuses the node without
72
+ * the process wrapper, `auto` wraps when a wrapper is available and otherwise
73
+ * degrades, `off` never wraps. Default: `SWENY_SANDBOX`, else `auto` in CI
74
+ * and `off` locally.
75
+ */
76
+ sandbox?: SandboxMode;
77
+ /**
78
+ * The host's process sandbox wrapper (#360 step 2). `undefined` = detect
79
+ * srt once per process; `null` = none (test seam).
80
+ */
81
+ sandboxWrapper?: SandboxWrapper | null;
82
+ /** `strict` refuses a node with unenforceable opinions. Default: {@link resolveHarnessPolicy}. */
83
+ policy?: HarnessPolicyMode;
84
+ /** The pi CLI. Default: `SWENY_PI_PATH`, else `pi` on PATH. Test seam for the fake. */
85
+ piCommand?: {
86
+ command: string;
87
+ args: string[];
88
+ };
89
+ /**
90
+ * A `models.json` (custom providers and models) copied into pi's scratch
91
+ * agent dir. Default: `SWENY_PI_MODELS_JSON`. Without it pi sees only its
92
+ * built-in model catalog; the operator's own `~/.pi/agent` is never read.
93
+ */
94
+ modelsFile?: string;
95
+ /** Shim command override for the tool bridge (test seam). */
96
+ toolBridgeShim?: {
97
+ command: string;
98
+ args: string[];
99
+ };
100
+ /** How long to wait for pi to stop after the `abort` command before SIGTERM (default: 3000 ms). */
101
+ abortGraceMs?: number;
102
+ /** Grace period between SIGTERM and SIGKILL when stopping pi (default: 2000 ms). */
103
+ killGraceMs?: number;
104
+ /** How long to wait for `get_session_stats` after the run settles (default: 5000 ms). */
105
+ statsTimeoutMs?: number;
106
+ }
107
+ /**
108
+ * Split `disallowed_tools` names into portable classes pi can act on, pi tools
109
+ * it can exclude by name, and the names it has no tool for.
110
+ */
111
+ export declare function translateDenyNames(names?: readonly string[]): {
112
+ classes: ToolClass[];
113
+ piTools: string[];
114
+ unknown: string[];
115
+ };
116
+ /** The name pi registers an MCP tool under (`src/extensions/mcp/tools.ts` createMcpToolName). */
117
+ export declare function piMcpToolName(server: string, tool: string, isTaken?: (name: string) => boolean): string;
118
+ /** pi registered names of the bridge's tools, and the way back to the sweny names. */
119
+ export declare function bridgeToolNames(tools: readonly Tool[]): {
120
+ names: string[];
121
+ back: Map<string, string>;
122
+ };
123
+ /**
124
+ * `mcp.json` entries for the external servers. Returns what pi cannot load
125
+ * (reported in `degraded`). Values using pi's `!command` or `${VAR}` syntax are
126
+ * refused, so a sweny config value can never run a command inside pi.
127
+ */
128
+ export declare function piMcpServers(servers: Record<string, McpServerConfig>): {
129
+ entries: Record<string, Record<string, unknown>>;
130
+ unsupported: string[];
131
+ };
132
+ /**
133
+ * `get_session_stats` data onto NodeUsage. pi reports 0 for what a provider did
134
+ * not report (and for a model it has no price for), so only positive values are
135
+ * kept: absent stays absent, never a guessed 0 or a fake free run.
136
+ */
137
+ export declare function mapStats(data: unknown): NodeUsage | undefined;
138
+ export declare class PiHarness implements AgentHarness {
139
+ readonly id: "pi";
140
+ readonly capabilities: HarnessCapabilities;
141
+ /**
142
+ * Empty on purpose: judges use the model pi would use anyway. Leaving this
143
+ * undefined would fall back to the Claude judge model, which pi may not have.
144
+ */
145
+ readonly defaultJudgeModel = "";
146
+ readonly logger: Logger;
147
+ private model;
148
+ private maxTurns;
149
+ private cwd;
150
+ private defaultContext;
151
+ private mcpServers;
152
+ private defaultMcpServers;
153
+ private envPassthrough;
154
+ private envScope;
155
+ private sandboxMode;
156
+ private sandboxWrapper;
157
+ private policyMode;
158
+ private piCommand;
159
+ private modelsFile;
160
+ private toolBridgeShim;
161
+ private abortGraceMs;
162
+ private killGraceMs;
163
+ private statsTimeoutMs;
164
+ private egressWarned;
165
+ private preflightResult;
166
+ private version;
167
+ constructor(opts?: PiHarnessOptions);
168
+ info(): HarnessInfo;
169
+ /** `pi --version`, checked against {@link MIN_PI_VERSION}. Cached per instance. */
170
+ preflight(): Promise<{
171
+ ok: true;
172
+ version: string;
173
+ } | {
174
+ ok: false;
175
+ reason: string;
176
+ }>;
177
+ /** Env for the pi process: scoped like Claude Code's, with pi's provider credentials instead of Anthropic's. */
178
+ private buildEnv;
179
+ /** The node policy after `disallowed_tools` names are translated into pi classes. */
180
+ private compilePolicy;
181
+ /** argv shared by run() and complete(); the prompt travels as an RPC command, never in argv. */
182
+ private baseArgs;
183
+ /** Spawn pi in rpc mode, send the prompt, and read events until it settles and exits. */
184
+ private exec;
185
+ /** Write pi's scratch agent dir: generated `mcp.json` only (plus an operator-supplied `models.json`). */
186
+ private writeAgentDir;
187
+ /**
188
+ * Run a node. The gate runs first: in strict mode a node whose opinions pi
189
+ * cannot honor is refused before pi starts. pi has no native sandbox, so the
190
+ * whole process runs inside the host's process sandbox wrapper (#360 step 2)
191
+ * when the sandbox mode is not off; `SWENY_SANDBOX=strict` refuses without one.
192
+ */
193
+ run(req: HarnessRunRequest): Promise<HarnessRunResult>;
194
+ /** Map one rpc outcome onto a NodeResult with Claude Code's semantics. */
195
+ private toResult;
196
+ /**
197
+ * One completion: no tools (`--no-tools`), no MCP, no extensions, an empty
198
+ * scratch agent dir. Nothing can act, so no process wrapper is needed.
199
+ * null when it failed (callers fail closed).
200
+ */
201
+ complete(req: HarnessCompleteRequest): Promise<string | null>;
202
+ }