@@ -143,6 +143,158 @@ describe("canUseTool MCP approval enforcement", () => {
143143 expect ( result . behavior ) . toBe ( "allow" ) ;
144144 } ) ;
145145
146+ it ( "bypasses the PostHog exec gate in auto mode" , async ( ) => {
147+ setMcpToolApprovalStates ( { mcp__posthog__exec : "approved" } ) ;
148+ const hasApproval = vi . fn ( ) . mockReturnValue ( false ) ;
149+ const addApproval = vi . fn ( ) . mockResolvedValue ( undefined ) ;
150+
151+ const context = createContext ( "mcp__posthog__exec" , {
152+ toolInput : { command : "call experiment-update {}" } ,
153+ session : {
154+ permissionMode : "auto" ,
155+ settingsManager : {
156+ getRepoRoot : vi . fn ( ) . mockReturnValue ( "/repo" ) ,
157+ hasPostHogExecApproval : hasApproval ,
158+ addPostHogExecApproval : addApproval ,
159+ } ,
160+ } ,
161+ } ) ;
162+ const result = await canUseTool ( context ) ;
163+
164+ expect ( result . behavior ) . toBe ( "allow" ) ;
165+ expect ( context . client . requestPermission ) . not . toHaveBeenCalled ( ) ;
166+ expect ( hasApproval ) . not . toHaveBeenCalled ( ) ;
167+ expect ( addApproval ) . not . toHaveBeenCalled ( ) ;
168+ } ) ;
169+
170+ it ( "bypasses the PostHog exec gate in bypassPermissions mode" , async ( ) => {
171+ setMcpToolApprovalStates ( { mcp__posthog__exec : "approved" } ) ;
172+
173+ const context = createContext ( "mcp__posthog__exec" , {
174+ toolInput : { command : "call feature-flag-delete {}" } ,
175+ session : {
176+ permissionMode : "bypassPermissions" ,
177+ settingsManager : {
178+ getRepoRoot : vi . fn ( ) . mockReturnValue ( "/repo" ) ,
179+ hasPostHogExecApproval : vi . fn ( ) . mockReturnValue ( false ) ,
180+ addPostHogExecApproval : vi . fn ( ) ,
181+ } ,
182+ } ,
183+ } ) ;
184+ const result = await canUseTool ( context ) ;
185+
186+ expect ( result . behavior ) . toBe ( "allow" ) ;
187+ expect ( context . client . requestPermission ) . not . toHaveBeenCalled ( ) ;
188+ } ) ;
189+
190+ it ( "short-circuits when a PostHog exec sub-tool was previously approved" , async ( ) => {
191+ setMcpToolApprovalStates ( { mcp__posthog__exec : "approved" } ) ;
192+
193+ const context = createContext ( "mcp__posthog__exec" , {
194+ toolInput : { command : "call experiment-update {}" } ,
195+ session : {
196+ permissionMode : "default" ,
197+ settingsManager : {
198+ getRepoRoot : vi . fn ( ) . mockReturnValue ( "/repo" ) ,
199+ hasPostHogExecApproval : vi
200+ . fn ( )
201+ . mockImplementation ( ( s : string ) => s === "experiment-update" ) ,
202+ addPostHogExecApproval : vi . fn ( ) ,
203+ } ,
204+ } ,
205+ } ) ;
206+ const result = await canUseTool ( context ) ;
207+
208+ expect ( result . behavior ) . toBe ( "allow" ) ;
209+ expect ( context . client . requestPermission ) . not . toHaveBeenCalled ( ) ;
210+ } ) ;
211+
212+ it ( "prompts for an unapproved destructive PostHog sub-tool and persists on allow_always" , async ( ) => {
213+ setMcpToolApprovalStates ( { mcp__posthog__exec : "approved" } ) ;
214+ const addApproval = vi . fn ( ) . mockResolvedValue ( undefined ) ;
215+
216+ const context = createContext ( "mcp__posthog__exec" , {
217+ toolInput : { command : "call notebooks-destroy {}" } ,
218+ session : {
219+ permissionMode : "default" ,
220+ settingsManager : {
221+ getRepoRoot : vi . fn ( ) . mockReturnValue ( "/repo" ) ,
222+ hasPostHogExecApproval : vi . fn ( ) . mockReturnValue ( false ) ,
223+ addPostHogExecApproval : addApproval ,
224+ } ,
225+ } ,
226+ client : {
227+ sessionUpdate : vi . fn ( ) . mockResolvedValue ( undefined ) ,
228+ requestPermission : vi . fn ( ) . mockResolvedValue ( {
229+ outcome : { outcome : "selected" , optionId : "allow_always" } ,
230+ } ) ,
231+ } ,
232+ } ) ;
233+ const result = await canUseTool ( context ) ;
234+
235+ expect ( result . behavior ) . toBe ( "allow" ) ;
236+ expect ( context . client . requestPermission ) . toHaveBeenCalledWith (
237+ expect . objectContaining ( {
238+ toolCall : expect . objectContaining ( {
239+ title : "The agent wants to run `notebooks-destroy` on PostHog" ,
240+ } ) ,
241+ } ) ,
242+ ) ;
243+ expect ( addApproval ) . toHaveBeenCalledWith ( "notebooks-destroy" ) ;
244+ } ) ;
245+
246+ it ( "prompts but does not persist on allow_once" , async ( ) => {
247+ setMcpToolApprovalStates ( { mcp__posthog__exec : "approved" } ) ;
248+ const addApproval = vi . fn ( ) ;
249+
250+ const context = createContext ( "mcp__posthog__exec" , {
251+ toolInput : { command : "call experiment-delete {}" } ,
252+ session : {
253+ permissionMode : "default" ,
254+ settingsManager : {
255+ getRepoRoot : vi . fn ( ) . mockReturnValue ( "/repo" ) ,
256+ hasPostHogExecApproval : vi . fn ( ) . mockReturnValue ( false ) ,
257+ addPostHogExecApproval : addApproval ,
258+ } ,
259+ } ,
260+ client : {
261+ sessionUpdate : vi . fn ( ) . mockResolvedValue ( undefined ) ,
262+ requestPermission : vi . fn ( ) . mockResolvedValue ( {
263+ outcome : { outcome : "selected" , optionId : "allow" } ,
264+ } ) ,
265+ } ,
266+ } ) ;
267+ const result = await canUseTool ( context ) ;
268+
269+ expect ( result . behavior ) . toBe ( "allow" ) ;
270+ expect ( addApproval ) . not . toHaveBeenCalled ( ) ;
271+ } ) ;
272+
273+ it ( "does not gate non-destructive PostHog sub-tools" , async ( ) => {
274+ setMcpToolApprovalStates ( { mcp__posthog__exec : "approved" } ) ;
275+ const addApproval = vi . fn ( ) ;
276+
277+ const context = createContext ( "mcp__posthog__exec" , {
278+ toolInput : { command : "call experiment-get-all {}" } ,
279+ session : {
280+ permissionMode : "default" ,
281+ settingsManager : {
282+ getRepoRoot : vi . fn ( ) . mockReturnValue ( "/repo" ) ,
283+ hasPostHogExecApproval : vi . fn ( ) . mockReturnValue ( false ) ,
284+ addPostHogExecApproval : addApproval ,
285+ } ,
286+ } ,
287+ } ) ;
288+ const result = await canUseTool ( context ) ;
289+
290+ // Non-destructive sub-tool falls through the gate. With approved MCP state
291+ // and non-read-only tool metadata, it hits the default permission flow,
292+ // which auto-allows via our mocked requestPermission. The gate must not
293+ // have prompted with a PostHog-specific title, and must not have persisted.
294+ expect ( result . behavior ) . toBe ( "allow" ) ;
295+ expect ( addApproval ) . not . toHaveBeenCalled ( ) ;
296+ } ) ;
297+
146298 it ( "emits tool denial notification for do_not_use" , async ( ) => {
147299 setMcpToolApprovalStates ( {
148300 mcp__server__denied_tool : "do_not_use" ,
0 commit comments