@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.
- package/dist/agent-env.d.ts +24 -0
- package/dist/agent-env.js +40 -7
- package/dist/cli/config-file.d.ts +1 -1
- package/dist/cli/config-file.js +1 -1
- package/dist/cli/config.d.ts +2 -0
- package/dist/cli/config.js +15 -7
- package/dist/cli/main.js +30 -4
- package/dist/cli/run-history.d.ts +2 -0
- package/dist/cli/run-history.js +2 -0
- package/dist/cli/run-output.d.ts +13 -2
- package/dist/cli/run-output.js +27 -2
- package/dist/executor.d.ts +12 -0
- package/dist/executor.js +127 -7
- package/dist/harness/abort.d.ts +17 -0
- package/dist/harness/abort.js +53 -0
- package/dist/harness/agents.d.ts +11 -0
- package/dist/harness/agents.js +15 -0
- package/dist/harness/capabilities.d.ts +23 -0
- package/dist/harness/capabilities.js +35 -0
- package/dist/harness/claude-code.d.ts +20 -43
- package/dist/harness/claude-code.js +58 -312
- package/dist/harness/codex.d.ts +175 -0
- package/dist/harness/codex.js +921 -0
- package/dist/harness/index.d.ts +11 -3
- package/dist/harness/index.js +8 -8
- package/dist/harness/parse.d.ts +49 -0
- package/dist/harness/parse.js +253 -0
- package/dist/harness/policy.d.ts +23 -1
- package/dist/harness/policy.js +73 -15
- package/dist/harness/prompts.d.ts +7 -1
- package/dist/harness/prompts.js +16 -0
- package/dist/harness/types.d.ts +13 -2
- package/dist/harness/types.js +2 -2
- package/dist/index.d.ts +8 -4
- package/dist/index.js +6 -3
- package/dist/node-policy.d.ts +31 -0
- package/dist/node-policy.js +42 -0
- package/dist/safe-outputs.d.ts +103 -0
- package/dist/safe-outputs.js +460 -0
- package/dist/schema.d.ts +501 -12
- package/dist/schema.js +210 -4
- package/dist/skills/github.js +18 -0
- package/dist/types.d.ts +107 -1
- package/dist/types.js +29 -0
- 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
|
-
|
|
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
|
|
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
|
|
255
|
-
...(
|
|
256
|
-
...(
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
175
|
-
*
|
|
176
|
-
*
|
|
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;
|