@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
@@ -12,13 +12,15 @@ import { query, createSdkMcpServer, tool as sdkTool } from "@anthropic-ai/claude
12
12
  import { createRequire } from "node:module";
13
13
  import { consoleLogger } from "../types.js";
14
14
  import { reportWithheldEnv, parseList, resolveEnvScope, scopeAgentEnv, resolveAgentSandbox, } from "../agent-env.js";
15
- import { fenceUntrustedJson } from "../untrusted.js";
16
15
  import { policyGate } from "./policy.js";
17
16
  import { CLAUDE_CODE_CAPABILITIES } from "./capabilities.js";
18
- import { ask as coreAsk, evaluate as coreEvaluate, buildEvaluatePrompt } from "./prompts.js";
17
+ import { ask as coreAsk, evaluate as coreEvaluate, buildEvaluatePrompt, buildNodePrompt } from "./prompts.js";
19
18
  import { jsonSchemaToZodShape, toolErrorToMcpResult, toolOutputToMcpResult } from "./tool-bridge/protocol.js";
20
19
  import { startToolBridge } from "./tool-bridge/server.js";
20
+ import { parseToolResultContent, summarizeToolError, tryParseJSON } from "./parse.js";
21
+ import { makeAbort } from "./abort.js";
21
22
  export { buildEvaluatePrompt, CLAUDE_CODE_CAPABILITIES };
23
+ export { parseToolResultContent, summarizeToolError, makeAbort };
22
24
  let cachedSdkVersion;
23
25
  /** Version of the Claude Agent SDK this adapter drives; "unknown" when it cannot be read. */
24
26
  function claudeSdkVersion() {
@@ -72,6 +74,28 @@ export const READ_ONLY_DISALLOWED_TOOLS = [
72
74
  "Agent",
73
75
  "WebFetch",
74
76
  ];
77
+ /**
78
+ * Claude Code built-in tool names per portable tool class. A node's
79
+ * `tools.deny` classes compile to these names on `disallowedTools`.
80
+ */
81
+ export const CLAUDE_TOOLS_BY_CLASS = {
82
+ shell: ["Bash"],
83
+ write: ["Write", "NotebookEdit"],
84
+ edit: ["Edit", "MultiEdit"],
85
+ net: ["WebFetch", "WebSearch"],
86
+ subagent: ["Task", "Agent"],
87
+ };
88
+ /** Native `disallowedTools` for a policy: legacy names, compiled classes, and the read-only set. */
89
+ export function compileClaudeCodeDeny(policy, legacy = []) {
90
+ return [
91
+ ...new Set([
92
+ ...legacy,
93
+ ...(policy.nativeDeny ?? []),
94
+ ...policy.deny.flatMap((c) => CLAUDE_TOOLS_BY_CLASS[c] ?? []),
95
+ ...(policy.readOnly ? READ_ONLY_DISALLOWED_TOOLS : []),
96
+ ]),
97
+ ];
98
+ }
75
99
  /**
76
100
  * Decide which auth credentials survive into the Claude Code subprocess env.
77
101
  *
@@ -131,56 +155,6 @@ export function resolveAuthEnv(env, opts = {}) {
131
155
  opts.logger?.debug?.(`[sweny] auth mode: ${mode}`);
132
156
  return out;
133
157
  }
134
- /**
135
- * Wire an optional timeout + caller signal onto a single AbortController.
136
- *
137
- * Returns undefined when neither a timeout nor a signal is supplied so the
138
- * default code path (no abortController, no timer) is byte-for-byte unchanged.
139
- *
140
- * `reason()` reports whether the abort came from the timeout timer or an
141
- * external signal, so callers can log a distinct timeout message.
142
- */
143
- export function makeAbort(timeoutMs, signal) {
144
- if (!timeoutMs && !signal)
145
- return undefined;
146
- const controller = new AbortController();
147
- let reason;
148
- let timer;
149
- const onSignalAbort = () => {
150
- if (controller.signal.aborted)
151
- return;
152
- reason = "signal";
153
- controller.abort();
154
- };
155
- if (signal) {
156
- if (signal.aborted) {
157
- reason = "signal";
158
- controller.abort();
159
- }
160
- else {
161
- signal.addEventListener("abort", onSignalAbort, { once: true });
162
- }
163
- }
164
- if (timeoutMs && timeoutMs > 0 && !controller.signal.aborted) {
165
- timer = setTimeout(() => {
166
- if (controller.signal.aborted)
167
- return;
168
- reason = "timeout";
169
- controller.abort();
170
- }, timeoutMs);
171
- // Don't keep the event loop alive just for the abort timer.
172
- timer?.unref?.();
173
- }
174
- return {
175
- controller,
176
- reason: () => reason,
177
- clear: () => {
178
- if (timer)
179
- clearTimeout(timer);
180
- signal?.removeEventListener("abort", onSignalAbort);
181
- },
182
- };
183
- }
184
158
  /**
185
159
  * Best-effort interrupt of an SDK query stream. Called from `finally` so an
186
160
  * early return / timeout / throw does not leak the underlying subprocess.
@@ -290,20 +264,44 @@ export class ClaudeCodeHarness {
290
264
  return { ok: true, version: claudeSdkVersion() };
291
265
  }
292
266
  /**
293
- * Run a node. The query itself is unchanged by the harness seam; this only
294
- * runs {@link policyGate} (always `degraded: []` for Claude Code) and tags
295
- * the result with the harness id and version.
267
+ * Run a node. Runs {@link policyGate} (always `degraded: []` for Claude
268
+ * Code), compiles the policy to native options, and tags the result with the
269
+ * harness id and version.
270
+ *
271
+ * Policy compile (#365): `policy.readOnly` (or the legacy `readOnly` flag)
272
+ * is a read-only run; `policy.deny` classes become native `disallowedTools`
273
+ * names, merged with `nativeDeny` and the legacy `disallowedTools`; and
274
+ * `policy.strict` makes MCP exclusive (`strictMcpConfig`) even for a
275
+ * write-capable node.
296
276
  */
297
277
  async run(req) {
298
278
  const policy = req.policy ?? {
299
279
  readOnly: !!req.readOnly,
300
- deny: [],
280
+ deny: req.deny ?? [],
301
281
  nativeDeny: req.disallowedTools,
302
282
  egress: req.agentAccess?.domains ?? [],
303
283
  strict: false,
304
284
  };
305
285
  const gate = policyGate(this.capabilities, policy);
306
- const result = await this.runQuery(req);
286
+ if (gate.refuse) {
287
+ this.logger.error(gate.refuse);
288
+ return {
289
+ status: "failed",
290
+ // `refused` keeps fail_soft from softening a policy refusal (executor.ts).
291
+ data: { error: gate.refuse, refused: true },
292
+ toolCalls: [],
293
+ harness: this.info(),
294
+ degraded: gate.degraded,
295
+ };
296
+ }
297
+ const readOnly = !!req.readOnly || policy.readOnly;
298
+ const disallowedTools = compileClaudeCodeDeny(policy, req.disallowedTools ?? []);
299
+ const result = await this.runQuery({
300
+ ...req,
301
+ readOnly,
302
+ disallowedTools: disallowedTools.length > 0 ? disallowedTools : undefined,
303
+ strictMcp: readOnly || policy.strict,
304
+ });
307
305
  return { ...result, harness: this.info(), degraded: gate.degraded };
308
306
  }
309
307
  async runQuery(opts) {
@@ -371,15 +369,7 @@ export class ClaudeCodeHarness {
371
369
  let bridge;
372
370
  // Build prompt. Context (workflow input such as issues/alerts, plus prior
373
371
  // node outputs) is fenced as untrusted data (#360).
374
- const prompt = [
375
- `## Instruction\n\n${instruction}`,
376
- `## Context\n\n${fenceUntrustedJson(context, "context")}`,
377
- outputSchema
378
- ? `## Required Output\n\nYou MUST end with a JSON object matching this schema:\n\`\`\`json\n${JSON.stringify(outputSchema, null, 2)}\n\`\`\``
379
- : "",
380
- ]
381
- .filter(Boolean)
382
- .join("\n\n");
372
+ const prompt = buildNodePrompt(instruction, context, outputSchema);
383
373
  const env = this.buildEnv(agentAccess?.envVars);
384
374
  let response = "";
385
375
  // CC-08: when the SDK populates typed structured output (because we passed
@@ -438,7 +428,8 @@ export class ClaudeCodeHarness {
438
428
  // project and local settings, including their MCP servers (and
439
429
  // project .mcp.json, plugins). strictMcpConfig limits MCP to the
440
430
  // servers passed above, which under readOnly is only sweny-core.
441
- ...(readOnly ? { strictMcpConfig: true } : {}),
431
+ // A strict policy (#365) makes it exclusive for write nodes too.
432
+ ...(readOnly || opts.strictMcp ? { strictMcpConfig: true } : {}),
442
433
  ...(disallowedTools && disallowedTools.length > 0 ? { disallowedTools } : {}),
443
434
  // CC-08: ask the SDK to produce validated structured output when the
444
435
  // node declares an output schema. The SDK then returns the parsed
@@ -749,112 +740,6 @@ export function coreToolToSdkTool(coreTool, defaultCtx) {
749
740
  }
750
741
  });
751
742
  }
752
- /**
753
- * Best-effort recovery of typed output from a tool_result's `content`.
754
- *
755
- * The MCP protocol sends tool results as string content. Our in-process
756
- * wrapper JSON-stringifies structured output before returning; external
757
- * MCP servers generally do the same for JSON payloads. If the string
758
- * looks like a JSON object or array and parses, return the parsed value
759
- * so verify's output-path walks work against typed data.
760
- *
761
- * Raw strings are preserved verbatim. JSON-primitive strings (e.g. the
762
- * literal four characters `"42"`) are intentionally NOT parsed - we
763
- * cannot distinguish a tool that returned the number 42 (wrapper sends
764
- * `"42"`) from a tool that returned the string "42" (wrapper also sends
765
- * `"42"`). Preserving the string is safer than guessing.
766
- */
767
- export function parseToolResultContent(content) {
768
- // The Anthropic tool_result `content` field can be a block array
769
- // (e.g. [{type:"text",text:"..."}, ...]) rather than a string. When it is,
770
- // concatenate the text of every {type:"text"} block (recursing through any
771
- // nested content), then run the same object/array JSON-parse logic on the
772
- // joined string. This keeps `call.output` the actual payload instead of the
773
- // raw wrapper array that eval/route logic would otherwise have to walk.
774
- if (Array.isArray(content)) {
775
- const text = collectBlockText(content);
776
- return parseJsonObjectOrArray(text);
777
- }
778
- if (typeof content !== "string")
779
- return content;
780
- return parseJsonObjectOrArray(content);
781
- }
782
- /**
783
- * Parse a string only when it is an unambiguous JSON object or array.
784
- *
785
- * Strings, numbers, booleans, and null would all corrupt or discard
786
- * information (we cannot distinguish the literal text `"42"` from the number
787
- * 42), so non-object/array inputs are preserved verbatim.
788
- */
789
- function parseJsonObjectOrArray(content) {
790
- const trimmed = content.trim();
791
- if (trimmed.length === 0)
792
- return content;
793
- const first = trimmed[0];
794
- if (first !== "{" && first !== "[")
795
- return content;
796
- try {
797
- return JSON.parse(trimmed);
798
- }
799
- catch {
800
- return content;
801
- }
802
- }
803
- /**
804
- * Concatenate the text of an array of content blocks. Handles {type:"text"}
805
- * blocks and recurses into any nested `content` array so deeply-wrapped tool
806
- * results still flatten to their underlying text.
807
- */
808
- function collectBlockText(blocks) {
809
- const parts = [];
810
- for (const block of blocks) {
811
- if (typeof block === "string") {
812
- parts.push(block);
813
- continue;
814
- }
815
- if (!block || typeof block !== "object")
816
- continue;
817
- const b = block;
818
- if (b.type === "text" && typeof b.text === "string") {
819
- parts.push(b.text);
820
- }
821
- else if (Array.isArray(b.content)) {
822
- parts.push(collectBlockText(b.content));
823
- }
824
- else if (typeof b.text === "string") {
825
- // Some producers omit an explicit type but still carry text.
826
- parts.push(b.text);
827
- }
828
- }
829
- return parts.join("");
830
- }
831
- /**
832
- * Produce a short, single-line description of a tool-error payload suitable
833
- * for the CI log stream. The full parsed value stays on the ToolCall for
834
- * verify and downstream tooling - this is only for inline observability.
835
- *
836
- * Collapses newlines, trims whitespace, and caps to 300 chars so a huge
837
- * API response body doesn't flood the log.
838
- */
839
- export function summarizeToolError(parsed) {
840
- let raw;
841
- if (typeof parsed === "string") {
842
- raw = parsed;
843
- }
844
- else if (parsed && typeof parsed === "object") {
845
- try {
846
- raw = JSON.stringify(parsed);
847
- }
848
- catch {
849
- raw = String(parsed);
850
- }
851
- }
852
- else {
853
- raw = String(parsed);
854
- }
855
- const collapsed = raw.replace(/\s+/g, " ").trim();
856
- return collapsed.length > 300 ? collapsed.slice(0, 297) + "..." : collapsed;
857
- }
858
743
  /** Strip MCP server prefix: "mcp__server__tool" → "tool" */
859
744
  function stripMcpPrefix(name) {
860
745
  const parts = name.split("__");
@@ -862,142 +747,3 @@ function stripMcpPrefix(name) {
862
747
  return parts.slice(2).join("__");
863
748
  return name;
864
749
  }
865
- // ─── JSON extraction ────────────────────────────────────────────
866
- /**
867
- * Extract a JSON object from Claude's text response.
868
- *
869
- * Strategy (in order):
870
- * 1. LAST ```json code block``` - models put their real final answer last, so
871
- * a prompt-injected fake fenced block placed earlier must not win.
872
- * 2. Last brace-delimited `{...}` block - handles inline JSON at end of text
873
- * 3. Full text parse - for responses that are pure JSON
874
- * 4. Empty object - safe fallback
875
- *
876
- * When `outputSchema` is supplied, the parsed object is checked against it.
877
- * A mismatch is logged (warning) rather than silently accepted, so a
878
- * non-conforming model answer is at least attributable.
879
- */
880
- function tryParseJSON(text, outputSchema, logger) {
881
- if (!text)
882
- return {};
883
- const finalize = (parsed) => {
884
- if (outputSchema) {
885
- const problems = schemaMismatches(parsed, outputSchema);
886
- if (problems.length > 0) {
887
- logger?.warn?.(`Claude output did not conform to outputSchema: ${problems.join("; ")}`);
888
- }
889
- }
890
- return parsed;
891
- };
892
- // 1. Code block - scan ALL fenced blocks, prefer the LAST that parses.
893
- const fences = [...text.matchAll(/```(?:json)?\s*([\s\S]*?)```/g)];
894
- for (let i = fences.length - 1; i >= 0; i--) {
895
- try {
896
- const parsed = JSON.parse(fences[i][1].trim());
897
- if (typeof parsed === "object" && parsed !== null)
898
- return finalize(parsed);
899
- }
900
- catch {
901
- /* try the next-earlier fence, then fall through to brace scan */
902
- }
903
- }
904
- // 2. Last brace-delimited block (scan backwards for matching braces)
905
- const lastBrace = text.lastIndexOf("}");
906
- if (lastBrace !== -1) {
907
- let depth = 0;
908
- for (let i = lastBrace; i >= 0; i--) {
909
- if (text[i] === "}")
910
- depth++;
911
- else if (text[i] === "{")
912
- depth--;
913
- if (depth === 0) {
914
- try {
915
- const parsed = JSON.parse(text.slice(i, lastBrace + 1));
916
- if (typeof parsed === "object" && parsed !== null)
917
- return finalize(parsed);
918
- }
919
- catch {
920
- /* try next strategy */
921
- }
922
- break;
923
- }
924
- }
925
- }
926
- // 3. Full text parse
927
- try {
928
- const parsed = JSON.parse(text.trim());
929
- if (typeof parsed === "object" && parsed !== null)
930
- return finalize(parsed);
931
- }
932
- catch {
933
- /* fall through */
934
- }
935
- return {};
936
- }
937
- /**
938
- * Lightweight structural check of a parsed object against a JSON Schema.
939
- *
940
- * Deliberately shallow: it verifies `required` keys are present and that the
941
- * top-level `type` and each declared property `type` match. It does NOT do
942
- * full JSON Schema validation (no `$ref`, `oneOf`, formats, nested arrays).
943
- * The contract here is "flag an obviously non-conforming object" so a mismatch
944
- * is logged rather than silently accepted; it is not a validation gate.
945
- *
946
- * Returns a list of human-readable problems; empty means no detected mismatch.
947
- */
948
- function schemaMismatches(value, schema) {
949
- const problems = [];
950
- const s = schema;
951
- if (s.type === "object" || s.properties || s.required) {
952
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
953
- return [`expected an object, got ${Array.isArray(value) ? "array" : typeof value}`];
954
- }
955
- const obj = value;
956
- const required = Array.isArray(s.required) ? s.required : [];
957
- for (const key of required) {
958
- if (!(key in obj) || obj[key] === undefined) {
959
- problems.push(`missing required property "${key}"`);
960
- }
961
- }
962
- const props = s.properties ?? {};
963
- for (const [key, propSchema] of Object.entries(props)) {
964
- if (!(key in obj) || obj[key] === undefined || obj[key] === null)
965
- continue;
966
- const expected = propSchema?.type;
967
- if (typeof expected === "string" && !jsonTypeMatches(obj[key], expected)) {
968
- problems.push(`property "${key}" expected ${expected}, got ${jsonTypeOf(obj[key])}`);
969
- }
970
- }
971
- }
972
- else if (typeof s.type === "string" && !jsonTypeMatches(value, s.type)) {
973
- problems.push(`expected ${s.type}, got ${jsonTypeOf(value)}`);
974
- }
975
- return problems;
976
- }
977
- function jsonTypeOf(value) {
978
- if (value === null)
979
- return "null";
980
- if (Array.isArray(value))
981
- return "array";
982
- return typeof value;
983
- }
984
- function jsonTypeMatches(value, expected) {
985
- switch (expected) {
986
- case "object":
987
- return typeof value === "object" && value !== null && !Array.isArray(value);
988
- case "array":
989
- return Array.isArray(value);
990
- case "string":
991
- return typeof value === "string";
992
- case "number":
993
- return typeof value === "number";
994
- case "integer":
995
- return typeof value === "number" && Number.isInteger(value);
996
- case "boolean":
997
- return typeof value === "boolean";
998
- case "null":
999
- return value === null;
1000
- default:
1001
- return true; // unknown/unsupported type keyword - don't flag
1002
- }
1003
- }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Codex harness (#331): runs a node on OpenAI Codex through `codex exec --json`.
3
+ *
4
+ * Interfaces used, all read from openai/codex at rust-v0.159.2:
5
+ * - `codex-rs/exec/src/cli.rs`: `exec --json`, `--output-schema <file>`,
6
+ * `--ignore-user-config`, `--ignore-rules`, `--ephemeral`,
7
+ * `--skip-git-repo-check`, `--sandbox`, `--cd`, `--model`, `-c key=value`,
8
+ * prompt on stdin with `-`.
9
+ * - `codex-rs/exec/src/exec_events.rs`: the JSONL events (`thread.started`,
10
+ * `turn.started|completed|failed`, `item.started|updated|completed`,
11
+ * `error`) and item kinds.
12
+ * - `sdk/typescript/src/exec.ts`: how the official SDK spawns the same CLI
13
+ * (config overrides as TOML values, stdin prompt, exit handling).
14
+ * - `codex-rs/core/config.schema.json`: `features.*`, `web_search`,
15
+ * `agents.enabled`, `sandbox_workspace_write.network_access`,
16
+ * `mcp_servers.<id>.{command,args,env,env_vars,url,http_headers,...}`.
17
+ * - `codex-rs/rmcp-client/src/utils.rs`: an MCP stdio server gets only a
18
+ * small default env plus the names listed in `env_vars`, so the tool
19
+ * bridge token travels by name, never in argv.
20
+ *
21
+ * Every sweny opinion in the Codex column of the enforcement matrix is
22
+ * enforced natively here, kept by sweny (watchdog, fencing, env scoping,
23
+ * output validation), reported in `degraded`, or refused under strict policy
24
+ * by `policyGate`. Nothing is dropped silently.
25
+ */
26
+ import type { Logger, McpServerConfig, ToolContext } from "../types.js";
27
+ import { type SandboxMode } from "../agent-env.js";
28
+ import type { AgentHarness, HarnessCapabilities, HarnessCompleteRequest, HarnessInfo, HarnessRunRequest, HarnessRunResult, ToolClass } from "./types.js";
29
+ import { CODEX_CAPABILITIES } from "./capabilities.js";
30
+ import { type HarnessPolicyMode } from "./policy.js";
31
+ import { type SandboxWrapper } from "./sandbox-wrapper.js";
32
+ export { CODEX_CAPABILITIES };
33
+ /** Oldest Codex CLI whose flags this adapter uses (`--ignore-user-config`, `--ignore-rules`). */
34
+ export declare const MIN_CODEX_VERSION = "0.159.0";
35
+ /**
36
+ * Codex features sweny always turns off, so a node sees only the tools sweny
37
+ * gave it: ChatGPT apps and plugins (their own MCP servers), tool suggestions,
38
+ * browser and computer use, lifecycle hooks, and image generation. Keys from
39
+ * `codex-rs/features/src/lib.rs` at rust-v0.159.2.
40
+ */
41
+ export declare const CODEX_ISOLATION_FEATURES_OFF: readonly ["apps", "plugins", "tool_suggest", "browser_use", "computer_use", "hooks", "image_generation"];
42
+ /**
43
+ * Hosts Codex itself needs (its model API and ChatGPT sign-in), always allowed
44
+ * through the sandbox wrapper, plus the host of `OPENAI_BASE_URL` when set.
45
+ */
46
+ export declare function codexBackendHosts(env?: Record<string, string | undefined>): string[];
47
+ export interface CodexHarnessOptions {
48
+ /** Model override (`--model`). Free text; sweny has no model opinion. */
49
+ model?: string;
50
+ /** Tool-call budget the watchdog enforces when a node sets no `max_turns` (default: 20). */
51
+ maxTurns?: number;
52
+ /** Working directory (default: process.cwd()). */
53
+ cwd?: string;
54
+ logger?: Logger;
55
+ /** Default tool context for standalone usage (not via the executor). */
56
+ defaultContext?: ToolContext;
57
+ /** Catalog defaults, overridden by per-run skill servers and explicit mcpServers. */
58
+ defaultMcpServers?: Record<string, McpServerConfig>;
59
+ /** Explicit external MCP servers, overriding defaults and per-run skill servers. */
60
+ mcpServers?: Record<string, McpServerConfig>;
61
+ /** Extra env var names passed through (see agent-env.ts). Default: `SWENY_ENV_PASSTHROUGH`. */
62
+ envPassthrough?: string[];
63
+ /** Scope the Codex env to the allowlist. Default: `SWENY_ENV_SCOPE`, else on in CI. */
64
+ envScope?: boolean;
65
+ /**
66
+ * Codex sandbox for commands: `strict` = `workspace-write` or fail, `auto` =
67
+ * `workspace-write` when the host can sandbox, `off` = `danger-full-access`.
68
+ * Default: `SWENY_SANDBOX`, else `auto` in CI and `off` locally.
69
+ */
70
+ sandbox?: SandboxMode;
71
+ /** Sandbox preflight probe (test seam). Returns a reason when the host cannot sandbox. */
72
+ sandboxProbe?: () => string | undefined;
73
+ /**
74
+ * The host's process sandbox wrapper (#360 step 2). `undefined` = detect
75
+ * srt once per process; `null` = none (test seam).
76
+ */
77
+ sandboxWrapper?: SandboxWrapper | null;
78
+ /** `strict` refuses a node with unenforceable opinions. Default: {@link resolveHarnessPolicy}. */
79
+ policy?: HarnessPolicyMode;
80
+ /** The Codex CLI. Default: `SWENY_CODEX_PATH`, else `codex` on PATH. Test seam for the fake. */
81
+ codexCommand?: {
82
+ command: string;
83
+ args: string[];
84
+ };
85
+ /** Shim command override for the tool bridge (test seam). */
86
+ toolBridgeShim?: {
87
+ command: string;
88
+ args: string[];
89
+ };
90
+ /** Grace period between SIGTERM and SIGKILL when stopping Codex (default: 2000 ms). */
91
+ killGraceMs?: number;
92
+ }
93
+ /** A TOML value for `-c key=value`, as the official SDK writes it (sdk/typescript/src/exec.ts). */
94
+ export declare function toTomlValue(value: unknown): string;
95
+ /**
96
+ * Split `disallowed_tools` names into portable classes Codex can act on and
97
+ * the names it has no tool for.
98
+ */
99
+ export declare function translateDenyNames(names?: readonly string[]): {
100
+ classes: ToolClass[];
101
+ unknown: string[];
102
+ };
103
+ /**
104
+ * Is this JSON schema accepted by OpenAI strict structured outputs? Codex
105
+ * sends `--output-schema` with `strict: true` (core/src/session/turn.rs), which
106
+ * requires `additionalProperties: false` and every property listed in
107
+ * `required` on every object. Not a full check; it catches the common misses.
108
+ */
109
+ export declare function isStrictCompatibleSchema(schema: unknown): boolean;
110
+ export declare class CodexHarness implements AgentHarness {
111
+ readonly id: "codex";
112
+ readonly capabilities: HarnessCapabilities;
113
+ /**
114
+ * Empty on purpose: judges use Codex's own default model. Leaving this
115
+ * undefined would fall back to the Claude judge model, which Codex cannot run.
116
+ */
117
+ readonly defaultJudgeModel = "";
118
+ readonly logger: Logger;
119
+ private model;
120
+ private maxTurns;
121
+ private cwd;
122
+ private defaultContext;
123
+ private mcpServers;
124
+ private defaultMcpServers;
125
+ private envPassthrough;
126
+ private envScope;
127
+ private sandboxMode;
128
+ private sandboxProbe;
129
+ private sandboxWarned;
130
+ private sandboxWrapper;
131
+ private loginWarned;
132
+ private policyMode;
133
+ private codexCommand;
134
+ private toolBridgeShim;
135
+ private killGraceMs;
136
+ private preflightResult;
137
+ private version;
138
+ constructor(opts?: CodexHarnessOptions);
139
+ info(): HarnessInfo;
140
+ /** `codex --version`, checked against {@link MIN_CODEX_VERSION}. Cached per instance. */
141
+ preflight(): Promise<{
142
+ ok: true;
143
+ version: string;
144
+ } | {
145
+ ok: false;
146
+ reason: string;
147
+ }>;
148
+ /** Env for the Codex process: scoped like Claude Code's, with Codex's auth vars instead of Anthropic's. */
149
+ private buildEnv;
150
+ /** The node policy after `disallowed_tools` names are translated into Codex classes. */
151
+ private compilePolicy;
152
+ /** Resolve the Codex `--sandbox` value for a normal (not read-only) run. */
153
+ private resolveSandbox;
154
+ /** argv shared by run() and complete(). */
155
+ private baseArgs;
156
+ /** `-c mcp_servers.<id>.*` for each server. Returns the args and anything Codex cannot load. */
157
+ private mcpArgs;
158
+ /** Spawn codex exec, feed the prompt on stdin, and read JSONL until it exits. */
159
+ private exec;
160
+ /**
161
+ * Run a node. The gate runs first: in strict mode a node whose opinions
162
+ * Codex cannot honor is refused before Codex starts. With a sandbox mode
163
+ * other than off, the whole Codex process runs inside the host's process
164
+ * sandbox wrapper (#360 step 2) when there is one, which also keeps the
165
+ * node's per-host egress allowlist that Codex cannot keep itself.
166
+ */
167
+ run(req: HarnessRunRequest): Promise<HarnessRunResult>;
168
+ /** Map one exec outcome onto a NodeResult with Claude Code's semantics. */
169
+ private toResult;
170
+ /**
171
+ * One completion: read-only sandbox, no shell, no web search, no subagents,
172
+ * no MCP. null when it failed (callers fail closed).
173
+ */
174
+ complete(req: HarnessCompleteRequest): Promise<string | null>;
175
+ }