@fusengine/harness 0.1.88 → 0.1.90
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/adapters/codex/index.mjs +2 -2
- package/dist/cli/bin.mjs +2 -2
- package/dist/cli/index.d.mts +6 -1
- package/dist/cli/index.mjs +1 -1
- package/dist/{handle-DHhukRdh.mjs → handle-CBaOcFZq.mjs} +36 -16
- package/dist/permission-mode-B-SFyR0X.mjs +226 -0
- package/dist/{run-DdWG0Meg.mjs → run-CdIlUtjv.mjs} +14 -4
- package/dist/runtime/index.mjs +1 -1
- package/package.json +1 -1
- package/dist/permission-mode-B0MUl7FT.mjs +0 -136
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as evaluate } from "../../evaluate-C1VSI5pb.mjs";
|
|
2
2
|
import { f as countLines } from "../../home-state-oUGFB4ds.mjs";
|
|
3
3
|
import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
|
|
4
|
-
import { n as
|
|
4
|
+
import { i as parseApplyPatch, n as canonicalizeMcpToolName, r as canonicalizeCodexShellTool, t as isBypassPermissions } from "../../permission-mode-B-SFyR0X.mjs";
|
|
5
5
|
import { _ as commandToString, c as readClaudeInput, i as denyResponse, r as contextResponse, s as informResponse } from "../../claude-5bC8TDAz.mjs";
|
|
6
6
|
//#region src/adapters/codex/index.ts
|
|
7
7
|
/**
|
|
@@ -51,7 +51,7 @@ function applyPatchPrompt(command) {
|
|
|
51
51
|
function resolvePrompt(input) {
|
|
52
52
|
const i = input.tool_input;
|
|
53
53
|
const r = evaluate({
|
|
54
|
-
tool: canonicalizeCodexShellTool("codex", input.tool_name ?? "Write"),
|
|
54
|
+
tool: canonicalizeCodexShellTool("codex", canonicalizeMcpToolName("codex", input.tool_name ?? "Write")),
|
|
55
55
|
filePath: i?.file_path,
|
|
56
56
|
content: i?.content ?? i?.new_string,
|
|
57
57
|
command: commandToString(i?.command),
|
package/dist/cli/bin.mjs
CHANGED
|
@@ -3,9 +3,9 @@ import { o as resolveStdinMaxBytes } from "../limits-DG5pl-5s.mjs";
|
|
|
3
3
|
import { c as resolveTtlSec, r as loadDotenv } from "../dotenv-BLBkBTww.mjs";
|
|
4
4
|
import { t as detectHarness } from "../harness-BMuLJ9lm.mjs";
|
|
5
5
|
import { t as claudeHome } from "../home-state-oUGFB4ds.mjs";
|
|
6
|
-
import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-
|
|
6
|
+
import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-CdIlUtjv.mjs";
|
|
7
7
|
import { n as writeInitFile, t as initFor } from "../run-TAXNRSpD.mjs";
|
|
8
|
-
import { $ as runningVersion, Q as runDoctor, Z as notify, et as versionBanner, i as respond, t as handleHook, zt as todayUtc } from "../handle-
|
|
8
|
+
import { $ as runningVersion, Q as runDoctor, Z as notify, et as versionBanner, i as respond, t as handleHook, zt as todayUtc } from "../handle-CBaOcFZq.mjs";
|
|
9
9
|
import { delimiter, join } from "node:path";
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, readSync, readdirSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
package/dist/cli/index.d.mts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
//#region src/cli/run.d.ts
|
|
2
2
|
/** Staged files (Added/Copied/Modified/Renamed). Uses `node:child_process` (Bun shell can hang on `git show`). */
|
|
3
3
|
declare function stagedFiles(): string[];
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Read a file's staged (index) content — not the working-tree version.
|
|
6
|
+
* Uses `execFileSync` with an argv array (never a shell), so a staged
|
|
7
|
+
* filename containing shell metacharacters (backticks, `$(...)`, quotes)
|
|
8
|
+
* reaches `git show` as a single literal argument — see issue #87.
|
|
9
|
+
*/
|
|
5
10
|
declare function stagedContent(path: string): string;
|
|
6
11
|
/**
|
|
7
12
|
* Evaluate staged code files against the policy core; return violation blocks.
|
package/dist/cli/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-
|
|
1
|
+
import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-CdIlUtjv.mjs";
|
|
2
2
|
export { checkStaged, stagedContent, stagedFiles };
|
|
@@ -14,7 +14,7 @@ import { a as nowStamp, l as throttleMs, n as readRoots, o as readState, s as se
|
|
|
14
14
|
import { d as loadIndex, i as cacheLookupMeta, n as webfetchCacheWrite, o as cacheLookupSubstringMeta, t as mcpCacheWrite, u as extractText } from "./mcp-store-BkBDmuxN.mjs";
|
|
15
15
|
import { t as loadRefs } from "./loader-AGz4nK7d.mjs";
|
|
16
16
|
import { C as apexAuthorizationGate, S as trivialCount, _ as recordBrainstormRequired, a as journalLogPath, b as recordTarget, c as appendEvent, d as signTrack, f as verifyTrack, g as recordAgent, h as emptyTrack, i as withTrack, l as withTrackLockSync, m as agentsFresh, o as readTrackSync, p as writeLastNonce, r as trackJournalEnabled, s as diffTrackEvents, t as loadTrack, u as LOCK_FAILED, v as recordDoc, x as recordTrivialEdit, y as recordRefRead } from "./store-5-ZPKb0u.mjs";
|
|
17
|
-
import { n as
|
|
17
|
+
import { i as parseApplyPatch, n as canonicalizeMcpToolName, r as canonicalizeCodexShellTool, t as isBypassPermissions } from "./permission-mode-B-SFyR0X.mjs";
|
|
18
18
|
import { _ as commandToString, d as collectFiles, f as pathExists, g as writeText, h as spawnCapture, i as denyResponse, l as systemMessage, m as sleep, n as blockResponse, p as readText$1, r as contextResponse, s as informResponse, t as attachSystemMessage } from "./claude-5bC8TDAz.mjs";
|
|
19
19
|
import { i as toKimiResponse, n as kimiDenyResponse } from "./kimi-D6X7b8wF.mjs";
|
|
20
20
|
import { r as toHermesResponse } from "./hermes-kP6NUQlG.mjs";
|
|
@@ -90,7 +90,7 @@ function normalizeEvent(id, payload) {
|
|
|
90
90
|
}
|
|
91
91
|
const event = str(payload.hook_event_name) ?? "";
|
|
92
92
|
const input = payload.tool_input ?? payload;
|
|
93
|
-
const tool = canonicalizeCodexShellTool(id, str(payload.tool_name) ?? "");
|
|
93
|
+
const tool = canonicalizeCodexShellTool(id, canonicalizeMcpToolName(id, str(payload.tool_name) ?? ""));
|
|
94
94
|
const base = {
|
|
95
95
|
phase: /post|after/i.test(event) ? "post" : "pre",
|
|
96
96
|
tool,
|
|
@@ -5326,8 +5326,13 @@ const RETRY_DELAYS = [
|
|
|
5326
5326
|
1e3,
|
|
5327
5327
|
2e3
|
|
5328
5328
|
];
|
|
5329
|
-
/**
|
|
5330
|
-
|
|
5329
|
+
/**
|
|
5330
|
+
* Extract the longest assistant synthesis + queried library ids from a transcript.
|
|
5331
|
+
* @param id - Harness adapter id, used to canonicalize `block.name` (Codex-mangled
|
|
5332
|
+
* underscore tool names) before the {@link TOOL_PATTERN} match — see
|
|
5333
|
+
* {@link cacheDocFromTranscript} for why this is a defensive, unconfirmed-case cover.
|
|
5334
|
+
*/
|
|
5335
|
+
async function extractSynthesis(path, id) {
|
|
5331
5336
|
const lines = readText$1(path).split("\n").filter(Boolean);
|
|
5332
5337
|
const libraries = [];
|
|
5333
5338
|
let synthesis = "";
|
|
@@ -5337,7 +5342,7 @@ async function extractSynthesis(path) {
|
|
|
5337
5342
|
const contents = entry?.message?.content;
|
|
5338
5343
|
if (!Array.isArray(contents)) continue;
|
|
5339
5344
|
for (const block of contents) {
|
|
5340
|
-
if (block.type === "tool_use" && TOOL_PATTERN.test(block.name ?? "")) {
|
|
5345
|
+
if (block.type === "tool_use" && TOOL_PATTERN.test(canonicalizeMcpToolName(id, block.name ?? ""))) {
|
|
5341
5346
|
const lib = block.input?.libraryId ?? block.input?.query ?? "";
|
|
5342
5347
|
if (lib && !libraries.includes(lib)) libraries.push(lib);
|
|
5343
5348
|
}
|
|
@@ -5354,17 +5359,28 @@ async function extractSynthesis(path) {
|
|
|
5354
5359
|
* @param transcript - Path to the agent JSONL transcript.
|
|
5355
5360
|
* @param cwd - Fallback project root.
|
|
5356
5361
|
* @param home - Home dir (defaults to `~`).
|
|
5357
|
-
|
|
5358
|
-
|
|
5362
|
+
* @param id - Harness adapter id (defaults to "claude-code"). On `"codex"`,
|
|
5363
|
+
* every `tool_use.name` read from the transcript is canonicalized before the
|
|
5364
|
+
* {@link TOOL_PATTERN} match, defensively covering the SAME Codex-mangled
|
|
5365
|
+
* underscore form as `doc-cache-gate.ts`'s `docCacheGate`. UNLIKE that gate,
|
|
5366
|
+
* this is NOT a confirmed-reproduced bug: the proof that Codex emits the
|
|
5367
|
+
* underscore form (`sanitize_responses_api_tool_name()`, openai/codex#14605,
|
|
5368
|
+
* `SCENARIO_DASH_TOOL` in #18385) is about the live PreToolUse HOOK payload,
|
|
5369
|
+
* never observed here against transcript `tool_use.name` content. The
|
|
5370
|
+
* canonicalization is a no-op when the transcript already carries the dash
|
|
5371
|
+
* form (idempotence proven in `test/mcp-tool-name.test.ts`, test 2), so this
|
|
5372
|
+
* costs nothing if the hypothesis turns out false.
|
|
5373
|
+
*/
|
|
5374
|
+
async function cacheDocFromTranscript(transcript, cwd, home = homedir(), id = "claude-code") {
|
|
5359
5375
|
if (!transcript || !pathExists(transcript)) return;
|
|
5360
5376
|
const projPath = projectRootFromPaths(await transcriptFilePaths(transcript)) ?? process.env.CLAUDE_PROJECT_DIR ?? cwd;
|
|
5361
5377
|
const cacheDir = cacheDirFor("doc", projPath, home);
|
|
5362
5378
|
const docsDir = join(cacheDir, "docs");
|
|
5363
|
-
let result = await extractSynthesis(transcript);
|
|
5379
|
+
let result = await extractSynthesis(transcript, id);
|
|
5364
5380
|
for (const delay of RETRY_DELAYS) {
|
|
5365
5381
|
if (result.text.length >= MIN_TEXT_SIZE && result.libraries.length > 0) break;
|
|
5366
5382
|
await sleep(delay);
|
|
5367
|
-
result = await extractSynthesis(transcript);
|
|
5383
|
+
result = await extractSynthesis(transcript, id);
|
|
5368
5384
|
}
|
|
5369
5385
|
const { text, libraries } = result;
|
|
5370
5386
|
if (text.length < MIN_TEXT_SIZE || libraries.length === 0) return;
|
|
@@ -5762,10 +5778,14 @@ function libraryOf(payload) {
|
|
|
5762
5778
|
* @param cwd - Fallback project root (uses `CLAUDE_PROJECT_DIR` first).
|
|
5763
5779
|
* @param now - Clock (defaults to `Date.now()`).
|
|
5764
5780
|
* @param home - Home dir (defaults to `~`).
|
|
5781
|
+
* @param id - Harness adapter id (defaults to "claude-code"); on `"codex"`,
|
|
5782
|
+
* `payload.tool_name` is canonicalized before the {@link GATED_TOOLS} match
|
|
5783
|
+
* so the Codex-mangled underscore form (`mcp__context7__query_docs`) gates
|
|
5784
|
+
* exactly like the dash form does on every other harness.
|
|
5765
5785
|
* @returns A native deny response, or `null` to fall through to the live call.
|
|
5766
5786
|
*/
|
|
5767
|
-
async function docCacheGate(payload, cwd, now = Date.now(), home = homedir()) {
|
|
5768
|
-
const tool = String(payload.tool_name ?? "");
|
|
5787
|
+
async function docCacheGate(payload, cwd, now = Date.now(), home = homedir(), id = "claude-code") {
|
|
5788
|
+
const tool = canonicalizeMcpToolName(id, String(payload.tool_name ?? ""));
|
|
5769
5789
|
if (!GATED_TOOLS.test(tool)) return null;
|
|
5770
5790
|
const library = libraryOf(payload);
|
|
5771
5791
|
if (!library) return null;
|
|
@@ -5981,11 +6001,11 @@ async function onSubagentStart(payload, cwd, now, home, id) {
|
|
|
5981
6001
|
const agent = agentTypeOf(payload);
|
|
5982
6002
|
return combineContext(await injectApexSubagentContext(cwd, home, id), await injectLessonsCache(cwd, home, now), await typeSpecificCache(agent, cwd, now, home));
|
|
5983
6003
|
}
|
|
5984
|
-
/** SubagentStop routing: transcript-driven cache writers, then the universal SOLID check. */
|
|
5985
|
-
async function onSubagentStop(payload, cwd, home) {
|
|
6004
|
+
/** SubagentStop routing: transcript-driven cache writers, then the universal SOLID check. `id` selects the harness target (defaults to "claude-code"), threaded into `cacheDocFromTranscript`. */
|
|
6005
|
+
async function onSubagentStop(payload, cwd, home, id) {
|
|
5986
6006
|
const agent = agentTypeOf(payload);
|
|
5987
6007
|
const transcript = transcriptOf(payload);
|
|
5988
|
-
if (agent.includes("research-expert")) await cacheDocFromTranscript(transcript, cwd, home);
|
|
6008
|
+
if (agent.includes("research-expert")) await cacheDocFromTranscript(transcript, cwd, home, id);
|
|
5989
6009
|
if (agent.includes("sniper")) {
|
|
5990
6010
|
await cacheSniperLessons(transcript, cwd, home);
|
|
5991
6011
|
await cacheTestResults(transcript, cwd, home);
|
|
@@ -5999,12 +6019,12 @@ async function onSubagentStop(payload, cwd, home) {
|
|
|
5999
6019
|
*/
|
|
6000
6020
|
async function dispatchAipilot(event, payload, cwd, now, home = homedir(), id = "claude-code") {
|
|
6001
6021
|
if (event === "SubagentStart") return onSubagentStart(payload, cwd, now, home, id);
|
|
6002
|
-
if (event === "SubagentStop") return onSubagentStop(payload, cwd, home);
|
|
6022
|
+
if (event === "SubagentStop") return onSubagentStop(payload, cwd, home, id);
|
|
6003
6023
|
if (event === "SessionEnd" || event === "Stop") {
|
|
6004
6024
|
await cacheAnalyticsSave(home, now);
|
|
6005
6025
|
return "";
|
|
6006
6026
|
}
|
|
6007
|
-
if (event === "PreToolUse") return docCacheGate(payload, cwd, now, home);
|
|
6027
|
+
if (event === "PreToolUse") return docCacheGate(payload, cwd, now, home, id);
|
|
6008
6028
|
return null;
|
|
6009
6029
|
}
|
|
6010
6030
|
/** PostToolUse (Write/Edit SOLID check, else TaskCreate/TaskUpdate sync) for the ai-pilot scope; `id` selects the harness target (defaults to "claude-code"). */
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
//#region src/adapters/codex/apply-patch.ts
|
|
2
|
+
const BEGIN = "*** Begin Patch";
|
|
3
|
+
const END = "*** End Patch";
|
|
4
|
+
const ADD = "*** Add File: ";
|
|
5
|
+
const DEL = "*** Delete File: ";
|
|
6
|
+
const UPD = "*** Update File: ";
|
|
7
|
+
const MOVE = "*** Move to: ";
|
|
8
|
+
const EOF = "*** End of File";
|
|
9
|
+
const ENV = "*** Environment ID: ";
|
|
10
|
+
/**
|
|
11
|
+
* Parse a Codex freeform patch into its per-file changes. Lenient on whitespace
|
|
12
|
+
* around structural markers (as Codex's own parser is); returns `[]` when no
|
|
13
|
+
* recognizable hunk is present (malformed input fails open, not closed).
|
|
14
|
+
* @param text - Raw patch body from `tool_input.command`.
|
|
15
|
+
* @returns One {@link PatchedFile} per Add/Update/Delete hunk, in order.
|
|
16
|
+
*/
|
|
17
|
+
function parseApplyPatch(text) {
|
|
18
|
+
const files = [];
|
|
19
|
+
let cur = null;
|
|
20
|
+
let buf = [];
|
|
21
|
+
const flush = () => {
|
|
22
|
+
if (cur) {
|
|
23
|
+
cur.content = buf.join("\n");
|
|
24
|
+
files.push(cur);
|
|
25
|
+
}
|
|
26
|
+
cur = null;
|
|
27
|
+
buf = [];
|
|
28
|
+
};
|
|
29
|
+
for (const line of text.split("\n")) {
|
|
30
|
+
const marker = line.trimStart();
|
|
31
|
+
if (marker === BEGIN || marker.startsWith(ENV)) continue;
|
|
32
|
+
if (marker === END) {
|
|
33
|
+
flush();
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (marker.startsWith(ADD)) {
|
|
37
|
+
flush();
|
|
38
|
+
cur = {
|
|
39
|
+
path: marker.slice(14).trim(),
|
|
40
|
+
content: "",
|
|
41
|
+
op: "add"
|
|
42
|
+
};
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (marker.startsWith(DEL)) {
|
|
46
|
+
flush();
|
|
47
|
+
files.push({
|
|
48
|
+
path: marker.slice(17).trim(),
|
|
49
|
+
content: "",
|
|
50
|
+
op: "delete"
|
|
51
|
+
});
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (marker.startsWith(UPD)) {
|
|
55
|
+
flush();
|
|
56
|
+
cur = {
|
|
57
|
+
path: marker.slice(17).trim(),
|
|
58
|
+
content: "",
|
|
59
|
+
op: "update"
|
|
60
|
+
};
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (marker.startsWith(MOVE)) {
|
|
64
|
+
if (cur) cur.path = marker.slice(13).trim();
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (marker === EOF || !cur) continue;
|
|
68
|
+
if (cur.op === "add") {
|
|
69
|
+
if (line.startsWith("+")) buf.push(line.slice(1));
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (line.startsWith("@@")) continue;
|
|
73
|
+
if (line.startsWith("+")) buf.push(line.slice(1));
|
|
74
|
+
else if (line.startsWith("-")) continue;
|
|
75
|
+
else buf.push(line.startsWith(" ") ? line.slice(1) : line);
|
|
76
|
+
}
|
|
77
|
+
flush();
|
|
78
|
+
return files;
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
//#region src/runtime/codex-shell-tool.ts
|
|
82
|
+
/**
|
|
83
|
+
* @module codex-shell-tool
|
|
84
|
+
* Canonicalizes a Codex-only shell `tool_name` alias to `"Bash"`, so every
|
|
85
|
+
* existing `tool === "Bash"` consumer (activity credit, explore-tools
|
|
86
|
+
* classification, the protected-path guard, the design corpus read gate, …)
|
|
87
|
+
* treats a Code Mode exec dispatch exactly like a native Bash call — no
|
|
88
|
+
* per-consumer change needed.
|
|
89
|
+
*
|
|
90
|
+
* A Code Mode-wrapped call (`functions.exec` -> `tools.exec_command(...)`)
|
|
91
|
+
* surfaces via the generic `CoreToolRuntime` hook default (openai/codex#23757,
|
|
92
|
+
* commit 5c20513), which keeps the raw function-tool name `"exec_command"`
|
|
93
|
+
* verbatim — unlike a DIRECT `exec_command` dispatch, which
|
|
94
|
+
* `ExecCommandHandler`'s own `pre_tool_use_payload` override re-labels
|
|
95
|
+
* `"Bash"` (`tool_name: HookToolName::bash()`). This is the exact call chain
|
|
96
|
+
* a real Codex CLI 0.146.0 session was observed emitting for a `sed -n` skill
|
|
97
|
+
* read. Only ONE alias is confirmed here — `ToolName::plain("exec_command")`
|
|
98
|
+
* in `core/src/tools/handlers/unified_exec/exec_command.rs`; `shell`,
|
|
99
|
+
* `unified_exec`, `local_shell`, and `shell_command` were audited but not
|
|
100
|
+
* evidenced as a distinct hook `tool_name` value, so they are deliberately
|
|
101
|
+
* left out rather than guessed.
|
|
102
|
+
* @packageDocumentation
|
|
103
|
+
*/
|
|
104
|
+
/** Codex `tool_name` values that are a shell execution but not literally `"Bash"`. */
|
|
105
|
+
const CODEX_SHELL_TOOL_ALIASES = /* @__PURE__ */ new Set(["exec_command"]);
|
|
106
|
+
/**
|
|
107
|
+
* Canonicalize a Codex-only shell tool alias to `"Bash"`. Scoped to
|
|
108
|
+
* `id === "codex"` — every other harness id, and every non-aliased tool
|
|
109
|
+
* name, passes through unchanged (see `test/codex-shell-read-credit.test.ts`
|
|
110
|
+
* for the non-regression proof).
|
|
111
|
+
* @param id - Harness adapter id (e.g. "codex", "claude-code", "kimi").
|
|
112
|
+
* @param tool - Raw `tool_name` from the hook payload.
|
|
113
|
+
*/
|
|
114
|
+
function canonicalizeCodexShellTool(id, tool) {
|
|
115
|
+
return id === "codex" && CODEX_SHELL_TOOL_ALIASES.has(tool) ? "Bash" : tool;
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/runtime/mcp-tool-name.ts
|
|
119
|
+
/**
|
|
120
|
+
* @module mcp-tool-name
|
|
121
|
+
* Canonicalizes a Codex-only MCP `tool_name` alias back to its original
|
|
122
|
+
* hyphenated form, so every existing dash-keyed consumer (`SHOT_TOOLS`,
|
|
123
|
+
* `RESEARCH_TOOLS`/`classifyExplore`, `docSourceOf`, the design-pipeline
|
|
124
|
+
* `NAV`/`SCROLL`/`GEMINI` constants, …) recognizes a Codex MCP call exactly
|
|
125
|
+
* like the same call on Claude/Kimi/Cursor/Cline/Gemini — no per-consumer
|
|
126
|
+
* change needed.
|
|
127
|
+
*
|
|
128
|
+
* Codex CLI globally rewrites every `-` to `_` across the FULL qualified MCP
|
|
129
|
+
* tool identifier before it ever reaches a hook, in `sanitize_responses_api_tool_name()`
|
|
130
|
+
* (`codex-rs/core/src/mcp_connection_manager.rs`, introduced by
|
|
131
|
+
* openai/codex#14605 — "Normalize MCP tool names to code-mode safe form",
|
|
132
|
+
* code-mode forbids `-` in identifiers). openai/codex#18385's own
|
|
133
|
+
* `SCENARIO_DASH_TOOL` test proves the REWRITTEN form is what reaches hooks:
|
|
134
|
+
* the `echo-tool` tool of server `rmcp` is delivered to hooks as
|
|
135
|
+
* `mcp__rmcp__echo_tool`. Confirmed independently in THIS repo's terrain:
|
|
136
|
+
* `~/.codex/config.toml` declares `[mcp_servers.fuse-browser]` (dash) while
|
|
137
|
+
* Codex session logs and this harness's own dual-marketplace hook payloads
|
|
138
|
+
* (`~/.codex/plugins/.../hooks.json` matchers) observe `mcp__fuse_browser__*`
|
|
139
|
+
* (underscore) — Claude never emits that form (0 occurrences across its
|
|
140
|
+
* transcripts vs 137x the dash form).
|
|
141
|
+
*
|
|
142
|
+
* The rewrite is deliberately NOT undone with a generic `_` -> `-` regex:
|
|
143
|
+
* openai/codex#15832 and #15565 document it as NON-INVERSIBLE — a server
|
|
144
|
+
* literally named `autok_local` collides with a dash-named `autok-local`
|
|
145
|
+
* once both pass through the same rewrite, and OpenAI's own fix was a closed
|
|
146
|
+
* canonical table, never a general regex. The SAME ambiguity exists on our
|
|
147
|
+
* side: a generic reversal would corrupt tool segments that legitimately
|
|
148
|
+
* contain an underscore already (`browser_screenshot`, `node_repl`,
|
|
149
|
+
* `build_sim`, …). So this module only reverses the FOUR entries proven
|
|
150
|
+
* above to matter to this repo's gates — every other segment passes through
|
|
151
|
+
* untouched.
|
|
152
|
+
* @packageDocumentation
|
|
153
|
+
*/
|
|
154
|
+
/**
|
|
155
|
+
* Codex-mangled MCP server segment -> its real (dash) server name. Closed set
|
|
156
|
+
* — see module doc. `Object.create(null)` (never a `{}` literal): `serverKey`
|
|
157
|
+
* is an attacker-influenced string (an MCP server can register itself as
|
|
158
|
+
* `"__proto__"`/`"constructor"`/`"toString"`), and a `{}`-literal lookup by
|
|
159
|
+
* such a key returns the INHERITED `Object.prototype` member instead of
|
|
160
|
+
* `undefined` — an object/function, so `?? serverKey` never falls back and
|
|
161
|
+
* the result silently corrupts to `"mcp__[object Object]__…"` (proven in
|
|
162
|
+
* `test/mcp-tool-name.test.ts`, prototype-pollution-read case). A null-
|
|
163
|
+
* prototype object has no inherited members, so every lookup outside the two
|
|
164
|
+
* literal keys below is a clean miss.
|
|
165
|
+
*/
|
|
166
|
+
const SERVER_ALIASES = Object.assign(Object.create(null), {
|
|
167
|
+
fuse_browser: "fuse-browser",
|
|
168
|
+
gemini_design: "gemini-design"
|
|
169
|
+
});
|
|
170
|
+
/**
|
|
171
|
+
* Codex-mangled MCP tool segment -> its real (dash) tool name. Both entries
|
|
172
|
+
* are `context7`-exclusive tool names — applied ONLY when the canonical
|
|
173
|
+
* server is `"context7"` (see the explicit guard in
|
|
174
|
+
* {@link canonicalizeMcpToolName}), never globally. A same-named tool on any
|
|
175
|
+
* other server (e.g. a hypothetical `exa__query_docs`) passes through
|
|
176
|
+
* untouched: openai/codex#15832 documents exactly this class of collision as
|
|
177
|
+
* the reason the rewrite must stay a closed, scoped table, never a blanket
|
|
178
|
+
* one. Closed set, same null-prototype rationale as {@link SERVER_ALIASES}.
|
|
179
|
+
*/
|
|
180
|
+
const TOOL_ALIASES = Object.assign(Object.create(null), {
|
|
181
|
+
query_docs: "query-docs",
|
|
182
|
+
resolve_library_id: "resolve-library-id"
|
|
183
|
+
});
|
|
184
|
+
/**
|
|
185
|
+
* Canonicalize a Codex-only MCP tool-name alias to its dash form. Scoped to
|
|
186
|
+
* `id === "codex"` — every other harness id passes through unchanged. The
|
|
187
|
+
* server segment is rewritten via the closed {@link SERVER_ALIASES} table;
|
|
188
|
+
* the tool segment is rewritten via {@link TOOL_ALIASES} ONLY when the
|
|
189
|
+
* (already-resolved) canonical server is `"context7"` — every other server's
|
|
190
|
+
* tool segment is left untouched even if it happens to share a name with a
|
|
191
|
+
* context7 tool. Every other `mcp__<server>__<tool>` segment (or non-MCP
|
|
192
|
+
* tool name) is returned byte-identical (see `test/mcp-tool-name.test.ts`
|
|
193
|
+
* for the non-regression proof across ids, trap tool names, and the
|
|
194
|
+
* server-scoping cases).
|
|
195
|
+
* @param id - Harness adapter id (e.g. "codex", "claude-code", "kimi").
|
|
196
|
+
* @param tool - Raw `tool_name` from the hook payload.
|
|
197
|
+
*/
|
|
198
|
+
function canonicalizeMcpToolName(id, tool) {
|
|
199
|
+
if (id !== "codex") return tool;
|
|
200
|
+
const parts = tool.split("__");
|
|
201
|
+
if (parts.length < 3 || parts[0] !== "mcp") return tool;
|
|
202
|
+
const serverKey = parts[1] ?? "";
|
|
203
|
+
const toolKey = parts.slice(2).join("__");
|
|
204
|
+
const server = SERVER_ALIASES[serverKey] ?? serverKey;
|
|
205
|
+
return `mcp__${server}__${server === "context7" ? TOOL_ALIASES[toolKey] ?? toolKey : toolKey}`;
|
|
206
|
+
}
|
|
207
|
+
//#endregion
|
|
208
|
+
//#region src/adapters/codex/permission-mode.ts
|
|
209
|
+
/**
|
|
210
|
+
* Codex maps `AskForApproval::Never` -> `permission_mode: "bypassPermissions"`
|
|
211
|
+
* in its hook payload (codex-rs/core/src/hook_runtime.rs::hook_permission_mode)
|
|
212
|
+
* — the only value that isolates `never`; the other three approval policies
|
|
213
|
+
* (`untrusted`, `on-failure`, `on-request`) all collapse to `"default"`.
|
|
214
|
+
* Verified at openai/codex@342e4d4b, hook_runtime.rs run_pre_tool_use_hooks +
|
|
215
|
+
* pre_tool_use.rs PreToolUseRequest. Undocumented implementation detail, not
|
|
216
|
+
* a public contract — see the mapping-regression test that pins this string.
|
|
217
|
+
* @param permissionMode - The payload's `permission_mode` field, if present.
|
|
218
|
+
* @returns True only on an EXACT match — fail-closed. A near-miss
|
|
219
|
+
* (`"BypassPermissions"`, `"bypassPermissions "`, or a substring match) must
|
|
220
|
+
* never accidentally grant the neverApproval exemption.
|
|
221
|
+
*/
|
|
222
|
+
function isBypassPermissions(permissionMode) {
|
|
223
|
+
return permissionMode === "bypassPermissions";
|
|
224
|
+
}
|
|
225
|
+
//#endregion
|
|
226
|
+
export { parseApplyPatch as i, canonicalizeMcpToolName as n, canonicalizeCodexShellTool as r, isBypassPermissions as t };
|
|
@@ -1,15 +1,25 @@
|
|
|
1
1
|
import { t as isCodeFile } from "./project-root-3kk7gCOp.mjs";
|
|
2
2
|
import { t as evaluate } from "./evaluate-C1VSI5pb.mjs";
|
|
3
3
|
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
5
|
//#region src/cli/run.ts
|
|
6
6
|
/** Staged files (Added/Copied/Modified/Renamed). Uses `node:child_process` (Bun shell can hang on `git show`). */
|
|
7
7
|
function stagedFiles() {
|
|
8
|
-
return
|
|
8
|
+
return execFileSync("git", [
|
|
9
|
+
"diff",
|
|
10
|
+
"--cached",
|
|
11
|
+
"--name-only",
|
|
12
|
+
"--diff-filter=ACMR"
|
|
13
|
+
], { encoding: "utf8" }).trim().split("\n").filter(Boolean);
|
|
9
14
|
}
|
|
10
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Read a file's staged (index) content — not the working-tree version.
|
|
17
|
+
* Uses `execFileSync` with an argv array (never a shell), so a staged
|
|
18
|
+
* filename containing shell metacharacters (backticks, `$(...)`, quotes)
|
|
19
|
+
* reaches `git show` as a single literal argument — see issue #87.
|
|
20
|
+
*/
|
|
11
21
|
function stagedContent(path) {
|
|
12
|
-
return
|
|
22
|
+
return execFileSync("git", ["show", `:${path}`], { encoding: "utf8" });
|
|
13
23
|
}
|
|
14
24
|
/**
|
|
15
25
|
* Evaluate staged code files against the policy core; return violation blocks.
|
package/dist/runtime/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { r as projectLayout } from "../layout-KWoE_Mqn.mjs";
|
|
2
2
|
import { a as sanitizeSessionId, c as sessionsDir, i as loadSessionState, n as fuseHarnessHome, o as saveSessionState, r as fusengineCache, s as sessionStatePath, t as claudeHome } from "../home-state-oUGFB4ds.mjs";
|
|
3
|
-
import { A as trackMcpResearch, At as defaultStateDir, B as generateProjectMap, C as seoPostToolUse, Ct as trimLogFile, D as securityAdvisoryForPatch, Dt as claudeMdKey, E as securityAdvisory, Et as projectContext, F as dispatchAipilot, Ft as loadSecurityState, G as countFiles, H as writeTree, I as dispatchLessons, It as saveSecurityState, J as lessonsArchiveFileFor, K as getFileDesc, L as cartoSessionStart, Lt as securityStateDir, M as trackEnrichment, Mt as trackFile, N as dispatchLifecycle, Nt as normalizeEvent, O as postTrackingSideEffects, Ot as promptSubmitContext, P as aipilotPostToolUse, Pt as isoUtc, R as generateEcosystemMap, Rt as securityStatePath, S as postEditContext, St as removeOldFiles, T as dispatchMemory, Tt as gitContext, U as loadEnriched, V as isProject, W as mergeLines, X as lessonsStateFileFor, Y as lessonsFileFor, _ as preCommitGate, _t as readRules, a as recordActivity, at as saveApexState, b as extractSymbols, bt as pruneEmptyDirs, c as MCP_TTL_MS, ct as trackAgentMemory, d as isMcpTool, dt as validateSolidGate, f as queryOf, ft as checkFileSize, g as gate, gt as injectRules, h as TRIVIAL_BUDGET, ht as solidDetectStart, i as respond, it as cleanupSession, j as trackSkillRead, jt as projectHash, k as trackWatchResearch, kt as taskContext, l as WEBFETCH_TTL_MS, lt as subagentCacheContext, m as REQUIRED_AGENTS, mt as detectSolidProfile, n as activityFor, nt as trackSessionChanges, o as mcpPostStore, ot as logToolFailure, p as DEFAULT_WINDOW_MS, pt as countLoc, q as listChildren, r as handlePre, rt as validateRulesLoaded, s as mcpPreIntercept, st as validateTeammateOutput, t as handleHook, tt as postEditTypescript, u as cacheQueryOf, ut as validateTailwind, v as detectDuplication, vt as runSessionStartCleanups, w as seoPostToolUseResponse, wt as devContext, x as lifecycleStdout, xt as purgeTtlTree, y as dryGate, yt as sessionStartCore, z as writePluginMap, zt as todayUtc } from "../handle-
|
|
3
|
+
import { A as trackMcpResearch, At as defaultStateDir, B as generateProjectMap, C as seoPostToolUse, Ct as trimLogFile, D as securityAdvisoryForPatch, Dt as claudeMdKey, E as securityAdvisory, Et as projectContext, F as dispatchAipilot, Ft as loadSecurityState, G as countFiles, H as writeTree, I as dispatchLessons, It as saveSecurityState, J as lessonsArchiveFileFor, K as getFileDesc, L as cartoSessionStart, Lt as securityStateDir, M as trackEnrichment, Mt as trackFile, N as dispatchLifecycle, Nt as normalizeEvent, O as postTrackingSideEffects, Ot as promptSubmitContext, P as aipilotPostToolUse, Pt as isoUtc, R as generateEcosystemMap, Rt as securityStatePath, S as postEditContext, St as removeOldFiles, T as dispatchMemory, Tt as gitContext, U as loadEnriched, V as isProject, W as mergeLines, X as lessonsStateFileFor, Y as lessonsFileFor, _ as preCommitGate, _t as readRules, a as recordActivity, at as saveApexState, b as extractSymbols, bt as pruneEmptyDirs, c as MCP_TTL_MS, ct as trackAgentMemory, d as isMcpTool, dt as validateSolidGate, f as queryOf, ft as checkFileSize, g as gate, gt as injectRules, h as TRIVIAL_BUDGET, ht as solidDetectStart, i as respond, it as cleanupSession, j as trackSkillRead, jt as projectHash, k as trackWatchResearch, kt as taskContext, l as WEBFETCH_TTL_MS, lt as subagentCacheContext, m as REQUIRED_AGENTS, mt as detectSolidProfile, n as activityFor, nt as trackSessionChanges, o as mcpPostStore, ot as logToolFailure, p as DEFAULT_WINDOW_MS, pt as countLoc, q as listChildren, r as handlePre, rt as validateRulesLoaded, s as mcpPreIntercept, st as validateTeammateOutput, t as handleHook, tt as postEditTypescript, u as cacheQueryOf, ut as validateTailwind, v as detectDuplication, vt as runSessionStartCleanups, w as seoPostToolUseResponse, wt as devContext, x as lifecycleStdout, xt as purgeTtlTree, y as dryGate, yt as sessionStartCore, z as writePluginMap, zt as todayUtc } from "../handle-CBaOcFZq.mjs";
|
|
4
4
|
//#region src/runtime/storage.ts
|
|
5
5
|
/**
|
|
6
6
|
* The project's single state dir (`<root>/.harness`) — neutral + harness-agnostic,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fusengine/harness",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.90",
|
|
4
4
|
"description": "Harness-agnostic toolkit for AI coding agents: runtime harness detection (Claude Code, Codex, Cursor, Cline, Gemini, Aider...), pure policy core (env config, project/framework detection, SOLID/file-size limits, APEX freshness, guard patterns, portable prompts), cache, project memory, ref routing, state/locks, statusline, per-harness adapters (Claude/Cursor/Cline/Gemini) and a cli-mode harness-check binary. Bun-native, with a built dist for Node + bundlers.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "src/index.ts",
|
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
//#region src/adapters/codex/apply-patch.ts
|
|
2
|
-
const BEGIN = "*** Begin Patch";
|
|
3
|
-
const END = "*** End Patch";
|
|
4
|
-
const ADD = "*** Add File: ";
|
|
5
|
-
const DEL = "*** Delete File: ";
|
|
6
|
-
const UPD = "*** Update File: ";
|
|
7
|
-
const MOVE = "*** Move to: ";
|
|
8
|
-
const EOF = "*** End of File";
|
|
9
|
-
const ENV = "*** Environment ID: ";
|
|
10
|
-
/**
|
|
11
|
-
* Parse a Codex freeform patch into its per-file changes. Lenient on whitespace
|
|
12
|
-
* around structural markers (as Codex's own parser is); returns `[]` when no
|
|
13
|
-
* recognizable hunk is present (malformed input fails open, not closed).
|
|
14
|
-
* @param text - Raw patch body from `tool_input.command`.
|
|
15
|
-
* @returns One {@link PatchedFile} per Add/Update/Delete hunk, in order.
|
|
16
|
-
*/
|
|
17
|
-
function parseApplyPatch(text) {
|
|
18
|
-
const files = [];
|
|
19
|
-
let cur = null;
|
|
20
|
-
let buf = [];
|
|
21
|
-
const flush = () => {
|
|
22
|
-
if (cur) {
|
|
23
|
-
cur.content = buf.join("\n");
|
|
24
|
-
files.push(cur);
|
|
25
|
-
}
|
|
26
|
-
cur = null;
|
|
27
|
-
buf = [];
|
|
28
|
-
};
|
|
29
|
-
for (const line of text.split("\n")) {
|
|
30
|
-
const marker = line.trimStart();
|
|
31
|
-
if (marker === BEGIN || marker.startsWith(ENV)) continue;
|
|
32
|
-
if (marker === END) {
|
|
33
|
-
flush();
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (marker.startsWith(ADD)) {
|
|
37
|
-
flush();
|
|
38
|
-
cur = {
|
|
39
|
-
path: marker.slice(14).trim(),
|
|
40
|
-
content: "",
|
|
41
|
-
op: "add"
|
|
42
|
-
};
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
if (marker.startsWith(DEL)) {
|
|
46
|
-
flush();
|
|
47
|
-
files.push({
|
|
48
|
-
path: marker.slice(17).trim(),
|
|
49
|
-
content: "",
|
|
50
|
-
op: "delete"
|
|
51
|
-
});
|
|
52
|
-
continue;
|
|
53
|
-
}
|
|
54
|
-
if (marker.startsWith(UPD)) {
|
|
55
|
-
flush();
|
|
56
|
-
cur = {
|
|
57
|
-
path: marker.slice(17).trim(),
|
|
58
|
-
content: "",
|
|
59
|
-
op: "update"
|
|
60
|
-
};
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
if (marker.startsWith(MOVE)) {
|
|
64
|
-
if (cur) cur.path = marker.slice(13).trim();
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
if (marker === EOF || !cur) continue;
|
|
68
|
-
if (cur.op === "add") {
|
|
69
|
-
if (line.startsWith("+")) buf.push(line.slice(1));
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (line.startsWith("@@")) continue;
|
|
73
|
-
if (line.startsWith("+")) buf.push(line.slice(1));
|
|
74
|
-
else if (line.startsWith("-")) continue;
|
|
75
|
-
else buf.push(line.startsWith(" ") ? line.slice(1) : line);
|
|
76
|
-
}
|
|
77
|
-
flush();
|
|
78
|
-
return files;
|
|
79
|
-
}
|
|
80
|
-
//#endregion
|
|
81
|
-
//#region src/runtime/codex-shell-tool.ts
|
|
82
|
-
/**
|
|
83
|
-
* @module codex-shell-tool
|
|
84
|
-
* Canonicalizes a Codex-only shell `tool_name` alias to `"Bash"`, so every
|
|
85
|
-
* existing `tool === "Bash"` consumer (activity credit, explore-tools
|
|
86
|
-
* classification, the protected-path guard, the design corpus read gate, …)
|
|
87
|
-
* treats a Code Mode exec dispatch exactly like a native Bash call — no
|
|
88
|
-
* per-consumer change needed.
|
|
89
|
-
*
|
|
90
|
-
* A Code Mode-wrapped call (`functions.exec` -> `tools.exec_command(...)`)
|
|
91
|
-
* surfaces via the generic `CoreToolRuntime` hook default (openai/codex#23757,
|
|
92
|
-
* commit 5c20513), which keeps the raw function-tool name `"exec_command"`
|
|
93
|
-
* verbatim — unlike a DIRECT `exec_command` dispatch, which
|
|
94
|
-
* `ExecCommandHandler`'s own `pre_tool_use_payload` override re-labels
|
|
95
|
-
* `"Bash"` (`tool_name: HookToolName::bash()`). This is the exact call chain
|
|
96
|
-
* a real Codex CLI 0.146.0 session was observed emitting for a `sed -n` skill
|
|
97
|
-
* read. Only ONE alias is confirmed here — `ToolName::plain("exec_command")`
|
|
98
|
-
* in `core/src/tools/handlers/unified_exec/exec_command.rs`; `shell`,
|
|
99
|
-
* `unified_exec`, `local_shell`, and `shell_command` were audited but not
|
|
100
|
-
* evidenced as a distinct hook `tool_name` value, so they are deliberately
|
|
101
|
-
* left out rather than guessed.
|
|
102
|
-
* @packageDocumentation
|
|
103
|
-
*/
|
|
104
|
-
/** Codex `tool_name` values that are a shell execution but not literally `"Bash"`. */
|
|
105
|
-
const CODEX_SHELL_TOOL_ALIASES = /* @__PURE__ */ new Set(["exec_command"]);
|
|
106
|
-
/**
|
|
107
|
-
* Canonicalize a Codex-only shell tool alias to `"Bash"`. Scoped to
|
|
108
|
-
* `id === "codex"` — every other harness id, and every non-aliased tool
|
|
109
|
-
* name, passes through unchanged (see `test/codex-shell-read-credit.test.ts`
|
|
110
|
-
* for the non-regression proof).
|
|
111
|
-
* @param id - Harness adapter id (e.g. "codex", "claude-code", "kimi").
|
|
112
|
-
* @param tool - Raw `tool_name` from the hook payload.
|
|
113
|
-
*/
|
|
114
|
-
function canonicalizeCodexShellTool(id, tool) {
|
|
115
|
-
return id === "codex" && CODEX_SHELL_TOOL_ALIASES.has(tool) ? "Bash" : tool;
|
|
116
|
-
}
|
|
117
|
-
//#endregion
|
|
118
|
-
//#region src/adapters/codex/permission-mode.ts
|
|
119
|
-
/**
|
|
120
|
-
* Codex maps `AskForApproval::Never` -> `permission_mode: "bypassPermissions"`
|
|
121
|
-
* in its hook payload (codex-rs/core/src/hook_runtime.rs::hook_permission_mode)
|
|
122
|
-
* — the only value that isolates `never`; the other three approval policies
|
|
123
|
-
* (`untrusted`, `on-failure`, `on-request`) all collapse to `"default"`.
|
|
124
|
-
* Verified at openai/codex@342e4d4b, hook_runtime.rs run_pre_tool_use_hooks +
|
|
125
|
-
* pre_tool_use.rs PreToolUseRequest. Undocumented implementation detail, not
|
|
126
|
-
* a public contract — see the mapping-regression test that pins this string.
|
|
127
|
-
* @param permissionMode - The payload's `permission_mode` field, if present.
|
|
128
|
-
* @returns True only on an EXACT match — fail-closed. A near-miss
|
|
129
|
-
* (`"BypassPermissions"`, `"bypassPermissions "`, or a substring match) must
|
|
130
|
-
* never accidentally grant the neverApproval exemption.
|
|
131
|
-
*/
|
|
132
|
-
function isBypassPermissions(permissionMode) {
|
|
133
|
-
return permissionMode === "bypassPermissions";
|
|
134
|
-
}
|
|
135
|
-
//#endregion
|
|
136
|
-
export { canonicalizeCodexShellTool as n, parseApplyPatch as r, isBypassPermissions as t };
|