@fusengine/harness 0.1.67 → 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-Cu6rPxUZ.mjs → claude-CHpb1U0A.mjs} +1 -1
- package/dist/cli/bin.mjs +3 -3
- package/dist/cli/index.mjs +1 -1
- package/dist/{evaluate-CxgYAODk.mjs → evaluate-Dx6q1kmQ.mjs} +96 -25
- package/dist/{handle-CU5UdAei.mjs → handle-CVk0NlTg.mjs} +889 -129
- package/dist/{hermes-BpD0nlyR.mjs → hermes-CddsFoPN.mjs} +2 -2
- package/dist/{index-CU3oHxOe.d.mts → index-DzOJraAT.d.mts} +6 -0
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/policy/index.d.mts +1 -1
- package/dist/policy/index.mjs +1 -1
- package/dist/{run-BfJytXnW.mjs → run-B9Br7SEr.mjs} +1 -1
- package/dist/runtime/index.d.mts +21 -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 };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { r as resolveMaxLines } from "./limits-CHn8AIL1.mjs";
|
|
1
2
|
import { f as countLines, p as evaluateFileSize, r as fusengineCache, t as claudeHome } from "./home-state-D0RLWP8J.mjs";
|
|
2
3
|
import { dirname, join, normalize } from "node:path";
|
|
3
4
|
import { existsSync } from "node:fs";
|
|
@@ -146,6 +147,53 @@ function resolveSolidRefFramework(filePath) {
|
|
|
146
147
|
return "generic";
|
|
147
148
|
}
|
|
148
149
|
//#endregion
|
|
150
|
+
//#region src/policy/edit-outcome.ts
|
|
151
|
+
/**
|
|
152
|
+
* Count non-overlapping literal occurrences of `needle` in `haystack`. An
|
|
153
|
+
* empty `needle` returns 0 — treated as "not found" by callers, since Edit's
|
|
154
|
+
* `old_string` is never legitimately empty and an empty needle has no
|
|
155
|
+
* well-defined occurrence count.
|
|
156
|
+
* @param haystack - The on-disk file content to search.
|
|
157
|
+
* @param needle - The literal substring to count.
|
|
158
|
+
*/
|
|
159
|
+
function countOccurrences(haystack, needle) {
|
|
160
|
+
if (needle === "") return 0;
|
|
161
|
+
let count = 0;
|
|
162
|
+
let from = 0;
|
|
163
|
+
for (;;) {
|
|
164
|
+
const at = haystack.indexOf(needle, from);
|
|
165
|
+
if (at === -1) break;
|
|
166
|
+
count++;
|
|
167
|
+
from = at + needle.length;
|
|
168
|
+
}
|
|
169
|
+
return count;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Compute the line count an Edit would PRODUCE, before it lands on disk —
|
|
173
|
+
* `existingLines − countLines(old_string) + countLines(new_string)`, scaled
|
|
174
|
+
* by how many `old_string` occurrences are actually replaced (1 normally, or
|
|
175
|
+
* every occurrence under `replace_all`). Pure arithmetic on the literal
|
|
176
|
+
* strings — no regex/fuzzy matching, matching the real Edit tool's own
|
|
177
|
+
* exact-substring semantics.
|
|
178
|
+
*
|
|
179
|
+
* Returns `null` (fail-closed — caller must fall back to judging the stale
|
|
180
|
+
* on-disk count alone) when `old_string` is missing/empty, or does not occur
|
|
181
|
+
* in `existingContent` at all: an Edit whose `old_string` doesn't match the
|
|
182
|
+
* real file is stale or adversarial, never a case to special-case an allow for.
|
|
183
|
+
* @param existingContent - Full on-disk content of the file being edited.
|
|
184
|
+
* @param oldString - The Edit's `old_string` (tool_input, unmodified).
|
|
185
|
+
* @param newString - The Edit's `new_string` (tool_input, unmodified).
|
|
186
|
+
* @param replaceAll - The Edit's `replace_all` flag.
|
|
187
|
+
*/
|
|
188
|
+
function computeEditResultLines(existingContent, oldString, newString, replaceAll) {
|
|
189
|
+
if (!oldString) return null;
|
|
190
|
+
const occurrences = countOccurrences(existingContent, oldString);
|
|
191
|
+
if (occurrences === 0) return null;
|
|
192
|
+
const times = replaceAll ? occurrences : 1;
|
|
193
|
+
const delta = countLines(newString) - countLines(oldString);
|
|
194
|
+
return countLines(existingContent) + delta * times;
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
149
197
|
//#region src/policy/guards/security.ts
|
|
150
198
|
/** Critical patterns that must always be blocked — parity `security_rules.py`'s cumulated violation names. */
|
|
151
199
|
const CRITICAL_PATTERNS = [
|
|
@@ -822,6 +870,43 @@ function buildNeverApprovalPrompt(cmd) {
|
|
|
822
870
|
};
|
|
823
871
|
}
|
|
824
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
|
|
825
910
|
//#region src/policy/evaluate.ts
|
|
826
911
|
/**
|
|
827
912
|
* Evaluate a single tool-use against the bundled policies, returning a pure
|
|
@@ -845,35 +930,21 @@ function evaluate(ctx) {
|
|
|
845
930
|
prompt
|
|
846
931
|
};
|
|
847
932
|
}
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
return {
|
|
851
|
-
decision: "deny",
|
|
852
|
-
message: reason,
|
|
853
|
-
prompt: {
|
|
854
|
-
kind: "block",
|
|
855
|
-
title: "Destructive git command",
|
|
856
|
-
reason,
|
|
857
|
-
actions: ["Use a non-destructive alternative (e.g. --force-with-lease; avoid --hard / -D)"]
|
|
858
|
-
}
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
|
-
if (!ralphSafe && ctx.command && matchPatterns(ctx.command, GIT_ASK)) return {
|
|
862
|
-
decision: "deny",
|
|
863
|
-
message: `Git operation requires confirmation: ${ctx.command}`,
|
|
864
|
-
prompt: {
|
|
865
|
-
kind: "ask",
|
|
866
|
-
title: "Confirm git operation",
|
|
867
|
-
reason: `Authorize: ${ctx.command.trim()}`,
|
|
868
|
-
actions: ["Approve if this git operation is intended"]
|
|
869
|
-
}
|
|
870
|
-
};
|
|
933
|
+
const gitGate = evaluateGitGates(ctx.command, ralphSafe);
|
|
934
|
+
if (gitGate) return gitGate;
|
|
871
935
|
if (ctx.filePath && isFileSizeScoped(ctx.filePath) && ctx.agentType !== "Explore" && ctx.agentType !== "Plan") {
|
|
872
936
|
const incoming = ctx.content !== void 0 ? countLines(ctx.content) : 0;
|
|
873
|
-
const
|
|
937
|
+
const existing = ctx.existingLines ?? 0;
|
|
938
|
+
const max = ctx.maxLines ?? resolveMaxLines();
|
|
939
|
+
const editResult = ctx.tool === "Edit" && ctx.existingContent !== void 0 && ctx.content !== void 0 ? computeEditResultLines(ctx.existingContent, ctx.oldString, ctx.content, ctx.isReplaceAll === true) : null;
|
|
940
|
+
if (editResult !== null && (editResult <= max || editResult < existing)) return {
|
|
941
|
+
decision: "allow",
|
|
942
|
+
message: null
|
|
943
|
+
};
|
|
944
|
+
const lines = ctx.tool === "Edit" ? editResult ?? Math.max(incoming, existing) : incoming || existing;
|
|
874
945
|
const framework = resolveSolidRefFramework(ctx.filePath);
|
|
875
946
|
const displayLines = ctx.tool === "Write" ? ctx.existingLines ?? lines : lines;
|
|
876
|
-
const verdict = evaluateFileSize(lines,
|
|
947
|
+
const verdict = evaluateFileSize(lines, max, ctx.filePath, framework, displayLines);
|
|
877
948
|
if (lines > 0 && !verdict.ok) return {
|
|
878
949
|
decision: "deny",
|
|
879
950
|
message: verdict.message,
|