@sweny-ai/core 0.1.142 → 0.1.144

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 (45) hide show
  1. package/dist/agent-env.d.ts +24 -0
  2. package/dist/agent-env.js +40 -7
  3. package/dist/cli/config-file.d.ts +1 -1
  4. package/dist/cli/config-file.js +1 -1
  5. package/dist/cli/config.d.ts +2 -0
  6. package/dist/cli/config.js +15 -7
  7. package/dist/cli/main.js +30 -4
  8. package/dist/cli/run-history.d.ts +2 -0
  9. package/dist/cli/run-history.js +2 -0
  10. package/dist/cli/run-output.d.ts +13 -2
  11. package/dist/cli/run-output.js +27 -2
  12. package/dist/executor.d.ts +12 -0
  13. package/dist/executor.js +127 -7
  14. package/dist/harness/abort.d.ts +17 -0
  15. package/dist/harness/abort.js +53 -0
  16. package/dist/harness/agents.d.ts +11 -0
  17. package/dist/harness/agents.js +15 -0
  18. package/dist/harness/capabilities.d.ts +23 -0
  19. package/dist/harness/capabilities.js +35 -0
  20. package/dist/harness/claude-code.d.ts +20 -43
  21. package/dist/harness/claude-code.js +58 -312
  22. package/dist/harness/codex.d.ts +175 -0
  23. package/dist/harness/codex.js +921 -0
  24. package/dist/harness/index.d.ts +11 -3
  25. package/dist/harness/index.js +8 -8
  26. package/dist/harness/parse.d.ts +49 -0
  27. package/dist/harness/parse.js +253 -0
  28. package/dist/harness/policy.d.ts +23 -1
  29. package/dist/harness/policy.js +73 -15
  30. package/dist/harness/prompts.d.ts +7 -1
  31. package/dist/harness/prompts.js +16 -0
  32. package/dist/harness/types.d.ts +13 -2
  33. package/dist/harness/types.js +2 -2
  34. package/dist/index.d.ts +8 -4
  35. package/dist/index.js +6 -3
  36. package/dist/node-policy.d.ts +31 -0
  37. package/dist/node-policy.js +42 -0
  38. package/dist/safe-outputs.d.ts +103 -0
  39. package/dist/safe-outputs.js +460 -0
  40. package/dist/schema.d.ts +501 -12
  41. package/dist/schema.js +210 -4
  42. package/dist/skills/github.js +18 -0
  43. package/dist/types.d.ts +107 -1
  44. package/dist/types.js +29 -0
  45. package/package.json +1 -1
package/dist/executor.js CHANGED
@@ -23,6 +23,9 @@ import { validateWorkflow } from "./schema.js";
23
23
  import { resolveAgentAccess } from "./agent-env.js";
24
24
  import { fenceUntrusted } from "./untrusted.js";
25
25
  import { asClaude } from "./harness/compat.js";
26
+ import { isToolClass, policyGate } from "./harness/policy.js";
27
+ import { buildNodePolicy, resolveNodePermissions } from "./node-policy.js";
28
+ import { applySafeOutputs, createEmitOutputTool, createWriteStageState, resolveActor, safeOutputsInstruction, unresolvedOutputs, SAFE_OUTPUT_SCREEN_INSTRUCTION, } from "./safe-outputs.js";
26
29
  /**
27
30
  * Default workflow-level step budget. Counts total node executions across the
28
31
  * whole run (loop iterations included). Generous enough for legitimate deep
@@ -70,6 +73,13 @@ export async function execute(workflow, input, options) {
70
73
  let stepCount = 0; // total node executions across the run (loop iterations included)
71
74
  const trace = { steps: [], edges: [], sources: {} };
72
75
  validate(workflow, skills);
76
+ // Safe outputs (#365): run-wide caps and dedupe, the staged switch and the
77
+ // triggering actor, resolved once per run.
78
+ const writeState = createWriteStageState();
79
+ const stageOutputs = dryRun || options.stageOutputs === true || workflow.safe_outputs?.staged === true;
80
+ const runEnv = options.env ?? process.env;
81
+ const usesOutputs = Object.values(workflow.nodes).some((n) => (n.outputs?.length ?? 0) > 0);
82
+ const actor = usesOutputs ? resolveActor(runEnv, options.actor) : {};
73
83
  // Build an eval-time alias table from the loaded skills. Each skill owns
74
84
  // its own mapping between skill-tool names and equivalent MCP names. Core
75
85
  // stays vendor-neutral; this call just unions what the skills declare.
@@ -149,11 +159,32 @@ export async function execute(workflow, input, options) {
149
159
  // Dry run (#380): only `access: "read"` tools reach the node. Write and
150
160
  // unclassified tools are withheld (never registered, so never callable)
151
161
  // and recorded on the node result as `skippedWrites`.
152
- const tools = dryRun ? filteredTools.filter(isReadTool) : filteredTools;
162
+ // #365: `permissions: read` (explicit, inherited, or implied by `outputs`)
163
+ // gets the same tool gate as a dry run.
164
+ const permissions = resolveNodePermissions(node, workflow);
165
+ const declaresPolicy = node.permissions !== undefined || workflow.permissions !== undefined || (node.outputs?.length ?? 0) > 0;
166
+ const readOnlyNode = dryRun || permissions.access === "read";
167
+ const readTools = readOnlyNode ? filteredTools.filter(isReadTool) : filteredTools;
153
168
  const skippedWrites = dryRun ? filteredTools.filter((t) => !isReadTool(t)).map((t) => t.name) : [];
154
169
  if (skippedWrites.length > 0) {
155
170
  logger.info(` dry run: withheld write tools: ${skippedWrites.join(", ")}`, { node: currentId });
156
171
  }
172
+ else if (readOnlyNode && readTools.length < filteredTools.length) {
173
+ const withheld = filteredTools.filter((t) => !isReadTool(t)).map((t) => t.name);
174
+ logger.info(` permissions: read: withheld write tools: ${withheld.join(", ")}`, { node: currentId });
175
+ }
176
+ // Safe outputs (#365): the agent records write intents with emit_output;
177
+ // the write stage after the node applies them. One buffer per attempt.
178
+ const outputDecls = node.outputs ?? [];
179
+ const intents = [];
180
+ const tools = outputDecls.length > 0 ? [...readTools, createEmitOutputTool(outputDecls, intents)] : readTools;
181
+ if (outputDecls.length > 0 && !stageOutputs) {
182
+ const missing = unresolvedOutputs(outputDecls, node.skills, skills);
183
+ if (missing.length > 0) {
184
+ throw new Error(`Node "${currentId}" declares outputs [${missing.join(", ")}] but no configured skill can apply them ` +
185
+ `(github or linear). Set the skill's environment variables, or run with --stage to preview.`);
186
+ }
187
+ }
157
188
  const skillInstructions = resolveSkillInstructions(node.skills, skills);
158
189
  const skillMcpServers = resolveSkillMcpServers(node.skills, skills);
159
190
  // Runtime guard: if this node declares skills but none resolved, the node
@@ -226,7 +257,8 @@ export async function execute(workflow, input, options) {
226
257
  const effectiveRules = assembleCascaded("rules", currentId, workflow.nodes[currentId], inputRules.length, (workflow.rules ?? []).length, resolvedSources);
227
258
  const effectiveContext = assembleCascaded("context", currentId, workflow.nodes[currentId], inputContext.length, (workflow.context ?? []).length, resolvedSources);
228
259
  const baseInstruction = buildNodeInstruction(resolvedInstruction, effectiveRules, effectiveContext, input, skillInstructions);
229
- const instruction = dryRun ? `${dryRunNotice(skippedWrites)}\n\n---\n\n${baseInstruction}` : baseInstruction;
260
+ const nodeInstruction = outputDecls.length > 0 ? `${baseInstruction}\n\n${safeOutputsInstruction(outputDecls)}` : baseInstruction;
261
+ const instruction = dryRun ? `${dryRunNotice(skippedWrites)}\n\n---\n\n${nodeInstruction}` : nodeInstruction;
230
262
  // Run Claude on this node, with optional eval-failure retry loop.
231
263
  let attempt = 0;
232
264
  let result;
@@ -240,7 +272,43 @@ export async function execute(workflow, input, options) {
240
272
  // Per-node execution model: node.model ?? workflow.model. When undefined,
241
273
  // claude.run falls back to its own client default (then Claude Code's).
242
274
  const nodeModel = resolveExecutionModel(node, workflow);
275
+ // `tools.deny` entries that name a portable class (shell, write, edit, net,
276
+ // subagent) also deny that class of built-in agent tools; the harness
277
+ // compiles them or reports them in `degraded`.
278
+ const denyClasses = (node.tools?.deny ?? []).filter(isToolClass);
279
+ let degradedLogged = false;
280
+ const agentAccess = resolveAgentAccess(node.skills, skills);
281
+ // #365: a node or workflow that declares `permissions` or `outputs` gets
282
+ // one portable policy, compiled by each adapter. The gate also runs here so
283
+ // a strict refusal holds on every harness, before any spend. Nodes that
284
+ // declare neither pass no policy, so each harness keeps its own defaults.
285
+ const nodePolicy = declaresPolicy
286
+ ? buildNodePolicy({
287
+ permissions: { ...permissions, deny: [...new Set([...permissions.deny, ...denyClasses])] },
288
+ dryRun,
289
+ disallowedTools: node.disallowed_tools,
290
+ egress: agentAccess.domains,
291
+ })
292
+ : undefined;
293
+ // Egress is left to the adapter: only it knows whether a process sandbox
294
+ // wrapper covers what its native sandbox cannot.
295
+ const gate = nodePolicy && options.harness
296
+ ? policyGate(options.harness.capabilities, { ...nodePolicy, egress: [] })
297
+ : undefined;
243
298
  while (true) {
299
+ if (gate?.refuse) {
300
+ // Not an agent failure: fail_soft never softens a policy refusal.
301
+ logger.warn(` harness refused the node: ${gate.refuse}`, { node: currentId });
302
+ result = {
303
+ status: "failed",
304
+ data: { error: gate.refuse, refused: true },
305
+ toolCalls: [],
306
+ degraded: gate.degraded,
307
+ };
308
+ break;
309
+ }
310
+ // Only the final attempt's intents may be applied.
311
+ intents.length = 0;
244
312
  result = await claude.run({
245
313
  instruction: currentInstruction,
246
314
  context,
@@ -248,16 +316,24 @@ export async function execute(workflow, input, options) {
248
316
  outputSchema: node.output,
249
317
  maxTurns: node.max_turns,
250
318
  disallowedTools: node.disallowed_tools,
319
+ ...(denyClasses.length > 0 ? { deny: denyClasses } : {}),
251
320
  model: nodeModel,
252
321
  signal,
253
322
  timeoutMs,
254
- agentAccess: resolveAgentAccess(node.skills, skills),
255
- ...(dryRun ? {} : { mcpServers: skillMcpServers }),
256
- ...(dryRun ? { readOnly: true } : {}),
323
+ agentAccess,
324
+ ...(nodePolicy ? { policy: nodePolicy } : {}),
325
+ ...(readOnlyNode ? {} : { mcpServers: skillMcpServers }),
326
+ ...(readOnlyNode ? { readOnly: true } : {}),
257
327
  onProgress: (message) => {
258
328
  safeObserve(observer, { type: "node:progress", node: currentId, message }, logger);
259
329
  },
260
330
  });
331
+ // Opinions the harness could not honor natively: said once per node, never silently dropped.
332
+ if (!degradedLogged && result.degraded && result.degraded.length > 0) {
333
+ degradedLogged = true;
334
+ const who = result.harness?.id ?? "harness";
335
+ logger.warn(` ${who} could not honor natively: ${result.degraded.join("; ")}`, { node: currentId });
336
+ }
261
337
  // Retry only triggers on eval failure. Bail on tool/API errors.
262
338
  if (result.status !== "success") {
263
339
  agentRunFailed = true;
@@ -355,7 +431,9 @@ export async function execute(workflow, input, options) {
355
431
  // instead of failing outright. The original error stays in `data.error`
356
432
  // and `data.fail_soft` marks the downgrade for downstream nodes and
357
433
  // observers. Eval failures never take this path (agentRunFailed guards it).
358
- if (agentRunFailed && result.status === "failed" && node.fail_soft === true) {
434
+ // A strict-policy refusal (#331) is not an agent failure: fail_soft never softens it.
435
+ const refused = result.data?.refused === true;
436
+ if (agentRunFailed && result.status === "failed" && node.fail_soft === true && !refused) {
359
437
  const failError = result.data?.error;
360
438
  logger.warn(` fail_soft: node failed (${String(failError ?? "unknown error")}); continuing with partial output`, {
361
439
  node: currentId,
@@ -366,6 +444,45 @@ export async function execute(workflow, input, options) {
366
444
  data: { ...(result.data ?? {}), fail_soft: true },
367
445
  };
368
446
  }
447
+ // Safe outputs write stage (#365). Runs only after the agent finished and
448
+ // every eval passed; deterministic checks first, an optional model screen
449
+ // may veto, and only then does sweny call the skill's own write handler.
450
+ if (outputDecls.length > 0) {
451
+ if (result.status === "success" && !agentRunFailed) {
452
+ const stage = await applySafeOutputs({
453
+ nodeId: currentId,
454
+ declarations: outputDecls,
455
+ intents,
456
+ policy: workflow.safe_outputs,
457
+ nodeSkills: node.skills,
458
+ skills,
459
+ config,
460
+ env: runEnv,
461
+ actor,
462
+ staged: stageOutputs,
463
+ state: writeState,
464
+ logger,
465
+ screen: async (writes) => claude.ask({
466
+ instruction: SAFE_OUTPUT_SCREEN_INSTRUCTION,
467
+ context: { writes },
468
+ model: nodeModel,
469
+ signal,
470
+ timeoutMs,
471
+ }),
472
+ });
473
+ result = { ...result, outputs: stage.receipts };
474
+ if (stage.error) {
475
+ logger.warn(` ${stage.error}`, { node: currentId });
476
+ result = { ...result, status: "failed", data: { ...result.data, error: stage.error } };
477
+ }
478
+ }
479
+ else if (intents.length > 0) {
480
+ result = {
481
+ ...result,
482
+ outputs: intents.map((i) => ({ type: i.type, status: "skipped", reason: "node did not succeed" })),
483
+ };
484
+ }
485
+ }
369
486
  if (skippedWrites.length > 0)
370
487
  result = { ...result, skippedWrites };
371
488
  results.set(currentId, result);
@@ -749,7 +866,10 @@ function filterNodeTools(tools, filter, nodeId, logger) {
749
866
  if (!filter)
750
867
  return tools;
751
868
  const resolvedNames = new Set(tools.map((t) => t.name));
752
- for (const name of [...(filter.allow ?? []), ...(filter.deny ?? [])]) {
869
+ // A deny entry that names a tool class (write, shell, ...) targets built-in
870
+ // agent tools too, so it is not a typo when no skill tool has that name.
871
+ const denyNames = (filter.deny ?? []).filter((n) => !isToolClass(n));
872
+ for (const name of [...(filter.allow ?? []), ...denyNames]) {
753
873
  if (!resolvedNames.has(name)) {
754
874
  logger.warn(` tools filter: '${name}' does not match any tool resolved for node '${nodeId}'`, {
755
875
  node: nodeId,
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Timeout + caller-signal wiring shared by every adapter. No agent SDK imports.
3
+ */
4
+ /**
5
+ * Wire an optional timeout + caller signal onto a single AbortController.
6
+ *
7
+ * Returns undefined when neither a timeout nor a signal is supplied so the
8
+ * default code path (no abortController, no timer) is byte-for-byte unchanged.
9
+ *
10
+ * `reason()` reports whether the abort came from the timeout timer or an
11
+ * external signal, so callers can log a distinct timeout message.
12
+ */
13
+ export declare function makeAbort(timeoutMs?: number, signal?: AbortSignal): {
14
+ controller: AbortController;
15
+ clear: () => void;
16
+ reason: () => "timeout" | "signal" | undefined;
17
+ } | undefined;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Timeout + caller-signal wiring shared by every adapter. No agent SDK imports.
3
+ */
4
+ /**
5
+ * Wire an optional timeout + caller signal onto a single AbortController.
6
+ *
7
+ * Returns undefined when neither a timeout nor a signal is supplied so the
8
+ * default code path (no abortController, no timer) is byte-for-byte unchanged.
9
+ *
10
+ * `reason()` reports whether the abort came from the timeout timer or an
11
+ * external signal, so callers can log a distinct timeout message.
12
+ */
13
+ export function makeAbort(timeoutMs, signal) {
14
+ if (!timeoutMs && !signal)
15
+ return undefined;
16
+ const controller = new AbortController();
17
+ let reason;
18
+ let timer;
19
+ const onSignalAbort = () => {
20
+ if (controller.signal.aborted)
21
+ return;
22
+ reason = "signal";
23
+ controller.abort();
24
+ };
25
+ if (signal) {
26
+ if (signal.aborted) {
27
+ reason = "signal";
28
+ controller.abort();
29
+ }
30
+ else {
31
+ signal.addEventListener("abort", onSignalAbort, { once: true });
32
+ }
33
+ }
34
+ if (timeoutMs && timeoutMs > 0 && !controller.signal.aborted) {
35
+ timer = setTimeout(() => {
36
+ if (controller.signal.aborted)
37
+ return;
38
+ reason = "timeout";
39
+ controller.abort();
40
+ }, timeoutMs);
41
+ // Don't keep the event loop alive just for the abort timer.
42
+ timer?.unref?.();
43
+ }
44
+ return {
45
+ controller,
46
+ reason: () => reason,
47
+ clear: () => {
48
+ if (timer)
49
+ clearTimeout(timer);
50
+ signal?.removeEventListener("abort", onSignalAbort);
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The agent ids `--agent` / `coding-agent-provider` accept. Each one has an
3
+ * adapter and a green contract suite; anything else is refused, so sweny never
4
+ * runs one agent under another's name. No runtime imports (the CLI config and
5
+ * the harness factory share it).
6
+ */
7
+ export declare const SUPPORTED_AGENTS: readonly ["claude", "codex"];
8
+ export type SupportedAgent = (typeof SUPPORTED_AGENTS)[number];
9
+ export declare function isSupportedAgent(id: string): id is SupportedAgent;
10
+ /** The error for an agent id with no adapter. */
11
+ export declare function unsupportedAgentError(id: string): string;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The agent ids `--agent` / `coding-agent-provider` accept. Each one has an
3
+ * adapter and a green contract suite; anything else is refused, so sweny never
4
+ * runs one agent under another's name. No runtime imports (the CLI config and
5
+ * the harness factory share it).
6
+ */
7
+ export const SUPPORTED_AGENTS = ["claude", "codex"];
8
+ export function isSupportedAgent(id) {
9
+ return SUPPORTED_AGENTS.includes(id);
10
+ }
11
+ /** The error for an agent id with no adapter. */
12
+ export function unsupportedAgentError(id) {
13
+ 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.`);
15
+ }
@@ -5,3 +5,26 @@
5
5
  import type { HarnessCapabilities } from "./types.js";
6
6
  /** Claude Code enforces every sweny opinion natively, so `policyGate` never degrades or refuses it. */
7
7
  export declare const CLAUDE_CODE_CAPABILITIES: HarnessCapabilities;
8
+ /**
9
+ * Codex (`codex exec --json`), declared from the openai/codex source at
10
+ * rust-v0.159.2 and probed by the contract suite against the scripted fake:
11
+ * - structuredOutput native: `--output-schema <file>` (exec/src/cli.rs).
12
+ * - toolTrace full: `item.*` events for command_execution, file_change,
13
+ * mcp_tool_call and web_search, each with an id and a status
14
+ * (exec/src/exec_events.rs).
15
+ * - builtinDeny shell-only, plus net and subagent: `features.shell_tool=false`
16
+ * removes every shell tool (core/src/tools/spec_plan.rs add_shell_tools),
17
+ * `web_search="disabled"` removes web search, and `features.multi_agent=false`
18
+ * with `agents.enabled=false` removes subagents. apply_patch has no switch,
19
+ * so write and edit cannot be denied as classes.
20
+ * - mcp exclusive native: `--ignore-user-config` drops `$CODEX_HOME/config.toml`,
21
+ * and project `.codex/config.toml` layers stay disabled for a project that
22
+ * is not trusted in that ignored file (config/src/loader/mod.rs).
23
+ * - sandbox fs only: `--sandbox read-only|workspace-write`. Network is one
24
+ * on/off switch (`sandbox_workspace_write.network_access`), not a host list.
25
+ * - readOnly native: `--sandbox read-only` plus the shell, web search and
26
+ * subagent switches above.
27
+ * - turnLimit watchdog: codex exec has no turn limit; sweny counts tool calls.
28
+ * - usage tokens only: `turn.completed.usage`; Codex reports no cost.
29
+ */
30
+ export declare const CODEX_CAPABILITIES: HarnessCapabilities;
@@ -15,3 +15,38 @@ export const CLAUDE_CODE_CAPABILITIES = {
15
15
  cancel: "signal",
16
16
  resume: false,
17
17
  };
18
+ /**
19
+ * Codex (`codex exec --json`), declared from the openai/codex source at
20
+ * rust-v0.159.2 and probed by the contract suite against the scripted fake:
21
+ * - structuredOutput native: `--output-schema <file>` (exec/src/cli.rs).
22
+ * - toolTrace full: `item.*` events for command_execution, file_change,
23
+ * mcp_tool_call and web_search, each with an id and a status
24
+ * (exec/src/exec_events.rs).
25
+ * - builtinDeny shell-only, plus net and subagent: `features.shell_tool=false`
26
+ * removes every shell tool (core/src/tools/spec_plan.rs add_shell_tools),
27
+ * `web_search="disabled"` removes web search, and `features.multi_agent=false`
28
+ * with `agents.enabled=false` removes subagents. apply_patch has no switch,
29
+ * so write and edit cannot be denied as classes.
30
+ * - mcp exclusive native: `--ignore-user-config` drops `$CODEX_HOME/config.toml`,
31
+ * and project `.codex/config.toml` layers stay disabled for a project that
32
+ * is not trusted in that ignored file (config/src/loader/mod.rs).
33
+ * - sandbox fs only: `--sandbox read-only|workspace-write`. Network is one
34
+ * on/off switch (`sandbox_workspace_write.network_access`), not a host list.
35
+ * - readOnly native: `--sandbox read-only` plus the shell, web search and
36
+ * subagent switches above.
37
+ * - turnLimit watchdog: codex exec has no turn limit; sweny counts tool calls.
38
+ * - usage tokens only: `turn.completed.usage`; Codex reports no cost.
39
+ */
40
+ export const CODEX_CAPABILITIES = {
41
+ structuredOutput: "native",
42
+ toolTrace: "full",
43
+ builtinDeny: "shell-only",
44
+ denyClasses: ["shell", "net", "subagent"],
45
+ mcp: { inject: true, exclusive: "native" },
46
+ sandbox: { fs: true, network: false },
47
+ readOnly: "native",
48
+ turnLimit: "watchdog",
49
+ usage: { tokens: true, costUsd: false, live: false },
50
+ cancel: "kill",
51
+ resume: false,
52
+ };
@@ -10,10 +10,13 @@
10
10
  */
11
11
  import type { Claude, Tool, ToolContext, Logger, McpServerConfig } from "../types.js";
12
12
  import { type SandboxMode } from "../agent-env.js";
13
- import type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, HarnessInfo, HarnessRunRequest, HarnessRunResult } from "./types.js";
13
+ import type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, HarnessInfo, HarnessRunRequest, HarnessRunResult, NodePolicy, ToolClass } from "./types.js";
14
14
  import { CLAUDE_CODE_CAPABILITIES } from "./capabilities.js";
15
15
  import { buildEvaluatePrompt } from "./prompts.js";
16
+ import { parseToolResultContent, summarizeToolError } from "./parse.js";
17
+ import { makeAbort } from "./abort.js";
16
18
  export { buildEvaluatePrompt, CLAUDE_CODE_CAPABILITIES };
19
+ export { parseToolResultContent, summarizeToolError, makeAbort };
17
20
  /**
18
21
  * Built-in tools disallowed for the route-evaluation (`evaluate`) and
19
22
  * reflection/judge (`ask`) calls.
@@ -34,6 +37,13 @@ export declare const CLASSIFICATION_DISALLOWED_TOOLS: readonly ["Bash", "Write",
34
37
  * Read, Grep, Glob and WebSearch stay available for analysis.
35
38
  */
36
39
  export declare const READ_ONLY_DISALLOWED_TOOLS: readonly ["Bash", "Write", "Edit", "MultiEdit", "NotebookEdit", "Task", "Agent", "WebFetch"];
40
+ /**
41
+ * Claude Code built-in tool names per portable tool class. A node's
42
+ * `tools.deny` classes compile to these names on `disallowedTools`.
43
+ */
44
+ export declare const CLAUDE_TOOLS_BY_CLASS: Readonly<Record<ToolClass, readonly string[]>>;
45
+ /** Native `disallowedTools` for a policy: legacy names, compiled classes, and the read-only set. */
46
+ export declare function compileClaudeCodeDeny(policy: NodePolicy, legacy?: readonly string[]): string[];
37
47
  /** How sweny resolves which credentials reach the Claude Code subprocess. */
38
48
  export type SwenyAuthMode = "auto" | "api-key" | "oauth";
39
49
  export interface ResolveAuthEnvOpts {
@@ -120,20 +130,6 @@ export interface ClaudeCodeHarnessOptions {
120
130
  args: string[];
121
131
  };
122
132
  }
123
- /**
124
- * Wire an optional timeout + caller signal onto a single AbortController.
125
- *
126
- * Returns undefined when neither a timeout nor a signal is supplied so the
127
- * default code path (no abortController, no timer) is byte-for-byte unchanged.
128
- *
129
- * `reason()` reports whether the abort came from the timeout timer or an
130
- * external signal, so callers can log a distinct timeout message.
131
- */
132
- export declare function makeAbort(timeoutMs?: number, signal?: AbortSignal): {
133
- controller: AbortController;
134
- clear: () => void;
135
- reason: () => "timeout" | "signal" | undefined;
136
- } | undefined;
137
133
  export declare class ClaudeCodeHarness implements Claude, AgentHarness {
138
134
  readonly id: "claude-code";
139
135
  readonly capabilities: HarnessCapabilities;
@@ -171,9 +167,15 @@ export declare class ClaudeCodeHarness implements Claude, AgentHarness {
171
167
  version: string;
172
168
  }>;
173
169
  /**
174
- * Run a node. The query itself is unchanged by the harness seam; this only
175
- * runs {@link policyGate} (always `degraded: []` for Claude Code) and tags
176
- * the result with the harness id and version.
170
+ * Run a node. Runs {@link policyGate} (always `degraded: []` for Claude
171
+ * Code), compiles the policy to native options, and tags the result with the
172
+ * harness id and version.
173
+ *
174
+ * Policy compile (#365): `policy.readOnly` (or the legacy `readOnly` flag)
175
+ * is a read-only run; `policy.deny` classes become native `disallowedTools`
176
+ * names, merged with `nativeDeny` and the legacy `disallowedTools`; and
177
+ * `policy.strict` makes MCP exclusive (`strictMcpConfig`) even for a
178
+ * write-capable node.
177
179
  */
178
180
  run(req: HarnessRunRequest): Promise<HarnessRunResult>;
179
181
  private runQuery;
@@ -227,28 +229,3 @@ export type ClaudeClient = ClaudeCodeHarness;
227
229
  * @internal Exported for the tool bridge parity test (#414).
228
230
  */
229
231
  export declare function coreToolToSdkTool(coreTool: Tool, defaultCtx: ToolContext): import("@anthropic-ai/claude-agent-sdk").SdkMcpToolDefinition<Record<string, import("zod").ZodType<any, import("zod").ZodTypeDef, any>>>;
230
- /**
231
- * Best-effort recovery of typed output from a tool_result's `content`.
232
- *
233
- * The MCP protocol sends tool results as string content. Our in-process
234
- * wrapper JSON-stringifies structured output before returning; external
235
- * MCP servers generally do the same for JSON payloads. If the string
236
- * looks like a JSON object or array and parses, return the parsed value
237
- * so verify's output-path walks work against typed data.
238
- *
239
- * Raw strings are preserved verbatim. JSON-primitive strings (e.g. the
240
- * literal four characters `"42"`) are intentionally NOT parsed - we
241
- * cannot distinguish a tool that returned the number 42 (wrapper sends
242
- * `"42"`) from a tool that returned the string "42" (wrapper also sends
243
- * `"42"`). Preserving the string is safer than guessing.
244
- */
245
- export declare function parseToolResultContent(content: unknown): unknown;
246
- /**
247
- * Produce a short, single-line description of a tool-error payload suitable
248
- * for the CI log stream. The full parsed value stays on the ToolCall for
249
- * verify and downstream tooling - this is only for inline observability.
250
- *
251
- * Collapses newlines, trims whitespace, and caps to 300 chars so a huge
252
- * API response body doesn't flood the log.
253
- */
254
- export declare function summarizeToolError(parsed: unknown): string;