@fusengine/harness 0.1.32 → 0.1.34
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/claude/index.mjs +1 -1
- package/dist/adapters/cline/index.mjs +1 -1
- package/dist/adapters/codex/index.mjs +1 -1
- package/dist/adapters/cursor/index.mjs +1 -1
- package/dist/adapters/gemini/index.mjs +1 -1
- package/dist/claude-3PqBGt_7.mjs +137 -0
- package/dist/cli/bin.mjs +3 -3
- package/dist/cli/index.mjs +1 -1
- package/dist/{evaluate-9ch1K2kt.mjs → evaluate-zyxeVZPB.mjs} +44 -10
- package/dist/freshness/index.mjs +1 -1
- package/dist/{freshness-otdUpuvP.mjs → freshness-43gxYpiX.mjs} +1 -1
- package/dist/{handle-DJqHZcml.mjs → handle-DMHjTY1E.mjs} +192 -92
- package/dist/{index-DmbOUJK8.d.mts → index-QzK2dv0V.d.mts} +12 -2
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +4 -4
- package/dist/init/index.mjs +1 -1
- package/dist/{json-io-CAn72gI4.mjs → json-io-CvSumjtz.mjs} +6 -2
- package/dist/policy/index.d.mts +1 -1
- package/dist/policy/index.mjs +2 -2
- package/dist/{run-CQbtlAKa.mjs → run-CUL70W0k.mjs} +1 -1
- package/dist/{run-D91N4ul1.mjs → run-Do2JltgU.mjs} +2 -2
- package/dist/runtime/index.d.mts +29 -2
- package/dist/runtime/index.mjs +2 -2
- package/dist/state/index.mjs +1 -1
- package/dist/{state-ByhLeKyD.mjs → state-BthKK4Jj.mjs} +1 -1
- package/dist/store-7j02oGjt.mjs +184 -0
- package/dist/tracking/index.d.mts +12 -2
- package/dist/tracking/index.mjs +1 -1
- package/dist/{validate-CccewDwk.mjs → validate-DLrWtaDR.mjs} +1 -1
- package/package.json +1 -1
- package/dist/claude-BWZcrZbS.mjs +0 -66
- package/dist/store-D-ge2ZPI.mjs +0 -89
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as readClaudeInput, i as guard, n as denyResponse, o as toClaudeResponse, r as fileSizeGuard, t as contextResponse } from "../../claude-
|
|
1
|
+
import { a as readClaudeInput, i as guard, n as denyResponse, o as toClaudeResponse, r as fileSizeGuard, t as contextResponse } from "../../claude-3PqBGt_7.mjs";
|
|
2
2
|
export { contextResponse, denyResponse, fileSizeGuard, guard, readClaudeInput, toClaudeResponse };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as readClaudeInput, i as guard, n as denyResponse, t as contextResponse } from "../../claude-
|
|
1
|
+
import { a as readClaudeInput, i as guard, n as denyResponse, t as contextResponse } from "../../claude-3PqBGt_7.mjs";
|
|
2
2
|
export { contextResponse, denyResponse, guard, readClaudeInput as readCodexInput };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { t as evaluate } from "./evaluate-zyxeVZPB.mjs";
|
|
2
|
+
import { t as formatPrompt } from "./types-ernB1Dy3.mjs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { spawnSync } from "node:child_process";
|
|
6
|
+
//#region src/util/runtime-io.ts
|
|
7
|
+
/**
|
|
8
|
+
* Cross-runtime I/O helpers (Node + Bun). The published CLI ships a `node`
|
|
9
|
+
* shebang and is invoked via `npx`/`npm`/`bunx`/`bun`; using only `node:*` APIs
|
|
10
|
+
* here keeps the bundle runnable under EVERY runtime — replacing the Bun-only
|
|
11
|
+
* `Bun.file/write/spawn/sleep/stdin` calls and the `bun` `Glob` import that made
|
|
12
|
+
* the package crash with `Cannot find package 'bun'` under plain Node.
|
|
13
|
+
*/
|
|
14
|
+
/** Read a file as UTF-8 text (throws on missing/unreadable — callers catch). */
|
|
15
|
+
function readText(path) {
|
|
16
|
+
return readFileSync(path, "utf8");
|
|
17
|
+
}
|
|
18
|
+
/** True when `path` exists. */
|
|
19
|
+
function pathExists(path) {
|
|
20
|
+
return existsSync(path);
|
|
21
|
+
}
|
|
22
|
+
/** Write `data` to `path`, creating parent dirs (mirrors `Bun.write`). */
|
|
23
|
+
function writeText(path, data) {
|
|
24
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
25
|
+
writeFileSync(path, data, { encoding: "utf8" });
|
|
26
|
+
}
|
|
27
|
+
/** Resolve after `ms` milliseconds (`Bun.sleep` replacement). */
|
|
28
|
+
function sleep(ms) {
|
|
29
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
|
+
}
|
|
31
|
+
/** Run `cmd args` in `cwd` and capture stdout text ("" on failure/non-zero). */
|
|
32
|
+
function spawnCapture(cmd, args, cwd) {
|
|
33
|
+
try {
|
|
34
|
+
const r = spawnSync(cmd, args, {
|
|
35
|
+
cwd,
|
|
36
|
+
encoding: "utf8"
|
|
37
|
+
});
|
|
38
|
+
return r.status === 0 ? r.stdout ?? "" : "";
|
|
39
|
+
} catch {
|
|
40
|
+
return "";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Read the full process stdin as UTF-8 text (works under Node and Bun). */
|
|
44
|
+
async function readStdin() {
|
|
45
|
+
const chunks = [];
|
|
46
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
47
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Recursively collect files under `dir` whose extension is in `exts`, skipping
|
|
51
|
+
* `node_modules` and dot-dirs, capped at `cap`. `Bun.Glob().scan()` replacement.
|
|
52
|
+
* @param dir - Directory to walk.
|
|
53
|
+
* @param exts - Allowed extensions including the dot (e.g. `.ts`).
|
|
54
|
+
* @param out - Accumulator (mutated in place).
|
|
55
|
+
* @param cap - Max files to collect.
|
|
56
|
+
*/
|
|
57
|
+
function collectFiles(dir, exts, out, cap) {
|
|
58
|
+
if (out.length >= cap) return;
|
|
59
|
+
let entries;
|
|
60
|
+
try {
|
|
61
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
62
|
+
} catch {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const e of entries) {
|
|
66
|
+
if (out.length >= cap) return;
|
|
67
|
+
if (e.name === "node_modules" || e.name.startsWith(".")) continue;
|
|
68
|
+
const full = join(dir, e.name);
|
|
69
|
+
if (e.isDirectory()) collectFiles(full, exts, out, cap);
|
|
70
|
+
else if (exts.has(e.name.slice(e.name.lastIndexOf(".")))) out.push(full);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/adapters/claude/index.ts
|
|
75
|
+
/**
|
|
76
|
+
* Claude Code adapter — the thin Claude-only shim over the portable policy core.
|
|
77
|
+
* Reads the hook stdin payload and emits hookSpecificOutput responses.
|
|
78
|
+
*/
|
|
79
|
+
/** Read & parse the Claude hook payload from stdin (empty object on bad input). */
|
|
80
|
+
async function readClaudeInput() {
|
|
81
|
+
const text = await readStdin();
|
|
82
|
+
if (!text.trim()) return {};
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(text);
|
|
85
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
86
|
+
} catch {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** A `deny` hook response for a given event. */
|
|
91
|
+
function denyResponse(event, reason) {
|
|
92
|
+
return JSON.stringify({ hookSpecificOutput: {
|
|
93
|
+
hookEventName: event,
|
|
94
|
+
permissionDecision: "deny",
|
|
95
|
+
permissionDecisionReason: reason
|
|
96
|
+
} });
|
|
97
|
+
}
|
|
98
|
+
/** An `additionalContext` injection response. */
|
|
99
|
+
function contextResponse(event, text) {
|
|
100
|
+
return JSON.stringify({ hookSpecificOutput: {
|
|
101
|
+
hookEventName: event,
|
|
102
|
+
additionalContext: text
|
|
103
|
+
} });
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Render a portable {@link Prompt} as a Claude Code hook response:
|
|
107
|
+
* `block` → `permissionDecision: deny`, `ask` → `permissionDecision: ask`
|
|
108
|
+
* (interactive confirm), `inform` → `additionalContext`.
|
|
109
|
+
*/
|
|
110
|
+
function toClaudeResponse(event, prompt) {
|
|
111
|
+
const reason = formatPrompt(prompt);
|
|
112
|
+
if (prompt.kind === "block") return denyResponse(event, reason);
|
|
113
|
+
if (prompt.kind === "ask") return JSON.stringify({ hookSpecificOutput: {
|
|
114
|
+
hookEventName: event,
|
|
115
|
+
permissionDecision: "ask",
|
|
116
|
+
permissionDecisionReason: reason
|
|
117
|
+
} });
|
|
118
|
+
return contextResponse(event, reason);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Run the bundled policy over a Claude payload and return the native response
|
|
122
|
+
* string (deny/ask/additionalContext), or null to allow.
|
|
123
|
+
*/
|
|
124
|
+
function guard(input) {
|
|
125
|
+
const result = evaluate({
|
|
126
|
+
tool: input.tool_name ?? "Write",
|
|
127
|
+
filePath: input.tool_input?.file_path,
|
|
128
|
+
content: input.tool_input?.content ?? input.tool_input?.new_string,
|
|
129
|
+
command: input.tool_input?.command
|
|
130
|
+
});
|
|
131
|
+
if (result.decision === "allow" || !result.prompt) return null;
|
|
132
|
+
return toClaudeResponse(input.hook_event_name ?? "PreToolUse", result.prompt);
|
|
133
|
+
}
|
|
134
|
+
/** @deprecated use {@link guard}. Kept for back-compat. */
|
|
135
|
+
const fileSizeGuard = guard;
|
|
136
|
+
//#endregion
|
|
137
|
+
export { readClaudeInput as a, pathExists as c, spawnCapture as d, writeText as f, guard as i, readText as l, denyResponse as n, toClaudeResponse as o, fileSizeGuard as r, collectFiles as s, contextResponse as t, sleep as u };
|
package/dist/cli/bin.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { r as resolveTtlSec } from "../ttl-BG55s6HZ.mjs";
|
|
3
3
|
import { t as detectHarness } from "../harness-C8Nxxyn_.mjs";
|
|
4
|
-
import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-
|
|
5
|
-
import { n as writeInitFile, t as initFor } from "../run-
|
|
6
|
-
import { t as handleHook } from "../handle-
|
|
4
|
+
import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-CUL70W0k.mjs";
|
|
5
|
+
import { n as writeInitFile, t as initFor } from "../run-Do2JltgU.mjs";
|
|
6
|
+
import { t as handleHook } from "../handle-DMHjTY1E.mjs";
|
|
7
7
|
//#region src/cli/bin.ts
|
|
8
8
|
/**
|
|
9
9
|
* harness — CLI for @fusengine/harness.
|
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-CUL70W0k.mjs";
|
|
2
2
|
export { checkStaged, stagedContent, stagedFiles };
|
|
@@ -192,24 +192,58 @@ function securityGuard(ctx) {
|
|
|
192
192
|
}
|
|
193
193
|
//#endregion
|
|
194
194
|
//#region src/policy/guards/protected-path.ts
|
|
195
|
-
/** Path fragments that mark a location as internal/generated state
|
|
195
|
+
/** Path fragments that mark a location as internal/generated state. */
|
|
196
196
|
const PROTECTED_FRAGMENTS = [
|
|
197
197
|
".claude/plugins/marketplaces",
|
|
198
198
|
".claude/plugins/cache",
|
|
199
199
|
".claude/logs/00-apex",
|
|
200
200
|
".claude/fusengine-cache",
|
|
201
|
-
".git/"
|
|
201
|
+
".git/",
|
|
202
|
+
".claude/apex/",
|
|
203
|
+
"/fuse-harness/",
|
|
204
|
+
".harness/track",
|
|
205
|
+
".harness/memory/state"
|
|
202
206
|
];
|
|
203
|
-
/**
|
|
207
|
+
/** Standard block response for any protected-path violation. */
|
|
208
|
+
const BLOCK = {
|
|
209
|
+
kind: "block",
|
|
210
|
+
title: "Protected path",
|
|
211
|
+
reason: "This is internal/generated enforcement state — do not edit it directly.",
|
|
212
|
+
actions: ["Edit the source, not the generated/cache/state copy"]
|
|
213
|
+
};
|
|
214
|
+
/** Returns true if `str` contains any protected fragment. */
|
|
215
|
+
function containsProtected(str) {
|
|
216
|
+
return PROTECTED_FRAGMENTS.some((f) => str.includes(f));
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Returns true if `cmd` contains a recognisable shell write operation.
|
|
220
|
+
*
|
|
221
|
+
* Best-effort: matches `>` / `>>` redirections, `tee`, `cp`, `mv`, `dd`, `sed -i`.
|
|
222
|
+
* Obfuscated shell (base64-decoded payloads, variable indirection, process
|
|
223
|
+
* substitution) can still evade this check — residual risk, documented. The
|
|
224
|
+
* real guarantee against a forged track is the transcript-grounded freshness
|
|
225
|
+
* gate (see `freshness/agent-evidence`), not this guard.
|
|
226
|
+
*/
|
|
227
|
+
function bashHasWriteOp(cmd) {
|
|
228
|
+
return />/.test(cmd) || /\btee\b/.test(cmd) || /\bcp\b/.test(cmd) || /\bmv\b/.test(cmd) || /\bdd\b/.test(cmd) || /\bsed\s+-[a-zA-Z]*i/.test(cmd);
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Blocks direct edits to internal/generated state directories.
|
|
232
|
+
*
|
|
233
|
+
* Covers:
|
|
234
|
+
* - Write / Edit tool calls whose `filePath` targets a protected fragment.
|
|
235
|
+
* - Bash commands that both reference a protected fragment *and* contain a
|
|
236
|
+
* recognisable shell write operation (best-effort; see `bashHasWriteOp`).
|
|
237
|
+
*
|
|
238
|
+
* @param ctx - The guard context (tool, filePath, command).
|
|
239
|
+
* @returns A blocking {@link Prompt}, or null to allow.
|
|
240
|
+
*/
|
|
204
241
|
function protectedPathGuard(ctx) {
|
|
205
242
|
if ((ctx.tool === "Write" || ctx.tool === "Edit") && ctx.filePath) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
reason: "This is internal/generated state — do not edit it directly.",
|
|
211
|
-
actions: ["Edit the source, not the generated/cache copy"]
|
|
212
|
-
};
|
|
243
|
+
if (containsProtected(ctx.filePath)) return BLOCK;
|
|
244
|
+
}
|
|
245
|
+
if (ctx.tool === "Bash" && ctx.command) {
|
|
246
|
+
if (containsProtected(ctx.command) && bashHasWriteOp(ctx.command)) return BLOCK;
|
|
213
247
|
}
|
|
214
248
|
return null;
|
|
215
249
|
}
|
package/dist/freshness/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { i as resolveSessions, n as formatDocSatisfactionStatus, r as isDocConsulted, t as formatDocDeny } from "../doc-helpers-Dd_x1-tZ.mjs";
|
|
2
|
-
import { t as incrementTrivialEditCounter } from "../freshness-
|
|
2
|
+
import { t as incrementTrivialEditCounter } from "../freshness-43gxYpiX.mjs";
|
|
3
3
|
export { formatDocDeny, formatDocSatisfactionStatus, incrementTrivialEditCounter, isDocConsulted, resolveSessions };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as writeJsonFile, i as readJsonFile, n as ensureDir } from "./json-io-CvSumjtz.mjs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
//#region src/freshness/trivial-edit-counter.ts
|
|
4
4
|
/**
|