@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.
@@ -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-BWZcrZbS.mjs";
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,4 +1,4 @@
1
- import { t as evaluate } from "../../evaluate-9ch1K2kt.mjs";
1
+ import { t as evaluate } from "../../evaluate-zyxeVZPB.mjs";
2
2
  import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
3
3
  //#region src/adapters/cline/index.ts
4
4
  /**
@@ -1,2 +1,2 @@
1
- import { a as readClaudeInput, i as guard, n as denyResponse, t as contextResponse } from "../../claude-BWZcrZbS.mjs";
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 };
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "../../evaluate-9ch1K2kt.mjs";
1
+ import { t as evaluate } from "../../evaluate-zyxeVZPB.mjs";
2
2
  import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
3
3
  //#region src/adapters/cursor/index.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- import { t as evaluate } from "../../evaluate-9ch1K2kt.mjs";
1
+ import { t as evaluate } from "../../evaluate-zyxeVZPB.mjs";
2
2
  import { t as formatPrompt } from "../../types-ernB1Dy3.mjs";
3
3
  //#region src/adapters/gemini/index.ts
4
4
  /**
@@ -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-CQbtlAKa.mjs";
5
- import { n as writeInitFile, t as initFor } from "../run-D91N4ul1.mjs";
6
- import { t as handleHook } from "../handle-DJqHZcml.mjs";
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.
@@ -1,2 +1,2 @@
1
- import { n as stagedContent, r as stagedFiles, t as checkStaged } from "../run-CQbtlAKa.mjs";
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 (off-limits to Write/Edit). */
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
- /** Blocks direct edits to internal/generated state directories. */
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
- const path = ctx.filePath;
207
- if (PROTECTED_FRAGMENTS.some((fragment) => path.includes(fragment))) return {
208
- kind: "block",
209
- title: "Protected path",
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
  }
@@ -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-otdUpuvP.mjs";
2
+ import { t as incrementTrivialEditCounter } from "../freshness-43gxYpiX.mjs";
3
3
  export { formatDocDeny, formatDocSatisfactionStatus, incrementTrivialEditCounter, isDocConsulted, resolveSessions };
@@ -1,4 +1,4 @@
1
- import { i as writeJsonFile, n as ensureDir, r as readJsonFile } from "./json-io-CAn72gI4.mjs";
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
  /**