failproofai 0.0.9 → 0.0.10-beta.0
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/.next/standalone/.cursor/hooks.json +47 -0
- package/.next/standalone/.gemini/settings.json +147 -0
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -3
- package/.next/standalone/.next/required-server-files.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js +1 -1
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +7 -7
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +2 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +17 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +17 -17
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +3 -3
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js +2 -1
- package/.next/standalone/.next/server/app/api/download/[project]/[session]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +16 -16
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +4 -4
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +11 -11
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/page.js +1 -1
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/policies/page/server-reference-manifest.json +8 -8
- package/.next/standalone/.next/server/app/policies/page.js +1 -1
- package/.next/standalone/.next/server/app/policies/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/policies/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page.js +2 -2
- package/.next/standalone/.next/server/app/project/[name]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/react-loadable-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page/server-reference-manifest.json +2 -2
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js +5 -5
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/project/[name]/session/[sessionId]/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/projects/page/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/server/app/projects/page.js +2 -2
- package/.next/standalone/.next/server/app/projects/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/projects/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0.~nmr9._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0yspgjy._.js → [root-of-the-server]__010i6f5._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__08px0ym._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0b57.gk._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0dtn9lr._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kjo7d_._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0vlhtkc._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0wu7fr7._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0yfq1yr._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0z4c5dj._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0zso~62._.js +3 -0
- package/.next/standalone/.next/server/chunks/package_json_[json]_cjs_0z7w.hh._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0-2wr.c._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0.~m-w2._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__09icjsf._.js → [root-of-the-server]__0709m8.._.js} +3 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0bz245.._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0dl0kgt._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gmhxyo._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0mup1hi._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ohb3gc._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0qbpe_v._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0s~gy6y._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0t5l7a5._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0ymlddl._.js +152 -6
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__0_b7pgn._.js → [root-of-the-server]__0ymn496._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/{[root-of-the-server]__01g_w_e._.js → [root-of-the-server]__10h.ggz._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_03d7qyt._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/{_07a1g.3._.js → _0zx~s__._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/_10lm7or._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/app_0cdqd9w._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_global-error_tsx_0xerkr6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/app_policies_hooks-client_tsx_0q-m0y-._.js +2 -2
- package/.next/standalone/.next/server/chunks/ssr/lib_codex-projects_ts_0eosib~._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/lib_copilot-projects_ts_0r8xkn8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_cursor-projects_ts_0qt1scg._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_gemini-projects_ts_0sl~yqr._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_opencode-projects_ts_0op9gyp._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/lib_pi-projects_ts_103tsh1._.js +3 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/pages/404.html +2 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +9 -9
- package/.next/standalone/.next/static/chunks/{0n-_j_6fo6jex.js → 00ay03h8bq4b~.js} +2 -2
- package/.next/standalone/.next/static/chunks/{11kt_9zaooda3.js → 0agmlhk5ml7x5.js} +1 -1
- package/.next/standalone/.next/static/chunks/0bi2r.m~yokoo.js +1 -0
- package/.next/standalone/.next/static/chunks/{095l4hc7-h.~~.js → 0en4v5k2nnxks.js} +1 -1
- package/.next/standalone/.next/static/chunks/0q5bmqop--9yk.js +1 -0
- package/.next/standalone/.next/static/chunks/{0756i.7omnnl6.js → 0s6nux54y~l~r.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0t~iusm_fxoao.js → 0tpse0wu2wwo0.js} +1 -1
- package/.next/standalone/.next/static/chunks/12po2vpc-4_c1.css +1 -0
- package/.next/standalone/.next/static/chunks/{0u-ys71jc4y68.js → 1400rtd5ywbt..js} +2 -2
- package/.next/standalone/.next/static/chunks/{09ose_165ra4d.js → 14lmf8boay-zu.js} +1 -1
- package/.next/standalone/.next/static/chunks/{0pr7k36o_.du1.js → 17htukxga7bil.js} +1 -1
- package/.next/standalone/.opencode/opencode.json +4 -0
- package/.next/standalone/.opencode/plugins/failproofai.mjs +131 -0
- package/.next/standalone/.pi/settings.json +5 -0
- package/.next/standalone/app/components/cli-badge.tsx +7 -11
- package/.next/standalone/app/components/project-list.tsx +32 -4
- package/.next/standalone/app/policies/hooks-client.tsx +31 -15
- package/.next/standalone/app/project/[name]/page.tsx +52 -16
- package/.next/standalone/app/project/[name]/session/[sessionId]/page.tsx +92 -15
- package/.next/standalone/assets/logos/copilot-dark.svg +1 -0
- package/.next/standalone/assets/logos/copilot-light.svg +1 -0
- package/.next/standalone/assets/logos/cursor-dark.svg +1 -0
- package/.next/standalone/assets/logos/cursor-light.svg +1 -0
- package/.next/standalone/assets/logos/gemini-dark.svg +13 -0
- package/.next/standalone/assets/logos/gemini-light.svg +13 -0
- package/.next/standalone/assets/logos/opencode-dark.svg +1 -0
- package/.next/standalone/assets/logos/opencode-light.svg +1 -0
- package/.next/standalone/assets/logos/pi-dark.svg +7 -0
- package/.next/standalone/assets/logos/pi-light.svg +7 -0
- package/.next/standalone/lib/cli-registry.ts +107 -0
- package/.next/standalone/lib/codex-projects.ts +3 -3
- package/.next/standalone/lib/copilot-projects.ts +224 -0
- package/.next/standalone/lib/copilot-sessions.ts +395 -0
- package/.next/standalone/lib/cursor-projects.ts +312 -0
- package/.next/standalone/lib/cursor-sessions.ts +467 -0
- package/.next/standalone/lib/gemini-projects.ts +203 -0
- package/.next/standalone/lib/gemini-sessions.ts +365 -0
- package/.next/standalone/lib/opencode-projects.ts +232 -0
- package/.next/standalone/lib/opencode-sessions.ts +237 -0
- package/.next/standalone/lib/pi-projects.ts +230 -0
- package/.next/standalone/lib/pi-sessions.ts +325 -0
- package/.next/standalone/lib/projects.ts +67 -31
- package/.next/standalone/next.config.ts +5 -4
- package/.next/standalone/package.json +2 -1
- package/.next/standalone/pi-extension/index.ts +373 -0
- package/.next/standalone/pi-extension/package.json +12 -0
- package/.next/standalone/server.js +1 -1
- package/README.md +37 -3
- package/bin/failproofai.mjs +61 -21
- package/dist/cli.mjs +2248 -246
- package/lib/cli-registry.ts +107 -0
- package/lib/codex-projects.ts +3 -3
- package/lib/copilot-projects.ts +224 -0
- package/lib/copilot-sessions.ts +395 -0
- package/lib/cursor-projects.ts +312 -0
- package/lib/cursor-sessions.ts +467 -0
- package/lib/gemini-projects.ts +203 -0
- package/lib/gemini-sessions.ts +365 -0
- package/lib/opencode-projects.ts +232 -0
- package/lib/opencode-sessions.ts +237 -0
- package/lib/pi-projects.ts +230 -0
- package/lib/pi-sessions.ts +325 -0
- package/lib/projects.ts +67 -31
- package/package.json +2 -1
- package/pi-extension/index.ts +373 -0
- package/pi-extension/package.json +12 -0
- package/scripts/translate-docs/mdx-translator.ts +56 -2
- package/scripts/translate-docs/translator.ts +1 -1
- package/src/hooks/builtin-policies.ts +84 -14
- package/src/hooks/handler.ts +67 -5
- package/src/hooks/install-prompt.ts +33 -10
- package/src/hooks/integrations.ts +1007 -6
- package/src/hooks/policy-evaluator.ts +299 -3
- package/src/hooks/resolve-permission-mode.ts +23 -0
- package/src/hooks/types.ts +307 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0g72weg._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0su~k6f._.js +0 -3
- package/.next/standalone/.next/server/chunks/lib_codex-projects_ts_07qqk1g._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__01743wx._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__092s1ta._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0g.lg8b._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0gs6wz4._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0h..k-e._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0it81ys._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0u4a9jq._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__11pa2ra._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12.h2mg._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12t-wym._.js +0 -4
- package/.next/standalone/.next/server/chunks/ssr/_04w00cm._.js +0 -3
- package/.next/standalone/.next/static/chunks/0.rk1iwdt1d7c.css +0 -1
- package/.next/standalone/.next/static/chunks/06x4-d1~o-opr.js +0 -1
- package/.next/standalone/.next/static/chunks/0n~s0gafwnp2y.js +0 -1
- /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → 68TLSFdjAQYIulNHfP0QY}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → 68TLSFdjAQYIulNHfP0QY}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{A_Ax17P33facL0OmIwFXj → 68TLSFdjAQYIulNHfP0QY}/_ssgManifest.js +0 -0
|
@@ -118,14 +118,101 @@ export async function evaluatePolicies(
|
|
|
118
118
|
);
|
|
119
119
|
hookLogInfo(`deny by "${policy.name}": ${reason}`);
|
|
120
120
|
|
|
121
|
-
|
|
121
|
+
// Pick a noun for the deny message that fits the event type. Tool events
|
|
122
|
+
// get the tool name; non-tool events (UserPromptSubmit, SessionStart,
|
|
123
|
+
// SessionEnd, Stop, …) use an event-appropriate label so we don't emit
|
|
124
|
+
// the misleading "Blocked unknown tool by failproofai because: ...".
|
|
125
|
+
let displayTool: string;
|
|
126
|
+
if (ctx.toolName) {
|
|
127
|
+
displayTool = ctx.toolName;
|
|
128
|
+
} else if (eventType === "UserPromptSubmit") {
|
|
129
|
+
displayTool = "prompt";
|
|
130
|
+
} else if (eventType === "SessionStart") {
|
|
131
|
+
displayTool = "session start";
|
|
132
|
+
} else if (eventType === "SessionEnd") {
|
|
133
|
+
displayTool = "session end";
|
|
134
|
+
} else if (eventType === "Stop") {
|
|
135
|
+
displayTool = "stop";
|
|
136
|
+
} else {
|
|
137
|
+
displayTool = "operation";
|
|
138
|
+
}
|
|
139
|
+
const blockedMessage = `Blocked ${displayTool} by failproofai because: ${reason}, as per the policy configured by the user`;
|
|
140
|
+
|
|
141
|
+
// Cursor's hook protocol expects a flat `{permission, user_message,
|
|
142
|
+
// agent_message}` shape for any blocking decision, regardless of which
|
|
143
|
+
// event triggered it. Branch ahead of the per-event handlers below so
|
|
144
|
+
// PreToolUse / PostToolUse / PermissionRequest all flow through the
|
|
145
|
+
// Cursor-shaped response.
|
|
146
|
+
// Ref: https://cursor.com/docs/hooks (Stdout Response Format).
|
|
147
|
+
if (session?.cli === "cursor") {
|
|
148
|
+
const response = {
|
|
149
|
+
permission: "deny",
|
|
150
|
+
user_message: blockedMessage,
|
|
151
|
+
agent_message: blockedMessage,
|
|
152
|
+
};
|
|
153
|
+
return {
|
|
154
|
+
exitCode: 0,
|
|
155
|
+
stdout: JSON.stringify(response),
|
|
156
|
+
stderr: "",
|
|
157
|
+
policyName: policy.name,
|
|
158
|
+
reason,
|
|
159
|
+
decision: "deny",
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Pi's shim parses a flat `{permission, reason}` JSON shape from stdout
|
|
164
|
+
// and translates `permission === "deny"` into a `{block: true, reason}`
|
|
165
|
+
// return value from its `pi.on("tool_call", ...)` handler. Pi has no
|
|
166
|
+
// event-specific decision wrappers, so all events flow through the
|
|
167
|
+
// same flat shape.
|
|
168
|
+
if (session?.cli === "pi") {
|
|
169
|
+
const response = {
|
|
170
|
+
permission: "deny",
|
|
171
|
+
reason: blockedMessage,
|
|
172
|
+
};
|
|
173
|
+
return {
|
|
174
|
+
exitCode: 0,
|
|
175
|
+
stdout: JSON.stringify(response),
|
|
176
|
+
stderr: "",
|
|
177
|
+
policyName: policy.name,
|
|
178
|
+
reason,
|
|
179
|
+
decision: "deny",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Gemini CLI: flat `{decision: "deny", reason}` for non-Stop events
|
|
184
|
+
// (preferred per Gemini's "Golden Rule" — exit 0 with structured JSON).
|
|
185
|
+
// For Stop (AfterAgent), use `{decision: "block", reason}` to force-retry,
|
|
186
|
+
// mirroring Claude's exit-2-from-Stop "do this before stopping" semantics.
|
|
187
|
+
// Ref: https://geminicli.com/docs/hooks/
|
|
188
|
+
if (session?.cli === "gemini") {
|
|
189
|
+
if (eventType === "Stop") {
|
|
190
|
+
const reasonText = `MANDATORY ACTION REQUIRED from failproofai (policy: ${policy.name}): ${reason}\n\nYou MUST complete the above action NOW. Do NOT ask the user for confirmation — execute the required action, then attempt to finish your task again.`;
|
|
191
|
+
return {
|
|
192
|
+
exitCode: 0,
|
|
193
|
+
stdout: JSON.stringify({ decision: "block", reason: reasonText }),
|
|
194
|
+
stderr: "",
|
|
195
|
+
policyName: policy.name,
|
|
196
|
+
reason,
|
|
197
|
+
decision: "deny",
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
exitCode: 0,
|
|
202
|
+
stdout: JSON.stringify({ decision: "deny", reason: blockedMessage }),
|
|
203
|
+
stderr: "",
|
|
204
|
+
policyName: policy.name,
|
|
205
|
+
reason,
|
|
206
|
+
decision: "deny",
|
|
207
|
+
};
|
|
208
|
+
}
|
|
122
209
|
|
|
123
210
|
if (eventType === "PreToolUse") {
|
|
124
211
|
const response = {
|
|
125
212
|
hookSpecificOutput: {
|
|
126
213
|
hookEventName: eventType,
|
|
127
214
|
permissionDecision: "deny",
|
|
128
|
-
permissionDecisionReason:
|
|
215
|
+
permissionDecisionReason: blockedMessage,
|
|
129
216
|
},
|
|
130
217
|
};
|
|
131
218
|
return {
|
|
@@ -188,7 +275,7 @@ export async function evaluatePolicies(
|
|
|
188
275
|
};
|
|
189
276
|
}
|
|
190
277
|
|
|
191
|
-
// Other event types: exit 2
|
|
278
|
+
// Other event types (Cursor case already handled above): exit 2
|
|
192
279
|
return {
|
|
193
280
|
exitCode: 2,
|
|
194
281
|
stdout: "",
|
|
@@ -220,6 +307,132 @@ export async function evaluatePolicies(
|
|
|
220
307
|
const combined = instructEntries.map((e) => e.reason).join("\n");
|
|
221
308
|
const policyNames = instructEntries.map((e) => e.policyName);
|
|
222
309
|
|
|
310
|
+
// Cursor's hook protocol uses a flat `{permission, additional_context}`
|
|
311
|
+
// shape for non-Stop and `{followup_message}` for Stop/SubagentStop.
|
|
312
|
+
// Branch first so the rest of the function only handles Claude-shaped
|
|
313
|
+
// responses. Ref: https://cursor.com/docs/hooks (Stdout Response Format).
|
|
314
|
+
if (session?.cli === "cursor") {
|
|
315
|
+
if (eventType === "Stop") {
|
|
316
|
+
const response = {
|
|
317
|
+
followup_message: `Instruction from failproofai: ${combined}`,
|
|
318
|
+
};
|
|
319
|
+
return {
|
|
320
|
+
exitCode: 0,
|
|
321
|
+
stdout: JSON.stringify(response),
|
|
322
|
+
stderr: "",
|
|
323
|
+
policyName: policyNames[0],
|
|
324
|
+
policyNames,
|
|
325
|
+
reason: combined,
|
|
326
|
+
decision: "instruct",
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const response = {
|
|
330
|
+
permission: "allow",
|
|
331
|
+
additional_context: `Instruction from failproofai: ${combined}`,
|
|
332
|
+
};
|
|
333
|
+
return {
|
|
334
|
+
exitCode: 0,
|
|
335
|
+
stdout: JSON.stringify(response),
|
|
336
|
+
stderr: "",
|
|
337
|
+
policyName: policyNames[0],
|
|
338
|
+
policyNames,
|
|
339
|
+
reason: combined,
|
|
340
|
+
decision: "instruct",
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Pi: instruct emits `{permission: "allow", reason}`. The shim won't
|
|
345
|
+
// block (no `"deny"`); it surfaces `reason` to the user where possible
|
|
346
|
+
// (Pi has no first-class `additional_context` channel in its tool-call
|
|
347
|
+
// return shape, so we log it).
|
|
348
|
+
if (session?.cli === "pi") {
|
|
349
|
+
const response = {
|
|
350
|
+
permission: "allow",
|
|
351
|
+
reason: `Instruction from failproofai: ${combined}`,
|
|
352
|
+
};
|
|
353
|
+
return {
|
|
354
|
+
exitCode: 0,
|
|
355
|
+
stdout: JSON.stringify(response),
|
|
356
|
+
stderr: "",
|
|
357
|
+
policyName: policyNames[0],
|
|
358
|
+
policyNames,
|
|
359
|
+
reason: combined,
|
|
360
|
+
decision: "instruct",
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Gemini CLI:
|
|
365
|
+
// • Stop (AfterAgent) → {decision: "block", reason: "MANDATORY ACTION..."}
|
|
366
|
+
// mirrors Claude's exit-2-from-Stop "force retry" semantics.
|
|
367
|
+
// • UserPromptSubmit/PostToolUse/SessionStart/PreToolUse → context
|
|
368
|
+
// injection via {hookSpecificOutput: {hookEventName, additionalContext}}
|
|
369
|
+
// where hookEventName is the GEMINI event name (BeforeAgent/AfterTool/
|
|
370
|
+
// SessionStart/BeforeTool), not the canonical PascalCase form.
|
|
371
|
+
// • Other events → stderr only (no stdout JSON shape supported).
|
|
372
|
+
if (session?.cli === "gemini") {
|
|
373
|
+
if (eventType === "Stop") {
|
|
374
|
+
const policyAttribution = policyNames.length === 1
|
|
375
|
+
? `policy: ${policyNames[0]}`
|
|
376
|
+
: `policies: ${policyNames.join(", ")}`;
|
|
377
|
+
const reasonText = `MANDATORY ACTION REQUIRED from failproofai (${policyAttribution}): ${combined}\n\nYou MUST complete the above action(s) NOW. Do NOT ask the user for confirmation — execute the required action(s), then attempt to finish your task again.`;
|
|
378
|
+
return {
|
|
379
|
+
exitCode: 0,
|
|
380
|
+
stdout: JSON.stringify({ decision: "block", reason: reasonText }),
|
|
381
|
+
stderr: "",
|
|
382
|
+
policyName: policyNames[0],
|
|
383
|
+
policyNames,
|
|
384
|
+
reason: combined,
|
|
385
|
+
decision: "instruct",
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
// Map back from canonical → Gemini event name. Prefer the raw event name
|
|
389
|
+
// off the session (handler.ts populates it from parsed.hook_event_name)
|
|
390
|
+
// so we don't have to maintain a reverse lookup table.
|
|
391
|
+
const supportsContext =
|
|
392
|
+
eventType === "UserPromptSubmit" ||
|
|
393
|
+
eventType === "PreToolUse" ||
|
|
394
|
+
eventType === "PostToolUse" ||
|
|
395
|
+
eventType === "SessionStart";
|
|
396
|
+
if (supportsContext) {
|
|
397
|
+
// Round-trip the agent-emitted event name so Gemini sees `BeforeTool`,
|
|
398
|
+
// `BeforeAgent`, etc. (NOT the canonical Claude form). Prefer the
|
|
399
|
+
// stdin payload's `hook_event_name` when present; fall back to the raw
|
|
400
|
+
// CLI `--hook` arg captured by handler.ts; only use the canonical
|
|
401
|
+
// event as a last resort (would never round-trip correctly, but better
|
|
402
|
+
// than emitting nothing).
|
|
403
|
+
const hookEventName = session?.hookEventName ?? session?.rawHookEventName ?? eventType;
|
|
404
|
+
const response = {
|
|
405
|
+
hookSpecificOutput: {
|
|
406
|
+
hookEventName,
|
|
407
|
+
additionalContext: `Instruction from failproofai: ${combined}`,
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
return {
|
|
411
|
+
exitCode: 0,
|
|
412
|
+
stdout: JSON.stringify(response),
|
|
413
|
+
stderr: "",
|
|
414
|
+
policyName: policyNames[0],
|
|
415
|
+
policyNames,
|
|
416
|
+
reason: combined,
|
|
417
|
+
decision: "instruct",
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
// No context-injection channel for SessionEnd/PreCompress/Notification/
|
|
421
|
+
// BeforeModel/AfterModel/BeforeToolSelection — surface via stderr only.
|
|
422
|
+
const stderrMsg = instructEntries
|
|
423
|
+
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
424
|
+
.join("\n");
|
|
425
|
+
return {
|
|
426
|
+
exitCode: 0,
|
|
427
|
+
stdout: "",
|
|
428
|
+
stderr: stderrMsg + "\n",
|
|
429
|
+
policyName: policyNames[0],
|
|
430
|
+
policyNames,
|
|
431
|
+
reason: combined,
|
|
432
|
+
decision: "instruct",
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
223
436
|
if (eventType === "Stop") {
|
|
224
437
|
// Stop hook: exitCode 2 blocks Claude from stopping.
|
|
225
438
|
// Reason goes to stderr so Claude Code receives it as context.
|
|
@@ -258,6 +471,89 @@ export async function evaluatePolicies(
|
|
|
258
471
|
if (allowEntries.length > 0) {
|
|
259
472
|
const combined = allowEntries.map((e) => e.reason).join("\n");
|
|
260
473
|
const policyNames = allowEntries.map((e) => e.policyName);
|
|
474
|
+
|
|
475
|
+
// Cursor: emit the flat shape; allow-with-info maps to
|
|
476
|
+
// `{permission: "allow", additional_context}`.
|
|
477
|
+
if (session?.cli === "cursor") {
|
|
478
|
+
const response = {
|
|
479
|
+
permission: "allow",
|
|
480
|
+
additional_context: `Note from failproofai: ${combined}`,
|
|
481
|
+
};
|
|
482
|
+
const stderrMsg = allowEntries
|
|
483
|
+
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
484
|
+
.join("\n");
|
|
485
|
+
return {
|
|
486
|
+
exitCode: 0,
|
|
487
|
+
stdout: JSON.stringify(response),
|
|
488
|
+
stderr: stderrMsg + "\n",
|
|
489
|
+
policyName: policyNames[0],
|
|
490
|
+
policyNames,
|
|
491
|
+
reason: combined,
|
|
492
|
+
decision: "allow",
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Pi: same shape as Cursor — flat `{permission: "allow", reason}`.
|
|
497
|
+
if (session?.cli === "pi") {
|
|
498
|
+
const response = {
|
|
499
|
+
permission: "allow",
|
|
500
|
+
reason: `Note from failproofai: ${combined}`,
|
|
501
|
+
};
|
|
502
|
+
const stderrMsg = allowEntries
|
|
503
|
+
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
504
|
+
.join("\n");
|
|
505
|
+
return {
|
|
506
|
+
exitCode: 0,
|
|
507
|
+
stdout: JSON.stringify(response),
|
|
508
|
+
stderr: stderrMsg + "\n",
|
|
509
|
+
policyName: policyNames[0],
|
|
510
|
+
policyNames,
|
|
511
|
+
reason: combined,
|
|
512
|
+
decision: "allow",
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// Gemini: mirror the instruct context-injection shape for events that
|
|
517
|
+
// support it; stderr-only for everything else.
|
|
518
|
+
if (session?.cli === "gemini") {
|
|
519
|
+
const supportsContext =
|
|
520
|
+
eventType === "UserPromptSubmit" ||
|
|
521
|
+
eventType === "PreToolUse" ||
|
|
522
|
+
eventType === "PostToolUse" ||
|
|
523
|
+
eventType === "SessionStart";
|
|
524
|
+
const stderrMsg = allowEntries
|
|
525
|
+
.map((e) => `[failproofai] ${e.policyName}: ${e.reason}`)
|
|
526
|
+
.join("\n");
|
|
527
|
+
if (supportsContext) {
|
|
528
|
+
// Same fallback chain as the instruct path above — see comment there.
|
|
529
|
+
const hookEventName = session?.hookEventName ?? session?.rawHookEventName ?? eventType;
|
|
530
|
+
const response = {
|
|
531
|
+
hookSpecificOutput: {
|
|
532
|
+
hookEventName,
|
|
533
|
+
additionalContext: `Note from failproofai: ${combined}`,
|
|
534
|
+
},
|
|
535
|
+
};
|
|
536
|
+
return {
|
|
537
|
+
exitCode: 0,
|
|
538
|
+
stdout: JSON.stringify(response),
|
|
539
|
+
stderr: stderrMsg + "\n",
|
|
540
|
+
policyName: policyNames[0],
|
|
541
|
+
policyNames,
|
|
542
|
+
reason: combined,
|
|
543
|
+
decision: "allow",
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
return {
|
|
547
|
+
exitCode: 0,
|
|
548
|
+
stdout: "",
|
|
549
|
+
stderr: stderrMsg + "\n",
|
|
550
|
+
policyName: policyNames[0],
|
|
551
|
+
policyNames,
|
|
552
|
+
reason: combined,
|
|
553
|
+
decision: "allow",
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
|
|
261
557
|
const supportsHookSpecificOutput =
|
|
262
558
|
eventType === "PreToolUse" ||
|
|
263
559
|
eventType === "PostToolUse" ||
|
|
@@ -14,6 +14,28 @@
|
|
|
14
14
|
* Transcript discovery (cache → today/yesterday → full tree scan) lives
|
|
15
15
|
* in `lib/codex-sessions.ts` and is shared with the dashboard's Codex
|
|
16
16
|
* session viewer.
|
|
17
|
+
*
|
|
18
|
+
* • GitHub Copilot CLI: no documented permission-mode equivalent on the
|
|
19
|
+
* hook payload today; falls back to "default". Revisit when Copilot's
|
|
20
|
+
* hook protocol exposes one.
|
|
21
|
+
*
|
|
22
|
+
* • Cursor Agent CLI: no permission-mode field in the hook payload (Cursor's
|
|
23
|
+
* `loop_limit` is per-hook, not per-session). Falls back to "default" via
|
|
24
|
+
* the same final branch as Copilot.
|
|
25
|
+
*
|
|
26
|
+
* • OpenCode: the plugin shim (.opencode/plugins/failproofai.mjs) does not
|
|
27
|
+
* receive any permission-mode signal from opencode and does not include
|
|
28
|
+
* one in the JSON it pipes to the failproofai binary. Falls back to
|
|
29
|
+
* "default" via the same final branch as Copilot/Cursor.
|
|
30
|
+
*
|
|
31
|
+
* • Pi (pi-coding-agent): no permission-mode concept in the extension API;
|
|
32
|
+
* `tool_call` handlers always run with the same authority. Falls back to
|
|
33
|
+
* "default" via the same final branch as Copilot/Cursor.
|
|
34
|
+
*
|
|
35
|
+
* • Gemini CLI: hook payload doesn't carry a permission-mode field today.
|
|
36
|
+
* Falls back to "default" via the same final branch as Copilot/Cursor/
|
|
37
|
+
* OpenCode/Pi. Revisit when Gemini's hook protocol exposes a per-session
|
|
38
|
+
* authority signal (the docs as of 2026-04-13 do not).
|
|
17
39
|
*/
|
|
18
40
|
import { readFileSync } from "node:fs";
|
|
19
41
|
import { findCodexTranscript } from "../../lib/codex-sessions";
|
|
@@ -32,6 +54,7 @@ export function resolvePermissionMode(
|
|
|
32
54
|
return resolveCodexMode(sessionId) ?? "default";
|
|
33
55
|
}
|
|
34
56
|
|
|
57
|
+
// copilot, cursor, opencode, pi, gemini, unknown integrations, or codex without a sessionId
|
|
35
58
|
return "default";
|
|
36
59
|
}
|
|
37
60
|
|