@sweny-ai/core 0.1.148 → 0.1.150

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.
Files changed (40) hide show
  1. package/dist/cli/builtin-workflows.d.ts +45 -0
  2. package/dist/cli/builtin-workflows.js +129 -0
  3. package/dist/cli/check.d.ts +2 -8
  4. package/dist/cli/check.js +2 -34
  5. package/dist/cli/config.js +8 -3
  6. package/dist/cli/main.js +46 -11
  7. package/dist/cli/packs.d.ts +9 -5
  8. package/dist/cli/packs.js +100 -50
  9. package/dist/executor.js +15 -3
  10. package/dist/harness/acp-rpc.d.ts +75 -0
  11. package/dist/harness/acp-rpc.js +204 -0
  12. package/dist/harness/acp.d.ts +189 -0
  13. package/dist/harness/acp.js +942 -0
  14. package/dist/harness/agents.d.ts +5 -1
  15. package/dist/harness/agents.js +8 -1
  16. package/dist/harness/auth.d.ts +38 -0
  17. package/dist/harness/auth.js +93 -0
  18. package/dist/harness/capabilities.d.ts +30 -0
  19. package/dist/harness/capabilities.js +41 -0
  20. package/dist/harness/claude-code.d.ts +8 -0
  21. package/dist/harness/claude-code.js +8 -1
  22. package/dist/harness/codex.d.ts +10 -1
  23. package/dist/harness/codex.js +17 -3
  24. package/dist/harness/index.d.ts +7 -3
  25. package/dist/harness/index.js +7 -2
  26. package/dist/harness/parse.d.ts +12 -0
  27. package/dist/harness/parse.js +1 -1
  28. package/dist/harness/policy.js +6 -0
  29. package/dist/harness/types.d.ts +6 -0
  30. package/dist/index.d.ts +1 -1
  31. package/dist/node-policy.js +1 -0
  32. package/dist/safe-outputs.d.ts +17 -5
  33. package/dist/safe-outputs.js +85 -11
  34. package/dist/schema.d.ts +80 -0
  35. package/dist/schema.js +23 -0
  36. package/dist/types.d.ts +13 -0
  37. package/dist/workflows/browser.js +2 -2
  38. package/dist/workflows/implement.yml +61 -19
  39. package/dist/workflows/triage.yml +171 -139
  40. package/package.json +1 -1
@@ -6,6 +6,10 @@
6
6
  */
7
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;
@@ -5,8 +5,15 @@
5
5
  * the harness factory share it).
6
6
  */
7
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) {
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Agent login probes (#339): does the agent a harness drives have a way to
3
+ * authenticate? Each adapter's `preflight()` calls its probe, so `sweny
4
+ * workflow run` and `sweny check` fail before any node runs, with the fix.
5
+ *
6
+ * Presence only. No probe reads a credential's value, and the macOS Keychain
7
+ * probe asks for the entry's attributes, never its secret.
8
+ */
9
+ export type AuthProbeResult = {
10
+ ok: true;
11
+ via: string;
12
+ } | {
13
+ ok: false;
14
+ reason: string;
15
+ };
16
+ /** Test seam: adapters take one in their options instead of reading the host. */
17
+ export type AuthProbe = () => AuthProbeResult;
18
+ /** Same shape as the CLI's other "Missing:" lines; names the agent so the fix is unambiguous. */
19
+ export declare const CLAUDE_CODE_AUTH_HINT: string;
20
+ export declare const CODEX_AUTH_HINT = "Missing: OPENAI_API_KEY or CODEX_API_KEY, or a Codex login (`codex login`), for --agent codex";
21
+ /**
22
+ * A local Claude Code login: `~/.claude/.credentials.json` (or under
23
+ * `CLAUDE_CONFIG_DIR`) and, on macOS, the Keychain entry (attributes only).
24
+ */
25
+ export declare function detectClaudeCodeLogin(opts?: {
26
+ env?: Record<string, string | undefined>;
27
+ home?: string;
28
+ platform?: NodeJS.Platform;
29
+ keychainHasLogin?: () => boolean;
30
+ }): boolean;
31
+ /**
32
+ * Claude Code auth, the way the spawned agent will see it: Bedrock or Vertex
33
+ * routing, an env credential that survives `SWENY_AUTH` (`oauth` keeps only
34
+ * `CLAUDE_CODE_OAUTH_TOKEN`), or a stored Claude Code login.
35
+ */
36
+ export declare function claudeCodeAuth(env?: Record<string, string | undefined>, login?: () => boolean): AuthProbeResult;
37
+ /** Codex auth: an API key or access token in env, or a stored `codex login`. */
38
+ export declare function codexAuth(env?: Record<string, string | undefined>, login?: () => boolean): AuthProbeResult;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Agent login probes (#339): does the agent a harness drives have a way to
3
+ * authenticate? Each adapter's `preflight()` calls its probe, so `sweny
4
+ * workflow run` and `sweny check` fail before any node runs, with the fix.
5
+ *
6
+ * Presence only. No probe reads a credential's value, and the macOS Keychain
7
+ * probe asks for the entry's attributes, never its secret.
8
+ */
9
+ import * as fs from "node:fs";
10
+ import * as os from "node:os";
11
+ import * as path from "node:path";
12
+ import { spawnSync } from "node:child_process";
13
+ import { hasCodexLogin } from "../agent-env.js";
14
+ /** Same shape as the CLI's other "Missing:" lines; names the agent so the fix is unambiguous. */
15
+ export const CLAUDE_CODE_AUTH_HINT = "Missing: ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN (from `claude setup-token`), " +
16
+ "or a Claude Code login (run `claude`, then /login), for --agent claude";
17
+ export const CODEX_AUTH_HINT = "Missing: OPENAI_API_KEY or CODEX_API_KEY, or a Codex login (`codex login`), for --agent codex";
18
+ function set(v) {
19
+ return typeof v === "string" && v.trim() !== "";
20
+ }
21
+ function truthy(v) {
22
+ if (!set(v))
23
+ return false;
24
+ const s = v.trim().toLowerCase();
25
+ return s !== "0" && s !== "false" && s !== "no" && s !== "off";
26
+ }
27
+ /**
28
+ * A local Claude Code login: `~/.claude/.credentials.json` (or under
29
+ * `CLAUDE_CONFIG_DIR`) and, on macOS, the Keychain entry (attributes only).
30
+ */
31
+ export function detectClaudeCodeLogin(opts = {}) {
32
+ const env = opts.env ?? process.env;
33
+ const home = opts.home ?? os.homedir();
34
+ const platform = opts.platform ?? process.platform;
35
+ const configDir = env.CLAUDE_CONFIG_DIR || path.join(home, ".claude");
36
+ try {
37
+ if (fs.existsSync(path.join(configDir, ".credentials.json")))
38
+ return true;
39
+ }
40
+ catch {
41
+ // fall through
42
+ }
43
+ if (platform === "darwin") {
44
+ const probe = opts.keychainHasLogin ??
45
+ (() => {
46
+ const r = spawnSync("security", ["find-generic-password", "-s", "Claude Code-credentials"], {
47
+ stdio: "ignore",
48
+ timeout: 3000,
49
+ });
50
+ return r.status === 0;
51
+ });
52
+ try {
53
+ return probe();
54
+ }
55
+ catch {
56
+ return false;
57
+ }
58
+ }
59
+ return false;
60
+ }
61
+ /**
62
+ * Claude Code auth, the way the spawned agent will see it: Bedrock or Vertex
63
+ * routing, an env credential that survives `SWENY_AUTH` (`oauth` keeps only
64
+ * `CLAUDE_CODE_OAUTH_TOKEN`), or a stored Claude Code login.
65
+ */
66
+ export function claudeCodeAuth(env = process.env, login = () => detectClaudeCodeLogin({ env })) {
67
+ if (truthy(env.CLAUDE_CODE_USE_BEDROCK))
68
+ return { ok: true, via: "Amazon Bedrock" };
69
+ if (truthy(env.CLAUDE_CODE_USE_VERTEX))
70
+ return { ok: true, via: "Google Vertex AI" };
71
+ const mode = (env.SWENY_AUTH ?? "").trim().toLowerCase();
72
+ if (set(env.CLAUDE_CODE_OAUTH_TOKEN))
73
+ return { ok: true, via: "CLAUDE_CODE_OAUTH_TOKEN" };
74
+ if (mode !== "oauth") {
75
+ if (set(env.ANTHROPIC_API_KEY))
76
+ return { ok: true, via: "ANTHROPIC_API_KEY" };
77
+ if (set(env.ANTHROPIC_AUTH_TOKEN))
78
+ return { ok: true, via: "ANTHROPIC_AUTH_TOKEN" };
79
+ }
80
+ if (login())
81
+ return { ok: true, via: "Claude Code login" };
82
+ return { ok: false, reason: CLAUDE_CODE_AUTH_HINT };
83
+ }
84
+ /** Codex auth: an API key or access token in env, or a stored `codex login`. */
85
+ export function codexAuth(env = process.env, login = () => hasCodexLogin(env)) {
86
+ for (const k of ["CODEX_API_KEY", "OPENAI_API_KEY", "CODEX_ACCESS_TOKEN"]) {
87
+ if (set(env[k]))
88
+ return { ok: true, via: k };
89
+ }
90
+ if (login())
91
+ return { ok: true, via: "codex login" };
92
+ return { ok: false, reason: CODEX_AUTH_HINT };
93
+ }
@@ -56,3 +56,33 @@ export declare const CODEX_CAPABILITIES: HarnessCapabilities;
56
56
  * - cancel rpc: the `abort` command, then SIGTERM and SIGKILL.
57
57
  */
58
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;
@@ -90,3 +90,44 @@ export const PI_CAPABILITIES = {
90
90
  cancel: "rpc",
91
91
  resume: false,
92
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
+ };
@@ -15,6 +15,7 @@ import { CLAUDE_CODE_CAPABILITIES } from "./capabilities.js";
15
15
  import { buildEvaluatePrompt } from "./prompts.js";
16
16
  import { parseToolResultContent, summarizeToolError } from "./parse.js";
17
17
  import { makeAbort } from "./abort.js";
18
+ import { type AuthProbe } from "./auth.js";
18
19
  export { buildEvaluatePrompt, CLAUDE_CODE_CAPABILITIES };
19
20
  export { parseToolResultContent, summarizeToolError, makeAbort };
20
21
  /**
@@ -117,6 +118,8 @@ export interface ClaudeCodeHarnessOptions {
117
118
  sandboxAllowedDomains?: string[];
118
119
  /** Sandbox preflight probe (test seam). Returns a reason when the sandbox cannot run. */
119
120
  sandboxProbe?: () => string | undefined;
121
+ /** Login probe for `preflight()` (test seam). Default: {@link claudeCodeAuth} over `process.env`. */
122
+ authProbe?: AuthProbe;
120
123
  /**
121
124
  * Serve skill tools through the SwenyToolBridge (a stdio MCP shim over a
122
125
  * per-run unix socket, #414) instead of the in-process SDK MCP server.
@@ -149,6 +152,7 @@ export declare class ClaudeCodeHarness implements Claude, AgentHarness {
149
152
  private envScope;
150
153
  private toolBridge;
151
154
  private toolBridgeShim;
155
+ private authProbe;
152
156
  constructor(opts?: ClaudeCodeHarnessOptions);
153
157
  /**
154
158
  * Build env for the Claude Code subprocess (#360). Applies auth precedence
@@ -162,9 +166,13 @@ export declare class ClaudeCodeHarness implements Claude, AgentHarness {
162
166
  private buildEnv;
163
167
  /** Which harness (and SDK version) produced a result. */
164
168
  info(): HarnessInfo;
169
+ /** Checks the agent can authenticate (#339): an env credential, Bedrock/Vertex, or a Claude Code login. */
165
170
  preflight(): Promise<{
166
171
  ok: true;
167
172
  version: string;
173
+ } | {
174
+ ok: false;
175
+ reason: string;
168
176
  }>;
169
177
  /**
170
178
  * Run a node. Runs {@link policyGate} (always `degraded: []` for Claude
@@ -19,6 +19,7 @@ import { jsonSchemaToZodShape, toolErrorToMcpResult, toolOutputToMcpResult } fro
19
19
  import { startToolBridge } from "./tool-bridge/server.js";
20
20
  import { parseToolResultContent, summarizeToolError, tryParseJSON } from "./parse.js";
21
21
  import { makeAbort } from "./abort.js";
22
+ import { claudeCodeAuth } from "./auth.js";
22
23
  export { buildEvaluatePrompt, CLAUDE_CODE_CAPABILITIES };
23
24
  export { parseToolResultContent, summarizeToolError, makeAbort };
24
25
  let cachedSdkVersion;
@@ -218,6 +219,7 @@ export class ClaudeCodeHarness {
218
219
  envScope;
219
220
  toolBridge;
220
221
  toolBridgeShim;
222
+ authProbe;
221
223
  constructor(opts = {}) {
222
224
  this.model = opts.model;
223
225
  this.maxTurns = opts.maxTurns ?? 20;
@@ -233,6 +235,7 @@ export class ClaudeCodeHarness {
233
235
  this.sandboxProbe = opts.sandboxProbe;
234
236
  this.toolBridge = opts.toolBridge ?? process.env.SWENY_TOOL_BRIDGE === "1";
235
237
  this.toolBridgeShim = opts.toolBridgeShim;
238
+ this.authProbe = opts.authProbe ?? (() => claudeCodeAuth(process.env));
236
239
  }
237
240
  /**
238
241
  * Build env for the Claude Code subprocess (#360). Applies auth precedence
@@ -260,7 +263,11 @@ export class ClaudeCodeHarness {
260
263
  info() {
261
264
  return { id: this.id, version: claudeSdkVersion() };
262
265
  }
266
+ /** Checks the agent can authenticate (#339): an env credential, Bedrock/Vertex, or a Claude Code login. */
263
267
  async preflight() {
268
+ const auth = this.authProbe();
269
+ if (!auth.ok)
270
+ return { ok: false, reason: auth.reason };
264
271
  return { ok: true, version: claudeSdkVersion() };
265
272
  }
266
273
  /**
@@ -301,7 +308,7 @@ export class ClaudeCodeHarness {
301
308
  ...req,
302
309
  readOnly,
303
310
  disallowedTools: disallowedTools.length > 0 ? disallowedTools : undefined,
304
- strictMcp: readOnly || policy.strict,
311
+ strictMcp: readOnly || policy.strict || policy.exclusiveMcp === true,
305
312
  sandboxMode: policy.sandbox,
306
313
  agentAccess: { envVars: req.agentAccess?.envVars ?? [], domains: policy.egress },
307
314
  });
@@ -29,6 +29,7 @@ import type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, Harness
29
29
  import { CODEX_CAPABILITIES } from "./capabilities.js";
30
30
  import { type HarnessPolicyMode } from "./policy.js";
31
31
  import { type SandboxWrapper } from "./sandbox-wrapper.js";
32
+ import { type AuthProbe } from "./auth.js";
32
33
  export { CODEX_CAPABILITIES };
33
34
  /** Oldest Codex CLI whose flags this adapter uses (`--ignore-user-config`, `--ignore-rules`). */
34
35
  export declare const MIN_CODEX_VERSION = "0.159.0";
@@ -89,6 +90,8 @@ export interface CodexHarnessOptions {
89
90
  };
90
91
  /** Grace period between SIGTERM and SIGKILL when stopping Codex (default: 2000 ms). */
91
92
  killGraceMs?: number;
93
+ /** Login probe for `preflight()` (test seam). Default: {@link codexAuth} over `process.env`. */
94
+ authProbe?: AuthProbe;
92
95
  }
93
96
  /** A TOML value for `-c key=value`, as the official SDK writes it (sdk/typescript/src/exec.ts). */
94
97
  export declare function toTomlValue(value: unknown): string;
@@ -135,9 +138,13 @@ export declare class CodexHarness implements AgentHarness {
135
138
  private killGraceMs;
136
139
  private preflightResult;
137
140
  private version;
141
+ private authProbe;
138
142
  constructor(opts?: CodexHarnessOptions);
139
143
  info(): HarnessInfo;
140
- /** `codex --version`, checked against {@link MIN_CODEX_VERSION}. Cached per instance. */
144
+ /**
145
+ * Before any node runs (#339): the Codex CLI is installed and new enough,
146
+ * and Codex can authenticate (an API key, an access token, or `codex login`).
147
+ */
141
148
  preflight(): Promise<{
142
149
  ok: true;
143
150
  version: string;
@@ -145,6 +152,8 @@ export declare class CodexHarness implements AgentHarness {
145
152
  ok: false;
146
153
  reason: string;
147
154
  }>;
155
+ /** `codex --version`, checked against {@link MIN_CODEX_VERSION}. Cached per instance. */
156
+ private checkCli;
148
157
  /** Env for the Codex process: scoped like Claude Code's, with Codex's auth vars instead of Anthropic's. */
149
158
  private buildEnv;
150
159
  /** The node policy after `disallowed_tools` names are translated into Codex classes. */
@@ -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
- preflight() {
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.preflight();
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.preflight();
939
+ const pre = await this.checkCli();
926
940
  if (!pre.ok)
927
941
  return failClosed(pre.reason);
928
942
  const args = [
@@ -4,6 +4,7 @@
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";
7
8
  import { PiHarness, type PiHarnessOptions } from "./pi.js";
8
9
  export { claudeCompat, asClaude } from "./compat.js";
9
10
  export type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, HarnessId, HarnessInfo, HarnessRunRequest, HarnessRunResult, NodePolicy, PolicyGateResult, PolicyWrappers, ToolClass, } from "./types.js";
@@ -14,12 +15,14 @@ export { ClaudeCodeHarness, CLAUDE_CODE_CAPABILITIES } from "./claude-code.js";
14
15
  export type { ClaudeCodeHarnessOptions } from "./claude-code.js";
15
16
  export { CodexHarness, CODEX_CAPABILITIES, MIN_CODEX_VERSION } from "./codex.js";
16
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";
17
20
  export { PiHarness, PI_CAPABILITIES, MIN_PI_VERSION } from "./pi.js";
18
21
  export type { PiHarnessOptions } from "./pi.js";
19
22
  export { startToolBridge } from "./tool-bridge/server.js";
20
23
  export type { ToolBridge, ToolBridgeOptions } from "./tool-bridge/server.js";
21
- export { SUPPORTED_AGENTS, isSupportedAgent, unsupportedAgentError } from "./agents.js";
22
- export type { SupportedAgent } from "./agents.js";
24
+ export { SUPPORTED_AGENTS, isSupportedAgent, parseAcpAgent, unsupportedAgentError } from "./agents.js";
25
+ export type { AcpAgentId, SupportedAgent } from "./agents.js";
23
26
  /**
24
27
  * Build the harness for an agent id. `claude` is the historical `--agent` /
25
28
  * `coding-agent-provider` value and means the same as `claude-code`.
@@ -28,4 +31,5 @@ export type { SupportedAgent } from "./agents.js";
28
31
  export declare function createHarness(id: "claude" | "claude-code", opts?: ClaudeCodeHarnessOptions): ClaudeCodeHarness;
29
32
  export declare function createHarness(id: "codex", opts?: CodexHarnessOptions): CodexHarness;
30
33
  export declare function createHarness(id: "pi", opts?: PiHarnessOptions): PiHarness;
31
- export declare function createHarness(id: string, opts?: ClaudeCodeHarnessOptions & CodexHarnessOptions & PiHarnessOptions): ClaudeCodeHarness | CodexHarness | 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,17 +4,22 @@
4
4
  */
5
5
  import { ClaudeCodeHarness } from "./claude-code.js";
6
6
  import { CodexHarness } from "./codex.js";
7
+ import { AcpHarness } from "./acp.js";
7
8
  import { PiHarness } from "./pi.js";
8
- import { unsupportedAgentError } from "./agents.js";
9
+ import { parseAcpAgent, unsupportedAgentError } from "./agents.js";
9
10
  export { claudeCompat, asClaude } from "./compat.js";
10
11
  export { policyGate, nativeDenyClasses, resolveHarnessPolicy, isToolClass, TOOL_CLASSES } from "./policy.js";
11
12
  export { ask, evaluate, buildAskPrompt, buildEvaluatePrompt } from "./prompts.js";
12
13
  export { ClaudeCodeHarness, CLAUDE_CODE_CAPABILITIES } from "./claude-code.js";
13
14
  export { CodexHarness, CODEX_CAPABILITIES, MIN_CODEX_VERSION } from "./codex.js";
15
+ export { AcpHarness, ACP_CAPABILITIES, ACP_PROTOCOL_VERSION } from "./acp.js";
14
16
  export { PiHarness, PI_CAPABILITIES, MIN_PI_VERSION } from "./pi.js";
15
17
  export { startToolBridge } from "./tool-bridge/server.js";
16
- export { SUPPORTED_AGENTS, isSupportedAgent, unsupportedAgentError } from "./agents.js";
18
+ export { SUPPORTED_AGENTS, isSupportedAgent, parseAcpAgent, unsupportedAgentError } from "./agents.js";
17
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) ?? "" });
18
23
  switch (id) {
19
24
  case "claude":
20
25
  case "claude-code":
@@ -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) {
@@ -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
  }
@@ -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";
@@ -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
  }
@@ -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, labels,
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
- /** The skill that applies a declaration: its `via`, else the first node skill (then any loaded skill) that can. */
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.