@sweny-ai/core 0.1.209 → 0.1.210
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/executor.d.ts +16 -1
- package/dist/executor.js +85 -2
- package/dist/harness/claude-code.d.ts +39 -4
- package/dist/harness/claude-code.js +98 -11
- package/dist/harness/types.d.ts +8 -0
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +16 -0
- package/package.json +1 -1
package/dist/executor.d.ts
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
*
|
|
13
13
|
* This replaces ~8k lines of engine + recipe step code.
|
|
14
14
|
*/
|
|
15
|
-
import type { Workflow, Node, Skill, Tool, Claude, Observer, NodeResult, Logger, ExecutionResult, JSONSchema } from "./types.js";
|
|
15
|
+
import type { Workflow, Node, Skill, Tool, Claude, Observer, NodeResult, Logger, ExecutionResult, JSONSchema, EvalResult, Evaluator, ToolCall } from "./types.js";
|
|
16
|
+
import type { ToolAliases } from "./eval/index.js";
|
|
16
17
|
import type { HarnessPolicyMode } from "./harness/policy.js";
|
|
17
18
|
import type { AgentHarness } from "./harness/types.js";
|
|
18
19
|
import type { DeciderOperatorConfig } from "./decider.js";
|
|
@@ -190,6 +191,20 @@ export declare const COUNT_OF_KEYWORD = "x-sweny-count-of";
|
|
|
190
191
|
* nested shapes and additionalProperties stay the author's concern.
|
|
191
192
|
*/
|
|
192
193
|
export declare function outputContractProblems(data: unknown, output: JSONSchema | undefined): ContractProblem[];
|
|
194
|
+
/**
|
|
195
|
+
* Required tools the agent never called, when that is the only reason a node
|
|
196
|
+
* failed its evals and another turn could call them: every failure is a
|
|
197
|
+
* `function` evaluator, none saw a forbidden call, every required tool it names
|
|
198
|
+
* is absent from the trace (not called and failed: that is the retry path's
|
|
199
|
+
* job), and every one is reachable, i.e. on `agentTools`, the tool list the
|
|
200
|
+
* harness reported for the run, by its own name or an alias (an MCP tool
|
|
201
|
+
* counts by its bare name). Empty otherwise, and empty when the harness
|
|
202
|
+
* reported no list. An `any_tool_called` set needs one reachable member and
|
|
203
|
+
* reads "one of a, b" over its reachable members.
|
|
204
|
+
*/
|
|
205
|
+
export declare function uncalledRequiredTools(evaluators: Evaluator[] | undefined, failures: EvalResult[], toolCalls: ToolCall[], agentTools: readonly string[] | undefined, aliases?: ToolAliases): string[];
|
|
206
|
+
/** The repair turn for {@link uncalledRequiredTools}: names the tools, then the step again. */
|
|
207
|
+
export declare function requiredToolsRepairInstruction(uncalled: string[], instruction: string): string;
|
|
193
208
|
/**
|
|
194
209
|
* Dry-run tool gate (#380). Only an explicit `access: "read"` passes; a tool
|
|
195
210
|
* with no `access` is treated as a write so new tools fail safe.
|
package/dist/executor.js
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { consoleLogger } from "./types.js";
|
|
16
16
|
import { resolveSources } from "./source-resolver.js";
|
|
17
17
|
import { evaluateAll, aggregateEval } from "./eval/index.js";
|
|
18
|
+
import { checkAllToolsCalled, checkAnyToolCalled, checkNoToolCalled } from "./eval/function.js";
|
|
18
19
|
import { evaluateRequires } from "./requires.js";
|
|
19
20
|
import { evaluateExpression, isWhenExpression, parseExpression, whenLabel } from "./when.js";
|
|
20
21
|
import { buildRetryPreamble } from "./retry.js";
|
|
@@ -602,6 +603,8 @@ async function executeRun(workflow, input, options, results, trace, at) {
|
|
|
602
603
|
let currentInstruction = instruction;
|
|
603
604
|
// One structured-output repair per visit (#357), before any eval retry.
|
|
604
605
|
let repaired = false;
|
|
606
|
+
// One required-tool repair per visit, before any eval retry.
|
|
607
|
+
let toolRepaired = false;
|
|
605
608
|
const retry = node.retry;
|
|
606
609
|
// Per-node execution model: node.model ?? workflow.model. When undefined,
|
|
607
610
|
// claude.run falls back to its own client default (then Claude Code's).
|
|
@@ -723,8 +726,8 @@ async function executeRun(workflow, input, options, results, trace, at) {
|
|
|
723
726
|
// Resume (#363): spend is journaled as it is reported, so a crash mid-node
|
|
724
727
|
// does not hand the resumed run a fresh budget.
|
|
725
728
|
const usageNode = currentId;
|
|
726
|
-
//
|
|
727
|
-
const usageAttempt = attempt + (repaired ? 1 : 0);
|
|
729
|
+
// Each repair call (at most one of each) shifts later attempts by one, so every agent call has its own key.
|
|
730
|
+
const usageAttempt = attempt + (repaired ? 1 : 0) + (toolRepaired ? 1 : 0);
|
|
728
731
|
let liveUsage;
|
|
729
732
|
// A live usage record the journal cannot write stops the agent at once:
|
|
730
733
|
// its spend would be invisible to a resume.
|
|
@@ -957,6 +960,30 @@ async function executeRun(workflow, input, options, results, trace, at) {
|
|
|
957
960
|
}
|
|
958
961
|
if (outcome.pass)
|
|
959
962
|
break;
|
|
963
|
+
// Required tools: a node whose only failures are `function` evals naming
|
|
964
|
+
// tools its agent never called, all of them on the tool list the harness
|
|
965
|
+
// says the agent had, gets one repair turn that names them, before any
|
|
966
|
+
// eval retry. Same footing as the output contract repair: part
|
|
967
|
+
// of this visit (no extra max_steps slot), its spend counted. Agents
|
|
968
|
+
// sometimes answer on turn 1 without the tools they were given (dogfood
|
|
969
|
+
// run 42890245104: `called: [StructuredOutput]`).
|
|
970
|
+
const uncalled = toolRepaired
|
|
971
|
+
? []
|
|
972
|
+
: uncalledRequiredTools(node.eval, outcome.failures, result.toolCalls, result.agentTools, toolAliases);
|
|
973
|
+
if (uncalled.length > 0) {
|
|
974
|
+
toolRepaired = true;
|
|
975
|
+
logger.warn(` required tools not called: ${uncalled.join(", ")}; asking the agent once to call them`, {
|
|
976
|
+
node: currentId,
|
|
977
|
+
});
|
|
978
|
+
safeObserve(observer, {
|
|
979
|
+
type: "node:warning",
|
|
980
|
+
node: currentId,
|
|
981
|
+
reason: "required tools not called; one repair attempt",
|
|
982
|
+
fields: uncalled,
|
|
983
|
+
}, logger);
|
|
984
|
+
currentInstruction = requiredToolsRepairInstruction(uncalled, currentInstruction);
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
960
987
|
// Apply the failure to the result.
|
|
961
988
|
result.status = "failed";
|
|
962
989
|
result.data = { ...result.data, error: outcome.error };
|
|
@@ -1599,6 +1626,62 @@ export function outputContractProblems(data, output) {
|
|
|
1599
1626
|
}
|
|
1600
1627
|
return problems;
|
|
1601
1628
|
}
|
|
1629
|
+
/**
|
|
1630
|
+
* Required tools the agent never called, when that is the only reason a node
|
|
1631
|
+
* failed its evals and another turn could call them: every failure is a
|
|
1632
|
+
* `function` evaluator, none saw a forbidden call, every required tool it names
|
|
1633
|
+
* is absent from the trace (not called and failed: that is the retry path's
|
|
1634
|
+
* job), and every one is reachable, i.e. on `agentTools`, the tool list the
|
|
1635
|
+
* harness reported for the run, by its own name or an alias (an MCP tool
|
|
1636
|
+
* counts by its bare name). Empty otherwise, and empty when the harness
|
|
1637
|
+
* reported no list. An `any_tool_called` set needs one reachable member and
|
|
1638
|
+
* reads "one of a, b" over its reachable members.
|
|
1639
|
+
*/
|
|
1640
|
+
export function uncalledRequiredTools(evaluators, failures, toolCalls, agentTools, aliases) {
|
|
1641
|
+
if (!agentTools)
|
|
1642
|
+
return [];
|
|
1643
|
+
const listed = new Set(agentTools.map(bareToolName));
|
|
1644
|
+
const reachable = (name) => listed.has(name) || [...(aliases?.get(name) ?? [])].some((a) => listed.has(a));
|
|
1645
|
+
const uncalled = [];
|
|
1646
|
+
// A call to any of these (by name or alias), successful or not.
|
|
1647
|
+
const invoked = (names) => checkNoToolCalled(names, toolCalls, aliases) !== null;
|
|
1648
|
+
for (const failure of failures) {
|
|
1649
|
+
const rule = failure.kind === "function"
|
|
1650
|
+
? evaluators?.find((e) => e.kind === "function" && e.name === failure.name)?.rule
|
|
1651
|
+
: undefined;
|
|
1652
|
+
if (!rule)
|
|
1653
|
+
return [];
|
|
1654
|
+
if (rule.no_tool_called && rule.no_tool_called.length > 0 && invoked(rule.no_tool_called))
|
|
1655
|
+
return [];
|
|
1656
|
+
for (const name of rule.all_tools_called ?? []) {
|
|
1657
|
+
if (checkAllToolsCalled([name], toolCalls, aliases) === null)
|
|
1658
|
+
continue;
|
|
1659
|
+
if (invoked([name]) || !reachable(name))
|
|
1660
|
+
return [];
|
|
1661
|
+
uncalled.push(name);
|
|
1662
|
+
}
|
|
1663
|
+
const any = rule.any_tool_called ?? [];
|
|
1664
|
+
if (any.length > 0 && checkAnyToolCalled(any, toolCalls, aliases) !== null) {
|
|
1665
|
+
const open = any.filter(reachable);
|
|
1666
|
+
if (invoked(any) || open.length === 0)
|
|
1667
|
+
return [];
|
|
1668
|
+
uncalled.push(open.length === 1 ? open[0] : `one of ${open.join(", ")}`);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
return [...new Set(uncalled)];
|
|
1672
|
+
}
|
|
1673
|
+
/** `mcp__<server>__<tool>` → `<tool>`; any other name unchanged. Tool calls are recorded the same way. */
|
|
1674
|
+
function bareToolName(name) {
|
|
1675
|
+
const parts = name.split("__");
|
|
1676
|
+
return parts.length >= 3 && parts[0] === "mcp" ? parts.slice(2).join("__") : name;
|
|
1677
|
+
}
|
|
1678
|
+
/** The repair turn for {@link uncalledRequiredTools}: names the tools, then the step again. */
|
|
1679
|
+
export function requiredToolsRepairInstruction(uncalled, instruction) {
|
|
1680
|
+
const listed = uncalled.map((t) => `- ${t}`).join("\n");
|
|
1681
|
+
return (`This step requires these tools, and your previous attempt finished without calling them:\n${listed}\n\n` +
|
|
1682
|
+
`Do the step again: call each listed tool, use what it returns, then return the complete output.` +
|
|
1683
|
+
`\n\n---\n\n${instruction}`);
|
|
1684
|
+
}
|
|
1602
1685
|
/** The output fields of `nodeId` that some `{ expr }` in the workflow reads. */
|
|
1603
1686
|
function routingFieldsOf(workflow, nodeId) {
|
|
1604
1687
|
const out = new Set();
|
|
@@ -34,11 +34,12 @@ export declare const CLASSIFICATION_DISALLOWED_TOOLS: readonly ["Bash", "Write",
|
|
|
34
34
|
/**
|
|
35
35
|
* Built-in tools disallowed for a node run with `readOnly: true` (dry run,
|
|
36
36
|
* #380): everything that can edit the workspace, shell out (and so reach
|
|
37
|
-
* `gh`, `curl`, `git push`), spawn a subagent that could,
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* `gh`, `curl`, `git push`), spawn a subagent that could, fetch an arbitrary
|
|
38
|
+
* URL (a GET can still trigger a webhook or exfiltrate data) or search the web
|
|
39
|
+
* (a query is an egress channel that can carry repo data out). Read, Grep and
|
|
40
|
+
* Glob stay available for analysis.
|
|
40
41
|
*/
|
|
41
|
-
export declare const READ_ONLY_DISALLOWED_TOOLS: readonly ["Bash", "Write", "Edit", "MultiEdit", "NotebookEdit", "Task", "Agent", "WebFetch"];
|
|
42
|
+
export declare const READ_ONLY_DISALLOWED_TOOLS: readonly ["Bash", "Write", "Edit", "MultiEdit", "NotebookEdit", "Task", "Agent", "WebFetch", "WebSearch"];
|
|
42
43
|
/**
|
|
43
44
|
* Claude Code built-in tool names per portable tool class. A node's
|
|
44
45
|
* `tools.deny` classes compile to these names on `disallowedTools`.
|
|
@@ -71,6 +72,40 @@ export declare function claudeReadDenyRule(absPath: string): string;
|
|
|
71
72
|
export declare function claudeStateDirDenyRules(root?: string): string[];
|
|
72
73
|
/** Native `disallowedTools` for a policy: legacy names, compiled classes, and the read-only set. */
|
|
73
74
|
export declare function compileClaudeCodeDeny(policy: NodePolicy, legacy?: readonly string[]): string[];
|
|
75
|
+
/**
|
|
76
|
+
* The only Claude Code built-in tools a node may get, by policy. Every node
|
|
77
|
+
* reads and searches the checkout, and a read-only node gets nothing else: no
|
|
78
|
+
* web search or fetch, since either can carry repo data out. A write node adds
|
|
79
|
+
* the shell, file-edit and web built-ins (its `tools.deny` classes and
|
|
80
|
+
* `disallowed_tools` names then leave the list). No node gets the subagent
|
|
81
|
+
* tools (Task, Agent) by default: nothing shows a subagent is held to its
|
|
82
|
+
* parent's `tools`, so the default must not depend on it.
|
|
83
|
+
*
|
|
84
|
+
* Passed as the SDK's `tools` option, which "Specif[ies] the base set of
|
|
85
|
+
* available built-in tools" (sdk.d.ts); `allowedTools` only auto-approves
|
|
86
|
+
* ("To restrict which tools are available, use the `tools` option instead").
|
|
87
|
+
* Anything not named here never reaches a node: agent-to-agent messaging
|
|
88
|
+
* (SendMessage, ListAgents), scheduling (Cron*, ScheduleWakeup), remote
|
|
89
|
+
* triggers, notifications, worktrees, design sync, Skill, Workflow and
|
|
90
|
+
* ToolSearch (added back only while a server defers its tools, see
|
|
91
|
+
* {@link runQuery}). Dogfood run 42890468912 listed 23 tools for a read-only
|
|
92
|
+
* node that needed two.
|
|
93
|
+
*/
|
|
94
|
+
export declare const CLAUDE_READ_ONLY_BUILTIN_TOOLS: readonly ["Read", "Grep", "Glob"];
|
|
95
|
+
export declare const CLAUDE_WRITE_BUILTIN_TOOLS: readonly ["Read", "Grep", "Glob", ...string[]];
|
|
96
|
+
/**
|
|
97
|
+
* The built-in allow-list for one node: the policy's list minus every name the
|
|
98
|
+
* compiled deny list removes. Path-scoped rules (`Read(//dir/**)`) narrow a
|
|
99
|
+
* tool, they do not remove it, so they are not matched here.
|
|
100
|
+
*/
|
|
101
|
+
export declare function claudeCodeBuiltinTools(policy: {
|
|
102
|
+
readOnly: boolean;
|
|
103
|
+
}, deny?: readonly string[]): string[];
|
|
104
|
+
/**
|
|
105
|
+
* Env vars the harness itself sets on the agent (not passed through from the
|
|
106
|
+
* parent): `ENABLE_TOOL_SEARCH`, see {@link ClaudeCodeHarness} runQuery.
|
|
107
|
+
*/
|
|
108
|
+
export declare const CLAUDE_CODE_HARNESS_VARS: readonly ["ENABLE_TOOL_SEARCH"];
|
|
74
109
|
/** How sweny resolves which credentials reach the Claude Code subprocess. */
|
|
75
110
|
export type SwenyAuthMode = "auto" | "api-key" | "oauth";
|
|
76
111
|
export interface ResolveAuthEnvOpts {
|
|
@@ -68,9 +68,10 @@ export const CLASSIFICATION_DISALLOWED_TOOLS = [
|
|
|
68
68
|
/**
|
|
69
69
|
* Built-in tools disallowed for a node run with `readOnly: true` (dry run,
|
|
70
70
|
* #380): everything that can edit the workspace, shell out (and so reach
|
|
71
|
-
* `gh`, `curl`, `git push`), spawn a subagent that could,
|
|
72
|
-
*
|
|
73
|
-
*
|
|
71
|
+
* `gh`, `curl`, `git push`), spawn a subagent that could, fetch an arbitrary
|
|
72
|
+
* URL (a GET can still trigger a webhook or exfiltrate data) or search the web
|
|
73
|
+
* (a query is an egress channel that can carry repo data out). Read, Grep and
|
|
74
|
+
* Glob stay available for analysis.
|
|
74
75
|
*/
|
|
75
76
|
export const READ_ONLY_DISALLOWED_TOOLS = [
|
|
76
77
|
"Bash",
|
|
@@ -81,6 +82,7 @@ export const READ_ONLY_DISALLOWED_TOOLS = [
|
|
|
81
82
|
"Task",
|
|
82
83
|
"Agent",
|
|
83
84
|
"WebFetch",
|
|
85
|
+
"WebSearch",
|
|
84
86
|
];
|
|
85
87
|
/**
|
|
86
88
|
* Claude Code built-in tool names per portable tool class. A node's
|
|
@@ -146,6 +148,55 @@ export function compileClaudeCodeDeny(policy, legacy = []) {
|
|
|
146
148
|
]),
|
|
147
149
|
];
|
|
148
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* The only Claude Code built-in tools a node may get, by policy. Every node
|
|
153
|
+
* reads and searches the checkout, and a read-only node gets nothing else: no
|
|
154
|
+
* web search or fetch, since either can carry repo data out. A write node adds
|
|
155
|
+
* the shell, file-edit and web built-ins (its `tools.deny` classes and
|
|
156
|
+
* `disallowed_tools` names then leave the list). No node gets the subagent
|
|
157
|
+
* tools (Task, Agent) by default: nothing shows a subagent is held to its
|
|
158
|
+
* parent's `tools`, so the default must not depend on it.
|
|
159
|
+
*
|
|
160
|
+
* Passed as the SDK's `tools` option, which "Specif[ies] the base set of
|
|
161
|
+
* available built-in tools" (sdk.d.ts); `allowedTools` only auto-approves
|
|
162
|
+
* ("To restrict which tools are available, use the `tools` option instead").
|
|
163
|
+
* Anything not named here never reaches a node: agent-to-agent messaging
|
|
164
|
+
* (SendMessage, ListAgents), scheduling (Cron*, ScheduleWakeup), remote
|
|
165
|
+
* triggers, notifications, worktrees, design sync, Skill, Workflow and
|
|
166
|
+
* ToolSearch (added back only while a server defers its tools, see
|
|
167
|
+
* {@link runQuery}). Dogfood run 42890468912 listed 23 tools for a read-only
|
|
168
|
+
* node that needed two.
|
|
169
|
+
*/
|
|
170
|
+
export const CLAUDE_READ_ONLY_BUILTIN_TOOLS = ["Read", "Grep", "Glob"];
|
|
171
|
+
export const CLAUDE_WRITE_BUILTIN_TOOLS = [
|
|
172
|
+
"Read",
|
|
173
|
+
"Grep",
|
|
174
|
+
"Glob",
|
|
175
|
+
...CLAUDE_TOOLS_BY_CLASS.shell,
|
|
176
|
+
...CLAUDE_TOOLS_BY_CLASS.write,
|
|
177
|
+
...CLAUDE_TOOLS_BY_CLASS.edit,
|
|
178
|
+
...CLAUDE_TOOLS_BY_CLASS.net,
|
|
179
|
+
];
|
|
180
|
+
/**
|
|
181
|
+
* The built-in allow-list for one node: the policy's list minus every name the
|
|
182
|
+
* compiled deny list removes. Path-scoped rules (`Read(//dir/**)`) narrow a
|
|
183
|
+
* tool, they do not remove it, so they are not matched here.
|
|
184
|
+
*/
|
|
185
|
+
export function claudeCodeBuiltinTools(policy, deny = []) {
|
|
186
|
+
const denied = new Set(deny);
|
|
187
|
+
const base = policy.readOnly ? CLAUDE_READ_ONLY_BUILTIN_TOOLS : CLAUDE_WRITE_BUILTIN_TOOLS;
|
|
188
|
+
return base.filter((t) => !denied.has(t));
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Env vars the harness itself sets on the agent (not passed through from the
|
|
192
|
+
* parent): `ENABLE_TOOL_SEARCH`, see {@link ClaudeCodeHarness} runQuery.
|
|
193
|
+
*/
|
|
194
|
+
export const CLAUDE_CODE_HARNESS_VARS = ["ENABLE_TOOL_SEARCH"];
|
|
195
|
+
/** CI: a truthy `CI` (not "", "0", "false", "no", "off"), or GitHub Actions itself. */
|
|
196
|
+
function runningInCi(env = process.env) {
|
|
197
|
+
const ci = (env.CI ?? "").trim().toLowerCase();
|
|
198
|
+
return env.GITHUB_ACTIONS === "true" || !["", "0", "false", "no", "off"].includes(ci);
|
|
199
|
+
}
|
|
149
200
|
/**
|
|
150
201
|
* Decide which auth credentials survive into the Claude Code subprocess env.
|
|
151
202
|
*
|
|
@@ -562,6 +613,8 @@ export class ClaudeCodeHarness {
|
|
|
562
613
|
// Fail closed: a stream that ends with no terminal `result` message
|
|
563
614
|
// (subprocess died, stream truncated) is not a success.
|
|
564
615
|
let sawResult = false;
|
|
616
|
+
// The tool names the agent was given (init message), for the executor's repair rule.
|
|
617
|
+
let agentTools;
|
|
565
618
|
// Timeout / abort wiring (back-compat: only armed when requested).
|
|
566
619
|
// A single AbortController drives both an optional caller signal and an
|
|
567
620
|
// optional timeout timer. The SDK aborts the subprocess when this fires.
|
|
@@ -596,6 +649,20 @@ export class ClaudeCodeHarness {
|
|
|
596
649
|
else if (sdkTools.length > 0) {
|
|
597
650
|
allMcpServers["sweny-core"] = mcpServer;
|
|
598
651
|
}
|
|
652
|
+
// Deterministic tool loading: when every server loads its tools up front
|
|
653
|
+
// (sweny-core always does; a node's skill servers do unless they opt out),
|
|
654
|
+
// tool search has nothing to defer, so it is off and ToolSearch is not on
|
|
655
|
+
// the list. `ENABLE_TOOL_SEARCH=false` is the bundled CLI's switch (its
|
|
656
|
+
// own docs: "To turn it off ... set `ENABLE_TOOL_SEARCH` to `false`"; CLI
|
|
657
|
+
// 0.3.319 drops ToolSearch from the init tool list with it). A client-level
|
|
658
|
+
// server not marked alwaysLoad keeps tool search, and ToolSearch, so its
|
|
659
|
+
// deferred tools stay reachable.
|
|
660
|
+
const defers = Object.entries(allMcpServers).some(([name, cfg]) => name !== "sweny-core" && cfg?.alwaysLoad !== true);
|
|
661
|
+
if (!defers)
|
|
662
|
+
env.ENABLE_TOOL_SEARCH = "false";
|
|
663
|
+
const builtinTools = opts.nativeNone
|
|
664
|
+
? []
|
|
665
|
+
: [...claudeCodeBuiltinTools({ readOnly: !!readOnly }, disallowedTools), ...(defers ? ["ToolSearch"] : [])];
|
|
599
666
|
stream = query({
|
|
600
667
|
prompt,
|
|
601
668
|
options: {
|
|
@@ -622,10 +689,13 @@ export class ClaudeCodeHarness {
|
|
|
622
689
|
strictMcpConfig: true,
|
|
623
690
|
settingSources: [],
|
|
624
691
|
...(disallowedTools && disallowedTools.length > 0 ? { disallowedTools } : {}),
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
|
|
692
|
+
// Least privilege: only the built-ins this node's policy names (see
|
|
693
|
+
// CLAUDE_WRITE_BUILTIN_TOOLS). permissions.native: none passes `tools: []`,
|
|
694
|
+
// which removes every built-in tool whatever its name.
|
|
695
|
+
tools: builtinTools,
|
|
696
|
+
// `settingSources: []` loads no user, project or local settings, CLAUDE.md or skills
|
|
697
|
+
// (already the default above since #497; kept so native: none states its whole contract).
|
|
698
|
+
...(opts.nativeNone ? { settingSources: [] } : {}),
|
|
629
699
|
// CC-08: ask the SDK to produce validated structured output when the
|
|
630
700
|
// node declares an output schema. The SDK then returns the parsed
|
|
631
701
|
// value on `result.structured_output`, which we prefer over the
|
|
@@ -635,11 +705,27 @@ export class ClaudeCodeHarness {
|
|
|
635
705
|
});
|
|
636
706
|
for await (const message of stream) {
|
|
637
707
|
if (message.type === "system" && message.subtype === "init") {
|
|
638
|
-
// What the agent was actually given: the tool
|
|
639
|
-
// loudly, a node tool server that did not
|
|
708
|
+
// What the agent was actually given: the tool names (never inputs)
|
|
709
|
+
// and, loudly, a node tool or tool server that did not make it onto
|
|
710
|
+
// the list. In CI the names go out as one warning-level line, the
|
|
711
|
+
// level the default (non-verbose) run log shows, so a failed node is
|
|
712
|
+
// diagnosable without a verbose rerun; locally they stay at info.
|
|
640
713
|
const init = message;
|
|
641
|
-
if (Array.isArray(init.tools))
|
|
642
|
-
|
|
714
|
+
if (Array.isArray(init.tools)) {
|
|
715
|
+
agentTools = init.tools.filter((t) => typeof t === "string");
|
|
716
|
+
const line = ` agent tools: ${init.tools.join(", ")}`;
|
|
717
|
+
if (runningInCi())
|
|
718
|
+
this.logger.warn(line);
|
|
719
|
+
else
|
|
720
|
+
this.logger.info(line);
|
|
721
|
+
const listed = new Set(init.tools);
|
|
722
|
+
const absent = allMcpServers["sweny-core"]
|
|
723
|
+
? tools.map((t) => t.name).filter((n) => !listed.has(`mcp__sweny-core__${n}`))
|
|
724
|
+
: [];
|
|
725
|
+
if (absent.length > 0) {
|
|
726
|
+
this.logger.warn(` node tools missing from the agent's tool list: ${absent.join(", ")}`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
643
729
|
const core = Array.isArray(init.mcp_servers)
|
|
644
730
|
? init.mcp_servers.find((s) => s?.name === "sweny-core")
|
|
645
731
|
: undefined;
|
|
@@ -833,6 +919,7 @@ export class ClaudeCodeHarness {
|
|
|
833
919
|
data: { summary: response, ...parsedData },
|
|
834
920
|
toolCalls,
|
|
835
921
|
...(usage ? { usage } : {}),
|
|
922
|
+
...(agentTools ? { agentTools } : {}),
|
|
836
923
|
};
|
|
837
924
|
}
|
|
838
925
|
/**
|
package/dist/harness/types.d.ts
CHANGED
|
@@ -152,6 +152,14 @@ export interface HarnessRunRequest extends ClaudeRunRequest {
|
|
|
152
152
|
export interface HarnessRunResult extends NodeResult {
|
|
153
153
|
harness: HarnessInfo;
|
|
154
154
|
degraded: string[];
|
|
155
|
+
/**
|
|
156
|
+
* Names of every tool the agent was given for this run, as the agent itself
|
|
157
|
+
* listed them (Claude Code: the init message; MCP tools as
|
|
158
|
+
* `mcp__<server>__<tool>`). The executor repairs a skipped required tool
|
|
159
|
+
* only when it is on this list. Absent when the harness does not report it,
|
|
160
|
+
* and then no such repair happens. Never journaled.
|
|
161
|
+
*/
|
|
162
|
+
agentTools?: string[];
|
|
155
163
|
}
|
|
156
164
|
export interface HarnessCompleteRequest {
|
|
157
165
|
prompt: string;
|
package/dist/schema.d.ts
CHANGED
|
@@ -1035,7 +1035,7 @@ export declare function parseWorkflow(raw: unknown): {
|
|
|
1035
1035
|
} | undefined;
|
|
1036
1036
|
};
|
|
1037
1037
|
export interface WorkflowError {
|
|
1038
|
-
code: "MISSING_ENTRY" | "UNKNOWN_EDGE_SOURCE" | "UNKNOWN_EDGE_TARGET" | "UNREACHABLE_NODE" | "UNKNOWN_SKILL" | "SELF_LOOP" | "UNBOUNDED_CYCLE" | "AMBIGUOUS_EDGES" | "UNSUPPORTED_EVAL_POLICY" | "INVALID_INLINE_SKILL" | "EDGE_ITERATIONS_EXCEEDED" | "RETRY_MAX_EXCEEDED" | "PERMISSION_CEILING" | "AGENT_ENV_READ_ONLY" | "BUDGET_CEILING" | "OUTPUT_NOT_ALLOWED" | "DUPLICATE_OUTPUT" | "UNSUPPORTED_OUTPUT" | "INVALID_WHEN_EXPRESSION" | "MIXED_EDGE_CONDITIONS" | "DUPLICATE_EDGE";
|
|
1038
|
+
code: "MISSING_ENTRY" | "UNKNOWN_EDGE_SOURCE" | "UNKNOWN_EDGE_TARGET" | "UNREACHABLE_NODE" | "UNKNOWN_SKILL" | "SELF_LOOP" | "UNBOUNDED_CYCLE" | "AMBIGUOUS_EDGES" | "UNSUPPORTED_EVAL_POLICY" | "DUPLICATE_EVALUATOR" | "INVALID_INLINE_SKILL" | "EDGE_ITERATIONS_EXCEEDED" | "RETRY_MAX_EXCEEDED" | "PERMISSION_CEILING" | "AGENT_ENV_READ_ONLY" | "BUDGET_CEILING" | "OUTPUT_NOT_ALLOWED" | "DUPLICATE_OUTPUT" | "UNSUPPORTED_OUTPUT" | "INVALID_WHEN_EXPRESSION" | "MIXED_EDGE_CONDITIONS" | "DUPLICATE_EDGE";
|
|
1039
1039
|
message: string;
|
|
1040
1040
|
nodeId?: string;
|
|
1041
1041
|
}
|
package/dist/schema.js
CHANGED
|
@@ -710,6 +710,22 @@ export function validateWorkflow(workflow, knownSkills) {
|
|
|
710
710
|
});
|
|
711
711
|
}
|
|
712
712
|
}
|
|
713
|
+
// Evaluator names are a node's eval keys: results, retry preambles, routes
|
|
714
|
+
// (`node.evals.<name>.pass`) and the required-tool repair look a verdict up
|
|
715
|
+
// by name, so two evaluators with one name would make that lookup ambiguous.
|
|
716
|
+
for (const [nodeId, node] of Object.entries(workflow.nodes)) {
|
|
717
|
+
const seen = new Set();
|
|
718
|
+
for (const e of node.eval ?? []) {
|
|
719
|
+
if (seen.has(e.name)) {
|
|
720
|
+
errors.push({
|
|
721
|
+
code: "DUPLICATE_EVALUATOR",
|
|
722
|
+
message: `Node "${nodeId}" declares two evaluators named "${e.name}"; evaluator names must be unique within a node`,
|
|
723
|
+
nodeId,
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
seen.add(e.name);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
713
729
|
// Inline skills need usage instructions; the engine server name is reserved.
|
|
714
730
|
for (const [skillId, def] of Object.entries(workflow.skills ?? {})) {
|
|
715
731
|
if (!def.instruction?.trim()) {
|