/ src / utils / permissions / bypassPermissionsKillswitch.ts
bypassPermissionsKillswitch.ts
  1  import { feature } from 'bun:bundle'
  2  import { useEffect, useRef } from 'react'
  3  import {
  4    type AppState,
  5    useAppState,
  6    useAppStateStore,
  7    useSetAppState,
  8  } from 'src/state/AppState.js'
  9  import type { ToolPermissionContext } from 'src/Tool.js'
 10  import { getIsRemoteMode } from '../../bootstrap/state.js'
 11  import {
 12    createDisabledBypassPermissionsContext,
 13    shouldDisableBypassPermissions,
 14    verifyAutoModeGateAccess,
 15  } from './permissionSetup.js'
 16  
 17  let bypassPermissionsCheckRan = false
 18  
 19  export async function checkAndDisableBypassPermissionsIfNeeded(
 20    toolPermissionContext: ToolPermissionContext,
 21    setAppState: (f: (prev: AppState) => AppState) => void,
 22  ): Promise<void> {
 23    // Check if bypassPermissions should be disabled based on Statsig gate
 24    // Do this only once, before the first query, to ensure we have the latest gate value
 25    if (bypassPermissionsCheckRan) {
 26      return
 27    }
 28    bypassPermissionsCheckRan = true
 29  
 30    if (!toolPermissionContext.isBypassPermissionsModeAvailable) {
 31      return
 32    }
 33  
 34    const shouldDisable = await shouldDisableBypassPermissions()
 35    if (!shouldDisable) {
 36      return
 37    }
 38  
 39    setAppState(prev => {
 40      return {
 41        ...prev,
 42        toolPermissionContext: createDisabledBypassPermissionsContext(
 43          prev.toolPermissionContext,
 44        ),
 45      }
 46    })
 47  }
 48  
 49  /**
 50   * Reset the run-once flag for checkAndDisableBypassPermissionsIfNeeded.
 51   * Call this after /login so the gate check re-runs with the new org.
 52   */
 53  export function resetBypassPermissionsCheck(): void {
 54    bypassPermissionsCheckRan = false
 55  }
 56  
 57  export function useKickOffCheckAndDisableBypassPermissionsIfNeeded(): void {
 58    const toolPermissionContext = useAppState(s => s.toolPermissionContext)
 59    const setAppState = useSetAppState()
 60  
 61    // Run once, when the component mounts
 62    useEffect(() => {
 63      if (getIsRemoteMode()) return
 64      void checkAndDisableBypassPermissionsIfNeeded(
 65        toolPermissionContext,
 66        setAppState,
 67      )
 68      // eslint-disable-next-line react-hooks/exhaustive-deps
 69    }, [])
 70  }
 71  
 72  let autoModeCheckRan = false
 73  
 74  export async function checkAndDisableAutoModeIfNeeded(
 75    toolPermissionContext: ToolPermissionContext,
 76    setAppState: (f: (prev: AppState) => AppState) => void,
 77    fastMode?: boolean,
 78  ): Promise<void> {
 79    if (feature('TRANSCRIPT_CLASSIFIER')) {
 80      if (autoModeCheckRan) {
 81        return
 82      }
 83      autoModeCheckRan = true
 84  
 85      const { updateContext, notification } = await verifyAutoModeGateAccess(
 86        toolPermissionContext,
 87        fastMode,
 88      )
 89      setAppState(prev => {
 90        // Apply the transform to CURRENT context, not the stale snapshot we
 91        // passed to verifyAutoModeGateAccess. The async GrowthBook await inside
 92        // can be outrun by a mid-turn shift-tab; spreading a stale context here
 93        // would revert the user's mode change.
 94        const nextCtx = updateContext(prev.toolPermissionContext)
 95        const newState =
 96          nextCtx === prev.toolPermissionContext
 97            ? prev
 98            : { ...prev, toolPermissionContext: nextCtx }
 99        if (!notification) return newState
100        return {
101          ...newState,
102          notifications: {
103            ...newState.notifications,
104            queue: [
105              ...newState.notifications.queue,
106              {
107                key: 'auto-mode-gate-notification',
108                text: notification,
109                color: 'warning' as const,
110                priority: 'high' as const,
111              },
112            ],
113          },
114        }
115      })
116    }
117  }
118  
119  /**
120   * Reset the run-once flag for checkAndDisableAutoModeIfNeeded.
121   * Call this after /login so the gate check re-runs with the new org.
122   */
123  export function resetAutoModeGateCheck(): void {
124    autoModeCheckRan = false
125  }
126  
127  export function useKickOffCheckAndDisableAutoModeIfNeeded(): void {
128    const mainLoopModel = useAppState(s => s.mainLoopModel)
129    const mainLoopModelForSession = useAppState(s => s.mainLoopModelForSession)
130    const fastMode = useAppState(s => s.fastMode)
131    const setAppState = useSetAppState()
132    const store = useAppStateStore()
133    const isFirstRunRef = useRef(true)
134  
135    // Runs on mount (startup check) AND whenever the model or fast mode changes
136    // (kick-out / carousel-restore). Watching both model fields covers /model,
137    // Cmd+P picker, /config, and bridge onSetModel paths; fastMode covers
138    // /fast on|off for the tengu_auto_mode_config.disableFastMode circuit
139    // breaker. The print.ts headless paths are covered by the sync
140    // isAutoModeGateEnabled() check.
141    useEffect(() => {
142      if (getIsRemoteMode()) return
143      if (isFirstRunRef.current) {
144        isFirstRunRef.current = false
145      } else {
146        resetAutoModeGateCheck()
147      }
148      void checkAndDisableAutoModeIfNeeded(
149        store.getState().toolPermissionContext,
150        setAppState,
151        fastMode,
152      )
153      // eslint-disable-next-line react-hooks/exhaustive-deps
154    }, [mainLoopModel, mainLoopModelForSession, fastMode])
155  }