@seanmozeik/tripwire 0.7.0 → 0.7.2
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 +16 -14
- package/dist/index.js +35 -0
- package/dist/tripwire-cli.js +3 -0
- package/dist/tripwire-hook.js +3 -0
- package/dist/tripwire-pi.js +6 -4
- package/dist/tripwire.js +141 -0
- package/dist/types/dispatch.d.ts +18 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/lib/bash.d.ts +27 -0
- package/dist/types/lib/config.d.ts +110 -0
- package/dist/types/lib/cursor.d.ts +16 -0
- package/dist/types/lib/decision.d.ts +13 -0
- package/dist/types/lib/diff.d.ts +3 -0
- package/dist/types/lib/event.d.ts +45 -0
- package/dist/types/lib/log.d.ts +2 -0
- package/dist/types/lib/secrets.d.ts +41 -0
- package/dist/types/rules/bash-deny.d.ts +5 -0
- package/dist/types/rules/bash-git.d.ts +5 -0
- package/dist/types/rules/bash-network-install.d.ts +4 -0
- package/dist/types/rules/bash-redirect.d.ts +4 -0
- package/dist/types/rules/bash-scoped-rm.d.ts +5 -0
- package/dist/types/rules/bash-tar-explosion.d.ts +4 -0
- package/dist/types/rules/config-custom.d.ts +6 -0
- package/dist/types/rules/lazy-code.d.ts +4 -0
- package/dist/types/rules/path-protect.d.ts +12 -0
- package/dist/types/rules/post-secret-scrub.d.ts +12 -0
- package/dist/types/rules/read-protect.d.ts +4 -0
- package/dist/types/rules/tool-policy.d.ts +5 -0
- package/package.json +16 -13
- package/dist/tripwire +0 -0
- package/scripts/tripwire-cli +0 -12
- package/src/cli.ts +0 -271
- package/src/dispatch.ts +0 -562
- package/src/index.ts +0 -6
- package/src/lib/bash.ts +0 -1328
- package/src/lib/config.ts +0 -174
- package/src/lib/cursor.ts +0 -336
- package/src/lib/decision.ts +0 -36
- package/src/lib/diff.ts +0 -29
- package/src/lib/event.ts +0 -105
- package/src/lib/install.ts +0 -610
- package/src/lib/log.ts +0 -23
- package/src/lib/secrets.ts +0 -184
- package/src/main.ts +0 -31
- package/src/pi-extension.ts +0 -337
- package/src/rules/bash-deny.ts +0 -404
- package/src/rules/bash-git.ts +0 -590
- package/src/rules/bash-network-install.ts +0 -75
- package/src/rules/bash-redirect.ts +0 -91
- package/src/rules/bash-scoped-rm.ts +0 -84
- package/src/rules/bash-tar-explosion.ts +0 -77
- package/src/rules/config-custom.ts +0 -166
- package/src/rules/lazy-code.ts +0 -95
- package/src/rules/path-protect.ts +0 -131
- package/src/rules/post-secret-scrub.ts +0 -49
- package/src/rules/read-protect.ts +0 -57
- package/src/rules/tool-policy.ts +0 -54
package/README.md
CHANGED
|
@@ -13,28 +13,22 @@ $ tripwire test 'git status'
|
|
|
13
13
|
|
|
14
14
|
## Package support
|
|
15
15
|
|
|
16
|
-
The
|
|
16
|
+
The main package installs on every platform supported by Bun 1.4. It contains a minified Bun runtime bundle, a separate library bundle, and TypeScript declarations.
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
19
|
bun install --global @seanmozeik/tripwire
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
On Apple Silicon Macs, npm also installs the optional `@seanmozeik/tripwire-darwin-arm64` package. Tripwire selects its compiled Bun bytecode executable. On Linux, Windows, and Intel Macs, Tripwire runs the portable JavaScript bundle with the installed Bun runtime.
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
- `tripwire` is a small POSIX launcher for interactive CLI commands. It executes the same native file in CLI mode.
|
|
24
|
+
The main package installs two commands:
|
|
26
25
|
|
|
27
|
-
|
|
26
|
+
- `tripwire` runs interactive commands.
|
|
27
|
+
- `tripwire-hook` handles agent hook events.
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
git clone https://github.com/seanmozeik/tripwire.git
|
|
31
|
-
cd tripwire
|
|
32
|
-
bun install --frozen-lockfile
|
|
33
|
-
bun run build
|
|
34
|
-
./dist/tripwire --version
|
|
35
|
-
```
|
|
29
|
+
Run `tripwire install <host>` after installation. On Apple Silicon, the installer writes the direct native executable path into agent settings to keep hook startup fast. Other platforms use Bun and the portable bundle. Pi and Oh My Pi use the same runtime selection through their adapter.
|
|
36
30
|
|
|
37
|
-
|
|
31
|
+
Library imports always use `dist/index.js` and `dist/types`. They do not load or execute the platform binary.
|
|
38
32
|
|
|
39
33
|
## Secret scanner requirement
|
|
40
34
|
|
|
@@ -222,10 +216,18 @@ bun run build
|
|
|
222
216
|
bun run verify
|
|
223
217
|
```
|
|
224
218
|
|
|
225
|
-
`bun run build`
|
|
219
|
+
`bun run build` creates these artifacts:
|
|
220
|
+
|
|
221
|
+
- `dist/tripwire.js`: minified portable runtime for Bun 1.4 or later.
|
|
222
|
+
- `dist/index.js` and `dist/types`: public library bundle and declarations.
|
|
223
|
+
- `dist/tripwire-cli.js` and `dist/tripwire-hook.js`: runtime-selecting command launchers.
|
|
224
|
+
- `dist/tripwire-pi.js`: Pi and Oh My Pi adapter.
|
|
225
|
+
- `packages/darwin-arm64/bin/tripwire`: Apple Silicon bytecode executable.
|
|
226
226
|
|
|
227
227
|
`bun run verify` runs the format check, lint, type check, tests, and build. `prepublishOnly` calls the same local command.
|
|
228
228
|
|
|
229
|
+
Publish `@seanmozeik/tripwire-darwin-arm64` before `@seanmozeik/tripwire` for each release. The main package uses an exact optional dependency on the matching native package version.
|
|
230
|
+
|
|
229
231
|
## License
|
|
230
232
|
|
|
231
233
|
MIT. See [LICENSE](LICENSE).
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
var fe={allow:0,warn:1,ask:2,deny:3},a=(e)=>({kind:"allow",rule:e,message:""}),x=(e,t)=>({kind:"warn",rule:e,message:t}),y=(e,t)=>({kind:"ask",rule:e,message:t}),d=(e,t)=>({kind:"deny",rule:e,message:t}),D=(e)=>{let t=a("none");for(let n of e)if(fe[n.kind]>fe[t.kind])t=n;return t};import{BunRuntime as ps}from"@effect/platform-bun";import{Cause as L,Data as mt,Effect as S,Exit as J,Schema as q}from"effect";import{parse as xt,quote as H}from"shell-quote";var Et=["dist","build","_build","out","target",".next",".nuxt",".svelte-kit",".output",".astro",".angular",".vite",".parcel-cache",".turbo",".vercel",".netlify",".fly",".wrangler",".serverless","coverage",".nyc_output",".cache",".ruff_cache",".mypy_cache",".pytest_cache",".ty_cache",".tox","__pycache__",".venv","venv","node_modules",".gradle","DerivedData",".bundle",".cargo-target","tmp",".tmp",".state",".terraform",".yarn/cache",".yarn/install-state.gz",".pnpm-store",".bun"],ye=["/tmp","/var/tmp","/var/folders","/private/tmp","/private/var/tmp","/private/var/folders"],ke=new Set([">",">>","<","<<","<<<","<>",">&","<&","&>","&>>"]),be=new Set([";","&&","||","|","|&","&"]),Se=(e)=>typeof e==="string",E=(e)=>{if(typeof e==="object"&&"op"in e&&typeof e.op==="string")return e.op;return null},_t=(e)=>typeof e==="object"&&("comment"in e),Ct=(e)=>{try{let t=[...new Bun.Glob(e).scanSync({onlyFiles:!1,dot:!0})];if(t.length>0)return t}catch{}return[e]},Pt=(e)=>{if(Se(e))return[e];if(typeof e==="object"&&"op"in e&&e.op==="glob"&&"pattern"in e)return Ct(String(e.pattern));let t=E(e);if(t!==null){if(ke.has(t)||be.has(t))return[];return[`__op_${t}__`]}if(_t(e))return[];return["__tripwire_cmd_sub__"]},ge=(e,t)=>{let n=[],r=[],o=[],s=[],i=0;while(i<e.length){let g=e[i],p=E(g);if(p!==null&&ke.has(p)){let w=n.at(-1);if(w!==void 0&&/^[0-9]+$/.test(w)&&t.remaining>0)n.pop(),t.remaining-=1;let C=e[i+1];if(C!==void 0&&Se(C)){s.push({op:p,target:C}),i+=2;continue}i+=1;continue}for(let w of Pt(g))n.push(w);i+=1}if(n.length===0)return null;for(let g=1;g<n.length;g+=1){let p=n[g];if(p.startsWith("-")&&p!=="-")o.push(p);else r.push(p)}let l=n[0],u=l.lastIndexOf("/");return{head:u===-1?l:l.slice(u+1),tokens:n,args:r,flags:o,redirects:s,raw:n.join(" ")}},Ht=(e)=>`$${e}`,Tt=(e)=>{let t=[];for(let n=0;n<e.length;n+=1){let r=e[n],o=e[n+1];if(E(r)==="&"&&o!==void 0&&(E(o)===">"||E(o)===">>")){let s={op:E(o)===">"?"&>":"&>>"};t.push(s),n+=1;continue}if(E(r)===">"&&o!==void 0&&E(o)==="|"){t.push({op:">"}),n+=1;continue}t.push(r)}return t},Dt=(e)=>e.match(/(?<![\w$])\d+(?=[<>])/g)?.length??0,K=(e)=>{let t=/<<-?\s*(?:"(?<quoted>[^"]+)"|'(?<single>[^']+)'|(?<unquoted>[A-Za-z_][A-Za-z0-9_]*))/u.exec(e);return t?.groups?.quoted??t?.groups?.single??t?.groups?.unquoted??null},It=/(?:^|[|;&]\s*)(?:\/(?:usr\/bin|bin|usr\/local\/bin|opt\/homebrew\/bin)\/)?(?:sh|bash|zsh|dash|ksh|ash)(?:\s|$)/u,Z=(e)=>It.test(e),we=(e)=>{let t=e.split(`
|
|
3
|
+
`),n=[];for(let r=0;r<t.length;r+=1){let o=t[r];n.push(o);let s=K(o);if(s===null||Z(o))continue;r+=1;while(r<t.length&&t[r].trim()!==s)r+=1;if(r<t.length)n.push("__HEREDOC_BODY__",t[r])}return n.join(`
|
|
4
|
+
`)},Re=(e)=>{let t=new Map,n=e.split(`
|
|
5
|
+
`);for(let r=0;r<n.length;r+=1){let o=n[r],s=K(o);if(s===null||Z(o))continue;let i=[];r+=1;while(r<n.length&&n[r].trim()!==s)i.push(n[r]),r+=1;if(!t.has(s))t.set(s,i.join(`
|
|
6
|
+
`))}return t},Ft=(e)=>{let t=e.split(`
|
|
7
|
+
`),n=[];for(let r=0;r<t.length;r+=1){let o=t[r],s=K(o);if(s===null||!Z(o))continue;let i=[];r+=1;while(r<t.length&&t[r].trim()!==s)i.push(t[r]),r+=1;if(i.length>0)n.push(i.join(`
|
|
8
|
+
`))}return n},$t=(e,t)=>{for(let n=t;n<e.length;n+=1){let r=e[n];if(r==="\\"){n+=1;continue}if(r==="`")return n}return null},At=(e,t)=>{let n=1,r=null;for(let o=t;o<e.length;o+=1){let s=e[o];if(s==="\\"){o+=1;continue}if(r==="single"){if(s==="'")r=null;continue}if(s==="'"){r??="single";continue}if(s==='"'){r=r==="double"?null:"double";continue}if(s==="("){n+=1;continue}if(s===")"){if(n-=1,n===0)return o}}return null},Wt=(e)=>{let t=[],n=null;for(let r=0;r<e.length;r+=1){let o=e[r];if(o==="\\"){r+=1;continue}if(n==="single"){if(o==="'")n=null;continue}if(o==="'"){n??="single";continue}if(o==='"'){n=n==="double"?null:"double";continue}if(o==="`"){let f=$t(e,r+1);if(f!==null)t.push(e.slice(r+1,f)),r=f;continue}let s=e[r+1];if(!(o==="$"&&s==="(")&&!(n===null&&(o==="<"||o===">")&&s==="("))continue;let u=At(e,r+2);if(u!==null)t.push(e.slice(r+2,u)),r=u}return t},ve=/^\$\{?HOME\}?(?:\/|$)/,xe=(e)=>{if(e===""||e==="-")return!1;if(e==="/"||e==="~"||e==="."||e==="..")return!0;if(e.startsWith("/")||e.startsWith("~")||e.startsWith("./")||e.startsWith("../")||ve.test(e))return!0;return!1},M=(e)=>{if(e==="/")return 100;if(e==="~"||ve.test(e))return 90;if(/^\/(?<dir>etc|usr|bin|sbin|System|Library|var|boot|root|home)(?<suffix>\/|$)/.test(e))return 80;if(e.startsWith("/Users/"))return 70;if(e.startsWith("/")||e.startsWith("~"))return 60;if(e.startsWith("../"))return 40;if(e===".."||e==="."||e.startsWith("./"))return 10;return 50},Bt=new Set(["-e","--extension","-t","--type","-E","--exclude","-d","--max-depth","--min-depth","--exact-depth","-c","--color","--changed-within","--changed-before","-S","--size","-o","--owner","-j","--threads","-g","--glob","--format","--max-results","--ignore-file","--search-path","--base-directory","--path-separator","--and"]),Ot=(e,t)=>{let n=[],r=1;while(r<t){let o=e[r];if(Bt.has(o)){r+=2;continue}if(o.startsWith("-")){r+=1;continue}if(xe(o))n.push(o);r+=1}if(n.length===0)return".";return n.sort((o,s)=>M(s)-M(o)),n[0]},Lt=(e,t)=>{let n=[];for(let r=1;r<t;r+=1){let o=e[r];if(o.startsWith("-"))break;if(xe(o))n.push(o)}if(n.length===0)return".";return n.sort((r,o)=>M(o)-M(r)),n[0]},he={execFlags:new Set(["-x","-X","--exec","--exec-batch"]),placeholders:new Set(["{}","{/}","{//}","{.}","{/.}"]),pickRoot:Ot},me={execFlags:new Set(["-exec","-execdir","-ok","-okdir"]),placeholders:new Set(["{}"]),pickRoot:Lt},Mt=Object.assign(Object.create(null),{fd:he,fdfind:he,find:me,gfind:me}),Nt=(e,t,n)=>e.map((r)=>t.placeholders.has(r)?n:r),jt=(e)=>{let t=Mt[e.head];if(t===void 0)return[];let n=[],{tokens:r}=e;for(let o=1;o<r.length;o+=1){if(!t.execFlags.has(r[o]))continue;let s=[],i=o+1;while(i<r.length){let f=r[i];if(f===";"||f==="+")break;s.push(f),i+=1}if(s.length===0)continue;let l=s[0];if(t.placeholders.has(l))continue;let u=t.pickRoot(r,o);n.push(H(Nt(s,t,u))),o=i}return n},Ut=/\$\(\s*cat\s+<<-?\s*['"]?(?<delimiter>\w+)['"]?\s*\n(?<body>[\s\S]*?)\n\s*\k<delimiter>\s*\)/u,zt=/\$\(\s*(?:printf|echo)\s+(?:-[a-zA-Z]+\s+)*'(?<content>[^']*)'/u,j=(e,t)=>{let n=Ut.exec(e);if(n!==null){let o=n.groups?.body??e;if(t!==void 0&&o.trim()==="__HEREDOC_BODY__"){let s=n.groups?.delimiter,i=s===void 0?void 0:t.get(s);if(i!==void 0)return i}return o}let r=zt.exec(e);if(r!==null)return r.groups?.content??e;return e},Gt=new Set(["sh","bash","zsh","dash","ksh","ash"]),Jt=(e)=>{if(!Gt.has(e.head))return[];let{tokens:t}=e;for(let n=1;n<t.length;n+=1){let r=t[n];if(r==="-c"&&n+1<t.length)return[t[n+1]];if(r.startsWith("-")&&!r.startsWith("--")&&r.endsWith("c")&&r.length>2&&n+1<t.length)return[t[n+1]]}return[]},qt=new Set(["command","exec","env","time","nohup","setsid","nice","ionice","chronic","stdbuf","unbuffer","script","taskset","sudo","doas","xargs","watch"]),Vt={command:new Set,env:new Set(["-u","--unset","-C","--chdir","-S","--split-string","--block-signal"]),time:new Set(["-f","--format","-o","--output"]),nice:new Set(["-n","--adjustment"]),ionice:new Set(["-c","--class","-n","--classdata","-p","--pid"]),stdbuf:new Set(["-i","--input","-o","--output","-e","--error"]),script:new Set(["-c","--command"]),taskset:new Set,sudo:new Set(["-u","--user","-g","--group","-C","--close-from","-D","--chdir","-h","--host","-p","--prompt","-r","--role","-t","--type","-U","--other-user","-R","--chroot","-T","--command-timeout"]),doas:new Set(["-a","-C","-u"]),xargs:new Set(["-I","-i","-J","-n","--max-args","-P","--max-procs","-s","--max-chars","-L","--max-lines","-E","--eof","-d","--delimiter","-a","--arg-file","--replace"]),watch:new Set(["-n","--interval"])},Kt=(e)=>/^[A-Za-z_][A-Za-z0-9_]*=.*/u.test(e),Zt=(e)=>{let t=e[0],n=Vt[t]??new Set,r=1;while(r<e.length){let o=e[r];if(t==="env"&&Kt(o)){r+=1;continue}if(n.has(o)){r+=2;continue}if(o.includes("=")&&n.has(o.slice(0,o.indexOf("=")))){r+=1;continue}if(o.startsWith("--")&&o!=="--"){r+=1;continue}if(o.startsWith("-")&&o!=="-"){r+=1;continue}break}return r},Xt=(e)=>{if(!qt.has(e.head))return[];if(e.head==="script")for(let r=1;r<e.tokens.length-1;r+=1){let o=e.tokens[r];if(o==="-c"||o==="--command")return[e.tokens[r+1]];if(o.startsWith("--command="))return[o.slice(10)]}let t=Zt(e.tokens),n=H(e.tokens.slice(t));return n===""?[]:[n]},Yt=new Set(["eval"]),Qt=(e)=>{if(!Yt.has(e.head))return[];if(e.tokens.length===2)return[e.tokens[1]];let t=H(e.tokens.slice(1));return t===""?[]:[t]},en=new Set(["run","proxy","err","test","summary"]),tn=(e)=>e==="rtk",nn=(e)=>{let t=1;while(t<e.length){let n=e[t];if(n.startsWith("-")&&n!=="-"&&n!=="--"){t+=1;continue}break}return t},Ee=(e,t)=>{for(let n=t;n<e.length;n+=1){let r=e[n];if((r==="-c"||r==="--command")&&n+1<e.length)return e[n+1];if(r.startsWith("--command="))return r.slice(10)}return null},rn=(e)=>{if(!tn(e.head))return[];let t=nn(e.tokens),n=e.tokens[t];if(n===void 0)return[];if(en.has(n)){let o=Ee(e.tokens,t+1);if(o!==null)return o===""?[]:[o];let s=t+1;while(s<e.tokens.length){let l=e.tokens[s];if(l==="--"){s+=1;break}if(l.startsWith("-")&&l!=="-"){s+=1;continue}break}let i=H(e.tokens.slice(s));return i===""?[]:[i]}let r=H(e.tokens.slice(t));return r===""?[]:[r]},on={timeout:{valueFlags:new Set(["-s","--signal","-k","--kill-after"]),skipPositionals:1,dashCommand:!1},gtimeout:{valueFlags:new Set(["-s","--signal","-k","--kill-after"]),skipPositionals:1,dashCommand:!1},chroot:{valueFlags:new Set(["--userspec","--groups"]),skipPositionals:1,dashCommand:!1},flock:{valueFlags:new Set(["-w","--wait","--timeout","-E","--conflict-exit-code"]),skipPositionals:1,dashCommand:!0},su:{valueFlags:new Set,skipPositionals:0,dashCommand:!0}},sn=(e)=>{let t=on[e.head];if(t===void 0)return[];if(t.dashCommand){let o=Ee(e.tokens,1);if(o!==null)return o===""?[]:[o]}let n=1;while(n<e.tokens.length){let o=e.tokens[n];if(t.valueFlags.has(o)){n+=2;continue}if(o.includes("=")&&t.valueFlags.has(o.slice(0,o.indexOf("=")))){n+=1;continue}if(o==="--"){n+=1;break}if(o.startsWith("-")&&o!=="-"){n+=1;continue}break}n+=t.skipPositionals;let r=H(e.tokens.slice(n));return r===""?[]:[r]},_e=new Set(["!","if","elif","then","else","while","until","do"]),an=(e)=>{if(!_e.has(e.head)||e.tokens.length<2)return[];return[H(e.tokens.slice(1))]},cn=[jt,Jt,Xt,Qt,rn,sn,an],N="__tripwire_unsupported_shell__",pe=(e)=>({head:N,tokens:[N],args:[],flags:[],redirects:[],raw:e}),ln=(e)=>e.some((t)=>{if(t.head==="case"||t.head==="function"||t.head==="{")return!0;if(t.tokens.includes("__op_(__")||t.tokens.includes("__op_)__"))return!0;return _e.has(t.head)&&t.tokens[1]==="{"}),dn=(e)=>{let t="",n=!1,r=!1,o=!1;for(let s of e){if(o){t+=s,o=!1;continue}if(s==="\\"){t+=s,o=!0;continue}if(s==="'"&&!r){n=!n,t+=s;continue}if(s==='"'&&!n){r=!r,t+=s;continue}t+=s===`
|
|
9
|
+
`&&!n&&!r?" ; ":s}return t},I=(e)=>{let t,n=dn(we(e));try{t=xt(n,Ht)}catch{return[pe(n)]}t=Tt(t);let r={remaining:Dt(n)},o=[],s=[];for(let u of t){let f=E(u);if(f!==null&&be.has(f)){let g=ge(s,r);if(g!==null)o.push(g);s=[];continue}s.push(u)}let i=ge(s,r);if(i!==null)o.push(i);if(ln(o))o.push(pe(n));for(let u of[...Wt(e),...Ft(e)])for(let f of I(u))o.push(f);let l=o.length;for(let u=0;u<l;u+=1){let f=o[u];for(let g of cn)for(let p of g(f))for(let w of I(p))o.push(w)}return o},un=(e)=>e.startsWith("./")?e.slice(2):e,X=(e,t=[],n=[])=>{if(e==="")return!1;let r=un(e);if(r===".."||r.startsWith("../")||r.includes("/../"))return!1;for(let o of[...ye,...n])if(r===o||r.startsWith(`${o}/`))return!0;for(let o of[...Et,...t])if(r===o||r.startsWith(`${o}/`))return!0;return!1},Ce=(e=[],t=[])=>{let n={"build outputs":["dist","build","_build","out","target"],"js framework outputs":[".next",".nuxt",".svelte-kit",".output",".astro",".angular",".vite",".parcel-cache",".turbo",".vercel",".netlify",".fly",".wrangler",".serverless"],"tests / coverage":["coverage",".nyc_output"],caches:[".cache",".ruff_cache",".mypy_cache",".pytest_cache",".ty_cache",".tox"],"language / package":["__pycache__",".venv","venv","node_modules",".gradle","DerivedData",".bundle",".cargo-target"],"tmp / state":["tmp",".tmp",".state",...ye],iac:[".terraform"],"bundler dev":[".yarn/cache",".yarn/install-state.gz",".pnpm-store",".bun"]};if(e.length>0)n["custom relative"]=e;if(t.length>0)n["custom absolute"]=t;return Object.entries(n).map(([r,o])=>` ${r}: ${o.join(", ")}`).join(`
|
|
10
|
+
`)},k=(e)=>/(?<prefix>^|\s)#\s*tripwire-allow:[ \t]*\S[^\r\n]*/.test(we(e));import{readFileSync as fn}from"fs";import{homedir as gn}from"os";import{Cause as hn,Data as He,Effect as R,Schema as c}from"effect";var Pe=c.Struct({pattern:c.String,message:c.String,action:c.optional(c.Union([c.Literal("deny"),c.Literal("ask")])),requiresFlags:c.optional(c.Array(c.String)),forbidsFlagValues:c.optional(c.Array(c.Struct({flag:c.String,values:c.Array(c.String)})))}),mn=c.Struct({protectedBranches:c.optional(c.Array(c.String)),enforceConventionalCommits:c.optional(c.Boolean)}),pn=c.Struct({relative:c.optional(c.Array(c.String)),absolute:c.optional(c.Array(c.String))}),yn=c.Struct({argumentsIncludeAll:c.optional(c.Array(c.String)),argumentsStartWith:c.optional(c.Array(c.String)),shortFlagsIncludeAll:c.optional(c.Array(c.String))}),kn=c.Struct({rule:c.String,executables:c.Array(c.String),action:c.Union([c.Literal("deny"),c.Literal("warn")]),message:c.String,match:c.optional(yn)}),bn=c.Struct({executable:c.String,timeoutMs:c.Finite.check(c.isGreaterThan(0))}),Sn=c.Struct({git:c.optional(mn),safePaths:c.optional(pn),toolPolicies:c.optional(c.Array(kn)),blockedCommands:c.optional(c.Array(Pe)),allowedCommands:c.optional(c.Array(Pe)),secretScanner:c.optional(bn)}),U=`${gn()}/.config/tripwire/config.json`;class Te extends He.TaggedError("ConfigReadError"){}class De extends He.TaggedError("ConfigParseError"){}var wn=(e)=>e instanceof Error&&("code"in e)&&e.code==="ENOENT",Rn=(e)=>R.try({try:()=>fn(e,"utf8"),catch:(t)=>new Te({cause:t})}).pipe(R.catchTag("ConfigReadError",(t)=>wn(t.cause)?R.succeed(null):R.fail(t))),vn=(e)=>R.try({try:()=>JSON.parse(e),catch:(t)=>new De({cause:t})}),xn=(e)=>c.decodeUnknownEffect(Sn)(e,{onExcessProperty:"error"}),B=()=>({git:{protectedBranches:[],enforceConventionalCommits:!1},safePaths:{},toolPolicies:[],blockedCommands:[],allowedCommands:[],secretScanner:{executable:"betterleaks",timeoutMs:5000}}),z=(e)=>{let t=B();return{git:{protectedBranches:e.git?.protectedBranches??t.git.protectedBranches,enforceConventionalCommits:e.git?.enforceConventionalCommits??t.git.enforceConventionalCommits},safePaths:{...t.safePaths,...e.safePaths},toolPolicies:e.toolPolicies??t.toolPolicies,blockedCommands:e.blockedCommands??t.blockedCommands,allowedCommands:e.allowedCommands??t.allowedCommands,secretScanner:e.secretScanner??t.secretScanner}},G=(e=U)=>R.gen(function*(){let n=yield*Rn(e);if(n===null)return{ok:!0,config:B()};let r=yield*vn(n),o=yield*xn(r);return{ok:!0,config:z(o)}}).pipe(R.timeout(1000),R.catchCause((t)=>{let n={ok:!1,error:hn.pretty(t)};return R.succeed(n)})),En=(e=U)=>G(e).pipe(R.flatMap((t)=>t.ok?R.succeed(t.config):R.die(Error(`[tripwire] config load failed (${e}): ${t.error}`))));var Ae=new Set(["preToolUse","beforeShellExecution","beforeMCPExecution","beforeReadFile","beforeTabFileRead"]),ee=new Set(["postToolUse","postToolUseFailure","afterShellExecution","afterMCPExecution","afterFileEdit","afterTabFileEdit"]),te=(e)=>({kind:"cursor",eventName:e,post:ee.has(e)}),_n=(e)=>Ae.has(e)||ee.has(e),T=(e)=>typeof e==="object"&&e!==null&&!Array.isArray(e),m=(e,...t)=>{for(let n of t){let r=e[n];if(typeof r==="string")return r}return},ne=(e,...t)=>{for(let n of t)if(n in e)return e[n];return},Q=(e,t,...n)=>{let r=m(e,...n);if(r!==void 0)return r;if(typeof t==="string"&&n.includes("command"))return t;return T(t)?m(t,...n):void 0},Ie=(e,t)=>{if(e===void 0)throw Error(`Cursor hook payload missing ${t}`);return e},Cn=(e)=>{let t=e.toLowerCase();if(t==="bash"||t==="exec"||t==="shell"||t==="run_command")return"Bash";if(t==="read"||t==="read_file")return"Read";if(t==="write"||t==="write_file")return"Write";if(t==="edit"||t==="edit_file"||t==="multiedit"||t==="apply_patch")return"Edit";return e},Pn=(e)=>e==="Read"||e==="Write"||e==="Edit",Hn=(e,t)=>{let n;if(typeof e==="string")n=e;else if(T(e))n=m(e,"command","cmd");if(t&&n===void 0)throw Error("Cursor hook payload missing command");if(typeof e==="string")return{command:e};if(!T(e)||n===void 0)return e;return{...e,command:n}},Tn=(e,t,n)=>{let r=m(t,"file_path","filePath","path");if(n&&r===void 0)throw Error(`Cursor ${e} hook payload missing file path`);if(e==="Read")return{...t,file_path:r??""};if(e==="Write"){let o=m(t,"content","fileText","text");if(n&&o===void 0)throw Error("Cursor Write hook payload missing content");return{...t,file_path:r??"",content:o??""}}if(e==="Edit"){let o=m(t,"old_string","oldString"),s=m(t,"new_string","newString");if(n&&(o===void 0||s===void 0))throw Error("Cursor Edit hook payload missing oldString or newString");return{...t,file_path:r??"",old_string:o??"",new_string:s??""}}return t},Y=(e,t,n=!0)=>{if(e==="Bash")return Hn(t,n);if(!Pn(e))return t;if(!T(t)){if(n)throw Error(`Cursor ${e} hook payload is missing tool input`);return t}return Tn(e,t,n)},Fe=(e,t)=>{if(typeof t!=="string")return t;if(e==="Bash")return{stdout:t,stderr:""};if(e==="Read")return{content:t};return{content:t}},Dn=(e,t)=>{if(e==="beforeShellExecution"||e==="afterShellExecution")return"Bash";if(e==="beforeReadFile"||e==="beforeTabFileRead"||e==="afterFileEdit"||e==="afterTabFileEdit")return e.startsWith("before")?"Read":"Edit";let n=m(t,"tool_name","toolName");if(n!==void 0)return Cn(n);let r=ne(t,"tool_input","toolInput");if(m(t,"command","cmd")!==void 0||typeof r==="string"||T(r)&&m(r,"command","cmd")!==void 0)return"Bash";if(m(t,"file_path","filePath","path")!==void 0||T(r)&&m(r,"file_path","filePath","path")!==void 0)return"Read";return""},In=(e,t)=>{if(e==="Bash")return{command:Q(t,void 0,"command","cmd")};let n={file_path:m(t,"file_path","filePath","path")};if(e==="Write")n.content=m(t,"content","fileText","text");if(e==="Edit")n.old_string=m(t,"old_string","oldString"),n.new_string=m(t,"new_string","newString");return n},Fn=(e,t,n,r)=>{let o=ne(n,"tool_input","toolInput"),s=!r;if(e==="beforeShellExecution"||e==="afterShellExecution"){let i=Q(n,o,"command","cmd");return{command:s?Ie(i,"command"):i??""}}if(e==="beforeReadFile"||e==="beforeTabFileRead"||e==="afterFileEdit"||e==="afterTabFileEdit"){let i=Q(n,o,"file_path","filePath","path");return Y(t,{file_path:s?Ie(i,"file path"):i??"",...Array.isArray(n.edits)&&{edits:n.edits}},s)}if(o===void 0&&["Bash","Read","Write","Edit"].includes(t))return Y(t,In(t,n),s);return Y(t,o,s)},$n=(e,t,n)=>{if(e==="afterShellExecution")return Fe(t,m(n,"output")??"");let r=ne(n,"tool_response","toolResponse","tool_output","toolOutput","result","result_json");return Fe(t,r)},$e=(e,t,n)=>{let r=Dn(t,e),o=Fn(t,r,e,n),s=n?$n(t,r,e):void 0,i=m(e,"cwd"),l=m(e,"conversation_id","session_id"),u=m(e,"tool_use_id");return{event:{hook_event_name:n?"PostToolUse":"PreToolUse",...r.length>0&&{tool_name:r},...o!==void 0&&{tool_input:o},...s!==void 0&&{tool_response:s},...i!==void 0&&{cwd:i},...l!==void 0&&{session_id:l},...u!==void 0&&{tool_use_id:u}},host:te(t)}},We=(e,t)=>{if(t!==void 0&&!_n(t))throw Error(`Unknown Cursor hook event "${t}"`);if(!T(e)){if(t!==void 0)throw Error(`Cursor ${t} hook payload must be a JSON object`);return{event:e,host:{kind:"native"}}}let n=t??m(e,"hook_event_name");if(n!==void 0&&Ae.has(n))return $e(e,n,!1);if(n!==void 0&&ee.has(n))return $e(e,n,!0);return{event:e,host:{kind:"native"}}};import{Schema as b}from"effect";var re=b.Struct({hook_event_name:b.String,tool_name:b.optional(b.String),tool_input:b.optional(b.Unknown),tool_response:b.optional(b.Unknown),cwd:b.optional(b.String),session_id:b.optional(b.String),turn_id:b.optional(b.String),tool_use_id:b.optional(b.String)}),F=(e)=>typeof e==="object"&&e!==null&&!Array.isArray(e),Be=(e)=>F(e)&&typeof e.command==="string",Oe=(e)=>F(e)&&typeof e.file_path==="string"&&typeof e.old_string==="string"&&typeof e.new_string==="string",Le=(e)=>F(e)&&typeof e.file_path==="string"&&typeof e.content==="string",Me=(e)=>F(e)&&typeof e.file_path==="string",Ne=(e,t)=>{if(!F(t))return"";if(e==="Bash"){let n=typeof t.stdout==="string"?t.stdout:"",r=typeof t.stderr==="string"?t.stderr:"";return[n,r].filter((o)=>o.length>0).join(`
|
|
11
|
+
`)}if(e==="Read"){if(typeof t.content==="string")return t.content;let{file:n}=t;return F(n)&&typeof n.content==="string"?n.content:""}return typeof t.content==="string"?t.content:""};import{appendFileSync as An,mkdirSync as Wn}from"fs";import{homedir as Bn}from"os";import On from"path";var je=`${Bn()}/.claude/tripwire.log`;try{Wn(On.dirname(je),{recursive:!0})}catch{}var v=(e,t)=>{let n=new Date().toISOString(),r=t instanceof Error?t.stack??t.message:String(t);try{An(je,`[${n}] [${e}] ${r}
|
|
12
|
+
`)}catch{}};var Ln=(e)=>e.tokens.slice(1).join(" "),Ue=(e,...t)=>{let n=new Set(e.flags);return t.some((r)=>n.has(r))},Mn=[{rule:"unsupported-shell-structure",action:"deny",message:"Tripwire cannot inspect every executable branch in this shell structure. Rewrite it as simple commands joined with `;`, `&&`, or `||` so each command can be checked.",match:(e)=>e.head===N},{rule:"rm-rf-root",action:"deny",message:"rm -rf / is catastrophic. If the goal is cleaning a subdirectory, scope the path inside the project (e.g. ./dist).",match:(e)=>e.head==="rm"&&Ue(e,"-rf","-fr","-Rf","-fR")&&e.tokens.includes("/")},{rule:"rm-rf-home",action:"deny",message:"rm -rf on $HOME / ~ would erase the home directory. Refuse.",match:(e)=>e.head==="rm"&&Ue(e,"-rf","-fr","-Rf","-fR")&&e.tokens.some((t)=>/^(?<home>~|\$HOME|\$\{HOME\})$/.test(t))},{rule:"fork-bomb",action:"deny",message:"Fork bomb pattern detected. Refuse.",match:(e,t)=>/:\(\)\s*\{\s*:\s*\|\s*:\s*&\s*\}\s*;/.test(t)},{rule:"source-script",action:"deny",message:"`source` / `.` executes a file in the current shell. Refuse arbitrary sourced scripts.",match:(e)=>(e.head==="source"||e.head===".")&&e.tokens.length>1},{rule:"dd-raw-device",action:"deny",message:"dd writing to a raw block device wipes the disk. Refuse.",match:(e)=>e.head==="dd"&&/\bof=\/dev\/(?<type>disk|sd|nvme|rdisk)/i.test(Ln(e))},{rule:"mkfs",action:"deny",message:"mkfs formats a filesystem. Refuse.",match:(e)=>/^mkfs(?<ext>\.[a-z0-9]+)?$/i.test(e.head)},{rule:"kill-all",action:"deny",message:"kill -9 -1 kills every process you own. Refuse.",match:(e)=>e.head==="kill"&&e.tokens.includes("-9")&&e.tokens.includes("-1")},{rule:"chmod-777-recursive",action:"deny",message:"Recursive chmod 777 makes everything world-writable. Use 755 for directories, 644 for files, scoped narrowly.",match:(e)=>e.head==="chmod"&&e.flags.some((t)=>t.includes("R"))&&e.tokens.includes("777")},{rule:"no-verify",action:"deny",message:"--no-verify skips git hooks. Per policy: never skip hooks. Fix the underlying issue.",match:(e)=>e.tokens.includes("--no-verify")},{rule:"no-gpg-sign",action:"deny",message:"Bypassing GPG signing is off-limits unless explicitly requested.",match:(e,t)=>/--no-gpg-sign\b|-c\s+commit\.gpgsign=false/.test(t)},{rule:"sudo",action:"ask",message:"sudo escalates privileges and is almost never needed in a coding session. If genuinely required, explain why; otherwise find a non-sudo path.",match:(e)=>e.head==="sudo"},{rule:"shutdown",action:"deny",message:"shutdown / reboot / halt control the machine. Refuse \u2014 system control should be done manually.",match:(e)=>["shutdown","reboot","halt","poweroff"].includes(e.head)},{rule:"launchctl-mutation",action:"deny",message:"launchctl load/unload/bootstrap/bootout/kickstart mutates system services. Refuse \u2014 surface the intent instead.",match:(e)=>e.head==="launchctl"&&typeof e.tokens[1]==="string"&&["load","unload","bootstrap","bootout","kickstart","enable","disable"].includes(e.tokens[1])},{rule:"defaults-write",action:"deny",message:"`defaults write` mutates macOS preferences. Refuse \u2014 surface the intent.",match:(e)=>e.head==="defaults"&&e.tokens[1]==="write"},{rule:"csrutil",action:"deny",message:"csrutil controls System Integrity Protection. Refuse.",match:(e)=>e.head==="csrutil"},{rule:"nvram",action:"deny",message:"nvram modifies firmware variables. Refuse.",match:(e)=>e.head==="nvram"},{rule:"diskutil-destructive",action:"deny",message:"diskutil eraseDisk / reformat / partitionDisk wipes disks. Refuse.",match:(e)=>e.head==="diskutil"&&typeof e.tokens[1]==="string"&&["eraseDisk","reformat","partitionDisk","eraseVolume","secureErase"].includes(e.tokens[1])},{rule:"tmutil-destructive",action:"deny",message:"tmutil delete / disablelocal touches Time Machine. Refuse.",match:(e)=>e.head==="tmutil"&&typeof e.tokens[1]==="string"&&["delete","disablelocal"].includes(e.tokens[1])},{rule:"osascript",action:"ask",message:"osascript runs arbitrary AppleScript and can do almost anything (move files, send emails, drive apps). Confirm with the user what you want done before running it.",match:(e)=>e.head==="osascript"},{rule:"topgrade",action:"deny",message:"topgrade upgrades everything (brew, mas, npm globals, rust, mise). System upgrades should be done manually.",match:(e)=>e.head==="topgrade"},{rule:"rsync-delete",action:"deny",message:"rsync --delete removes files at the destination. High blast radius \u2014 surface the intent instead.",match:(e)=>e.head==="rsync"&&e.flags.includes("--delete")},{rule:"softwareupdate-install",action:"deny",message:"`softwareupdate --install / -i / -d` triggers macOS system updates. Refuse \u2014 system updates should be done manually.",match:(e)=>e.head==="softwareupdate"&&e.flags.some((t)=>t==="--install"||t==="-i"||t==="-d"||t.startsWith("--download"))},{rule:"pmset-write",action:"deny",message:"`pmset` (with arguments) writes power-management settings. Read-only `pmset -g` is fine; mutations need the user.",match:(e)=>e.head==="pmset"&&e.tokens.length>1&&!e.tokens.includes("-g")&&!e.tokens.includes("-G")},{rule:"dscl-mutate",action:"deny",message:"`dscl . -create / -delete / -append / -change / -merge` modifies the local directory service (your user account, groups, etc.). Refuse.",match:(e)=>e.head==="dscl"&&e.tokens.some((t)=>["-create","-delete","-append","-change","-merge","-passwd"].includes(t))},{rule:"xattr-quarantine-bypass",action:"deny",message:"`xattr -d com.apple.quarantine` removes Gatekeeper's quarantine bit \u2014 the macOS protection against running untrusted binaries. Refuse.",match:(e)=>e.head==="xattr"&&e.tokens.includes("-d")&&e.tokens.some((t)=>t.includes("com.apple.quarantine"))},{rule:"spctl-disable",action:"deny",message:"`spctl --master-disable` / `--global-disable` disables Gatekeeper system-wide. Refuse.",match:(e)=>e.head==="spctl"&&e.flags.some((t)=>t==="--master-disable"||t==="--global-disable"||t==="--disable")},{rule:"kextload",action:"deny",message:"Loading a kernel extension (`kextload`, `kmutil load`) is a system-level mutation. Refuse \u2014 the user handles this manually.",match:(e)=>e.head==="kextload"||e.head==="kextunload"||e.head==="kmutil"&&(e.tokens[1]==="load"||e.tokens[1]==="unload")},{rule:"security-keychain-destructive",action:"deny",message:"`security delete-keychain / delete-generic-password / delete-internet-password / set-keychain-settings` mutates your Keychain (where CLIs store their secrets). Refuse \u2014 Keychain should be managed manually.",match:(e)=>e.head==="security"&&typeof e.tokens[1]==="string"&&["delete-keychain","delete-generic-password","delete-internet-password","delete-certificate","delete-identity","set-keychain-settings","unlock-keychain","lock-keychain","create-keychain"].includes(e.tokens[1])},{rule:"security-keychain-add-write",action:"ask",message:"`security add-generic-password / add-internet-password / add-certificate` writes to your Keychain. Other tools manage their own entries \u2014 confirm this is the right path before adding anything else manually.",match:(e)=>e.head==="security"&&typeof e.tokens[1]==="string"&&["add-generic-password","add-internet-password","add-certificate"].includes(e.tokens[1])},{rule:"systemsetup",action:"deny",message:"`systemsetup -set...` writes machine-wide settings (timezone, sleep, network time, restart-on-power-failure). Refuse.",match:(e)=>e.head==="systemsetup"&&e.tokens.some((t)=>t.startsWith("-set"))},{rule:"scutil-set",action:"deny",message:"`scutil --set` writes system configuration (computer name, hostname, LocalHostName). Refuse \u2014 read-only `scutil --get` is fine.",match:(e)=>e.head==="scutil"&&e.tokens.includes("--set")},{rule:"brew-mutation",action:"ask",message:"`brew install/uninstall/upgrade/untap` modifies the machine globally. Confirm before running.",match:(e)=>e.head==="brew"&&typeof e.tokens[1]==="string"&&["install","uninstall","upgrade","reinstall","untap","tap"].includes(e.tokens[1])},{rule:"mas-mutation",action:"ask",message:"`mas install/uninstall` changes installed Mac App Store apps. Confirm.",match:(e)=>e.head==="mas"&&typeof e.tokens[1]==="string"&&["install","uninstall","purchase"].includes(e.tokens[1])},{rule:"gh-destructive",action:"deny",message:"gh repo/release/issue delete is destructive on shared state. Refuse.",match:(e)=>e.head==="gh"&&typeof e.tokens[1]==="string"&&typeof e.tokens[2]==="string"&&["repo","release","issue","pr"].includes(e.tokens[1])&&["delete","destroy","remove"].includes(e.tokens[2])},{rule:"flyctl-destroy",action:"deny",message:"flyctl apps destroy / volumes destroy nukes deployed infra. Refuse.",match:(e)=>e.head==="flyctl"&&e.tokens.some((t)=>t==="destroy"||t==="delete")},{rule:"gcloud-delete",action:"deny",message:"`gcloud ... delete` affects cloud resources. Refuse.",match:(e)=>e.head==="gcloud"&&e.tokens.includes("delete")}],Nn=new Set(["unsupported-shell-structure","rm-rf-root","rm-rf-home","fork-bomb","dd-raw-device","mkfs","kill-all","diskutil-destructive","tmutil-destructive","shutdown","csrutil","nvram","kextload","spctl-disable","xattr-quarantine-bypass","topgrade","softwareupdate-install","systemsetup","scutil-set","security-keychain-destructive","no-verify","no-gpg-sign"]),ze=(e,t)=>{let n=k(t),r=[];for(let o of e)for(let s of Mn){if(!s.match(o,t))continue;if(n&&!Nn.has(s.rule))continue;r.push(s.action==="deny"?d(s.rule,s.message):y(s.rule,s.message));break}return r.length===0?a("bash-deny"):D(r)};var oe=(e)=>e.protectedBranches??[],jn=/^(?<type>feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(?<scope>\([\w./\- ]+\))?!?:\s+\S/,Un=new Set(["-C","-c","--git-dir","--work-tree","--namespace","--super-prefix"]),zn=new Set(["--bare","--paginate","-p","--no-pager","--no-replace-objects","--literal-pathspecs","--glob-pathspecs","--noglob-pathspecs","--icase-pathspecs","--no-optional-locks","--exec-path","--html-path","--man-path","--info-path"]),Gn=(e)=>{if(e.head!=="git")return null;let t=e.tokens.slice(1),n=0;while(n<t.length){let r=t[n];if(r===void 0)break;if(Un.has(r)){n+=2;continue}if(zn.has(r)){n+=1;continue}if(r.startsWith("--git-dir=")||r.startsWith("--work-tree=")||r.startsWith("--namespace=")||r.startsWith("--super-prefix=")||r.startsWith("--exec-path=")){n+=1;continue}if(r.startsWith("-")){n+=1;continue}return{subcommand:r,subArgs:t.slice(n+1)}}return null},Jn=(e,t)=>{for(let n=0;n<e.length;n+=1){let r=e[n];if(r===void 0)break;if(r==="-m"||r==="--message"){let o=e[n+1];return o===void 0?null:j(o,t)}if(r.startsWith("--message="))return j(r.slice(10),t);if(/^-[a-zA-Z]*m[a-zA-Z]*$/.test(r)){let o=e[n+1];return o===void 0?null:j(o,t)}}return null},qn=(e,t)=>{let n=oe(t);for(let r of e)for(let o of n)if(r===o||r.endsWith(`:${o}`)||r.endsWith(`/${o}`))return o;return null},Vn=(e)=>e.filter((t)=>!t.startsWith("-")),Kn=(e)=>e.filter((t)=>t.startsWith("-")),h=(e,...t)=>{let n=new Set(e);return t.some((r)=>n.has(r))},Zn=({subArgs:e,positional:t})=>{if(h(e,"--global","--system"))return d("git-config-global","Modifying global / system git config is off-limits \u2014 that is your personal identity. Read-only `git config --get` is fine.");if(!h(e,"--get","-l","--list","--get-all","--get-regexp")&&t.length>=2)return d("git-config-write","Local git config writes should be done explicitly. To read a value, use `git config --get <key>`.");return a("bash-git")},Xn=({subArgs:e})=>{if(h(e,"--cached"))return a("bash-git");return y("git-rm","`git rm <path>` removes from the index AND the working tree. To untrack-only, use `git rm --cached <path>`. To delete the file separately, use `trash` / `rip`. Confirm intent.")},Yn=({subArgs:e,positional:t})=>{if(h(e,"--staged","-S")&&!h(e,"--worktree","-W"))return a("bash-git");if(t.length>0)return d("git-restore-discard","`git restore <path>` discards uncommitted changes in the working tree. Refuse \u2014 `git diff <path>` to inspect first, or `git stash push <path>` to preserve.");return a("bash-git")},Qn=({subArgs:e,positional:t})=>{if(h(e,"-b","-B"))return a("bash-git");if(h(e,"-f","--force"))return d("git-checkout-force","`git checkout -f` overwrites the working tree without preserving uncommitted changes. Refuse.");if(e.includes("--"))return d("git-checkout-discard","`git checkout -- <path>` discards uncommitted working-tree changes. Refuse \u2014 use `git stash push <path>` to preserve, or `git diff <path>` to inspect first.");let[n]=t;if(n!==void 0&&t.length===1&&(n==="."||n.startsWith("./")))return d("git-checkout-discard-all","`git checkout .` discards ALL uncommitted working-tree changes. Refuse \u2014 `git stash` to preserve, or `git diff` to inspect first.");return a("bash-git")},er=({subArgs:e})=>{if(h(e,"-f","--force","--discard-changes"))return d("git-switch-force","`git switch -f / --discard-changes` throws away uncommitted working-tree changes. Refuse.");return a("bash-git")},tr=({subArgs:e,flags:t,positional:n})=>{if(h(e,"--hard"))return d("git-reset-hard","`git reset --hard` discards all uncommitted changes AND moves HEAD. Refuse \u2014 describe the intent in chat. If undoing a published commit, `git revert <sha>` is safer.");if(h(e,"--keep"))return y("git-reset-keep","`git reset --keep` resets HEAD but preserves uncommitted local changes. Confirm intent.");if(n.length===0&&t.length===0)return a("bash-git");return y("git-reset-mixed","`git reset` moves HEAD. Confirm intent \u2014 if undoing a commit, `git revert` is usually safer.")},nr=({flags:e})=>{if(e.some((t)=>/^-[a-zA-Z]*[df]/.test(t)||t==="--force"))return d("git-clean-fd","`git clean -fd` deletes untracked files (often your in-progress work). Refuse \u2014 inspect with `git clean -dn` (dry run) first. If genuinely needed, append ` # tripwire-allow: <reason>`.");return a("bash-git")},rr=({subArgs:e,positional:t,config:n})=>{if(h(e,"--abort","--quit","--continue","--skip","--edit-todo"))return a("bash-git");if(h(e,"-i","--interactive"))return d("git-rebase-interactive","`git rebase -i` rewrites history interactively. Refuse \u2014 too easy to lose commits in the agent loop. If this is genuinely required, do it manually outside the agent.");let[r]=t,o=oe(n);if(r!==void 0&&o.includes(r))return y("git-rebase-onto-protected",`Rebasing onto \`${r}\` rewrites history of the current branch. \`git merge ${r}\` is usually safer. Confirm intent.`);return y("git-rebase","`git rebase` rewrites commit history. `git merge` is usually safer. Confirm intent.")},or=({subArgs:e})=>{if(h(e,"--abort","--quit","--continue","--skip"))return a("bash-git");return y("git-cherry-pick","`git cherry-pick` applies commits onto the current branch and can create conflicts. Confirm intent.")},sr=({subArgs:e})=>{if(h(e,"--abort","--continue","--quit"))return a("bash-git");return y("git-merge","`git merge <branch>` may create merge conflicts. Confirm intent.")},ir=({subArgs:e,config:t,heredocBodies:n})=>{if(h(e,"--amend"))return d("git-commit-amend","`git commit --amend` rewrites the last commit. If it has been pushed, this causes upstream divergence. Refuse \u2014 surface the intent.");let r=Jn(e,n),o=h(e,"-F","--file","-c","-C","--reuse-message","--reedit-message"),s=h(e,"--no-edit");if(r===null&&!o&&!s)return d("git-commit-no-message",'`git commit` without `-m "..."` opens an editor and hangs the agent. Use `git commit -m "<conventional message>"`.');if(r!==null&&t.enforceConventionalCommits===!0&&!jn.test(r))return d("git-commit-non-conventional",["Commit message must follow Conventional Commits format:"," `<type>(<scope>)?(!)?: <description>`","Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert.","Examples:"," `fix(auth): handle expired token refresh`"," `feat: add bash-git rule`"," `chore: bump deps`",`Got: ${JSON.stringify(r)}`].join(`
|
|
13
|
+
`));if(h(e,"-a","--all")||e.some((i)=>/^-[a-zA-Z]*a[a-zA-Z]*$/.test(i)))return y("git-commit-auto-stage","`git commit -a / --all` auto-stages every tracked change. Explicit `git add <files>` first is usually clearer about what is being committed. Confirm.");return a("bash-git")},ar=({subArgs:e,flags:t,positional:n,config:r})=>{if(t.some((s)=>s==="--force"||s==="-f"||s.startsWith("--force-with-lease")))return d("git-force-push","Force push is forbidden. If a branch needs to be reset upstream, surface the intent \u2014 there is almost always a non-force path.");if(h(e,"--delete","--mirror")||e.some((s)=>s.startsWith(":")))return d("git-push-delete","Refusing to delete a remote branch via push. If genuinely needed, surface the intent.");let o=qn(n,r);if(o!==null)return d("git-push-protected",`Refusing to push directly to protected branch \`${o}\`. Open a PR instead: \`gh pr create\`.`);return a("bash-git")},cr=({subArgs:e,flags:t,positional:n,config:r})=>{let o=t.find((s)=>s==="-D"||s==="-d"||s==="--delete"||/^-[a-zA-Z]*D/.test(s)||/^-[a-zA-Z]*d/.test(s));if(o!==void 0){let s=n,i=new Set(oe(r)),l=s.find((u)=>i.has(u));if(l!==void 0)return d("git-branch-delete-protected",`Refusing to delete protected branch \`${l}\`.`);if(o==="-D"||o.includes("D"))return d("git-branch-force-delete",`\`git branch -D ${s.join(" ")}\` force-deletes branches even if unmerged (potential data loss). To delete a merged branch, use \`-d\`. To force, append \` # tripwire-allow: <reason>\`.`);return y("git-branch-delete",`\`git branch -d ${s.join(" ")}\` deletes a branch (merged-only check). Confirm intent.`)}if(h(e,"-m","-M","--move"))return y("git-branch-move","Renaming a branch can confuse pushed remotes. Confirm intent.");return a("bash-git")},lr=({subArgs:e})=>{if(h(e,"-d","--delete"))return d("git-tag-delete","`git tag -d` deletes a tag. Refuse \u2014 surface intent.");return a("bash-git")},dr=({subArgs:e})=>{let t=e[0]??"push";if(t==="drop"||t==="clear")return d("git-stash-drop",`\`git stash ${t}\` discards stashed work. Refuse \u2014 \`git stash list\` and \`git stash show\` to inspect first.`);return a("bash-git")},ur=({flags:e})=>{if(e.some((t)=>t.startsWith("--prune=")||t==="--aggressive"))return d("git-gc-prune","`git gc --prune=now` / `--aggressive` destroys reflog recovery options. Refuse.");return a("bash-git")},fr=({subArgs:e})=>{let[t]=e;if(t==="add"||t==="remove"||t==="rm"||t==="set-url"||t==="rename")return y("git-remote-mutate",`\`git remote ${t}\` changes which remote you're pushing to. Confirm \u2014 accidentally pointing at the wrong remote is high blast-radius.`);return a("bash-git")},Ge=({subcommand:e,subArgs:t})=>{let n=t[0]??"";if(["add","remove","rm","deinit","sync","set-url"].includes(n))return y("git-submodule-worktree-mutate",`\`git ${e} ${n}\` modifies repo structure. Confirm intent.`);return a("bash-git")},gr=new Map([["config",Zn],["add",()=>a("bash-git")],["mv",()=>a("bash-git")],["rm",Xn],["restore",Yn],["checkout",Qn],["switch",er],["reset",tr],["clean",nr],["rebase",rr],["cherry-pick",or],["merge",sr],["commit",ir],["push",ar],["branch",cr],["tag",lr],["stash",dr],["filter-branch",({subcommand:e})=>d("git-filter",`\`git ${e}\` rewrites entire repo history. Refuse.`)],["filter-repo",({subcommand:e})=>d("git-filter",`\`git ${e}\` rewrites entire repo history. Refuse.`)],["gc",ur],["update-ref",()=>d("git-update-ref","`git update-ref` directly mutates refs and bypasses normal git operations. Refuse.")],["remote",fr],["submodule",Ge],["worktree",Ge],["init",({subcommand:e})=>x(`git-${e}`,`\`git ${e}\` is allowed but unusual mid-session. Make sure this is what the user asked for.`)],["clone",({subcommand:e})=>x(`git-${e}`,`\`git ${e}\` is allowed but unusual mid-session. Make sure this is what the user asked for.`)]]),hr=(e,t,n)=>{let{subcommand:r,subArgs:o}=e,s=Kn(o),i=Vn(o);if(new Set(["status","diff","log","show","blame","rev-parse","rev-list","ls-files","ls-tree","cat-file","reflog","describe","shortlog","whatchanged","archive","bundle","fsck","fetch","ls-remote","help","version","grep","name-rev","merge-base","symbolic-ref","check-ignore","count-objects","verify-commit","verify-tag"]).has(r)){if(r==="reflog"&&(o[0]==="expire"||h(o,"--expire")))return d("git-reflog-expire","`git reflog expire` destroys git's recovery history. Refuse \u2014 surface the intent.");if(r==="symbolic-ref"&&i.length>=2)return d("git-symbolic-ref-write","`git symbolic-ref <name> <ref>` rewrites a symbolic ref. Refuse.");return a("bash-git")}let u=gr.get(r);if(u!==void 0)return u({subcommand:r,subArgs:o,flags:s,positional:i,config:t,heredocBodies:n});return x("git-unknown-subcommand",`\`git ${r}\` is not classified by tripwire. Allowing \u2014 flag if this looks like history-rewriting or data-loss territory.`)},Je=(e,t,n)=>{if(k(t))return a("bash-git");let r=Re(t);for(let o of e){let s=Gn(o);if(s===null)continue;let i=hr(s,n,r);if(i!==null&&i.kind!=="allow")return i}return a("bash-git")};var mr=new Set(["curl","wget","wget2","aria2c","xh"]),pr=new Set(["bash","sh","zsh","fish"]),yr=(e)=>{for(let t=0;t<e.length-1;t+=1){let n=e[t],r=e[t+1];if(n===void 0||r===void 0)continue;if(mr.has(n.head)&&pr.has(r.head))return!0}return!1},kr=[{head:"cargo",subcommand:"install",rule:"cargo-install",message:"Confirm before `cargo install <crate>`: this builds and installs arbitrary code from crates.io into ~/.cargo/bin globally."},{head:"go",subcommand:"install",rule:"go-install",message:"Confirm before `go install`: this fetches and installs arbitrary Go code globally."},{head:"gem",subcommand:"install",rule:"gem-install",message:"Confirm before `gem install`: pulls arbitrary code from rubygems.org."}],qe=(e,t)=>{if(k(t))return a("bash-network-install");if(yr(e))return d("curl-pipe-shell","Piping `curl` / `wget` directly into a shell runs whatever the remote URL serves. Refuse \u2014 download to a file, inspect, then run if appropriate. If you genuinely need this, append ` # tripwire-allow: <reason>` (and explain to the user what you're running).");for(let n of e)for(let r of kr)if(n.head===r.head&&n.tokens[1]===r.subcommand)return y(r.rule,r.message);return a("bash-network-install")};import{lstatSync as br,readlinkSync as Sr,realpathSync as wr}from"fs";import A from"path";var Rr=[{pattern:/(?<prefix>^|\/)\.env(?<ext>\.[^/]+)?$/,rule:"env-file",message:".env files hold secrets that should never be sent to the model. Refuse to write or edit. If an example is needed, create .env.example with redacted placeholders."},{pattern:/(?<prefix>^|\/)\.dev\.vars(?<ext>\.[^/]+)?$/,rule:"dev-vars",message:".dev.vars holds Cloudflare/Wrangler secrets. Do not modify."},{pattern:/(?<prefix>^|\/)\.ssh\//,rule:"ssh-dir",message:"Never write into ~/.ssh/. Refuse."},{pattern:/(?<prefix>^|\/)(?<key>id_rsa|id_ed25519|id_ecdsa|id_dsa)(?<pub>\.pub)?$/,rule:"ssh-key",message:"SSH key file. Refuse."},{pattern:/\.(?<ext>pem|key|p12|pfx)$/i,rule:"private-key",message:"Private key file. Refuse to overwrite. If generating a new key, use a different filename and let the user review."},{pattern:/(?<prefix>^|\/)secrets?\.(?<ext>json|ya?ml|toml|env)$/i,rule:"secrets-file",message:"Secrets file. Refuse."},{pattern:/(?<prefix>^|\/)\.aws\/credentials$/,rule:"aws-credentials",message:"AWS credentials file. Refuse."},{pattern:/(?<prefix>^|\/)\.netrc$/,rule:"netrc",message:".netrc holds host credentials. Refuse."}],Ve=(e)=>{try{return wr(e)}catch{return null}},se=(e,t=new Set)=>{if(t.has(e))return e;t.add(e);let n=Ve(e);if(n!==null)return n;try{if(br(e).isSymbolicLink()){let o=Sr(e);return se(A.resolve(A.dirname(e),o),t)}}catch{}let r=A.dirname(e);if(r===e)return e;return A.join(se(r,t),A.basename(e))},O=(e,t,n)=>{let r=A.resolve(e),o=t==="write"?se(r):Ve(r),s=[e,r];if(o!==null&&!s.includes(o))s.push(o);for(let i of s)for(let l of n)if(l.pattern.test(i))return l;return null},ie=(e)=>{let t=O(e.file_path,"write",Rr);if(t!==null)return d(t.rule,t.message);return a("path-protect")};var vr=[{rule:"redirect-env",pattern:/(?<prefix>^|\/)\.env(?<ext>\.[^/]+)?$/,message:"Refusing to write into a .env file via shell redirect / tee / cp / mv. .env files hold secrets \u2014 never overwrite from a tool call."},{rule:"redirect-dev-vars",pattern:/(?<prefix>^|\/)\.dev\.vars(?<ext>\.[^/]+)?$/,message:"Refusing to write into .dev.vars (Cloudflare/Wrangler secrets)."},{rule:"redirect-ssh",pattern:/(?<prefix>^|\/)\.ssh\//,message:"Refusing to write into ~/.ssh/ via shell."},{rule:"redirect-key",pattern:/\.(?<ext>pem|key|p12|pfx)$/i,message:"Refusing to overwrite a private-key-shaped file via shell."},{rule:"redirect-aws-credentials",pattern:/(?<prefix>^|\/)\.aws\/credentials$/,message:"Refusing to write into ~/.aws/credentials via shell."},{rule:"redirect-netrc",pattern:/(?<prefix>^|\/)\.netrc$/,message:"Refusing to write into ~/.netrc via shell."},{rule:"redirect-block-device",pattern:/^\/dev\/(?<type>sd|disk|nvme|rdisk)/i,message:"Redirecting into a raw block device wipes the disk. Refuse."}],ae=(e)=>{let t=O(e,"write",vr);if(t!==null)return d(t.rule,t.message);return null},Ke=(e,t)=>{if(k(t))return a("bash-redirect");for(let n of e){for(let r of n.redirects)if(r.op===">"||r.op===">>"){let o=ae(r.target);if(o!==null)return o}if(n.head==="tee")for(let r of n.args){let o=ae(r);if(o!==null)return o}if(n.head==="cp"||n.head==="mv"){let r=n.args.at(-1);if(r!==void 0){let o=ae(r);if(o!==null)return o}}}return a("bash-redirect")};var xr=(e,t)=>{let n=!1,r=[];for(let i of e.tokens.slice(1)){if(!n&&i==="--"){n=!0;continue}if(!n&&i.startsWith("-")&&i!=="-")continue;r.push(i)}let o=t.relative??[],s=t.absolute??[];return r.filter((i)=>!X(i,o,s))},Er=(e,t)=>{if(!e.tokens.includes("-delete"))return null;let n=[];for(let i of e.tokens.slice(1)){if(i.startsWith("-"))break;n.push(i)}let r=n.length===0?["."]:n,o=t.relative??[],s=t.absolute??[];return r.filter((i)=>!X(i,o,s))},Ze=(e,t,n)=>{if(k(t))return a("bash-scoped-rm");let r=[];for(let l of e){if(l.head==="rm"){let u=xr(l,n);if(u.length>0)r.push({kind:"rm",targets:u});continue}if(l.head==="find"){let u=Er(l,n);if(u!==null&&u.length>0)r.push({kind:"find -delete",targets:u})}}if(r.length===0)return a("bash-scoped-rm");let o=n.relative??[],s=n.absolute??[],i=r.map((l)=>` \u2022 ${l.kind} on: ${l.targets.map((u)=>JSON.stringify(u)).join(", ")}`).join(`
|
|
14
|
+
`);return d("destructive-outside-safe-paths",`Destructive deletion outside known-safe scopes is blocked. Use a recoverable deletion tool or limit the target to an ephemeral build, cache, state, or temporary directory:
|
|
15
|
+
${Ce(o,s)}
|
|
16
|
+
|
|
17
|
+
Flagged targets:
|
|
18
|
+
${i}
|
|
19
|
+
|
|
20
|
+
If raw deletion is genuinely needed, append \` # tripwire-allow: <reason>\` to the command.`)};var _r=(e)=>e==="--extract"||e.startsWith("-")&&!e.startsWith("--")&&e.slice(1).includes("x"),Cr=(e)=>{for(let t=0;t<e.tokens.length;t+=1){let n=e.tokens[t];if(n===void 0)continue;if(n==="-C"||n==="--directory")return e.tokens[t+1]??null;if(n.startsWith("--directory="))return n.slice(12)}return null},Xe=(e)=>e==="/"||/^(?<home>~|\$HOME|\$\{HOME\})$/.test(e),Ye=(e,t)=>{if(k(t))return a("bash-tar-explosion");for(let n of e){if(n.head!=="tar")continue;let[,r]=n.tokens;if(!(n.flags.some(_r)||n.tokens.includes("--extract")||r!==void 0&&/^[a-zA-Z]+$/.test(r)&&r.includes("x")))continue;let s=Cr(n);if(s!==null&&Xe(s))return d("tar-extract-to-root",`tar -x with -C ${s} can overwrite arbitrary system files. Refuse \u2014 extract to a contained directory (e.g. ./tmp/extract) and inspect before moving anything elsewhere.`)}for(let n of e){if(n.head!=="unzip")continue;for(let r=0;r<n.tokens.length;r+=1)if(n.tokens[r]==="-d"){let o=n.tokens[r+1];if(o!==void 0&&Xe(o))return d("unzip-to-root",`unzip -d ${o} can overwrite arbitrary system files. Refuse \u2014 extract to a contained directory.`)}}return a("bash-tar-explosion")};var Pr="If this is intentional, append ` # tripwire-allow: <reason>` to the command.",Hr=new Map([["add","create"],["new","create"],["edit","update"],["set","update"],["rm","delete"],["del","delete"],["remove","delete"]]),Qe=(e)=>Hr.get(e)??e,et=(e)=>{let t=e.lastIndexOf("/");return t===-1?e:e.slice(t+1)},Tr=(e,t)=>e.some((n)=>n===t||n.startsWith(`${t}=`)),Dr=(e,t)=>{for(let n=0;n<e.length;n+=1){let r=e[n];if(r===void 0)continue;if(r===t)return e[n+1]??"";if(r.startsWith(`${t}=`))return r.slice(t.length+1)}return null},Ir=(e)=>{let t=[],n=e.tokens.slice(1);for(let r=0;r<n.length;r+=1){let o=n[r];if(o===void 0)continue;if(o.startsWith("-")){let s=n[r+1];if(!o.includes("=")&&s!==void 0&&!s.startsWith("-"))r+=1;continue}t.push(o)}return t},tt=(e,t)=>{let{pattern:n}=t,r=I(n);if(r.length===0)return!1;let[o]=r;if(o===void 0)return!1;let[s,...i]=o.tokens;if(s===void 0)return!1;for(let l of e){if(et(l.head)!==et(s))continue;if(i.length>0){let g=Ir(l);if(!i.every((w,C)=>g[C]!==void 0&&Qe(g[C])===Qe(w)))continue}if((t.requiresFlags??[]).some((g)=>!Tr(l.tokens,g)))continue;if(!(t.forbidsFlagValues??[]).every((g)=>{let p=Dr(l.tokens,g.flag);return p!==null&&g.values.includes(p)}))continue;if(i.length===0&&t.requiresFlags===void 0&&t.forbidsFlagValues===void 0)return!0;return!0}return!1},nt=(e,t,n,r)=>{if(k(t))return a("config-custom");for(let o of r)if(tt(e,o))return a("config-custom");for(let o of n)if(tt(e,o)){let s=o.message.includes("tripwire-allow")?o.message:`${o.message} ${Pr}`;return o.action==="ask"?y("config-custom",s):d("config-custom",s)}return a("config-custom")};import{readFileSync as Fr}from"fs";var rt=(e,t)=>{let n=new Set(e.split(`
|
|
21
|
+
`).map((r)=>r.trim()).filter((r)=>r.length>0));return t.split(`
|
|
22
|
+
`).filter((r)=>r.trim().length>0).filter((r)=>!n.has(r.trim()))},ot=(e)=>{try{return Fr(e,"utf8")}catch(t){if(t instanceof Error&&"code"in t&&t.code==="ENOENT")return"";throw t}};var $r=[/\bTODO\s*:/i,/\bFIXME\s*:/i,/\bXXX\s*:/i,/\bHACK\s*:/i,/\bfor now\b/i,/\bnot implemented\b/i,/\bNotImplementedError\b/,/\btemp fix\b/i,/\bfallback\b/i,/\bplaceholder\b/i,/\bbackwards?[ -]?compat(?<ibility>ibility)?\b/i,/\bfor later\b/i,/\blater on\b/i,/\bget back to\b/i,/\bI'?ll fix\b/i,/\bto be implemented\b/i,/\bnot yet (?<state>implemented|done)\b/i,/\bstubbed\b/i],Ar=/\.(?<ext>ts|tsx|js|jsx|mjs|cjs|py|rs|go|rb|java|kt|swift|c|cc|cpp|h|hpp|cs|php|sh|zsh|bash|lua|ex|exs|clj|scala|dart)$/i,Wr=/(?<prefix>^|\/)(?<dir>__tests__|tests?|spec|fixtures?|mocks?|__mocks__|stories)(?<suffix>\/|$)|\.(?<ext>test|spec|fixture|mock|stories)\.[^/]+$/i,Br=/tripwire-allow:[ \t]*\S/,Or=(e)=>{if(Br.test(e))return!1;for(let t of $r)if(t.test(e))return!0;return!1},ce=(e)=>{let t=e.file_path;if(!Ar.test(t)||Wr.test(t))return a("lazy-code");let n="content"in e?e.content:e.new_string,r="content"in e?ot(t):e.old_string,o=[];for(let l of rt(r,n))if(Or(l))o.push(l.slice(0,200));if(o.length===0)return a("lazy-code");let s=o.slice(0,3).map((l)=>` \u2022 ${l}`).join(`
|
|
23
|
+
`),i=o.length>3?`
|
|
24
|
+
\u2026and ${o.length-3} more`:"";return x("lazy-code-marker",["Heads up: line(s) you just added contain words that often signal incomplete or deferred work. The write went through \u2014 this is a flag, not a block.","",'Why this exists: AI coding agents have a strong pull toward stubbing things, deferring "for now," and shipping half-built fallbacks instead of finishing the work in the same turn. The point of this warning is to push back on that pull on every iteration. If you stubbed something out for time, finish it this turn rather than leaving deferred work for later.',"",'If the marker is genuinely permanent \u2014 a real product term ("auth fallback flow", "retry fallback chain", an HTML input placeholder, a public API field literally named "placeholder"), a logging tag, or a comment intentionally left for a human reader \u2014 no action needed. To silence the flag on subsequent edits of that line, append `tripwire-allow: <one-line reason>` (any comment syntax: `//`, `#`, `--`, etc.).',"","Flagged additions:",s+i].join(`
|
|
25
|
+
`))};import{spawnSync as Lr}from"child_process";import{Result as Mr,Schema as _}from"effect";var Nr=_.Struct({RuleID:_.String,Description:_.String,StartLine:_.Finite,EndLine:_.Finite,Secret:_.String,Match:_.String}),jr=_.Array(Nr),Ur=["stdin","--report-format","json","--report-path","-","--exit-code","0","--no-banner","--no-color","--log-level","error"],zr=({executable:e,args:t,input:n,timeoutMs:r})=>{let o=Lr(e,t,{encoding:"utf8",input:n,maxBuffer:67108864,stdio:["pipe","pipe","ignore"],timeout:r}),s={status:o.status,stdout:typeof o.stdout==="string"?o.stdout:""};return o.error===void 0?s:{...s,error:o.error}},Gr=(e)=>{if(typeof e!=="object"||e===null||!("code"in e))return;return typeof e.code==="string"?e.code:void 0},Jr=(e)=>{let t=Gr(e);if(t==="ENOENT")return"missing-executable";if(t==="ETIMEDOUT")return"timeout";return"non-zero-exit"},qr=(e)=>{let t=new Map;for(let n of e)t.set(n.RuleID,(t.get(n.RuleID)??0)+1);return[...t.entries()].map(([n,r])=>({rule:n,count:r}))};var Vr=(e,t)=>{let n=e,r=[...t].toSorted((o,s)=>s.Secret.length-o.Secret.length);for(let o of r){if(o.Secret==="")continue;n=n.replaceAll(o.Secret,`[REDACTED:${o.RuleID}]`)}return n},st=(e,t,n=zr)=>{if(e.length===0)return{ok:!0,hits:[],redacted:e};let r;try{r=n({executable:t.executable,args:Ur,input:e,timeoutMs:t.timeoutMs})}catch{return{ok:!1,category:"non-zero-exit"}}if(r.error!==void 0)return{ok:!1,category:Jr(r.error)};if(r.status!==0)return{ok:!1,category:"non-zero-exit"};let o;try{o=JSON.parse(r.stdout)}catch{return{ok:!1,category:"malformed-json"}}let s=_.decodeUnknownResult(jr)(o);if(Mr.isFailure(s))return{ok:!1,category:"malformed-json"};let i=s.success;return{ok:!0,hits:qr(i),redacted:Vr(e,i)}};var Kr=(e)=>`Tripwire could not verify this tool output because the configured secret scanner failed (${e}). The original output was withheld. Install Betterleaks 1.5.0 or later, check secretScanner.executable and secretScanner.timeoutMs, then run the original tool again.`,it=(e)=>{let t=Ne(e.toolName,e.response);if(t.length===0)return a("post-secret-scrub");let n=st(t,e.secretScanner,e.scannerRunner);if(!n.ok)return d("secret-scanner-failed",Kr(n.category));if(n.hits.length===0)return a("post-secret-scrub");let r=n.hits.map((o)=>`${o.rule}\xD7${o.count}`).join(", ");return d("secrets-in-output",[`tripwire intercepted ${n.hits.length} secret pattern(s) in this tool's output (${r}). The original output was withheld so the secret never enters the model context. A redacted form is below \u2014 work from this, do not re-run the same command in a way that re-fetches the underlying secret.`,"","Redacted output:",n.redacted.slice(0,16000)+(n.redacted.length>16000?`
|
|
26
|
+
\u2026[truncated]`:"")].join(`
|
|
27
|
+
`))};var Zr=[{rule:"read-env",pattern:/(?<prefix>^|\/)\.env(?<ext>\.[^/]+)?$/,message:".env files hold secrets that should never enter the model context. Refuse to read. If the goal is documenting required env vars, look at .env.example or describe the schema from memory."},{rule:"read-dev-vars",pattern:/(?<prefix>^|\/)\.dev\.vars(?<ext>\.[^/]+)?$/,message:"Refuse to read .dev.vars (Cloudflare/Wrangler secrets)."},{rule:"read-ssh",pattern:/(?<prefix>^|\/)\.ssh\//,message:"Refuse to read files inside ~/.ssh/."},{rule:"read-ssh-key",pattern:/(?<prefix>^|\/)(?<key>id_rsa|id_ed25519|id_ecdsa|id_dsa)$/,message:"Refuse to read SSH private key files."},{rule:"read-private-key",pattern:/\.(?<ext>pem|key|p12|pfx)$/i,message:"Refuse to read private-key-shaped files."},{rule:"read-aws-credentials",pattern:/(?<prefix>^|\/)\.aws\/credentials$/,message:"Refuse to read ~/.aws/credentials."},{rule:"read-netrc",pattern:/(?<prefix>^|\/)\.netrc$/,message:"Refuse to read ~/.netrc (host credentials)."},{rule:"read-secrets-file",pattern:/(?<prefix>^|\/)secrets?\.(?<ext>json|ya?ml|toml|env)$/i,message:"Refuse to read a file named secrets.{json,yaml,toml,env}."}],at=(e)=>{let t=O(e.file_path,"read",Zr);if(t!==null)return d(t.rule,t.message);return a("read-protect")};var Xr=(e,t)=>e.some((n)=>n.startsWith("-")&&!n.startsWith("--")&&n.slice(1).includes(t)),Yr=(e,t)=>{if(!t.executables.includes(e.head))return!1;let n=e.tokens.slice(1),r=new Set(n);if(!(t.match?.argumentsIncludeAll??[]).every((o)=>r.has(o)))return!1;if(!(t.match?.argumentsStartWith??[]).every((o,s)=>n[s]===o))return!1;return(t.match?.shortFlagsIncludeAll??[]).every((o)=>Xr(e.flags,o))},ct=(e,t,n)=>{if(k(t))return a("tool-policy");for(let r of e)for(let o of n)if(Yr(r,o))return o.action==="deny"?d(o.rule,o.message):x(o.rule,o.message);return a("tool-policy")};var Qr=async()=>{let e=[];for await(let t of process.stdin){let n=t;if(n instanceof Uint8Array)e.push(Buffer.from(n));else throw TypeError("Tripwire received a non-byte stdin chunk")}return Buffer.concat(e).toString("utf8")},ue={kind:"native"},lt=250,eo="--tripwire-hook",to=q.Array(re).check(q.isMinLength(1)),W=(e=ue)=>{if(e.kind==="cursor"&&!e.post){process.stdout.write(`{"continue":true,"permission":"allow"}
|
|
28
|
+
`);return}process.stdout.write(`{"continue": true}
|
|
29
|
+
`)},le=(e)=>`[tripwire:${e.rule}] ${e.message}`,pt=(e)=>{let t=e.kind==="ask"?`${le(e)} This unattended Cursor run cannot ask a human, so the action is denied.`:le(e);process.stdout.write(`${JSON.stringify({continue:!0,permission:"deny",user_message:t,agent_message:t})}
|
|
30
|
+
`)},yt=(e)=>e.turn_id!==void 0,dt=(e,t,n)=>{if(n.kind==="cursor"){process.stdout.write(`${JSON.stringify({continue:!0,permission:"allow",agent_message:le(t)})}
|
|
31
|
+
`);return}let r=e.hook_event_name,o=`[tripwire:${t.rule}] ${t.message}`;if(yt(e)){process.stdout.write(`${JSON.stringify({continue:!0,systemMessage:o})}
|
|
32
|
+
`);return}let s={hookEventName:r,additionalContext:o};process.stdout.write(`${JSON.stringify({continue:!0,hookSpecificOutput:s})}
|
|
33
|
+
`)},de=(e,t,n=ue)=>{if(n.kind==="cursor"){pt(t);return}let r={hookSpecificOutput:{hookEventName:e,permissionDecision:t.kind==="deny"?"deny":"ask",permissionDecisionReason:`[tripwire:${t.rule}] ${t.message}`}};process.stdout.write(`${JSON.stringify(r)}
|
|
34
|
+
`)},no=(e,t)=>{if(t.kind==="cursor"){W(t);return}let n={continue:!0,decision:"block",reason:`[tripwire:${e.rule}] ${e.message}`};process.stdout.write(`${JSON.stringify(n)}
|
|
35
|
+
`)},V=(e)=>{let t=e.toLowerCase();if(t==="bash"||t==="exec"||t==="shell"||t==="run_command")return"Bash";if(t==="read"||t==="read_file")return"Read";if(t==="write"||t==="write_file")return"Write";if(t==="edit"||t==="edit_file"||t==="multiedit"||t==="apply_patch")return"Edit";if(t==="webfetch"||t==="web_fetch"||t==="fetch")return"WebFetch";if(t==="powershell")return"PowerShell";return e};class kt extends mt.TaggedError("RuleExecutionError"){}class bt extends mt.TaggedError("HookInputParseError"){}var ro=(e,t,n)=>S.gen(function*(){let o=yield*S.exit(S.try({try:t,catch:(s)=>new kt({cause:s})}).pipe(S.timeout(n)));if(J.isSuccess(o))return o.value;return v(e,L.pretty(o.cause)),a(e)}),St=(e,t,n)=>{let r=[];if(e==="PowerShell")return r.push({name:"powershell-unsupported",fn:()=>d("powershell-unsupported","PowerShell commands are blocked because Tripwire cannot parse PowerShell grammar. Use the Bash tool, or add a PowerShell parser before you enable this tool.")}),r;if(e==="Bash"&&Be(t)){let i=t,l=I(i.command);return r.push({name:"bash-deny",fn:()=>ze(l,i.command)},{name:"bash-git",fn:()=>Je(l,i.command,n.git)},{name:"bash-scoped-rm",fn:()=>Ze(l,i.command,n.safePaths)},{name:"bash-redirect",fn:()=>Ke(l,i.command)},{name:"bash-network-install",fn:()=>qe(l,i.command)},{name:"bash-tar-explosion",fn:()=>Ye(l,i.command)},{name:"tool-policy",fn:()=>ct(l,i.command,n.toolPolicies)},{name:"config-custom",fn:()=>nt(l,i.command,n.blockedCommands,n.allowedCommands)}),r}if(e==="Read"&&Me(t)){let i=t;return r.push({name:"read-protect",fn:()=>at(i)}),r}let o=(e==="Edit"||e==="MultiEdit")&&Oe(t),s=e==="Write"&&Le(t);if(o){let i=t;r.push({name:"path-protect",fn:()=>ie(i)},{name:"lazy-code",fn:()=>ce(i)})}else if(s){let i=t;r.push({name:"path-protect",fn:()=>ie(i)},{name:"lazy-code",fn:()=>ce(i)})}return r},wt=(e,t,n)=>{if(e==="Bash"||e==="PowerShell"||e==="Read"||e==="WebFetch"){let r=e==="PowerShell"?"Bash":e;return[{name:"post-secret-scrub",fn:()=>it({toolName:r,response:t,secretScanner:n})}]}return[]},ut=(e,t)=>S.gen(function*(){if(e.length===0)return a("no-rules");let r=[];for(let o of e)r.push(yield*ro(o.name,o.fn,t));return D(r)}),ft=(e)=>{if(e.length===0)return a("no-rules");let t=[];for(let n of e)try{t.push(n.fn())}catch(r){v(n.name,r),t.push(a(n.name))}return D(t)},oo=(e,t={})=>{let n=z(t),r=V(e.tool_name??"");if(e.hook_event_name==="PreToolUse")return ft(St(r,e.tool_input,n));if(e.hook_event_name==="PostToolUse")return ft(wt(r,e.tool_response,n.secretScanner));return a("no-rules")},so=(e,t,n)=>{let r=e.hook_event_name,o=V(e.tool_name??"");if(r==="PreToolUse"&&o==="Bash"){if(t.kind==="warn"){dt(e,t,n);return}W(n);return}if(t.kind==="warn"){dt(e,t,n);return}W(n)},io=(e)=>`tripwire config at ${U} failed to load, so ALL custom safety policy is inactive. Failing closed until it is fixed. Fix the JSON, then this clears on the next hook call. Restart any long-running consumer that caches the loaded config. Error: ${e}`,Rt=()=>{let e=process.argv.indexOf("--cursor-event");if(e!==-1)return process.argv[e+1]??"";return process.argv.find((n)=>n.startsWith("--cursor-event="))?.slice(15)},ao=()=>process.argv.includes(eo),vt=()=>{let e=Rt();return e===void 0?ue:te(e)},P=(e,t=!1)=>{let n=vt();if(t){de("PreToolUse",d("tripwire-batch-error",`Tripwire batch input could not be processed (${e}). Failing closed.`),n);return}if(n.kind==="cursor"&&!n.post){pt(d("cursor-hook-error",`Cursor hook input could not be processed (${e}). Failing closed.`));return}W(n)},gt=(e,t)=>{if(t.kind==="cursor")return`cursor:${t.eventName}`;return yt(e)?"codex":"native"},ht=(e)=>e.length===1?e[0]??a("no-rules"):D(e),co=(e)=>S.gen(function*(){let n=Array.isArray(e);if(n&&!ao())return v("decode","Batch hook input is only available through --tripwire-hook"),P("unsupported batch input"),{ok:!1};let r,o;if(n){let u=yield*S.exit(q.decodeEffect(to)(e));if(J.isFailure(u))return v("decode",L.pretty(u.cause)),P("unsupported batch event shape",!0),{ok:!1};r=u.value,o=vt()}else{let u=We(e,Rt()),f=yield*S.exit(q.decodeUnknownEffect(re)(u.event));if(J.isFailure(f))return v("decode",L.pretty(f.cause)),P("unsupported event shape"),{ok:!1};r=[f.value],{host:o}=u}let[s]=r;if(s===void 0)return P("empty batch",!0),{ok:!1};let i=s.hook_event_name,l=gt(s,o);if(r.some((u)=>u.hook_event_name!==i))return v("decode","Batch hook input mixes event phases"),P("mixed event phases",!0),{ok:!1};if(r.some((u)=>gt(u,o)!==l))return v("decode","Batch hook input mixes hosts"),P("mixed hosts",!0),{ok:!1};return{ok:!0,events:r,firstEvent:s,host:o,phase:i}}),lo=S.gen(function*(){let t=yield*G(),n=yield*S.promise(Qr),r=yield*S.exit(S.try({try:()=>JSON.parse(n),catch:(f)=>new bt({cause:f})}));if(J.isFailure(r)){v("parse",L.pretty(r.cause)),P("invalid JSON");return}let o=yield*co(r.value);if(!o.ok)return;let{events:s,firstEvent:i,host:l,phase:u}=o;if(u==="PreToolUse"){if(!t.ok){de(u,d("config-error",io(t.error)),l);return}let f=[];for(let p of s){let w=V(p.tool_name??"");f.push(yield*ut(St(w,p.tool_input,t.config),lt))}let g=ht(f);if(g.kind==="deny"||g.kind==="ask"){de(u,g,l);return}so(i,g,l);return}if(u==="PostToolUse"){let f=t.ok?t.config.secretScanner:B().secretScanner,g=[];for(let w of s){let C=V(w.tool_name??"");g.push(yield*ut(wt(C,w.tool_response,f),lt))}let p=ht(g);if(p.kind==="deny"){no(p,l);return}W(l);return}W(l)}),Ws=lo.pipe(S.catchCause((e)=>(v("dispatch-fatal",L.pretty(e)),P("dispatcher failure"),S.void)));export{a as allow,y as ask,oo as decide,d as deny,B as getDefaultConfig,En as loadConfig,G as loadConfigResult,z as mergeWithDefaults,x as warn};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var a=(e)=>{let n=e?.trim();return n===void 0||n.length===0?void 0:n},c=()=>({bunExecutable:a(process.env.TRIPWIRE_BUN),forcePortable:process.env.TRIPWIRE_FORCE_PORTABLE==="1",packageDist:a(process.env.TRIPWIRE_PACKAGE_DIST)}),s=(e)=>({...process.env,TRIPWIRE_PACKAGE_DIST:e});import{accessSync as p,constants as f,realpathSync as u}from"fs";import{createRequire as g}from"module";import m from"path";import{fileURLToPath as b}from"url";var v=["@seanmozeik","tripwire-darwin-arm64"].join("/"),R=(e)=>{try{return p(e,f.X_OK),u(e)}catch{return}},E=(e,n)=>{try{let r=n?.()??g(e).resolve(v);return R(r)}catch{return}},h=(e)=>{let n=b(e),r=n;try{r=u(n)}catch{}return m.join(m.dirname(r),"tripwire.js")},l=(e={})=>{let n=e.moduleUrl??import.meta.url,r=e.platform??process.platform,i=e.architecture??process.arch,t=c();if(!t.forcePortable&&r==="darwin"&&i==="arm64"){let o=E(n,e.nativeResolver);if(o!==void 0)return{arguments:[],executable:o,kind:"native"}}return{arguments:[e.portableExecutable??h(n)],executable:e.bunExecutable??t.bunExecutable??"bun",kind:"portable"}};var d=(e)=>{let n=l({moduleUrl:import.meta.url}),r=e==="hook"?"--tripwire-hook":"--tripwire-force-cli",i=import.meta.dirname,t=Bun.spawnSync([n.executable,...n.arguments,r,...process.argv.slice(2)],{stderr:"inherit",env:s(i),stdin:"inherit",stdout:"inherit"});process.exit(t.exitCode)};d("cli");
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var a=(e)=>{let n=e?.trim();return n===void 0||n.length===0?void 0:n},s=()=>({bunExecutable:a(process.env.TRIPWIRE_BUN),forcePortable:process.env.TRIPWIRE_FORCE_PORTABLE==="1",packageDist:a(process.env.TRIPWIRE_PACKAGE_DIST)}),c=(e)=>({...process.env,TRIPWIRE_PACKAGE_DIST:e});import{accessSync as p,constants as f,realpathSync as u}from"fs";import{createRequire as g}from"module";import m from"path";import{fileURLToPath as b}from"url";var v=["@seanmozeik","tripwire-darwin-arm64"].join("/"),R=(e)=>{try{return p(e,f.X_OK),u(e)}catch{return}},h=(e,n)=>{try{let r=n?.()??g(e).resolve(v);return R(r)}catch{return}},E=(e)=>{let n=b(e),r=n;try{r=u(n)}catch{}return m.join(m.dirname(r),"tripwire.js")},d=(e={})=>{let n=e.moduleUrl??import.meta.url,r=e.platform??process.platform,o=e.architecture??process.arch,t=s();if(!t.forcePortable&&r==="darwin"&&o==="arm64"){let i=h(n,e.nativeResolver);if(i!==void 0)return{arguments:[],executable:i,kind:"native"}}return{arguments:[e.portableExecutable??E(n)],executable:e.bunExecutable??t.bunExecutable??"bun",kind:"portable"}};var l=(e)=>{let n=d({moduleUrl:import.meta.url}),r=e==="hook"?"--tripwire-hook":"--tripwire-force-cli",o=import.meta.dirname,t=Bun.spawnSync([n.executable,...n.arguments,r,...process.argv.slice(2)],{stderr:"inherit",env:c(o),stdin:"inherit",stdout:"inherit"});process.exit(t.exitCode)};l("hook");
|
package/dist/tripwire-pi.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import{spawn as
|
|
2
|
-
`),new_string:
|
|
3
|
-
`)}}return{old_string:
|
|
4
|
-
`)
|
|
1
|
+
import{spawn as F}from"node:child_process";import{once as H}from"node:events";import{realpathSync as z}from"node:fs";import{fileURLToPath as W,pathToFileURL as K}from"node:url";import{accessSync as A,constants as L,realpathSync as E}from"node:fs";import{createRequire as U}from"node:module";import T from"node:path";import{fileURLToPath as O}from"node:url";var R=(e)=>{let n=e?.trim();return n===void 0||n.length===0?void 0:n},P=()=>({bunExecutable:R(process.env.TRIPWIRE_BUN),forcePortable:process.env.TRIPWIRE_FORCE_PORTABLE==="1",packageDist:R(process.env.TRIPWIRE_PACKAGE_DIST)});var N=["@seanmozeik","tripwire-darwin-arm64"].join("/"),D=(e)=>{try{return A(e,L.X_OK),E(e)}catch{return}},M=(e,n)=>{try{let t=n?.()??U(e).resolve(N);return D(t)}catch{return}},j=(e)=>{let n=O(e),t=n;try{t=E(n)}catch{}return T.join(T.dirname(t),"tripwire.js")},x=(e={})=>{let n=e.moduleUrl??import.meta.url,t=e.platform??process.platform,o=e.architecture??process.arch,r=P();if(!r.forcePortable&&t==="darwin"&&o==="arm64"){let s=M(n,e.nativeResolver);if(s!==void 0)return{arguments:[],executable:s,kind:"native"}}return{arguments:[e.portableExecutable??j(n)],executable:e.bunExecutable??r.bunExecutable??"bun",kind:"portable"}};var b=60000,B=(e=import.meta.url)=>{let n=W(e),t=n;try{t=z(n)}catch{}return x({moduleUrl:K(t)})},J=B(),m=(e,n,t)=>{for(let o of n){let r=e[o];if(typeof r==="string")return r}throw Error(`Pi ${t} is missing`)},G=(e,n)=>{let t=e[n];return Array.isArray(t)?t.filter((o)=>typeof o==="string"):[]},q=(e)=>{let{edits:n}=e;if(Array.isArray(n)){let t=[],o=[];for(let r of n){if(!f(r))continue;let s=a(r,"oldText")??a(r,"old_string"),i=a(r,"newText")??a(r,"new_string");if(s!==void 0)t.push(s);if(i!==void 0)o.push(i)}return{old_string:t.join(`
|
|
2
|
+
`),new_string:o.join(`
|
|
3
|
+
`)}}return{old_string:a(e,"oldText")??a(e,"old_string")??"",new_string:a(e,"newText")??a(e,"new_string")??a(e,"input")??a(e,"_input")??""}},V=(e)=>{let n=/^\*{3}\s+(?:Add|Update|Delete)\s+File\s*:\s*(?<path>\S.*?)\s*$/,t=/^\*{3}\s+Move\s+to\s*:\s*(?<path>\S.*?)\s*$/,o=new Map,r=[],s="*** Begin Patch",i="*** End Patch",l=e.split(/\r?\n/),u=l.findIndex((d)=>d.trim()===s);if(u===-1)return[];let c=l.slice(u+1),C=[...c,i].findIndex((d)=>d.trim()===i),I=c.slice(0,C),w=(d)=>{if(r.push(d),!o.has(d))o.set(d,{newLines:[],oldLines:[]})};for(let d of I){let h=n.exec(d)?.groups?.path?.trim();if(h!==void 0&&h.length>0){r=[],w(h);continue}if(r.length===0)continue;let y=t.exec(d)?.groups?.path?.trim();if(y!==void 0&&y.length>0){w(y);continue}let p;if(d.startsWith("+"))p="newLines";else if(d.startsWith("-"))p="oldLines";if(p!==void 0)for(let S of r)o.get(S)?.[p].push(d.slice(1))}return[...o].map(([d,g])=>({file_path:d,new_string:g.newLines.join(`
|
|
4
|
+
`),old_string:g.oldLines.join(`
|
|
5
|
+
`)}))},X=(e)=>{let{input:n,toolName:t}=e;if(t==="bash"||t==="powershell")return[{command:m(n,["command"],`${t} command`)}];if(t==="read")return[{file_path:m(n,["path","file_path"],"read path")}];if(t==="write")return[{content:m(n,["content"],"write content"),file_path:m(n,["path","file_path"],"write path")}];if(t==="edit"){let o=V(a(n,"input")??"");if(o.length>0)return o;let r=a(n,"path")??a(n,"file_path"),i=G(n,"paths");if(i.length===0&&r!==void 0)i=[r];if(i.length===0)throw Error("Pi edit path is missing");let l=q(n),u=[];for(let c of i)u.push({file_path:c,...l});return u}return[n]},Q=(e)=>{if(typeof e==="string")return e;if(!Array.isArray(e))return"";let n=[];for(let t of e)if(f(t)&&t.type==="text"&&typeof t.text==="string")n.push(t.text);return n.join(`
|
|
6
|
+
`)},Y=(e)=>{let n=Q(e.content);if(e.toolName==="bash"||e.toolName==="powershell")return{stdout:n,stderr:e.isError?n:"",interrupted:e.isError};if(e.toolName==="read")return{content:n};return{content:n,details:e.details,isError:e.isError}},k=(e,n,t)=>{let o={cwd:t,hook_event_name:n,tool_name:e.toolName,tool_use_id:e.toolCallId};if("content"in e)return[{...o,tool_input:e.input,tool_response:Y(e)}];let r=[];for(let s of X(e))r.push({...o,tool_input:s});return r},v=async(e)=>{e.setEncoding("utf8");let n="";for await(let t of e)n+=String(t);return n},Z=async(e,n)=>{let t=F(e.executable,[...e.arguments,"--tripwire-hook"],{stdio:["pipe","pipe","pipe"]}),o={timedOut:!1},r=setTimeout(()=>{o.timedOut=!0,t.kill("SIGKILL")},b);t.stdin.end(JSON.stringify(n));try{let[s,i,l]=await Promise.all([H(t,"close"),v(t.stdout),v(t.stderr)]);if(o.timedOut)throw Error(`Tripwire exceeded its ${b}ms timeout`);return{exitCode:typeof s[0]==="number"?s[0]:1,stderr:l,stdout:i}}finally{clearTimeout(r)}},f=(e)=>typeof e==="object"&&e!==null&&!Array.isArray(e),a=(e,n)=>{let t=e[n];return typeof t==="string"?t:void 0},_=(e)=>{if(e.exitCode!==0)return e.stderr.trim()||`Tripwire exited ${e.exitCode}`;if(e.stdout.trim().length===0)return"Tripwire returned no decision";try{let n=JSON.parse(e.stdout);if(!f(n))return"Tripwire returned invalid JSON";let t=f(n.hookSpecificOutput)?n.hookSpecificOutput:void 0,o=(t===void 0?void 0:a(t,"permissionDecision"))??a(n,"permissionDecision")??a(n,"permission"),r=(t===void 0?void 0:a(t,"permissionDecisionReason"))??a(n,"permissionDecisionReason")??a(n,"reason")??"Blocked by Tripwire";if(o==="deny"||o==="ask")return r;if(a(n,"decision")==="block"||n.continue===!1)return r;return n.continue===!0||o==="allow"?void 0:"Tripwire returned an unrecognized response"}catch{return"Tripwire returned invalid JSON"}},ee=(e,n=Z)=>{let t=typeof e==="string"?{arguments:[],executable:e,kind:"native"}:e;return(o)=>{o.on("tool_call",async(r,s)=>{try{let i=k(r,"PreToolUse",s.cwd),l=i.length===1?i[0]:i;if(l===void 0)throw Error("Tripwire could not normalize the Pi tool call");let u=_(await n(t,l));if(u!==void 0)return{block:!0,reason:u};return{}}catch(i){return{block:!0,reason:`Tripwire failed closed: ${i instanceof Error?i.message:String(i)}`}}}),o.on("tool_result",async(r,s)=>{try{let[i]=k(r,"PostToolUse",s.cwd);if(i===void 0)throw Error("Tripwire could not normalize the Pi tool result");let l=_(await n(t,i));if(l!==void 0)s.ui.notify(`Tripwire stopped the session: ${l}`,"error"),s.abort()}catch(i){s.ui.notify(`Tripwire failed closed after tool use: ${i instanceof Error?i.message:String(i)}`,"error"),s.abort()}})}},fe=ee(J);export{ee as createTripwirePiExtension,fe as default,k as hookInputs,B as resolveShippedHookCommand,_ as tripwirePiDenialReason};
|