@fusengine/harness 0.1.68 → 0.1.69
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/README.md +74 -5
- package/assets/song/finish.mp3 +0 -0
- package/assets/song/need-human.mp3 +0 -0
- package/assets/song/permission-need.mp3 +0 -0
- package/dist/adapters/claude/index.mjs +1 -1
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.mjs +2 -2
- package/dist/adapters/cursor/index.mjs +1 -1
- package/dist/adapters/gemini/index.mjs +1 -1
- package/dist/adapters/hermes/index.mjs +1 -1
- package/dist/{claude-BmchLlp7.mjs → claude-CHpb1U0A.mjs} +1 -1
- package/dist/cli/bin.mjs +3 -3
- package/dist/cli/index.mjs +1 -1
- package/dist/{evaluate-CRZ_Hq33.mjs → evaluate-Dx6q1kmQ.mjs} +39 -23
- package/dist/{handle-D8rvbAfw.mjs → handle-CVk0NlTg.mjs} +881 -127
- package/dist/{hermes-dbvFJRuY.mjs → hermes-CddsFoPN.mjs} +2 -2
- package/dist/index.mjs +1 -1
- package/dist/policy/index.mjs +1 -1
- package/dist/{run-BJH3Wx_b.mjs → run-B9Br7SEr.mjs} +1 -1
- package/dist/runtime/index.d.mts +17 -3
- package/dist/runtime/index.mjs +2 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ also works elsewhere.
|
|
|
125
125
|
| Harness | PreToolUse coverage | Lifecycle (Session/Subagent/Stop/Compact/…) | Known limit |
|
|
126
126
|
|---|---|---|---|
|
|
127
127
|
| **claude-code** | Full: `evaluate` + APEX gates via `handleHook` (`src/adapters/claude/index.ts`) | 14 event types implemented (`dispatch.ts`) — fires once wired into `.claude/settings.json` beyond the `init` default | None found; richer lifecycle needs manual/marketplace wiring (see Quickstart) |
|
|
128
|
-
| **codex** | Bash gated reliably; **`apply_patch` edits are now gated too** — the patch text is parsed per file (`adapters/codex/apply-patch.ts`), each hunk runs the file gates and ONE violating hunk denies the whole patch (`runtime/apply-patch-gate.ts`, sim scenario 22 incl. the multi-file smuggling case). `ask` prompts are **downgraded to explicit deny** (`respond.ts`, sim scenario 23) because Codex fails open on unsupported shapes. | Not wired by `harness init codex` (PreToolUse `Bash\|apply_patch` + PostToolUse only, `src/init/templates.ts:29-38`) | Upstream caveat: Codex itself does not always enforce a correct `apply_patch` deny (openai/codex#27833) — we emit the right verdict; enforcement is theirs. No interactive `ask`. |
|
|
128
|
+
| **codex** | Bash gated reliably; **`apply_patch` edits are now gated too** — the patch text is parsed per file (`adapters/codex/apply-patch.ts`), each hunk runs the file gates and ONE violating hunk denies the whole patch (`runtime/apply-patch-gate.ts`, sim scenario 22 incl. the multi-file smuggling case). `ask` prompts are **downgraded to explicit deny** (`respond.ts`, sim scenario 23) because Codex fails open on unsupported shapes. On the PostToolUse side, an allowed patch is fanned into one synthetic per-file event so the SOLID/tracking/post-edit handlers see every touched file (`runtime/post-fanout.ts`), and a non-blocking security advisory fires on the first qualifying file (`securityAdvisoryForPatch`). | Not wired by `harness init codex` (PreToolUse `Bash\|apply_patch` + PostToolUse only, `src/init/templates.ts:29-38`) — `Stop` (session cleanup + the SOLID/receipt completion check, since Codex never emits `SessionEnd`/`TaskCompleted`, `runtime/lifecycle/stop-core.ts`) and `SessionStart` (plugin agents/commands cache resync, `runtime/lifecycle/codex-resync/`) ARE implemented but, same caveat as claude-code's extra lifecycle above, only fire once the codex-plugins marketplace's own `hooks.json` wires them. | Upstream caveat: Codex itself does not always enforce a correct `apply_patch` deny (openai/codex#27833) — we emit the right verdict; enforcement is theirs. No interactive `ask`. Codex has no native `PostToolUseFailure`; a failure is inferred from the ordinary `PostToolUse` result shape (non-zero exit or an explicit error field only — never a guess) and journaled into the one-shot failure tally (`src/tracking/codex-post-failure.ts`). |
|
|
129
129
|
| **cursor** | `beforeShellExecution` can deny/ask (shell only, `cursor/index.ts:16-21`) | none | File edits are **advisory only**: `afterFileEdit` always returns `allow` + a `user_message` correction on violation — a `deny` there has no proven effect (hook was "informational only" at launch, and Cursor's deny-enforcement for file ops is confirmed broken upstream, forum.cursor.com/t/154377). Human sees the message; the model is never re-informed. Platform ceiling, documented in `cursor/index.ts`. |
|
|
130
130
|
| **gemini-cli** | `BeforeTool` denies via `{decision:"deny",reason}` (`gemini/index.ts:22-36`) | none | Thin stateless adapter — no session track, no APEX gates wired through it. |
|
|
131
131
|
| **cline** | `PreToolUse` only; block → `{cancel:true}`, non-block → `contextModification` (`cline/index.ts:24-36`) | none | Same as gemini-cli: stateless guard only. |
|
|
@@ -198,6 +198,72 @@ Features shipped since 0.1.44, each with its own test:
|
|
|
198
198
|
- **Hook simulator** — 18 end-to-end scenarios (payload in, expected verdict out)
|
|
199
199
|
replayed against the real CLI in both `src` and built `dist` modes in CI
|
|
200
200
|
(`test/sim/README.md`, `test/sim/scenarios/`).
|
|
201
|
+
- **Codex `multi_agent_v2` custom-agent evidence** — a `spawn_agent` call
|
|
202
|
+
carrying `tool_input.agent_type` is recorded as `subagent-<agent_type>` in the
|
|
203
|
+
same SessionTrack as Claude Task evidence, so a Codex explore-codebase/sniper
|
|
204
|
+
satisfies the APEX freshness gates exactly like a Claude subagent
|
|
205
|
+
(`src/tracking/session-state.ts`, sim scenario 32b).
|
|
206
|
+
- **File-size gate judges the real Edit outcome, not the on-disk count** — an
|
|
207
|
+
Edit on a file already over `FUSE_SOLID_MAX_LINES` used to be denied
|
|
208
|
+
unconditionally (the on-disk count always won); the gate now computes the
|
|
209
|
+
actual post-edit line count and allows a shrinking Edit while still denying a
|
|
210
|
+
computable growth past the limit (`src/policy/file-size.ts`, sim scenario 33).
|
|
211
|
+
- **Codex `apply_patch` fan-out to post-handlers** — each file in a patch
|
|
212
|
+
envelope becomes a synthetic per-file PostToolUse event (`add`→`Write`,
|
|
213
|
+
`update`→`Edit`, `delete` no-ops on the Write/Edit-only handlers) instead of
|
|
214
|
+
the whole-patch shape whose own `filePath`/`content` are always undefined
|
|
215
|
+
(`src/runtime/post-fanout.ts`, `test/apply-patch-post.test.ts`).
|
|
216
|
+
- **Non-blocking multi-file security advisory** — the same PreToolUse "read the
|
|
217
|
+
security skill" nudge a single Write/Edit gets is now evaluated per file
|
|
218
|
+
across a Codex `apply_patch` envelope, firing on the first qualifying
|
|
219
|
+
add/update code file (delete ignored outright) — `additionalContext` only,
|
|
220
|
+
never a deny (`src/runtime/lifecycle/security/check-skill.ts::securityAdvisoryForPatch`,
|
|
221
|
+
`test/security-advisory-patch.test.ts`).
|
|
222
|
+
- **Codex SessionStart agent/command resync** — the plugin agents/commands
|
|
223
|
+
cache under `~/.codex/{agents,prompts}` is re-materialized only when its
|
|
224
|
+
sha256 fingerprint changed (or a command symlink dangles) since the last
|
|
225
|
+
apply, otherwise a cheap no-op; agents are copied (Codex won't load
|
|
226
|
+
symlinked TOMLs), commands are symlinked; a best-effort inter-process lock
|
|
227
|
+
avoids a torn cache under concurrent session starts (`src/runtime/lifecycle/codex-resync/`,
|
|
228
|
+
`test/resync-agents.test.ts`).
|
|
229
|
+
- **Codex failure tracking inferred from PostToolUse** — Codex reports every
|
|
230
|
+
outcome, success or failure, on the same `PostToolUse` event; a failure is
|
|
231
|
+
classified from an explicit signal only (non-zero exit, an error field —
|
|
232
|
+
never "can't tell") and journaled into the existing one-shot failure tally
|
|
233
|
+
(`src/tracking/codex-post-failure.ts`, `test/codex-post-failure.test.ts`).
|
|
234
|
+
- **Shell-credited ref reads** — a `.md` SOLID/skill reference read through a
|
|
235
|
+
read-only shell command (`cat`/`head`/`tail`/`sed`/`rg`/`less`/`more`/`bat`,
|
|
236
|
+
including inside a `sh -c` wrapper) now credits the same ref-read evidence a
|
|
237
|
+
native `Read` would, so a Codex teammate's shell-first consultation still
|
|
238
|
+
satisfies `solidReadGate`; `sed -i`/`--in-place` and any non-whitelisted
|
|
239
|
+
command are never credited (`src/policy/shell-read-refs.ts`,
|
|
240
|
+
`test/shell-read-refs.test.ts`).
|
|
241
|
+
|
|
242
|
+
### Notification sounds
|
|
243
|
+
|
|
244
|
+
A native OS sound plays on two lifecycle events: the core-scope `Stop` under
|
|
245
|
+
Codex (`stopCore`, since Claude Code already voices its own turn-end via a
|
|
246
|
+
native `afplay` hook — this harness deliberately does not double it) and
|
|
247
|
+
`TeammateIdle` under Claude Code when there's an actionable notice (a missing
|
|
248
|
+
deliverable or a sniper reminder). **On by default**; opt out entirely with
|
|
249
|
+
`FUSE_HARNESS_SOUND=0`.
|
|
250
|
+
|
|
251
|
+
Resolution per event kind (`stop`/`permission`/`human`) cascades: (1) a
|
|
252
|
+
per-kind env override path (`FUSE_HARNESS_SOUND_STOP` / `_PERMISSION` /
|
|
253
|
+
`_HUMAN`) if it exists on disk; (2) the package's own bundled
|
|
254
|
+
`assets/song/{finish,permission-need,need-human}.mp3` (located by walking up
|
|
255
|
+
to the running `package.json`, so it survives the flat/hashed `dist/` build);
|
|
256
|
+
(3) `$CLAUDE_PLUGIN_ROOT/song/<file>.mp3` as a last-resort fallback. No file
|
|
257
|
+
resolves → silent no-op. The `permission` kind and its `.mp3`/env var exist in
|
|
258
|
+
the module for symmetry but have no current call site — permission prompts
|
|
259
|
+
stay native-only on every harness today.
|
|
260
|
+
|
|
261
|
+
Playback is native per platform (`afplay` on darwin, `paplay` on linux,
|
|
262
|
+
PowerShell's `SoundPlayer` on win32) and **absolutely fail-open**: an
|
|
263
|
+
unsupported platform, a missing player binary, an undecodable file, or a
|
|
264
|
+
non-zero exit is swallowed — a broken or absent player can never break a hook
|
|
265
|
+
(`src/runtime/notifications.ts`, `src/runtime/notification-sound.ts`,
|
|
266
|
+
`test/notifications.test.ts`).
|
|
201
267
|
|
|
202
268
|
### Environment
|
|
203
269
|
|
|
@@ -214,6 +280,8 @@ Features shipped since 0.1.44, each with its own test:
|
|
|
214
280
|
| `FUSE_WEBFETCH_TTL_SEC` | WebFetch cache freshness, seconds (default 24h — pages stale faster than docs). |
|
|
215
281
|
| `RALPH_MODE` | **Opt-in (default off).** Exempts safe git commands (`add`/`commit`/`checkout -b`/`status`/`diff`/`log`) from the confirmation ask and auto-approves project installs. Destructive git (force-push, `reset --hard`) and system installs still gate. |
|
|
216
282
|
| `CLAUDE_PROJECT_DIR` | Overrides the project root used to hash the out-of-tree state dir (`src/runtime/paths.ts:20`). |
|
|
283
|
+
| `FUSE_HARNESS_SOUND` | **On by default.** Set to `0` to disable every lifecycle notification sound. |
|
|
284
|
+
| `FUSE_HARNESS_SOUND_STOP` / `_PERMISSION` / `_HUMAN` | Per-kind override: an absolute path to a sound file to play instead of the bundled `assets/song/*.mp3` for that kind (see [Notification sounds](#notification-sounds)). |
|
|
217
285
|
|
|
218
286
|
## Library usage
|
|
219
287
|
|
|
@@ -286,9 +354,10 @@ Run `bun run docs:api` for the generated typedoc API reference.
|
|
|
286
354
|
|
|
287
355
|
## Known limitations
|
|
288
356
|
|
|
289
|
-
- **Codex
|
|
290
|
-
|
|
291
|
-
|
|
357
|
+
- **Codex `apply_patch` enforcement depends on Codex itself.** The harness
|
|
358
|
+
parses the patch and emits the right verdict per file, but Codex does not
|
|
359
|
+
always enforce a correct deny on its end (openai/codex#27833, see
|
|
360
|
+
[Compatibility](#compatibility)) — enforcement beyond the verdict is theirs.
|
|
292
361
|
- **Cursor file edits are advisory-only.** `afterFileEdit` fires after the edit
|
|
293
362
|
already happened — a platform limit, not something this harness can work around.
|
|
294
363
|
- **Hook fan-out is mitigated, not eliminated.** The ~11-sibling-plugin burst is
|
|
@@ -306,7 +375,7 @@ Run `bun run docs:api` for the generated typedoc API reference.
|
|
|
306
375
|
## Develop
|
|
307
376
|
|
|
308
377
|
```sh
|
|
309
|
-
bun test #
|
|
378
|
+
bun test # 662 tests (120 files)
|
|
310
379
|
bunx tsc --noEmit # typecheck (isolatedDeclarations)
|
|
311
380
|
bun run build # dist + .d.mts via tsdown (for Node/bundler consumers)
|
|
312
381
|
bun run docs:api # generate the typedoc API reference
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as fileSizeGuard, c as readClaudeInput, i as denyResponse, l as systemMessage, n as blockResponse, o as guard, r as contextResponse, s as informResponse, t as attachSystemMessage, u as toClaudeResponse } from "../../claude-
|
|
1
|
+
import { a as fileSizeGuard, c as readClaudeInput, i as denyResponse, l as systemMessage, n as blockResponse, o as guard, r as contextResponse, s as informResponse, t as attachSystemMessage, u as toClaudeResponse } from "../../claude-CHpb1U0A.mjs";
|
|
2
2
|
export { attachSystemMessage, blockResponse, contextResponse, denyResponse, fileSizeGuard, guard, informResponse, readClaudeInput, systemMessage, toClaudeResponse };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { f as countLines } from "../../home-state-D0RLWP8J.mjs";
|
|
2
|
-
import { t as evaluate } from "../../evaluate-
|
|
2
|
+
import { t as evaluate } from "../../evaluate-Dx6q1kmQ.mjs";
|
|
3
3
|
import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
|
|
4
4
|
import { n as parseApplyPatch, t as isBypassPermissions } from "../../permission-mode-BN3MNgbm.mjs";
|
|
5
|
-
import { c as readClaudeInput, i as denyResponse, r as contextResponse, s as informResponse, v as commandToString } from "../../claude-
|
|
5
|
+
import { c as readClaudeInput, i as denyResponse, r as contextResponse, s as informResponse, v as commandToString } from "../../claude-CHpb1U0A.mjs";
|
|
6
6
|
//#region src/adapters/codex/index.ts
|
|
7
7
|
/**
|
|
8
8
|
* OpenAI Codex CLI adapter (hook-mode). Codex's `PreToolUse` hook (since 2026)
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as readHermesInput, r as toHermesResponse, t as guard } from "../../hermes-
|
|
1
|
+
import { n as readHermesInput, r as toHermesResponse, t as guard } from "../../hermes-CddsFoPN.mjs";
|
|
2
2
|
export { guard, readHermesInput, toHermesResponse };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as evaluate } from "./evaluate-
|
|
1
|
+
import { t as evaluate } from "./evaluate-Dx6q1kmQ.mjs";
|
|
2
2
|
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
package/dist/cli/bin.mjs
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
import { r as loadDotenv, s as resolveTtlSec } from "../dotenv-Jj8aL1FL.mjs";
|
|
3
3
|
import { t as detectHarness } from "../harness-Cb9xR8dC.mjs";
|
|
4
4
|
import { t as claudeHome } from "../home-state-D0RLWP8J.mjs";
|
|
5
|
-
import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-
|
|
5
|
+
import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-B9Br7SEr.mjs";
|
|
6
6
|
import { n as writeInitFile, t as initFor } from "../run-Do2JltgU.mjs";
|
|
7
|
-
import {
|
|
8
|
-
import { p as readStdin$1 } from "../claude-
|
|
7
|
+
import { $ as versionBanner, Q as runningVersion, Rt as todayUtc, Z as runDoctor, t as handleHook } from "../handle-CVk0NlTg.mjs";
|
|
8
|
+
import { p as readStdin$1 } from "../claude-CHpb1U0A.mjs";
|
|
9
9
|
import { delimiter, join } from "node:path";
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
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-B9Br7SEr.mjs";
|
|
2
2
|
export { checkStaged, stagedContent, stagedFiles };
|
|
@@ -870,6 +870,43 @@ function buildNeverApprovalPrompt(cmd) {
|
|
|
870
870
|
};
|
|
871
871
|
}
|
|
872
872
|
//#endregion
|
|
873
|
+
//#region src/policy/git-gates.ts
|
|
874
|
+
/**
|
|
875
|
+
* Evaluate the two git confirmation gates — destructive block ({@link
|
|
876
|
+
* GIT_BLOCKED}) and confirmation ask ({@link GIT_ASK}) — against `command`.
|
|
877
|
+
* Skipped entirely when `ralphSafe` is true: Ralph mode already silent-
|
|
878
|
+
* allowed the command upstream (parity with evaluate.ts's pre-existing
|
|
879
|
+
* `!ralphSafe &&` guards). Returns the matching gate's {@link PolicyResult},
|
|
880
|
+
* or `null` when neither gate matches.
|
|
881
|
+
*/
|
|
882
|
+
function evaluateGitGates(command, ralphSafe) {
|
|
883
|
+
if (ralphSafe || !command) return null;
|
|
884
|
+
if (matchPatterns(command, GIT_BLOCKED)) {
|
|
885
|
+
const reason = `Destructive git command: ${command}`;
|
|
886
|
+
return {
|
|
887
|
+
decision: "deny",
|
|
888
|
+
message: reason,
|
|
889
|
+
prompt: {
|
|
890
|
+
kind: "block",
|
|
891
|
+
title: "Destructive git command",
|
|
892
|
+
reason,
|
|
893
|
+
actions: ["Use a non-destructive alternative (e.g. --force-with-lease; avoid --hard / -D)"]
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
if (matchPatterns(command, GIT_ASK)) return {
|
|
898
|
+
decision: "deny",
|
|
899
|
+
message: `Git operation requires confirmation: ${command}`,
|
|
900
|
+
prompt: {
|
|
901
|
+
kind: "ask",
|
|
902
|
+
title: "Confirm git operation",
|
|
903
|
+
reason: `Authorize: ${command.trim()}`,
|
|
904
|
+
actions: ["Approve if this git operation is intended"]
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
909
|
+
//#endregion
|
|
873
910
|
//#region src/policy/evaluate.ts
|
|
874
911
|
/**
|
|
875
912
|
* Evaluate a single tool-use against the bundled policies, returning a pure
|
|
@@ -893,29 +930,8 @@ function evaluate(ctx) {
|
|
|
893
930
|
prompt
|
|
894
931
|
};
|
|
895
932
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
return {
|
|
899
|
-
decision: "deny",
|
|
900
|
-
message: reason,
|
|
901
|
-
prompt: {
|
|
902
|
-
kind: "block",
|
|
903
|
-
title: "Destructive git command",
|
|
904
|
-
reason,
|
|
905
|
-
actions: ["Use a non-destructive alternative (e.g. --force-with-lease; avoid --hard / -D)"]
|
|
906
|
-
}
|
|
907
|
-
};
|
|
908
|
-
}
|
|
909
|
-
if (!ralphSafe && ctx.command && matchPatterns(ctx.command, GIT_ASK)) return {
|
|
910
|
-
decision: "deny",
|
|
911
|
-
message: `Git operation requires confirmation: ${ctx.command}`,
|
|
912
|
-
prompt: {
|
|
913
|
-
kind: "ask",
|
|
914
|
-
title: "Confirm git operation",
|
|
915
|
-
reason: `Authorize: ${ctx.command.trim()}`,
|
|
916
|
-
actions: ["Approve if this git operation is intended"]
|
|
917
|
-
}
|
|
918
|
-
};
|
|
933
|
+
const gitGate = evaluateGitGates(ctx.command, ralphSafe);
|
|
934
|
+
if (gitGate) return gitGate;
|
|
919
935
|
if (ctx.filePath && isFileSizeScoped(ctx.filePath) && ctx.agentType !== "Explore" && ctx.agentType !== "Plan") {
|
|
920
936
|
const incoming = ctx.content !== void 0 ? countLines(ctx.content) : 0;
|
|
921
937
|
const existing = ctx.existingLines ?? 0;
|