@sweny-ai/core 0.1.142 → 0.1.143
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 +29 -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 +23 -2
- package/dist/executor.js +20 -2
- 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 +9 -40
- package/dist/harness/claude-code.js +21 -308
- package/dist/harness/codex.d.ts +175 -0
- package/dist/harness/codex.js +917 -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 +2 -2
- package/dist/index.js +2 -2
- package/dist/schema.d.ts +12 -11
- package/dist/schema.js +7 -3
- package/dist/types.d.ts +8 -1
- package/package.json +1 -1
package/dist/harness/index.d.ts
CHANGED
|
@@ -3,17 +3,25 @@
|
|
|
3
3
|
* public harness types. Every construction site goes through `createHarness`.
|
|
4
4
|
*/
|
|
5
5
|
import { ClaudeCodeHarness, type ClaudeCodeHarnessOptions } from "./claude-code.js";
|
|
6
|
+
import { CodexHarness, type CodexHarnessOptions } from "./codex.js";
|
|
6
7
|
export { claudeCompat, asClaude } from "./compat.js";
|
|
7
8
|
export type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, HarnessId, HarnessInfo, HarnessRunRequest, HarnessRunResult, NodePolicy, PolicyGateResult, PolicyWrappers, ToolClass, } from "./types.js";
|
|
8
|
-
export { policyGate } from "./policy.js";
|
|
9
|
+
export { policyGate, nativeDenyClasses, resolveHarnessPolicy, isToolClass, TOOL_CLASSES } from "./policy.js";
|
|
10
|
+
export type { HarnessPolicyMode } from "./policy.js";
|
|
9
11
|
export { ask, evaluate, buildAskPrompt, buildEvaluatePrompt } from "./prompts.js";
|
|
10
12
|
export { ClaudeCodeHarness, CLAUDE_CODE_CAPABILITIES } from "./claude-code.js";
|
|
11
13
|
export type { ClaudeCodeHarnessOptions } from "./claude-code.js";
|
|
14
|
+
export { CodexHarness, CODEX_CAPABILITIES, MIN_CODEX_VERSION } from "./codex.js";
|
|
15
|
+
export type { CodexHarnessOptions } from "./codex.js";
|
|
12
16
|
export { startToolBridge } from "./tool-bridge/server.js";
|
|
13
17
|
export type { ToolBridge, ToolBridgeOptions } from "./tool-bridge/server.js";
|
|
18
|
+
export { SUPPORTED_AGENTS, isSupportedAgent, unsupportedAgentError } from "./agents.js";
|
|
19
|
+
export type { SupportedAgent } from "./agents.js";
|
|
14
20
|
/**
|
|
15
21
|
* Build the harness for an agent id. `claude` is the historical `--agent` /
|
|
16
22
|
* `coding-agent-provider` value and means the same as `claude-code`.
|
|
17
|
-
* Unknown ids
|
|
23
|
+
* Unknown ids throw: sweny never runs one agent under another's name.
|
|
18
24
|
*/
|
|
19
|
-
export declare function createHarness(id:
|
|
25
|
+
export declare function createHarness(id: "claude" | "claude-code", opts?: ClaudeCodeHarnessOptions): ClaudeCodeHarness;
|
|
26
|
+
export declare function createHarness(id: "codex", opts?: CodexHarnessOptions): CodexHarness;
|
|
27
|
+
export declare function createHarness(id: string, opts?: ClaudeCodeHarnessOptions & CodexHarnessOptions): ClaudeCodeHarness | CodexHarness;
|
package/dist/harness/index.js
CHANGED
|
@@ -3,23 +3,23 @@
|
|
|
3
3
|
* public harness types. Every construction site goes through `createHarness`.
|
|
4
4
|
*/
|
|
5
5
|
import { ClaudeCodeHarness } from "./claude-code.js";
|
|
6
|
+
import { CodexHarness } from "./codex.js";
|
|
7
|
+
import { unsupportedAgentError } from "./agents.js";
|
|
6
8
|
export { claudeCompat, asClaude } from "./compat.js";
|
|
7
|
-
export { policyGate } from "./policy.js";
|
|
9
|
+
export { policyGate, nativeDenyClasses, resolveHarnessPolicy, isToolClass, TOOL_CLASSES } from "./policy.js";
|
|
8
10
|
export { ask, evaluate, buildAskPrompt, buildEvaluatePrompt } from "./prompts.js";
|
|
9
11
|
export { ClaudeCodeHarness, CLAUDE_CODE_CAPABILITIES } from "./claude-code.js";
|
|
12
|
+
export { CodexHarness, CODEX_CAPABILITIES, MIN_CODEX_VERSION } from "./codex.js";
|
|
10
13
|
export { startToolBridge } from "./tool-bridge/server.js";
|
|
11
|
-
|
|
12
|
-
* Build the harness for an agent id. `claude` is the historical `--agent` /
|
|
13
|
-
* `coding-agent-provider` value and means the same as `claude-code`.
|
|
14
|
-
* Unknown ids fail with the same error `sweny` has always given.
|
|
15
|
-
*/
|
|
14
|
+
export { SUPPORTED_AGENTS, isSupportedAgent, unsupportedAgentError } from "./agents.js";
|
|
16
15
|
export function createHarness(id, opts = {}) {
|
|
17
16
|
switch (id) {
|
|
18
17
|
case "claude":
|
|
19
18
|
case "claude-code":
|
|
20
19
|
return new ClaudeCodeHarness(opts);
|
|
20
|
+
case "codex":
|
|
21
|
+
return new CodexHarness(opts);
|
|
21
22
|
default:
|
|
22
|
-
throw new Error(
|
|
23
|
-
`(headless Claude Code). Remove --agent / coding-agent-provider or set it to claude.`);
|
|
23
|
+
throw new Error(unsupportedAgentError(id));
|
|
24
24
|
}
|
|
25
25
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness-neutral parsing of agent output: tool results and the final JSON
|
|
3
|
+
* answer. Shared by every adapter so a node's `data` and `toolCalls` mean the
|
|
4
|
+
* same thing whichever harness ran it. Moved out of claude-code.ts unchanged
|
|
5
|
+
* (the contract suite's structured-output golden cases pin the behavior).
|
|
6
|
+
*
|
|
7
|
+
* No agent SDK imports.
|
|
8
|
+
*/
|
|
9
|
+
import type { JSONSchema, Logger } from "../types.js";
|
|
10
|
+
/**
|
|
11
|
+
* Best-effort recovery of typed output from a tool_result's `content`.
|
|
12
|
+
*
|
|
13
|
+
* The MCP protocol sends tool results as string content. Our in-process
|
|
14
|
+
* wrapper JSON-stringifies structured output before returning; external
|
|
15
|
+
* MCP servers generally do the same for JSON payloads. If the string
|
|
16
|
+
* looks like a JSON object or array and parses, return the parsed value
|
|
17
|
+
* so verify's output-path walks work against typed data.
|
|
18
|
+
*
|
|
19
|
+
* Raw strings are preserved verbatim. JSON-primitive strings (e.g. the
|
|
20
|
+
* literal four characters `"42"`) are intentionally NOT parsed - we
|
|
21
|
+
* cannot distinguish a tool that returned the number 42 (wrapper sends
|
|
22
|
+
* `"42"`) from a tool that returned the string "42" (wrapper also sends
|
|
23
|
+
* `"42"`). Preserving the string is safer than guessing.
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseToolResultContent(content: unknown): unknown;
|
|
26
|
+
/**
|
|
27
|
+
* Produce a short, single-line description of a tool-error payload suitable
|
|
28
|
+
* for the CI log stream. The full parsed value stays on the ToolCall for
|
|
29
|
+
* verify and downstream tooling - this is only for inline observability.
|
|
30
|
+
*
|
|
31
|
+
* Collapses newlines, trims whitespace, and caps to 300 chars so a huge
|
|
32
|
+
* API response body doesn't flood the log.
|
|
33
|
+
*/
|
|
34
|
+
export declare function summarizeToolError(parsed: unknown): string;
|
|
35
|
+
/**
|
|
36
|
+
* Extract a JSON object from an agent's final text response.
|
|
37
|
+
*
|
|
38
|
+
* Strategy (in order):
|
|
39
|
+
* 1. LAST ```json code block``` - models put their real final answer last, so
|
|
40
|
+
* a prompt-injected fake fenced block placed earlier must not win.
|
|
41
|
+
* 2. Last brace-delimited `{...}` block - handles inline JSON at end of text
|
|
42
|
+
* 3. Full text parse - for responses that are pure JSON
|
|
43
|
+
* 4. Empty object - safe fallback
|
|
44
|
+
*
|
|
45
|
+
* When `outputSchema` is supplied, the parsed object is checked against it.
|
|
46
|
+
* A mismatch is logged (warning) rather than silently accepted, so a
|
|
47
|
+
* non-conforming model answer is at least attributable.
|
|
48
|
+
*/
|
|
49
|
+
export declare function tryParseJSON(text: string, outputSchema?: JSONSchema, logger?: Pick<Logger, "warn">, label?: string): Record<string, unknown>;
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness-neutral parsing of agent output: tool results and the final JSON
|
|
3
|
+
* answer. Shared by every adapter so a node's `data` and `toolCalls` mean the
|
|
4
|
+
* same thing whichever harness ran it. Moved out of claude-code.ts unchanged
|
|
5
|
+
* (the contract suite's structured-output golden cases pin the behavior).
|
|
6
|
+
*
|
|
7
|
+
* No agent SDK imports.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Best-effort recovery of typed output from a tool_result's `content`.
|
|
11
|
+
*
|
|
12
|
+
* The MCP protocol sends tool results as string content. Our in-process
|
|
13
|
+
* wrapper JSON-stringifies structured output before returning; external
|
|
14
|
+
* MCP servers generally do the same for JSON payloads. If the string
|
|
15
|
+
* looks like a JSON object or array and parses, return the parsed value
|
|
16
|
+
* so verify's output-path walks work against typed data.
|
|
17
|
+
*
|
|
18
|
+
* Raw strings are preserved verbatim. JSON-primitive strings (e.g. the
|
|
19
|
+
* literal four characters `"42"`) are intentionally NOT parsed - we
|
|
20
|
+
* cannot distinguish a tool that returned the number 42 (wrapper sends
|
|
21
|
+
* `"42"`) from a tool that returned the string "42" (wrapper also sends
|
|
22
|
+
* `"42"`). Preserving the string is safer than guessing.
|
|
23
|
+
*/
|
|
24
|
+
export function parseToolResultContent(content) {
|
|
25
|
+
// The Anthropic tool_result `content` field can be a block array
|
|
26
|
+
// (e.g. [{type:"text",text:"..."}, ...]) rather than a string. When it is,
|
|
27
|
+
// concatenate the text of every {type:"text"} block (recursing through any
|
|
28
|
+
// nested content), then run the same object/array JSON-parse logic on the
|
|
29
|
+
// joined string. This keeps `call.output` the actual payload instead of the
|
|
30
|
+
// raw wrapper array that eval/route logic would otherwise have to walk.
|
|
31
|
+
if (Array.isArray(content)) {
|
|
32
|
+
const text = collectBlockText(content);
|
|
33
|
+
return parseJsonObjectOrArray(text);
|
|
34
|
+
}
|
|
35
|
+
if (typeof content !== "string")
|
|
36
|
+
return content;
|
|
37
|
+
return parseJsonObjectOrArray(content);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Parse a string only when it is an unambiguous JSON object or array.
|
|
41
|
+
*
|
|
42
|
+
* Strings, numbers, booleans, and null would all corrupt or discard
|
|
43
|
+
* information (we cannot distinguish the literal text `"42"` from the number
|
|
44
|
+
* 42), so non-object/array inputs are preserved verbatim.
|
|
45
|
+
*/
|
|
46
|
+
function parseJsonObjectOrArray(content) {
|
|
47
|
+
const trimmed = content.trim();
|
|
48
|
+
if (trimmed.length === 0)
|
|
49
|
+
return content;
|
|
50
|
+
const first = trimmed[0];
|
|
51
|
+
if (first !== "{" && first !== "[")
|
|
52
|
+
return content;
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(trimmed);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return content;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Concatenate the text of an array of content blocks. Handles {type:"text"}
|
|
62
|
+
* blocks and recurses into any nested `content` array so deeply-wrapped tool
|
|
63
|
+
* results still flatten to their underlying text.
|
|
64
|
+
*/
|
|
65
|
+
function collectBlockText(blocks) {
|
|
66
|
+
const parts = [];
|
|
67
|
+
for (const block of blocks) {
|
|
68
|
+
if (typeof block === "string") {
|
|
69
|
+
parts.push(block);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!block || typeof block !== "object")
|
|
73
|
+
continue;
|
|
74
|
+
const b = block;
|
|
75
|
+
if (b.type === "text" && typeof b.text === "string") {
|
|
76
|
+
parts.push(b.text);
|
|
77
|
+
}
|
|
78
|
+
else if (Array.isArray(b.content)) {
|
|
79
|
+
parts.push(collectBlockText(b.content));
|
|
80
|
+
}
|
|
81
|
+
else if (typeof b.text === "string") {
|
|
82
|
+
// Some producers omit an explicit type but still carry text.
|
|
83
|
+
parts.push(b.text);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return parts.join("");
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Produce a short, single-line description of a tool-error payload suitable
|
|
90
|
+
* for the CI log stream. The full parsed value stays on the ToolCall for
|
|
91
|
+
* verify and downstream tooling - this is only for inline observability.
|
|
92
|
+
*
|
|
93
|
+
* Collapses newlines, trims whitespace, and caps to 300 chars so a huge
|
|
94
|
+
* API response body doesn't flood the log.
|
|
95
|
+
*/
|
|
96
|
+
export function summarizeToolError(parsed) {
|
|
97
|
+
let raw;
|
|
98
|
+
if (typeof parsed === "string") {
|
|
99
|
+
raw = parsed;
|
|
100
|
+
}
|
|
101
|
+
else if (parsed && typeof parsed === "object") {
|
|
102
|
+
try {
|
|
103
|
+
raw = JSON.stringify(parsed);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
raw = String(parsed);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
raw = String(parsed);
|
|
111
|
+
}
|
|
112
|
+
const collapsed = raw.replace(/\s+/g, " ").trim();
|
|
113
|
+
return collapsed.length > 300 ? collapsed.slice(0, 297) + "..." : collapsed;
|
|
114
|
+
}
|
|
115
|
+
// ─── JSON extraction ────────────────────────────────────────────
|
|
116
|
+
/**
|
|
117
|
+
* Extract a JSON object from an agent's final text response.
|
|
118
|
+
*
|
|
119
|
+
* Strategy (in order):
|
|
120
|
+
* 1. LAST ```json code block``` - models put their real final answer last, so
|
|
121
|
+
* a prompt-injected fake fenced block placed earlier must not win.
|
|
122
|
+
* 2. Last brace-delimited `{...}` block - handles inline JSON at end of text
|
|
123
|
+
* 3. Full text parse - for responses that are pure JSON
|
|
124
|
+
* 4. Empty object - safe fallback
|
|
125
|
+
*
|
|
126
|
+
* When `outputSchema` is supplied, the parsed object is checked against it.
|
|
127
|
+
* A mismatch is logged (warning) rather than silently accepted, so a
|
|
128
|
+
* non-conforming model answer is at least attributable.
|
|
129
|
+
*/
|
|
130
|
+
export function tryParseJSON(text, outputSchema, logger, label = "Claude") {
|
|
131
|
+
if (!text)
|
|
132
|
+
return {};
|
|
133
|
+
const finalize = (parsed) => {
|
|
134
|
+
if (outputSchema) {
|
|
135
|
+
const problems = schemaMismatches(parsed, outputSchema);
|
|
136
|
+
if (problems.length > 0) {
|
|
137
|
+
logger?.warn?.(`${label} output did not conform to outputSchema: ${problems.join("; ")}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return parsed;
|
|
141
|
+
};
|
|
142
|
+
// 1. Code block - scan ALL fenced blocks, prefer the LAST that parses.
|
|
143
|
+
const fences = [...text.matchAll(/```(?:json)?\s*([\s\S]*?)```/g)];
|
|
144
|
+
for (let i = fences.length - 1; i >= 0; i--) {
|
|
145
|
+
try {
|
|
146
|
+
const parsed = JSON.parse(fences[i][1].trim());
|
|
147
|
+
if (typeof parsed === "object" && parsed !== null)
|
|
148
|
+
return finalize(parsed);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
/* try the next-earlier fence, then fall through to brace scan */
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// 2. Last brace-delimited block (scan backwards for matching braces)
|
|
155
|
+
const lastBrace = text.lastIndexOf("}");
|
|
156
|
+
if (lastBrace !== -1) {
|
|
157
|
+
let depth = 0;
|
|
158
|
+
for (let i = lastBrace; i >= 0; i--) {
|
|
159
|
+
if (text[i] === "}")
|
|
160
|
+
depth++;
|
|
161
|
+
else if (text[i] === "{")
|
|
162
|
+
depth--;
|
|
163
|
+
if (depth === 0) {
|
|
164
|
+
try {
|
|
165
|
+
const parsed = JSON.parse(text.slice(i, lastBrace + 1));
|
|
166
|
+
if (typeof parsed === "object" && parsed !== null)
|
|
167
|
+
return finalize(parsed);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
/* try next strategy */
|
|
171
|
+
}
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// 3. Full text parse
|
|
177
|
+
try {
|
|
178
|
+
const parsed = JSON.parse(text.trim());
|
|
179
|
+
if (typeof parsed === "object" && parsed !== null)
|
|
180
|
+
return finalize(parsed);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
/* fall through */
|
|
184
|
+
}
|
|
185
|
+
return {};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Lightweight structural check of a parsed object against a JSON Schema.
|
|
189
|
+
*
|
|
190
|
+
* Deliberately shallow: it verifies `required` keys are present and that the
|
|
191
|
+
* top-level `type` and each declared property `type` match. It does NOT do
|
|
192
|
+
* full JSON Schema validation (no `$ref`, `oneOf`, formats, nested arrays).
|
|
193
|
+
* The contract here is "flag an obviously non-conforming object" so a mismatch
|
|
194
|
+
* is logged rather than silently accepted; it is not a validation gate.
|
|
195
|
+
*
|
|
196
|
+
* Returns a list of human-readable problems; empty means no detected mismatch.
|
|
197
|
+
*/
|
|
198
|
+
function schemaMismatches(value, schema) {
|
|
199
|
+
const problems = [];
|
|
200
|
+
const s = schema;
|
|
201
|
+
if (s.type === "object" || s.properties || s.required) {
|
|
202
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
203
|
+
return [`expected an object, got ${Array.isArray(value) ? "array" : typeof value}`];
|
|
204
|
+
}
|
|
205
|
+
const obj = value;
|
|
206
|
+
const required = Array.isArray(s.required) ? s.required : [];
|
|
207
|
+
for (const key of required) {
|
|
208
|
+
if (!(key in obj) || obj[key] === undefined) {
|
|
209
|
+
problems.push(`missing required property "${key}"`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const props = s.properties ?? {};
|
|
213
|
+
for (const [key, propSchema] of Object.entries(props)) {
|
|
214
|
+
if (!(key in obj) || obj[key] === undefined || obj[key] === null)
|
|
215
|
+
continue;
|
|
216
|
+
const expected = propSchema?.type;
|
|
217
|
+
if (typeof expected === "string" && !jsonTypeMatches(obj[key], expected)) {
|
|
218
|
+
problems.push(`property "${key}" expected ${expected}, got ${jsonTypeOf(obj[key])}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else if (typeof s.type === "string" && !jsonTypeMatches(value, s.type)) {
|
|
223
|
+
problems.push(`expected ${s.type}, got ${jsonTypeOf(value)}`);
|
|
224
|
+
}
|
|
225
|
+
return problems;
|
|
226
|
+
}
|
|
227
|
+
function jsonTypeOf(value) {
|
|
228
|
+
if (value === null)
|
|
229
|
+
return "null";
|
|
230
|
+
if (Array.isArray(value))
|
|
231
|
+
return "array";
|
|
232
|
+
return typeof value;
|
|
233
|
+
}
|
|
234
|
+
function jsonTypeMatches(value, expected) {
|
|
235
|
+
switch (expected) {
|
|
236
|
+
case "object":
|
|
237
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
238
|
+
case "array":
|
|
239
|
+
return Array.isArray(value);
|
|
240
|
+
case "string":
|
|
241
|
+
return typeof value === "string";
|
|
242
|
+
case "number":
|
|
243
|
+
return typeof value === "number";
|
|
244
|
+
case "integer":
|
|
245
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
246
|
+
case "boolean":
|
|
247
|
+
return typeof value === "boolean";
|
|
248
|
+
case "null":
|
|
249
|
+
return value === null;
|
|
250
|
+
default:
|
|
251
|
+
return true; // unknown/unsupported type keyword - don't flag
|
|
252
|
+
}
|
|
253
|
+
}
|
package/dist/harness/policy.d.ts
CHANGED
|
@@ -5,6 +5,28 @@
|
|
|
5
5
|
* enforced natively, so the result is always `{ degraded: [] }`. A harness
|
|
6
6
|
* without a native sandbox is covered by a process wrapper when the host has
|
|
7
7
|
* one (`wrappers`; see `prepareAgentSpawn` in sandbox-wrapper.ts).
|
|
8
|
+
*
|
|
9
|
+
* Two kinds of `degraded` entry:
|
|
10
|
+
* - unenforced: nobody enforces the opinion (the harness cannot, and no host
|
|
11
|
+
* wrapper is active). Strict mode refuses the node on these.
|
|
12
|
+
* - wrapped: sweny keeps the opinion itself, but not natively (for example a
|
|
13
|
+
* watchdog in place of a native turn limit). Reported, never refused.
|
|
14
|
+
*/
|
|
15
|
+
import type { HarnessCapabilities, NodePolicy, PolicyGateResult, PolicyWrappers, ToolClass } from "./types.js";
|
|
16
|
+
/** `strict` refuses a node whose opinions the harness cannot honor; `warn` runs it and reports `degraded`. */
|
|
17
|
+
export type HarnessPolicyMode = "strict" | "warn";
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the harness policy mode: the explicit value (`--harness-policy`),
|
|
20
|
+
* else `SWENY_HARNESS_POLICY`, else `strict` under GitHub Actions and `warn`
|
|
21
|
+
* everywhere else. An unknown value warns and uses that default.
|
|
8
22
|
*/
|
|
9
|
-
|
|
23
|
+
export declare function resolveHarnessPolicy(env: Record<string, string | undefined>, explicit?: string, logger?: {
|
|
24
|
+
warn(msg: string): void;
|
|
25
|
+
}): HarnessPolicyMode;
|
|
26
|
+
/** Every portable tool class, in a stable order. */
|
|
27
|
+
export declare const TOOL_CLASSES: readonly ToolClass[];
|
|
28
|
+
/** Is `name` a portable tool class (`tools.deny: [write]`)? */
|
|
29
|
+
export declare function isToolClass(name: string): name is ToolClass;
|
|
30
|
+
/** The tool classes a harness can deny natively. */
|
|
31
|
+
export declare function nativeDenyClasses(caps: HarnessCapabilities): ToolClass[];
|
|
10
32
|
export declare function policyGate(caps: HarnessCapabilities, policy: NodePolicy, wrappers?: PolicyWrappers): PolicyGateResult;
|
package/dist/harness/policy.js
CHANGED
|
@@ -5,37 +5,95 @@
|
|
|
5
5
|
* enforced natively, so the result is always `{ degraded: [] }`. A harness
|
|
6
6
|
* without a native sandbox is covered by a process wrapper when the host has
|
|
7
7
|
* one (`wrappers`; see `prepareAgentSpawn` in sandbox-wrapper.ts).
|
|
8
|
+
*
|
|
9
|
+
* Two kinds of `degraded` entry:
|
|
10
|
+
* - unenforced: nobody enforces the opinion (the harness cannot, and no host
|
|
11
|
+
* wrapper is active). Strict mode refuses the node on these.
|
|
12
|
+
* - wrapped: sweny keeps the opinion itself, but not natively (for example a
|
|
13
|
+
* watchdog in place of a native turn limit). Reported, never refused.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the harness policy mode: the explicit value (`--harness-policy`),
|
|
17
|
+
* else `SWENY_HARNESS_POLICY`, else `strict` under GitHub Actions and `warn`
|
|
18
|
+
* everywhere else. An unknown value warns and uses that default.
|
|
8
19
|
*/
|
|
20
|
+
export function resolveHarnessPolicy(env, explicit, logger) {
|
|
21
|
+
const fallback = env.GITHUB_ACTIONS === "true" ? "strict" : "warn";
|
|
22
|
+
const raw = (explicit ?? env.SWENY_HARNESS_POLICY ?? "").trim().toLowerCase();
|
|
23
|
+
if (raw === "strict" || raw === "warn")
|
|
24
|
+
return raw;
|
|
25
|
+
if (raw !== "")
|
|
26
|
+
logger?.warn(`harness policy "${raw}" is not one of strict|warn; using ${fallback}`);
|
|
27
|
+
return fallback;
|
|
28
|
+
}
|
|
29
|
+
/** Every portable tool class, in a stable order. */
|
|
30
|
+
export const TOOL_CLASSES = ["shell", "write", "edit", "net", "subagent"];
|
|
31
|
+
/** Is `name` a portable tool class (`tools.deny: [write]`)? */
|
|
32
|
+
export function isToolClass(name) {
|
|
33
|
+
return TOOL_CLASSES.includes(name);
|
|
34
|
+
}
|
|
35
|
+
/** The tool classes a harness can deny natively. */
|
|
36
|
+
export function nativeDenyClasses(caps) {
|
|
37
|
+
const base = caps.builtinDeny === "by-name" || caps.builtinDeny === "by-class"
|
|
38
|
+
? TOOL_CLASSES
|
|
39
|
+
: caps.builtinDeny === "shell-only"
|
|
40
|
+
? ["shell"]
|
|
41
|
+
: [];
|
|
42
|
+
const set = new Set([...base, ...(caps.denyClasses ?? [])]);
|
|
43
|
+
return TOOL_CLASSES.filter((c) => set.has(c));
|
|
44
|
+
}
|
|
9
45
|
export function policyGate(caps, policy, wrappers = {}) {
|
|
10
|
-
const
|
|
46
|
+
const unenforced = [];
|
|
47
|
+
const wrapped = [];
|
|
11
48
|
if (policy.readOnly && caps.readOnly === "none" && !wrappers.readOnlyMount) {
|
|
12
|
-
|
|
49
|
+
unenforced.push("read-only: harness cannot enforce it and no read-only mount is active");
|
|
13
50
|
}
|
|
14
51
|
if (policy.deny.length > 0) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
52
|
+
const native = nativeDenyClasses(caps);
|
|
53
|
+
const unmappable = policy.deny.filter((c) => !native.includes(c));
|
|
54
|
+
if (unmappable.length > 0) {
|
|
55
|
+
const list = unmappable.join(", ");
|
|
56
|
+
if (native.length === 0) {
|
|
57
|
+
unenforced.push(`deny [${list}]: harness cannot deny built-in tools`);
|
|
58
|
+
}
|
|
59
|
+
else if (native.length === 1 && native[0] === "shell") {
|
|
60
|
+
unenforced.push(`deny [${list}]: harness can only deny the shell tool`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
unenforced.push(`deny [${list}]: harness can only deny [${native.join(", ")}]`);
|
|
22
64
|
}
|
|
23
65
|
}
|
|
24
66
|
}
|
|
25
|
-
if ((policy.nativeDeny?.length ?? 0) > 0
|
|
26
|
-
|
|
67
|
+
if ((policy.nativeDeny?.length ?? 0) > 0) {
|
|
68
|
+
if (caps.builtinDeny === "none") {
|
|
69
|
+
unenforced.push("disallowed_tools: harness cannot deny built-in tools");
|
|
70
|
+
}
|
|
71
|
+
else if (caps.builtinDeny !== "by-name") {
|
|
72
|
+
// Only a by-name harness can take tool names verbatim. Adapters translate
|
|
73
|
+
// the names they know into classes first; what is left is unenforced.
|
|
74
|
+
unenforced.push(`disallowed_tools [${policy.nativeDeny.join(", ")}]: harness has no tools by these names`);
|
|
75
|
+
}
|
|
27
76
|
}
|
|
28
77
|
if (policy.egress.length > 0 && !caps.sandbox.network && !wrappers.egress) {
|
|
29
|
-
|
|
78
|
+
unenforced.push("egress allowlist: harness has no network sandbox and no egress wrapper is active");
|
|
30
79
|
}
|
|
31
80
|
const sandboxMode = policy.sandbox ?? "off";
|
|
32
81
|
let sandboxGap;
|
|
33
82
|
if (sandboxMode !== "off" && !(caps.sandbox.fs && caps.sandbox.network) && !wrappers.sandbox) {
|
|
34
83
|
sandboxGap = "sandbox: harness has no native fs and network sandbox and no process sandbox wrapper is available";
|
|
35
|
-
|
|
84
|
+
unenforced.push(sandboxGap);
|
|
85
|
+
}
|
|
86
|
+
if (policy.maxTurns !== undefined && caps.turnLimit !== "native") {
|
|
87
|
+
if (caps.turnLimit === "watchdog") {
|
|
88
|
+
wrapped.push(`max_turns: no native turn limit; sweny stops the run after ${policy.maxTurns} tool calls`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
unenforced.push("max_turns: harness has no turn limit and no watchdog");
|
|
92
|
+
}
|
|
36
93
|
}
|
|
37
|
-
|
|
38
|
-
|
|
94
|
+
const degraded = [...unenforced, ...wrapped];
|
|
95
|
+
if (unenforced.length > 0 && policy.strict) {
|
|
96
|
+
return { degraded, refuse: `strict policy: ${unenforced.join("; ")}` };
|
|
39
97
|
}
|
|
40
98
|
if (sandboxGap && sandboxMode === "strict") {
|
|
41
99
|
return {
|
|
@@ -4,12 +4,18 @@
|
|
|
4
4
|
* `ClaudeClient.ask` / `ClaudeClient.evaluate` built before the harness seam
|
|
5
5
|
* (#330); prompts.test.ts pins them against the old builders.
|
|
6
6
|
*/
|
|
7
|
-
import type { Claude, Logger } from "../types.js";
|
|
7
|
+
import type { Claude, JSONSchema, Logger } from "../types.js";
|
|
8
8
|
import type { AgentHarness } from "./types.js";
|
|
9
9
|
/** @internal Registers the legacy object behind a compat harness. */
|
|
10
10
|
export declare function registerLegacyClaude(harness: AgentHarness, claude: Claude): void;
|
|
11
11
|
/** @internal The legacy `Claude` behind a compat harness, if any. */
|
|
12
12
|
export declare function legacyClaudeOf(harness: AgentHarness): Claude | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* The prompt for a node run: the instruction, the context fenced as untrusted
|
|
15
|
+
* data (#360), and the output schema when the node declares one. Every
|
|
16
|
+
* adapter sends this string, so fencing holds on every harness.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildNodePrompt(instruction: string, context: Record<string, unknown>, outputSchema?: JSONSchema): string;
|
|
13
19
|
/** The `ask` prompt: the instruction, then the fenced context when there is any. */
|
|
14
20
|
export declare function buildAskPrompt(instruction: string, context: Record<string, unknown>): string;
|
|
15
21
|
/**
|
package/dist/harness/prompts.js
CHANGED
|
@@ -16,6 +16,22 @@ export function registerLegacyClaude(harness, claude) {
|
|
|
16
16
|
export function legacyClaudeOf(harness) {
|
|
17
17
|
return LEGACY.get(harness);
|
|
18
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* The prompt for a node run: the instruction, the context fenced as untrusted
|
|
21
|
+
* data (#360), and the output schema when the node declares one. Every
|
|
22
|
+
* adapter sends this string, so fencing holds on every harness.
|
|
23
|
+
*/
|
|
24
|
+
export function buildNodePrompt(instruction, context, outputSchema) {
|
|
25
|
+
return [
|
|
26
|
+
`## Instruction\n\n${instruction}`,
|
|
27
|
+
`## Context\n\n${fenceUntrustedJson(context, "context")}`,
|
|
28
|
+
outputSchema
|
|
29
|
+
? `## Required Output\n\nYou MUST end with a JSON object matching this schema:\n\`\`\`json\n${JSON.stringify(outputSchema, null, 2)}\n\`\`\``
|
|
30
|
+
: "",
|
|
31
|
+
]
|
|
32
|
+
.filter(Boolean)
|
|
33
|
+
.join("\n\n");
|
|
34
|
+
}
|
|
19
35
|
/** The `ask` prompt: the instruction, then the fenced context when there is any. */
|
|
20
36
|
export function buildAskPrompt(instruction, context) {
|
|
21
37
|
return [instruction, Object.keys(context).length > 0 ? `\nContext:\n${fenceUntrustedJson(context, "context")}` : ""]
|
package/dist/harness/types.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AgentHarness: the seam between the executor and whatever agent runs a node.
|
|
3
3
|
*
|
|
4
|
-
* Design: research/2026-09-30-sota/harness-design.md, section 1.
|
|
5
|
-
* (`ClaudeCodeHarness`)
|
|
4
|
+
* Design: research/2026-09-30-sota/harness-design.md, section 1. Adapters:
|
|
5
|
+
* Claude Code (`ClaudeCodeHarness`) and Codex (`CodexHarness`). `evaluate` and `ask` are
|
|
6
6
|
* not adapter methods: core builds the prompt and calls `complete()` (see
|
|
7
7
|
* prompts.ts), so every adapter gets routing and judging for free.
|
|
8
8
|
*
|
|
@@ -18,6 +18,12 @@ export interface HarnessCapabilities {
|
|
|
18
18
|
/** full = built-in tool calls are visible with status; skill-only = only skill tools are traced. */
|
|
19
19
|
toolTrace: "full" | "skill-only";
|
|
20
20
|
builtinDeny: "by-name" | "by-class" | "shell-only" | "none";
|
|
21
|
+
/**
|
|
22
|
+
* Tool classes the harness can deny natively on top of what `builtinDeny`
|
|
23
|
+
* implies (Codex: shell-only by name, but web search and subagents are
|
|
24
|
+
* config switches). See `nativeDenyClasses()` in policy.ts.
|
|
25
|
+
*/
|
|
26
|
+
denyClasses?: ToolClass[];
|
|
21
27
|
mcp: {
|
|
22
28
|
inject: boolean;
|
|
23
29
|
exclusive: "native" | "home-isolation" | "none";
|
|
@@ -49,6 +55,11 @@ export interface NodePolicy {
|
|
|
49
55
|
egress: string[];
|
|
50
56
|
/** Refuse instead of degrade when an opinion cannot be enforced. */
|
|
51
57
|
strict: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Turn budget for the run. Set by adapters whose `turnLimit` is not native,
|
|
60
|
+
* so the gate reports how the budget is kept (watchdog) or that it is not.
|
|
61
|
+
*/
|
|
62
|
+
maxTurns?: number;
|
|
52
63
|
/**
|
|
53
64
|
* Process sandbox mode (`SWENY_SANDBOX`, #360). `auto` and `strict` need fs
|
|
54
65
|
* and network containment: native, or a process wrapper (sandbox-wrapper.ts).
|
package/dist/harness/types.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AgentHarness: the seam between the executor and whatever agent runs a node.
|
|
3
3
|
*
|
|
4
|
-
* Design: research/2026-09-30-sota/harness-design.md, section 1.
|
|
5
|
-
* (`ClaudeCodeHarness`)
|
|
4
|
+
* Design: research/2026-09-30-sota/harness-design.md, section 1. Adapters:
|
|
5
|
+
* Claude Code (`ClaudeCodeHarness`) and Codex (`CodexHarness`). `evaluate` and `ask` are
|
|
6
6
|
* not adapter methods: core builds the prompt and calls `complete()` (see
|
|
7
7
|
* prompts.ts), so every adapter gets routing and judging for free.
|
|
8
8
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -27,8 +27,8 @@ export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRA
|
|
|
27
27
|
export { consoleLogger } from "./types.js";
|
|
28
28
|
export { execute, RouteEvaluationError } from "./executor.js";
|
|
29
29
|
export type { ExecuteOptions } from "./executor.js";
|
|
30
|
-
export { createHarness, claudeCompat, asClaude, policyGate, ClaudeCodeHarness } from "./harness/index.js";
|
|
31
|
-
export type { AgentHarness, HarnessCapabilities, HarnessId, HarnessInfo, HarnessRunRequest, HarnessRunResult, NodePolicy, PolicyGateResult, PolicyWrappers, ToolClass, ClaudeCodeHarnessOptions, } from "./harness/index.js";
|
|
30
|
+
export { createHarness, claudeCompat, asClaude, policyGate, ClaudeCodeHarness, CodexHarness, SUPPORTED_AGENTS, } from "./harness/index.js";
|
|
31
|
+
export type { AgentHarness, HarnessCapabilities, HarnessId, HarnessInfo, HarnessRunRequest, HarnessRunResult, NodePolicy, PolicyGateResult, PolicyWrappers, ToolClass, ClaudeCodeHarnessOptions, CodexHarnessOptions, } from "./harness/index.js";
|
|
32
32
|
export { ClaudeClient, resolveAuthEnv } from "./harness/claude-code.js";
|
|
33
33
|
export type { ClaudeClientOptions, SwenyAuthMode, ResolveAuthEnvOpts } from "./harness/claude-code.js";
|
|
34
34
|
export { resolveExecutionModel } from "./model.js";
|
package/dist/index.js
CHANGED
|
@@ -27,8 +27,8 @@ export { EVALUATOR_KINDS, EVAL_POLICIES, REQUIRES_ON_FAIL, NODE_ON_FAIL, MCP_TRA
|
|
|
27
27
|
export { consoleLogger } from "./types.js";
|
|
28
28
|
// Executor
|
|
29
29
|
export { execute, RouteEvaluationError } from "./executor.js";
|
|
30
|
-
// Agent harness (the seam) and its Claude Code
|
|
31
|
-
export { createHarness, claudeCompat, asClaude, policyGate, ClaudeCodeHarness } from "./harness/index.js";
|
|
30
|
+
// Agent harness (the seam) and its adapters: Claude Code and Codex
|
|
31
|
+
export { createHarness, claudeCompat, asClaude, policyGate, ClaudeCodeHarness, CodexHarness, SUPPORTED_AGENTS, } from "./harness/index.js";
|
|
32
32
|
// Claude client (back-compat: ClaudeClient is ClaudeCodeHarness, @deprecated)
|
|
33
33
|
export { ClaudeClient, resolveAuthEnv } from "./harness/claude-code.js";
|
|
34
34
|
// Execution model resolution
|