@fusengine/harness 0.1.43 → 0.1.45

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.
Files changed (47) hide show
  1. package/dist/adapters/claude/index.d.mts +2 -2
  2. package/dist/adapters/claude/index.mjs +2 -2
  3. package/dist/adapters/cline/index.mjs +1 -1
  4. package/dist/adapters/codex/index.d.mts +1 -1
  5. package/dist/adapters/codex/index.mjs +1 -1
  6. package/dist/adapters/cursor/index.mjs +1 -1
  7. package/dist/adapters/gemini/index.mjs +1 -1
  8. package/dist/cache/index.d.mts +2 -2
  9. package/dist/cache/index.mjs +2 -2
  10. package/dist/{claude-CeRYMOaG.mjs → claude-DLh0fHWM.mjs} +6 -2
  11. package/dist/cli/bin.mjs +227 -7
  12. package/dist/cli/index.mjs +1 -1
  13. package/dist/config/index.d.mts +1 -1
  14. package/dist/config/index.mjs +1 -2
  15. package/dist/{doc-helpers-D14nkD5D.d.mts → doc-helpers-BNfYWvYv.d.mts} +9 -1
  16. package/dist/{doc-helpers-BhzDmJ18.mjs → doc-helpers-CWZegVdR.mjs} +14 -5
  17. package/dist/{dotenv-DGyLln7U.mjs → dotenv-B9nM4cuQ.mjs} +26 -1
  18. package/dist/evaluate-d7Pp8XJH.mjs +784 -0
  19. package/dist/freshness/index.d.mts +1 -1
  20. package/dist/freshness/index.mjs +1 -1
  21. package/dist/{handle-BTHcKWQ5.mjs → handle-DJaaD2BT.mjs} +849 -395
  22. package/dist/home-state-mKZxP4oZ.mjs +52 -0
  23. package/dist/{index-D7GpOmkl.d.mts → index-BA-SqNR7.d.mts} +1 -1
  24. package/dist/{index-BX-xcvhY.d.mts → index-BUwEmIK-.d.mts} +10 -1
  25. package/dist/{index-DXQfL1u8.d.mts → index-BXPySPxE.d.mts} +7 -1
  26. package/dist/{index-CwOdFBOr.d.mts → index-BxjzFraL.d.mts} +3 -1
  27. package/dist/{index-DN4cZDbU.d.mts → index-CVhw7eA0.d.mts} +111 -23
  28. package/dist/index.d.mts +6 -6
  29. package/dist/index.mjs +8 -9
  30. package/dist/{loader-Bn-DbZmt.mjs → loader-AGz4nK7d.mjs} +1 -1
  31. package/dist/{mcp-store-CnsW9oFj.mjs → mcp-store-BkBDmuxN.mjs} +22 -5
  32. package/dist/policy/index.d.mts +2 -2
  33. package/dist/policy/index.mjs +3 -3
  34. package/dist/refs/index.mjs +2 -2
  35. package/dist/{router-BfX0hJg8.mjs → router-PKVNBHge.mjs} +11 -1
  36. package/dist/{run-jgivVDv6.mjs → run-DLXtA5DH.mjs} +1 -1
  37. package/dist/runtime/index.d.mts +71 -14
  38. package/dist/runtime/index.mjs +3 -3
  39. package/dist/{session-state-Dzq6yrw7.d.mts → session-state-D4F_Dub6.d.mts} +1 -1
  40. package/dist/state/index.d.mts +1 -1
  41. package/dist/{store-CdWOQ9zD.mjs → store-CNjFenWe.mjs} +3 -50
  42. package/dist/tracking/index.d.mts +1 -1
  43. package/dist/tracking/index.mjs +1 -1
  44. package/dist/{validate-xi-zc-22.mjs → validate-DcQaKzau.mjs} +538 -84
  45. package/package.json +1 -1
  46. package/dist/evaluate-CeivW6G0.mjs +0 -477
  47. package/dist/ttl-BG55s6HZ.mjs +0 -20
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fusengine/harness",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
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,477 +0,0 @@
1
- import { i as splitTarget, r as resolveMaxLines } from "./limits-CHn8AIL1.mjs";
2
- import { t as isCodeFile } from "./project-root-3kk7gCOp.mjs";
3
- //#region src/policy/detect-framework.ts
4
- /**
5
- * Detect the framework from a file path extension + content patterns.
6
- * Aligned with the fusengine require-solid-read detection (distinct from
7
- * {@link detectProjectType}, which scans config files on disk).
8
- */
9
- function detectFramework(filePath, content) {
10
- if (/\.(tsx?|jsx?|vue|svelte)$/.test(filePath) || /from ['"]react|useState|className=/.test(content)) {
11
- if (/(page|layout|loading|error|route)\.(ts|tsx)$/.test(filePath) || /use client|use server/.test(content)) return "nextjs";
12
- return "react";
13
- }
14
- if (/\.swift$/.test(filePath)) return "swift";
15
- if (/\.php$/.test(filePath)) return "laravel";
16
- if (/\.java$/.test(filePath)) return "java";
17
- if (/\.go$/.test(filePath)) return "go";
18
- if (/\.rb$/.test(filePath)) return "ruby";
19
- if (/\.rs$/.test(filePath)) return "rust";
20
- if (/\.css$/.test(filePath) || /@tailwind|@apply/.test(content)) return "tailwind";
21
- return "generic";
22
- }
23
- //#endregion
24
- //#region src/policy/file-size.ts
25
- /**
26
- * Count physical lines — parity with the Python `enforce-file-size.py`
27
- * (`sum(1 for _ in f)`): every line counts (blanks and comments included), and a
28
- * single trailing newline does not add a phantom line. The SOLID ceiling is
29
- * measured on raw file length, not substantive code, to match the upstream plugin.
30
- */
31
- function countLines(content) {
32
- if (content === "") return 0;
33
- return content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
34
- }
35
- /**
36
- * Evaluate a file's line count against the SOLID limit.
37
- * @param lines - the file's line count
38
- * @param max - the limit (defaults to `resolveMaxLines()`)
39
- */
40
- function evaluateFileSize(lines, max = resolveMaxLines()) {
41
- if (lines <= max) return {
42
- ok: true,
43
- lines,
44
- max,
45
- message: null
46
- };
47
- return {
48
- ok: false,
49
- lines,
50
- max,
51
- message: `File has ${lines} lines (max: ${max}). Split into modules under ${splitTarget(max)} lines (Single Responsibility).`
52
- };
53
- }
54
- //#endregion
55
- //#region src/policy/patterns.ts
56
- /**
57
- * Guard pattern data, ported verbatim from the fusengine git/install guards.
58
- * Note (faithful): `git push.*--force` also matches `--force-with-lease` —
59
- * preserved from the source guard.
60
- */
61
- /** Destructive git operations to block outright. */
62
- const GIT_BLOCKED = [
63
- /git push.*--force/,
64
- /git push.*-f/,
65
- /git reset.*--hard/,
66
- /git clean.*-fd/,
67
- /git branch.*-D/,
68
- /git rebase.*--force/
69
- ];
70
- /** Git operations that warrant a confirmation prompt. */
71
- const GIT_ASK = [
72
- /git push/,
73
- /git checkout/,
74
- /git reset/,
75
- /git rebase/,
76
- /git merge/,
77
- /git stash/,
78
- /git clean/,
79
- /git rm/,
80
- /git mv/,
81
- /git restore/,
82
- /git revert/,
83
- /git cherry-pick/,
84
- /git commit/,
85
- /git add/,
86
- /git branch -d/
87
- ];
88
- /** System-level package installs (need confirmation). */
89
- const SYSTEM_INSTALL = [
90
- /brew install/,
91
- /brew upgrade/,
92
- /brew cask/,
93
- /apt install/,
94
- /apt-get install/,
95
- /dnf install/,
96
- /pacman -S/
97
- ];
98
- /** Project-level package installs. */
99
- const PROJECT_INSTALL = [
100
- /npm install/,
101
- /npm i /,
102
- /yarn add/,
103
- /pnpm add/,
104
- /pip install/,
105
- /pip3 install/,
106
- /composer require/,
107
- /bun add/,
108
- /bun install/,
109
- /cargo install/,
110
- /go install/,
111
- /gem install/,
112
- /pipx install/
113
- ];
114
- /** True when `cmd` matches any pattern in `patterns`. */
115
- function matchPatterns(cmd, patterns) {
116
- return patterns.some((re) => re.test(cmd));
117
- }
118
- //#endregion
119
- //#region src/policy/guards/security.ts
120
- /** Critical patterns that must always be blocked. */
121
- const CRITICAL_PATTERNS = [
122
- /\brm\s+(?:-[a-z]*\s+)*-[a-z]*[rf][a-z]*\s+(?:-[a-z]+\s+)*(?:\/|~)(?:\s|$)/,
123
- /:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;\s*:/,
124
- /\b(?:curl|wget)\b[^|]*\|\s*(?:sudo\s+)?(?:ba|z|da|k)?sh\b/,
125
- /\bchmod\s+(?:-[a-zA-Z]+\s+)*777\s+\//,
126
- /\bmkfs(?:\.[a-z0-9]+)?\b/,
127
- /\bdd\b[^\n]*\bof=\/dev\/(?:r?disk|sd|hd|nvme|mmcblk|vd|xvd)/i,
128
- /\bshred\b/,
129
- /\bfdisk\b/,
130
- /\bdiskutil\s+(?:erase|partitionDisk)/i,
131
- /(?:>|>>)\s*\/dev\/(?:sda|hda|nvme)/,
132
- /\brm\s+(?:-[a-z]*\s+)*-[a-z]*[rf][a-z]*\s+(?:-[a-z]+\s+)*\/(?:etc|usr|var|bin|sbin|boot|lib)\b/,
133
- /(?:^|[\s;|&])sudo(?:[\s;|&]|$)/,
134
- /(?:^|[\s;|&])su(?:[\s;|&]|$)/,
135
- /(?:^|[\s;|&])doas(?:[\s;|&]|$)/,
136
- /(?:^|[\s;|&])passwd(?:[\s;|&]|$)/,
137
- /(?:^|[\s;|&])del(?:[\s;|&]|$)/
138
- ];
139
- /** Patterns that warrant explicit confirmation before running. */
140
- const ASK_PATTERNS = [
141
- /\bchmod\s+(?:-[a-zA-Z]+\s+)*777\b/,
142
- /\bchown\s+-R\b/,
143
- /\beval\s/,
144
- /(?:>|>>|\btee\b)\s*\/etc\//,
145
- /\brm\s/,
146
- /\bunlink\s/
147
- ];
148
- /**
149
- * Strip heredoc bodies so their content isn't matched as a command (false positives).
150
- * Two-phase scan: a backreference-free opener regex finds `<<[-]DELIM`, then the body
151
- * is cut up to the closing delimiter via `indexOf` — O(n), no catastrophic backtracking.
152
- */
153
- function stripHeredoc(cmd) {
154
- const opener = /<<-?\s*['"]?(\w+)['"]?/g;
155
- let out = cmd;
156
- let m;
157
- while ((m = opener.exec(out)) !== null) {
158
- const delim = m[1] ?? "";
159
- const closer = new RegExp(`\\n[ \\t]*${delim}\\b`);
160
- const rest = out.slice(m.index + m[0].length);
161
- const hit = closer.exec(rest);
162
- if (!hit || hit.index === void 0) break;
163
- const end = m.index + m[0].length + hit.index + hit[0].length;
164
- out = `${out.slice(0, m.index)} ${out.slice(end)}`;
165
- opener.lastIndex = m.index;
166
- }
167
- return out;
168
- }
169
- /** Guards against dangerous Bash commands (critical → block, sensitive → ask). */
170
- function securityGuard(ctx) {
171
- if (ctx.tool !== "Bash" || !ctx.command) return null;
172
- const cmd = stripHeredoc(ctx.command);
173
- for (const re of CRITICAL_PATTERNS) if (re.test(cmd)) return {
174
- kind: "block",
175
- title: "Dangerous command",
176
- reason: "Command matches a destructive pattern (recursive root delete, fork bomb, remote-script piped to a shell, world-writable root, filesystem format, disk overwrite, or privilege escalation: sudo/su/doas/passwd).",
177
- actions: ["Remove the destructive command", "Scope the operation to a specific safe path"]
178
- };
179
- for (const re of ASK_PATTERNS) if (re.test(cmd)) return {
180
- kind: "ask",
181
- title: "Dangerous command",
182
- reason: "Command requests elevated privileges or alters sensitive targets (sudo, chmod 777, recursive chown, eval, or writing to /etc).",
183
- actions: ["Confirm this command is intended", "Run with least privilege"]
184
- };
185
- return null;
186
- }
187
- //#endregion
188
- //#region src/policy/guards/protected-path.ts
189
- /**
190
- * Path fragments that mark a location as internal/generated state.
191
- *
192
- * Parity with safe_paths.py: `~/.fuse-harness/cache` is a *writable*
193
- * cache the harness owns (lessons, MCP cache, per-type state) — only the
194
- * `cache/sessions` subtree is protected, not the whole cache.
195
- */
196
- const PROTECTED_FRAGMENTS = [
197
- ".claude/plugins/marketplaces",
198
- ".claude/plugins/cache",
199
- ".claude/logs/00-apex",
200
- ".fuse-harness/cache/sessions",
201
- ".claude/apex/",
202
- ".harness/track",
203
- ".harness/memory/state"
204
- ];
205
- /**
206
- * Matches a real `.git` directory segment (`/.git/`, `~/.git`, leading or
207
- * trailing `.git`) without matching unrelated names like `foo.git/` or
208
- * `.github/`. Kept separate from the substring fragments for precise scoping.
209
- */
210
- const PROTECTED_GIT_RE = /(?:^|[/~\s])\.git(?:\/|$)/;
211
- /** Standard block response for any protected-path violation. */
212
- const BLOCK = {
213
- kind: "block",
214
- title: "Protected path",
215
- reason: "This is internal/generated enforcement state — do not edit it directly.",
216
- actions: ["Edit the source, not the generated/cache/state copy"]
217
- };
218
- /** Returns true if `str` references a protected fragment or a real `.git` segment. */
219
- function containsProtected(str) {
220
- return PROTECTED_FRAGMENTS.some((f) => str.includes(f)) || PROTECTED_GIT_RE.test(str);
221
- }
222
- /** Strips surrounding quotes from a captured shell token. */
223
- function unquote(t) {
224
- return t.replace(/^['"]|['"]$/g, "");
225
- }
226
- /**
227
- * Extracts the genuine write *targets* of a shell command, so a protected path
228
- * appearing only as a *read source* (e.g. `grep x .claude/apex/ > out.txt`,
229
- * `cat .git/config > /dev/null`) is not mistaken for a write.
230
- *
231
- * Covers `>` / `>>` redirects (skipping `2>`, `&>`, `1>` fd-redirects and
232
- * `/dev/null`), `tee`, `dd of=`, and the destination / in-place file of
233
- * `cp` / `mv` / `sed -i` / `perl -i` (last path token of the segment).
234
- * Best-effort: obfuscated shell (base64, indirection, process substitution)
235
- * can still evade this — residual risk, mitigated by the freshness gate.
236
- */
237
- function extractWriteTargets(cmd) {
238
- const out = [];
239
- const push = (t) => {
240
- const v = unquote((t ?? "").trim());
241
- if (v && v !== "/dev/null") out.push(v);
242
- };
243
- for (const m of cmd.matchAll(/(?<![2&\d])>{1,2}\s*('[^']+'|"[^"]+"|\S+)/g)) push(m[1]);
244
- for (const m of cmd.matchAll(/\btee\b(?:\s+-\S+)*\s+('[^']+'|"[^"]+"|\S+)/g)) push(m[1]);
245
- for (const m of cmd.matchAll(/\bdd\b[^|;&]*\bof=('[^']+'|"[^"]+"|\S+)/g)) push(m[1]);
246
- for (const seg of cmd.split(/[;&|]+/)) {
247
- if (!/\b(?:cp|mv)\b|\b(?:sed|perl)\b[^|;&]*\s-i/.test(seg)) continue;
248
- const toks = (seg.split(/[<>]/)[0] ?? "").trim().split(/\s+/).filter((t) => Boolean(t) && !t.startsWith("-"));
249
- push(toks[toks.length - 1]);
250
- }
251
- return out;
252
- }
253
- /**
254
- * Blocks direct edits to internal/generated state directories.
255
- *
256
- * Covers:
257
- * - Write / Edit tool calls whose `filePath` targets a protected fragment.
258
- * - Bash commands whose actual write *target* is a protected fragment
259
- * (read sources are ignored; see `extractWriteTargets`).
260
- *
261
- * @param ctx - The guard context (tool, filePath, command).
262
- * @returns A blocking {@link Prompt}, or null to allow.
263
- */
264
- function protectedPathGuard(ctx) {
265
- if ((ctx.tool === "Write" || ctx.tool === "Edit") && ctx.filePath) {
266
- if (containsProtected(ctx.filePath)) return BLOCK;
267
- }
268
- if (ctx.tool === "Bash" && ctx.command) {
269
- if (extractWriteTargets(ctx.command).some(containsProtected)) return BLOCK;
270
- }
271
- return null;
272
- }
273
- //#endregion
274
- //#region src/policy/guards/bash-write.ts
275
- /** Redirect (`>`/`>>`) targeting a code-file extension. */
276
- const CODE_REDIRECT = /(?:>>?)\s*[^\s|;&]*\.(?:ts|tsx|js|jsx|py|go|rb|rs|java|kt|php|swift|vue|svelte|astro|css|c|cpp|h)\b/;
277
- /** Interpreters / tools that mutate source in place, plus heredoc-into-file.
278
- * `sed`/`perl`/`awk` allow intervening flags before `-i` (parity bash-write-guard.py). */
279
- const CODE_MUTATORS = /\bpython3?\s+-c\b|\bpython3?\s+-\s*<<|\bsed\b[^|]*\s-i|\bperl\b[^|]*\s-[pi]i?\b|\bawk\b[^|]*-i\s*inplace|\bpatch\b|<<[-~]?\s*['"]?\w+['"]?[\s\S]*?>/;
280
- /** File-mutating one-liners via `node -e` / `ruby -e` (parity NODE_WRITES/RUBY_WRITES). */
281
- const NODE_WRITES = /writeFile|appendFile|createWriteStream|fs\.(?:write|rename|unlink|mkdir|rmdir|copyFile)|execSync|spawnSync|child_process/;
282
- const RUBY_WRITES = /File\.(?:write|open|delete|rename)|IO\.write|FileUtils|\bsystem\b|\bexec\b|`[^`]/;
283
- /** Redirect to a non-code file. Excludes `/dev/null`, `2>`/`N>` and `>&N` fd
284
- * redirects via the `(?<![0-9&])` lookbehind + `(?!…|&)` (parity has_file_redirect). */
285
- const FILE_REDIRECT = /(?<![0-9&])\s*>>?\s*(?!\/dev\/null|&)[a-zA-Z./~$]/;
286
- /** Other ambiguous file writers (ASK): `tee <file>` (not `tee -a`/path) and `dd … of=`. */
287
- const ASK_WRITERS = /\btee\s+[^-/\s]|\bdd\b[^|]*\bof=/;
288
- /**
289
- * Blocks shell commands that mutate code files in place (and heredocs/redirects
290
- * to source files); asks before other file-writing shell commands. Forces use
291
- * of the Write/Edit tool so APEX/SOLID checks are not bypassed.
292
- */
293
- function bashWriteGuard(ctx) {
294
- if (ctx.tool !== "Bash" || !ctx.command) return null;
295
- const cmd = ctx.command;
296
- if (CODE_MUTATORS.test(cmd) || CODE_REDIRECT.test(cmd)) return {
297
- kind: "block",
298
- title: "Bash write to code file",
299
- reason: "Shell in-place edits / redirects to source files bypass APEX/SOLID checks.",
300
- actions: ["Use the Write/Edit tool instead"]
301
- };
302
- if (FILE_REDIRECT.test(cmd) || ASK_WRITERS.test(cmd) || /\bnode\s+-e\b/.test(cmd) && NODE_WRITES.test(cmd) || /\bruby\s+-e\b/.test(cmd) && RUBY_WRITES.test(cmd)) return {
303
- kind: "ask",
304
- title: "Bash file write",
305
- reason: "This command writes a file from the shell; confirm it is intended.",
306
- actions: ["Use the Write/Edit tool instead"]
307
- };
308
- return null;
309
- }
310
- //#endregion
311
- //#region src/policy/guards/interface-separation.ts
312
- /** TS/JS component files: top-level `interface`/`type Foo`. */
313
- const TS_DECL_RE = /^\s*(export\s+)?(interface|type)\s+[A-Z]/m;
314
- /** Python view models: class subclassing a schema/protocol base. */
315
- const PY_MODEL_RE = /^\s*class\s+\w+\((BaseModel|TypedDict|Protocol)\)/m;
316
- /**
317
- * PHP controllers: top-level `interface`, `abstract class`, or a concrete
318
- * `class …Interface/DTO/Request`. Union of the TS-only `abstract class` rule
319
- * and the Python rule (`class [A-Z].*(Interface|DTO|Request)`, enforce-interfaces.py:16).
320
- */
321
- const PHP_DECL_RE = /^\s*(?:abstract\s+class\b|interface\b|class\s+[A-Z].*(?:Interface|DTO|Request))/m;
322
- /** Swift views: top-level `protocol Foo`. */
323
- const SWIFT_PROTO_RE = /^\s*protocol\s+[A-Z]/m;
324
- /** Go handlers/controllers: top-level `type Foo interface`. */
325
- const GO_DECL_RE = /^\s*type\s+[A-Z]\w*\s+interface\b/m;
326
- /** Java/Kotlin controllers/handlers: top-level `interface`/`record`. */
327
- const JAVA_DECL_RE = /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:interface|record)\s+[A-Z]/m;
328
- /**
329
- * Blocks top-level interface/type/protocol declarations in component, view or
330
- * controller files (Interface Segregation). Fires only when BOTH the path
331
- * category AND the content pattern match.
332
- *
333
- * Parity note: enforce-interfaces.py only inspects `Write` (tool_input.content).
334
- * We deliberately also fire on `Edit` — an in-place edit can introduce the same
335
- * violation — and the path fragments accept singular *and* plural directory
336
- * names (`view/` + `views/`), mirroring the Python `s?` regexes.
337
- */
338
- function interfaceSeparationGuard(ctx) {
339
- if (ctx.tool !== "Write" && ctx.tool !== "Edit") return null;
340
- const path = ctx.filePath;
341
- const content = ctx.content;
342
- if (!path || !content) return null;
343
- const block = {
344
- kind: "block",
345
- title: "Separate the interface",
346
- reason: "Top-level interface/type/protocol declarations belong in their own file, not in a component/view/controller.",
347
- actions: ["Move the interface/type to its own file (Interface Segregation)"]
348
- };
349
- const inAny = (...frags) => frags.some((f) => path.includes(f));
350
- if (/\.(tsx|jsx|vue|svelte)$/.test(path) && TS_DECL_RE.test(content)) return block;
351
- if (/\.py$/.test(path) && inAny("view/", "views/", "controller/", "controllers/", "route/", "routes/") && PY_MODEL_RE.test(content)) return block;
352
- if (/\.go$/.test(path) && inAny("handler/", "handlers/", "controller/", "controllers/") && GO_DECL_RE.test(content)) return block;
353
- if (/\.(java|kt)$/.test(path) && inAny("controller/", "controllers/", "handler/", "handlers/") && JAVA_DECL_RE.test(content)) return block;
354
- if (/\.php$/.test(path) && inAny("Controller/", "Controllers/", "Handler/", "Handlers/") && PHP_DECL_RE.test(content)) return block;
355
- if (/\.swift$/.test(path) && inAny("View/", "Views/", "Component/", "Components/") && SWIFT_PROTO_RE.test(content)) return block;
356
- return null;
357
- }
358
- //#endregion
359
- //#region src/policy/guards/install.ts
360
- /** Asks for confirmation before a dependency or system package install. */
361
- function installGuard(ctx) {
362
- if (ctx.tool !== "Bash" || !ctx.command) return null;
363
- if (matchPatterns(ctx.command, PROJECT_INSTALL) || matchPatterns(ctx.command, SYSTEM_INSTALL)) return {
364
- kind: "ask",
365
- title: "Dependency install",
366
- reason: `This command installs packages: ${ctx.command.trim()}`,
367
- actions: ["Confirm this install is intended"]
368
- };
369
- return null;
370
- }
371
- //#endregion
372
- //#region src/policy/guards/index.ts
373
- /** Ordered guard chain: critical/security + protected first, then writes/installs. */
374
- const GUARDS = [
375
- securityGuard,
376
- protectedPathGuard,
377
- bashWriteGuard,
378
- interfaceSeparationGuard,
379
- installGuard
380
- ];
381
- /** Block prompt returned when a guard or gate throws (fail-closed). */
382
- const FAIL_CLOSED = {
383
- kind: "block",
384
- title: "Policy error",
385
- reason: "A policy check errored — blocked for safety (fail-closed).",
386
- actions: ["Fix the failing guard/gate, then retry"]
387
- };
388
- const USER_GUARDS = [];
389
- /** Register a user guard — runs AFTER the privileged core chain (two-tier). */
390
- function registerGuard(guard) {
391
- USER_GUARDS.push(guard);
392
- }
393
- /** Remove all registered user guards (mainly for tests). */
394
- function clearUserGuards() {
395
- USER_GUARDS.length = 0;
396
- }
397
- /**
398
- * Run the guard chain — privileged core guards first, then user guards — and
399
- * return the first firing Prompt, else null. Fail-closed: a guard that throws
400
- * blocks (never silently passes).
401
- */
402
- function runGuards(ctx) {
403
- for (const guard of [...GUARDS, ...USER_GUARDS]) {
404
- let hit;
405
- try {
406
- hit = guard(ctx);
407
- } catch {
408
- return FAIL_CLOSED;
409
- }
410
- if (hit) return hit;
411
- }
412
- return null;
413
- }
414
- //#endregion
415
- //#region src/policy/evaluate.ts
416
- /**
417
- * Evaluate a single tool-use against the bundled policies, returning a pure
418
- * decision plus a portable {@link Prompt}. Adapters translate the prompt into
419
- * their harness's native response (Claude `permissionDecision`, etc.).
420
- */
421
- function evaluate(ctx) {
422
- const guard = runGuards(ctx);
423
- if (guard) return {
424
- decision: "deny",
425
- message: guard.reason,
426
- prompt: guard
427
- };
428
- if (ctx.command && matchPatterns(ctx.command, GIT_BLOCKED)) {
429
- const reason = `Destructive git command: ${ctx.command}`;
430
- return {
431
- decision: "deny",
432
- message: reason,
433
- prompt: {
434
- kind: "block",
435
- title: "Destructive git command",
436
- reason,
437
- actions: ["Use a non-destructive alternative (e.g. --force-with-lease; avoid --hard / -D)"]
438
- }
439
- };
440
- }
441
- if (ctx.command && matchPatterns(ctx.command, GIT_ASK)) return {
442
- decision: "deny",
443
- message: `Git operation requires confirmation: ${ctx.command}`,
444
- prompt: {
445
- kind: "ask",
446
- title: "Confirm git operation",
447
- reason: `Authorize: ${ctx.command.trim()}`,
448
- actions: ["Approve if this git operation is intended"]
449
- }
450
- };
451
- if (ctx.filePath && isCodeFile(ctx.filePath) && ctx.agentType !== "Explore" && ctx.agentType !== "Plan") {
452
- const incoming = ctx.content !== void 0 ? countLines(ctx.content) : 0;
453
- const lines = ctx.tool === "Edit" ? Math.max(incoming, ctx.existingLines ?? 0) : incoming || (ctx.existingLines ?? 0);
454
- const verdict = evaluateFileSize(lines, ctx.maxLines);
455
- if (lines > 0 && !verdict.ok) return {
456
- decision: "deny",
457
- message: verdict.message,
458
- prompt: {
459
- kind: "block",
460
- title: "SOLID file-size limit",
461
- reason: verdict.message ?? "",
462
- actions: [`Split into modules under ${verdict.max} lines (Single Responsibility)`, "Then re-run the write"]
463
- },
464
- meta: {
465
- framework: detectFramework(ctx.filePath, ctx.content ?? ""),
466
- lines: verdict.lines,
467
- max: verdict.max
468
- }
469
- };
470
- }
471
- return {
472
- decision: "allow",
473
- message: null
474
- };
475
- }
476
- //#endregion
477
- export { matchPatterns as A, ASK_PATTERNS as C, GIT_BLOCKED as D, GIT_ASK as E, evaluateFileSize as M, detectFramework as N, PROJECT_INSTALL as O, protectedPathGuard as S, securityGuard as T, CODE_REDIRECT as _, registerGuard as a, PROTECTED_FRAGMENTS as b, GO_DECL_RE as c, PY_MODEL_RE as d, SWIFT_PROTO_RE as f, CODE_MUTATORS as g, ASK_WRITERS as h, clearUserGuards as i, countLines as j, SYSTEM_INSTALL as k, JAVA_DECL_RE as l, interfaceSeparationGuard as m, FAIL_CLOSED as n, runGuards as o, TS_DECL_RE as p, GUARDS as r, installGuard as s, evaluate as t, PHP_DECL_RE as u, FILE_REDIRECT as v, CRITICAL_PATTERNS as w, PROTECTED_GIT_RE as x, bashWriteGuard as y };
@@ -1,20 +0,0 @@
1
- import { a as parseEnvInt } from "./limits-CHn8AIL1.mjs";
2
- //#region src/config/ttl.ts
3
- /** Default enforcement-freshness window, in seconds (2 minutes). */
4
- const DEFAULT_TTL_SEC = 120;
5
- /** Default env var name carrying the TTL override. */
6
- const TTL_ENV_KEY = "FUSE_ENFORCE_TTL_SEC";
7
- /**
8
- * Resolve the enforcement TTL (seconds) from an env map.
9
- * @param env - environment map (defaults to `process.env`)
10
- * @param key - env var name (defaults to `FUSE_ENFORCE_TTL_SEC`)
11
- */
12
- function resolveTtlSec(env = process.env, key = TTL_ENV_KEY) {
13
- return parseEnvInt(env[key], 120);
14
- }
15
- /** Human label for a TTL: 120 -> "2min", 240 -> "4min", 90 -> "90s". */
16
- function ttlLabel(sec) {
17
- return sec % 60 === 0 ? `${sec / 60}min` : `${sec}s`;
18
- }
19
- //#endregion
20
- export { ttlLabel as i, TTL_ENV_KEY as n, resolveTtlSec as r, DEFAULT_TTL_SEC as t };