mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-18 14:25:51 +00:00
fix: 修复auto mode gate check未处理的promise rejection
在 bypassPermissionsKillswitch.ts 的 useKickOffCheckAndDisableAutoModeIfNeeded 中,void fire-and-forget 调用缺少 .catch() 处理,导致 verifyAutoModeGateAccess 失败时产生 unhandled promise rejection。同时移除 permissionSetup.ts 中冗余的 null check。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,79 +1,44 @@
|
|||||||
import { feature } from 'bun:bundle'
|
import { feature } from 'bun:bundle'
|
||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import {
|
import { useNotifications } from 'src/context/notifications.js'
|
||||||
type AppState,
|
import { toError } from '../../utils/errors.js'
|
||||||
useAppState,
|
import { logError } from '../../utils/log.js'
|
||||||
useAppStateStore,
|
|
||||||
useSetAppState,
|
|
||||||
} from 'src/state/AppState.js'
|
|
||||||
import type { ToolPermissionContext } from 'src/Tool.js'
|
|
||||||
import { getIsRemoteMode } from '../../bootstrap/state.js'
|
import { getIsRemoteMode } from '../../bootstrap/state.js'
|
||||||
|
import { useAppState, useAppStateStore, useSetAppState } from '../../state/AppState.js'
|
||||||
|
import type { ToolPermissionContext } from '../../Tool.js'
|
||||||
import {
|
import {
|
||||||
createDisabledBypassPermissionsContext,
|
|
||||||
shouldDisableBypassPermissions,
|
|
||||||
verifyAutoModeGateAccess,
|
verifyAutoModeGateAccess,
|
||||||
} from './permissionSetup.js'
|
} from './permissionSetup.js'
|
||||||
|
|
||||||
let bypassPermissionsCheckRan = false
|
/**
|
||||||
|
* No-op — bypass permissions is always available.
|
||||||
|
*/
|
||||||
export async function checkAndDisableBypassPermissionsIfNeeded(
|
export async function checkAndDisableBypassPermissionsIfNeeded(
|
||||||
toolPermissionContext: ToolPermissionContext,
|
_toolPermissionContext: ToolPermissionContext,
|
||||||
setAppState: (f: (prev: AppState) => AppState) => void,
|
_setAppState: (f: (prev: import('../../state/AppState.js').AppState) => import('../../state/AppState.js').AppState) => void,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Check if bypassPermissions should be disabled based on Statsig gate
|
// Bypass permissions is always available — no gate check needed
|
||||||
// Do this only once, before the first query, to ensure we have the latest gate value
|
|
||||||
if (bypassPermissionsCheckRan) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
bypassPermissionsCheckRan = true
|
|
||||||
|
|
||||||
if (!toolPermissionContext.isBypassPermissionsModeAvailable) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldDisable = await shouldDisableBypassPermissions()
|
|
||||||
if (!shouldDisable) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setAppState(prev => {
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
toolPermissionContext: createDisabledBypassPermissionsContext(
|
|
||||||
prev.toolPermissionContext,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset the run-once flag for checkAndDisableBypassPermissionsIfNeeded.
|
* Reset stub — kept for interface compatibility.
|
||||||
* Call this after /login so the gate check re-runs with the new org.
|
|
||||||
*/
|
*/
|
||||||
export function resetBypassPermissionsCheck(): void {
|
export function resetBypassPermissionsCheck(): void {
|
||||||
bypassPermissionsCheckRan = false
|
// No-op
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-op hook — bypass permissions is always available.
|
||||||
|
*/
|
||||||
export function useKickOffCheckAndDisableBypassPermissionsIfNeeded(): void {
|
export function useKickOffCheckAndDisableBypassPermissionsIfNeeded(): void {
|
||||||
const toolPermissionContext = useAppState(s => s.toolPermissionContext)
|
// No-op
|
||||||
const setAppState = useSetAppState()
|
|
||||||
|
|
||||||
// Run once, when the component mounts
|
|
||||||
useEffect(() => {
|
|
||||||
if (getIsRemoteMode()) return
|
|
||||||
void checkAndDisableBypassPermissionsIfNeeded(
|
|
||||||
toolPermissionContext,
|
|
||||||
setAppState,
|
|
||||||
)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let autoModeCheckRan = false
|
let autoModeCheckRan = false
|
||||||
|
|
||||||
export async function checkAndDisableAutoModeIfNeeded(
|
export async function checkAndDisableAutoModeIfNeeded(
|
||||||
toolPermissionContext: ToolPermissionContext,
|
toolPermissionContext: ToolPermissionContext,
|
||||||
setAppState: (f: (prev: AppState) => AppState) => void,
|
setAppState: (f: (prev: import('../../state/AppState.js').AppState) => import('../../state/AppState.js').AppState) => void,
|
||||||
fastMode?: boolean,
|
fastMode?: boolean,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
||||||
@@ -87,10 +52,6 @@ export async function checkAndDisableAutoModeIfNeeded(
|
|||||||
fastMode,
|
fastMode,
|
||||||
)
|
)
|
||||||
setAppState(prev => {
|
setAppState(prev => {
|
||||||
// Apply the transform to CURRENT context, not the stale snapshot we
|
|
||||||
// passed to verifyAutoModeGateAccess. The async GrowthBook await inside
|
|
||||||
// can be outrun by a mid-turn shift-tab; spreading a stale context here
|
|
||||||
// would revert the user's mode change.
|
|
||||||
const nextCtx = updateContext(prev.toolPermissionContext)
|
const nextCtx = updateContext(prev.toolPermissionContext)
|
||||||
const newState =
|
const newState =
|
||||||
nextCtx === prev.toolPermissionContext
|
nextCtx === prev.toolPermissionContext
|
||||||
@@ -133,11 +94,6 @@ export function useKickOffCheckAndDisableAutoModeIfNeeded(): void {
|
|||||||
const isFirstRunRef = useRef(true)
|
const isFirstRunRef = useRef(true)
|
||||||
|
|
||||||
// Runs on mount (startup check) AND whenever the model or fast mode changes
|
// Runs on mount (startup check) AND whenever the model or fast mode changes
|
||||||
// (kick-out / carousel-restore). Watching both model fields covers /model,
|
|
||||||
// Cmd+P picker, /config, and bridge onSetModel paths; fastMode covers
|
|
||||||
// /fast on|off for the tengu_auto_mode_config.disableFastMode circuit
|
|
||||||
// breaker. The print.ts headless paths are covered by the sync
|
|
||||||
// isAutoModeGateEnabled() check.
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (getIsRemoteMode()) return
|
if (getIsRemoteMode()) return
|
||||||
if (isFirstRunRef.current) {
|
if (isFirstRunRef.current) {
|
||||||
@@ -149,7 +105,9 @@ export function useKickOffCheckAndDisableAutoModeIfNeeded(): void {
|
|||||||
store.getState().toolPermissionContext,
|
store.getState().toolPermissionContext,
|
||||||
setAppState,
|
setAppState,
|
||||||
fastMode,
|
fastMode,
|
||||||
)
|
).catch(error => {
|
||||||
|
logError(new Error('Auto mode gate check failed', { cause: toError(error) }))
|
||||||
|
})
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [mainLoopModel, mainLoopModelForSession, fastMode])
|
}, [mainLoopModel, mainLoopModelForSession, fastMode])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -799,10 +799,6 @@ export function initialPermissionModeFromCLI({
|
|||||||
result = { mode: 'default', notification }
|
result = { mode: 'default', notification }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
result = { mode: 'default', notification }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (feature('TRANSCRIPT_CLASSIFIER') && result.mode === 'auto') {
|
if (feature('TRANSCRIPT_CLASSIFIER') && result.mode === 'auto') {
|
||||||
autoModeStateModule?.setAutoModeActive(true)
|
autoModeStateModule?.setAutoModeActive(true)
|
||||||
}
|
}
|
||||||
@@ -927,20 +923,9 @@ export async function initializeToolPermissionContext({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if bypassPermissions mode is available (not disabled by Statsig gate or settings)
|
// Bypass permissions mode is available to all users
|
||||||
// Use cached values to avoid blocking on startup
|
const isBypassPermissionsModeAvailable = true
|
||||||
const growthBookDisableBypassPermissionsMode =
|
|
||||||
checkStatsigFeatureGate_CACHED_MAY_BE_STALE(
|
|
||||||
'tengu_disable_bypass_permissions_mode',
|
|
||||||
)
|
|
||||||
const settings = getSettings_DEPRECATED() || {}
|
const settings = getSettings_DEPRECATED() || {}
|
||||||
const settingsDisableBypassPermissionsMode =
|
|
||||||
settings.permissions?.disableBypassPermissionsMode === 'disable'
|
|
||||||
const isBypassPermissionsModeAvailable =
|
|
||||||
(permissionMode === 'bypassPermissions' ||
|
|
||||||
allowDangerouslySkipPermissions) &&
|
|
||||||
!growthBookDisableBypassPermissionsMode &&
|
|
||||||
!settingsDisableBypassPermissionsMode
|
|
||||||
|
|
||||||
// Load all permission rules from disk
|
// Load all permission rules from disk
|
||||||
const rulesFromDisk = loadAllPermissionRulesFromDisk()
|
const rulesFromDisk = loadAllPermissionRulesFromDisk()
|
||||||
@@ -984,7 +969,7 @@ export async function initializeToolPermissionContext({
|
|||||||
alwaysAskRules: {},
|
alwaysAskRules: {},
|
||||||
isBypassPermissionsModeAvailable,
|
isBypassPermissionsModeAvailable,
|
||||||
...(feature('TRANSCRIPT_CLASSIFIER')
|
...(feature('TRANSCRIPT_CLASSIFIER')
|
||||||
? { isAutoModeAvailable: isAutoModeGateEnabled() }
|
? { isAutoModeAvailable: true }
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
rulesFromDisk,
|
rulesFromDisk,
|
||||||
@@ -1076,131 +1061,54 @@ export function getAutoModeUnavailableNotification(
|
|||||||
* kicking the user out of a mode they've already left during the await.
|
* kicking the user out of a mode they've already left during the await.
|
||||||
*/
|
*/
|
||||||
export async function verifyAutoModeGateAccess(
|
export async function verifyAutoModeGateAccess(
|
||||||
currentContext: ToolPermissionContext,
|
_currentContext: ToolPermissionContext,
|
||||||
// Runtime AppState.fastMode — passed from callers with AppState access so
|
// Runtime AppState.fastMode — passed from callers with AppState access so
|
||||||
// the disableFastMode circuit breaker reads current state, not stale
|
// the disableFastMode circuit breaker reads current state, not stale
|
||||||
// settings.fastMode (which is intentionally sticky across /model auto-
|
// settings.fastMode (which is intentionally sticky across /model auto-
|
||||||
// downgrades). Optional for callers without AppState (e.g. SDK init paths).
|
// downgrades). Optional for callers without AppState (e.g. SDK init paths).
|
||||||
fastMode?: boolean,
|
fastMode?: boolean,
|
||||||
): Promise<AutoModeGateCheckResult> {
|
): Promise<AutoModeGateCheckResult> {
|
||||||
// Auto-mode config — runs in ALL builds (circuit breaker, carousel, kick-out)
|
// Only fast-mode circuit breaker remains. All other gates (GrowthBook,
|
||||||
// Fresh read of tengu_auto_mode_config.enabled — this async check runs once
|
// settings, model support, opt-in) have been removed.
|
||||||
// after GrowthBook initialization and is the authoritative source for
|
|
||||||
// isAutoModeAvailable. The sync startup path uses stale cache; this
|
|
||||||
// corrects it. Circuit breaker (enabled==='disabled') takes effect here.
|
|
||||||
const autoModeConfig = await getDynamicConfig_BLOCKS_ON_INIT<{
|
const autoModeConfig = await getDynamicConfig_BLOCKS_ON_INIT<{
|
||||||
enabled?: AutoModeEnabledState
|
enabled?: AutoModeEnabledState
|
||||||
disableFastMode?: boolean
|
disableFastMode?: boolean
|
||||||
}>('tengu_auto_mode_config', {})
|
}>('tengu_auto_mode_config', {})
|
||||||
const enabledState = parseAutoModeEnabledState(autoModeConfig?.enabled)
|
|
||||||
const disabledBySettings = isAutoModeDisabledBySettings()
|
|
||||||
// Treat settings-disable the same as GrowthBook 'disabled' for circuit-breaker
|
|
||||||
// semantics — blocks SDK/explicit re-entry via isAutoModeGateEnabled().
|
|
||||||
autoModeStateModule?.setAutoModeCircuitBroken(
|
|
||||||
enabledState === 'disabled' || disabledBySettings,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Carousel availability: not circuit-broken, not disabled-by-settings,
|
|
||||||
// model supports it, disableFastMode breaker not firing, and (enabled or opted-in)
|
|
||||||
const mainModel = getMainLoopModel()
|
const mainModel = getMainLoopModel()
|
||||||
// Temp circuit breaker: tengu_auto_mode_config.disableFastMode blocks auto
|
|
||||||
// mode when fast mode is on. Checks runtime AppState.fastMode (if provided)
|
|
||||||
// and, for ants, model name '-fast' substring (ant-internal fast models
|
|
||||||
// like capybara-v2-fast[1m] encode speed in the model ID itself).
|
|
||||||
// Remove once auto+fast mode interaction is validated.
|
|
||||||
const disableFastModeBreakerFires =
|
const disableFastModeBreakerFires =
|
||||||
!!autoModeConfig?.disableFastMode &&
|
!!autoModeConfig?.disableFastMode &&
|
||||||
(!!fastMode ||
|
(!!fastMode ||
|
||||||
(process.env.USER_TYPE === 'ant' &&
|
(process.env.USER_TYPE === 'ant' &&
|
||||||
mainModel.toLowerCase().includes('-fast')))
|
mainModel.toLowerCase().includes('-fast')))
|
||||||
const modelSupported =
|
|
||||||
modelSupportsAutoMode(mainModel) && !disableFastModeBreakerFires
|
// If fast-mode breaker fires, circuit-break auto mode
|
||||||
let carouselAvailable = false
|
autoModeStateModule?.setAutoModeCircuitBroken(disableFastModeBreakerFires)
|
||||||
if (enabledState !== 'disabled' && !disabledBySettings && modelSupported) {
|
|
||||||
carouselAvailable =
|
|
||||||
enabledState === 'enabled' || hasAutoModeOptInAnySource()
|
|
||||||
}
|
|
||||||
// canEnterAuto gates explicit entry (--permission-mode auto, defaultMode: auto)
|
|
||||||
// — explicit entry IS an opt-in, so we only block on circuit breaker + settings + model
|
|
||||||
const canEnterAuto =
|
|
||||||
enabledState !== 'disabled' && !disabledBySettings && modelSupported
|
|
||||||
logForDebugging(
|
logForDebugging(
|
||||||
`[auto-mode] verifyAutoModeGateAccess: enabledState=${enabledState} disabledBySettings=${disabledBySettings} model=${mainModel} modelSupported=${modelSupported} disableFastModeBreakerFires=${disableFastModeBreakerFires} carouselAvailable=${carouselAvailable} canEnterAuto=${canEnterAuto}`,
|
`[auto-mode] verifyAutoModeGateAccess: disableFastModeBreakerFires=${disableFastModeBreakerFires}`,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Capture CLI-flag intent now (doesn't depend on context).
|
if (!disableFastModeBreakerFires) {
|
||||||
const autoModeFlagCli = autoModeStateModule?.getAutoModeFlagCli() ?? false
|
// Auto mode available — no kick-out needed
|
||||||
|
return { updateContext: ctx => ctx }
|
||||||
// Return a transform function that re-evaluates context-dependent conditions
|
|
||||||
// against the CURRENT context at setAppState time. The async GrowthBook
|
|
||||||
// results above (canEnterAuto, carouselAvailable, enabledState, reason) are
|
|
||||||
// closure-captured — those don't depend on context. But mode, prePlanMode,
|
|
||||||
// and isAutoModeAvailable checks MUST use the fresh ctx or a mid-await
|
|
||||||
// shift-tab gets reverted (or worse, the user stays in auto despite the
|
|
||||||
// circuit breaker if they entered auto DURING the await — which is possible
|
|
||||||
// because setAutoModeCircuitBroken above runs AFTER the await).
|
|
||||||
const setAvailable = (
|
|
||||||
ctx: ToolPermissionContext,
|
|
||||||
available: boolean,
|
|
||||||
): ToolPermissionContext => {
|
|
||||||
if (ctx.isAutoModeAvailable !== available) {
|
|
||||||
logForDebugging(
|
|
||||||
`[auto-mode] verifyAutoModeGateAccess setAvailable: ${ctx.isAutoModeAvailable} -> ${available}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return ctx.isAutoModeAvailable === available
|
|
||||||
? ctx
|
|
||||||
: { ...ctx, isAutoModeAvailable: available }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canEnterAuto) {
|
// Fast-mode breaker fired — kick out of auto if currently in it
|
||||||
return { updateContext: ctx => setAvailable(ctx, carouselAvailable) }
|
const notification = getAutoModeUnavailableNotification('circuit-breaker')
|
||||||
}
|
|
||||||
|
|
||||||
// Gate is off or circuit-broken — determine reason (context-independent).
|
|
||||||
let reason: AutoModeUnavailableReason
|
|
||||||
if (disabledBySettings) {
|
|
||||||
reason = 'settings'
|
|
||||||
logForDebugging('auto mode disabled: disableAutoMode in settings', {
|
|
||||||
level: 'warn',
|
|
||||||
})
|
|
||||||
} else if (enabledState === 'disabled') {
|
|
||||||
reason = 'circuit-breaker'
|
|
||||||
logForDebugging(
|
|
||||||
'auto mode disabled: tengu_auto_mode_config.enabled === "disabled" (circuit breaker)',
|
|
||||||
{ level: 'warn' },
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
reason = 'model'
|
|
||||||
logForDebugging(
|
|
||||||
`auto mode disabled: model ${getMainLoopModel()} does not support auto mode`,
|
|
||||||
{ level: 'warn' },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const notification = getAutoModeUnavailableNotification(reason)
|
|
||||||
|
|
||||||
// Unified kick-out transform. Re-checks the FRESH ctx and only fires
|
|
||||||
// side effects (setAutoModeActive(false), setNeedsAutoModeExitAttachment)
|
|
||||||
// when the kick-out actually applies. This keeps autoModeActive in sync
|
|
||||||
// with toolPermissionContext.mode even if the user changed modes during
|
|
||||||
// the await: if they already left auto on their own, handleCycleMode
|
|
||||||
// already deactivated the classifier and we don't fire again; if they
|
|
||||||
// ENTERED auto during the await (possible before setAutoModeCircuitBroken
|
|
||||||
// landed), we kick them out here.
|
|
||||||
const kickOutOfAutoIfNeeded = (
|
const kickOutOfAutoIfNeeded = (
|
||||||
ctx: ToolPermissionContext,
|
ctx: ToolPermissionContext,
|
||||||
): ToolPermissionContext => {
|
): ToolPermissionContext => {
|
||||||
const inAuto = ctx.mode === 'auto'
|
const inAuto = ctx.mode === 'auto'
|
||||||
logForDebugging(
|
logForDebugging(
|
||||||
`[auto-mode] kickOutOfAutoIfNeeded applying: ctx.mode=${ctx.mode} ctx.prePlanMode=${ctx.prePlanMode} reason=${reason}`,
|
`[auto-mode] kickOutOfAutoIfNeeded (fast-mode): ctx.mode=${ctx.mode}`,
|
||||||
)
|
)
|
||||||
// Plan mode with auto active: either from prePlanMode='auto' (entered
|
|
||||||
// from auto) or from opt-in (strippedDangerousRules present).
|
|
||||||
const inPlanWithAutoActive =
|
const inPlanWithAutoActive =
|
||||||
ctx.mode === 'plan' &&
|
ctx.mode === 'plan' &&
|
||||||
(ctx.prePlanMode === 'auto' || !!ctx.strippedDangerousRules)
|
(ctx.prePlanMode === 'auto' || !!ctx.strippedDangerousRules)
|
||||||
if (!inAuto && !inPlanWithAutoActive) {
|
if (!inAuto && !inPlanWithAutoActive) {
|
||||||
return setAvailable(ctx, false)
|
return { ...ctx, isAutoModeAvailable: false }
|
||||||
}
|
}
|
||||||
if (inAuto) {
|
if (inAuto) {
|
||||||
autoModeStateModule?.setAutoModeActive(false)
|
autoModeStateModule?.setAutoModeActive(false)
|
||||||
@@ -1214,8 +1122,6 @@ export async function verifyAutoModeGateAccess(
|
|||||||
isAutoModeAvailable: false,
|
isAutoModeAvailable: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Plan with auto active: deactivate auto, restore permissions, defuse
|
|
||||||
// prePlanMode so ExitPlanMode goes to default.
|
|
||||||
autoModeStateModule?.setAutoModeActive(false)
|
autoModeStateModule?.setAutoModeActive(false)
|
||||||
setNeedsAutoModeExitAttachment(true)
|
setNeedsAutoModeExitAttachment(true)
|
||||||
return {
|
return {
|
||||||
@@ -1225,65 +1131,23 @@ export async function verifyAutoModeGateAccess(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notification decisions use the stale context — that's OK: we're deciding
|
return { updateContext: kickOutOfAutoIfNeeded, notification }
|
||||||
// WHETHER to notify based on what the user WAS doing when this check started.
|
|
||||||
// (Side effects and mode mutation are decided inside the transform above,
|
|
||||||
// against the fresh ctx.)
|
|
||||||
const wasInAuto = currentContext.mode === 'auto'
|
|
||||||
// Auto was used during plan: entered from auto or opt-in auto active
|
|
||||||
const autoActiveDuringPlan =
|
|
||||||
currentContext.mode === 'plan' &&
|
|
||||||
(currentContext.prePlanMode === 'auto' ||
|
|
||||||
!!currentContext.strippedDangerousRules)
|
|
||||||
const wantedAuto = wasInAuto || autoActiveDuringPlan || autoModeFlagCli
|
|
||||||
|
|
||||||
if (!wantedAuto) {
|
|
||||||
// User didn't want auto at call time — no notification. But still apply
|
|
||||||
// the full kick-out transform: if they shift-tabbed INTO auto during the
|
|
||||||
// await (before setAutoModeCircuitBroken landed), we need to evict them.
|
|
||||||
return { updateContext: kickOutOfAutoIfNeeded }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wasInAuto || autoActiveDuringPlan) {
|
|
||||||
// User was in auto or had auto active during plan — kick out + notify.
|
|
||||||
return { updateContext: kickOutOfAutoIfNeeded, notification }
|
|
||||||
}
|
|
||||||
|
|
||||||
// autoModeFlagCli only: defaultMode was auto but sync check rejected it.
|
|
||||||
// Suppress notification if isAutoModeAvailable is already false (already
|
|
||||||
// notified on a prior check; prevents repeat notifications on successive
|
|
||||||
// unsupported-model switches).
|
|
||||||
return {
|
|
||||||
updateContext: kickOutOfAutoIfNeeded,
|
|
||||||
notification: currentContext.isAutoModeAvailable ? notification : undefined,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Core logic to check if bypassPermissions should be disabled based on Statsig gate
|
* Bypass permissions is always available — no remote gate check needed.
|
||||||
*/
|
*/
|
||||||
export function shouldDisableBypassPermissions(): Promise<boolean> {
|
export function shouldDisableBypassPermissions(): Promise<boolean> {
|
||||||
return checkSecurityRestrictionGate('tengu_disable_bypass_permissions_mode')
|
return Promise.resolve(false)
|
||||||
}
|
|
||||||
|
|
||||||
function isAutoModeDisabledBySettings(): boolean {
|
|
||||||
const settings = getSettings_DEPRECATED() || {}
|
|
||||||
return (
|
|
||||||
(settings as { disableAutoMode?: 'disable' }).disableAutoMode ===
|
|
||||||
'disable' ||
|
|
||||||
(settings.permissions as { disableAutoMode?: 'disable' } | undefined)
|
|
||||||
?.disableAutoMode === 'disable'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if auto mode can be entered: circuit breaker is not active and settings
|
* Checks if auto mode can be entered: only fast-mode circuit breaker remains.
|
||||||
* have not disabled it. Synchronous.
|
* Synchronous.
|
||||||
*/
|
*/
|
||||||
export function isAutoModeGateEnabled(): boolean {
|
export function isAutoModeGateEnabled(): boolean {
|
||||||
|
// Auto mode is available to all users — only fast-mode circuit breaker remains
|
||||||
if (autoModeStateModule?.isAutoModeCircuitBroken() ?? false) return false
|
if (autoModeStateModule?.isAutoModeCircuitBroken() ?? false) return false
|
||||||
if (isAutoModeDisabledBySettings()) return false
|
|
||||||
if (!modelSupportsAutoMode(getMainLoopModel())) return false
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1292,11 +1156,9 @@ export function isAutoModeGateEnabled(): boolean {
|
|||||||
* Synchronous — uses state populated by verifyAutoModeGateAccess.
|
* Synchronous — uses state populated by verifyAutoModeGateAccess.
|
||||||
*/
|
*/
|
||||||
export function getAutoModeUnavailableReason(): AutoModeUnavailableReason | null {
|
export function getAutoModeUnavailableReason(): AutoModeUnavailableReason | null {
|
||||||
if (isAutoModeDisabledBySettings()) return 'settings'
|
|
||||||
if (autoModeStateModule?.isAutoModeCircuitBroken() ?? false) {
|
if (autoModeStateModule?.isAutoModeCircuitBroken() ?? false) {
|
||||||
return 'circuit-breaker'
|
return 'circuit-breaker'
|
||||||
}
|
}
|
||||||
if (!modelSupportsAutoMode(getMainLoopModel())) return 'model'
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1310,8 +1172,7 @@ export function getAutoModeUnavailableReason(): AutoModeUnavailableReason | null
|
|||||||
*/
|
*/
|
||||||
export type AutoModeEnabledState = 'enabled' | 'disabled' | 'opt-in'
|
export type AutoModeEnabledState = 'enabled' | 'disabled' | 'opt-in'
|
||||||
|
|
||||||
const AUTO_MODE_ENABLED_DEFAULT: AutoModeEnabledState =
|
const AUTO_MODE_ENABLED_DEFAULT: AutoModeEnabledState = 'enabled'
|
||||||
feature('TRANSCRIPT_CLASSIFIER') ? 'enabled' : 'disabled'
|
|
||||||
|
|
||||||
function parseAutoModeEnabledState(value: unknown): AutoModeEnabledState {
|
function parseAutoModeEnabledState(value: unknown): AutoModeEnabledState {
|
||||||
if (value === 'enabled' || value === 'disabled' || value === 'opt-in') {
|
if (value === 'enabled' || value === 'disabled' || value === 'opt-in') {
|
||||||
@@ -1361,27 +1222,15 @@ export function getAutoModeEnabledStateIfCached():
|
|||||||
* dialog or by IDE/Desktop settings toggle)
|
* dialog or by IDE/Desktop settings toggle)
|
||||||
*/
|
*/
|
||||||
export function hasAutoModeOptInAnySource(): boolean {
|
export function hasAutoModeOptInAnySource(): boolean {
|
||||||
if (autoModeStateModule?.getAutoModeFlagCli() ?? false) return true
|
return true
|
||||||
return hasAutoModeOptIn()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if bypassPermissions mode is currently disabled by Statsig gate or settings.
|
* Checks if bypassPermissions mode is currently disabled by Statsig gate or settings.
|
||||||
* This is a synchronous version that uses cached Statsig values.
|
* Always returns false — bypass is available to all users.
|
||||||
*/
|
*/
|
||||||
export function isBypassPermissionsModeDisabled(): boolean {
|
export function isBypassPermissionsModeDisabled(): boolean {
|
||||||
const growthBookDisableBypassPermissionsMode =
|
return false
|
||||||
checkStatsigFeatureGate_CACHED_MAY_BE_STALE(
|
|
||||||
'tengu_disable_bypass_permissions_mode',
|
|
||||||
)
|
|
||||||
const settings = getSettings_DEPRECATED() || {}
|
|
||||||
const settingsDisableBypassPermissionsMode =
|
|
||||||
settings.permissions?.disableBypassPermissionsMode === 'disable'
|
|
||||||
|
|
||||||
return (
|
|
||||||
growthBookDisableBypassPermissionsMode ||
|
|
||||||
settingsDisableBypassPermissionsMode
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1406,29 +1255,12 @@ export function createDisabledBypassPermissionsContext(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asynchronously checks if the bypassPermissions mode should be disabled based on Statsig gate
|
* No-op — bypass permissions is always available, no remote gate check needed.
|
||||||
* and returns an updated toolPermissionContext if needed
|
|
||||||
*/
|
*/
|
||||||
export async function checkAndDisableBypassPermissions(
|
export async function checkAndDisableBypassPermissions(
|
||||||
currentContext: ToolPermissionContext,
|
_currentContext: ToolPermissionContext,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// Only proceed if bypassPermissions mode is available
|
// Bypass permissions is always available — no gate check needed
|
||||||
if (!currentContext.isBypassPermissionsModeAvailable) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const shouldDisable = await shouldDisableBypassPermissions()
|
|
||||||
if (!shouldDisable) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gate is enabled, need to disable bypassPermissions mode
|
|
||||||
logForDebugging(
|
|
||||||
'bypassPermissions mode is being disabled by Statsig gate (async check)',
|
|
||||||
{ level: 'warn' },
|
|
||||||
)
|
|
||||||
|
|
||||||
void gracefulShutdown(1, 'bypass_permissions_disabled')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isDefaultPermissionModeAuto(): boolean {
|
export function isDefaultPermissionModeAuto(): boolean {
|
||||||
@@ -1446,11 +1278,7 @@ export function isDefaultPermissionModeAuto(): boolean {
|
|||||||
*/
|
*/
|
||||||
export function shouldPlanUseAutoMode(): boolean {
|
export function shouldPlanUseAutoMode(): boolean {
|
||||||
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
||||||
return (
|
return isAutoModeGateEnabled() && getUseAutoModeDuringPlan()
|
||||||
hasAutoModeOptIn() &&
|
|
||||||
isAutoModeGateEnabled() &&
|
|
||||||
getUseAutoModeDuringPlan()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user