diff --git a/src/setup.ts b/src/setup.ts index cd9c116c..df31c82a 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -6,7 +6,6 @@ import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, } from 'src/services/analytics/index.js' -import { isAntEmployee } from 'src/utils/buildConfig.js' import { getCwd } from 'src/utils/cwd.js' import { checkForReleaseNotes } from 'src/utils/releaseNotes.js' import { setCwd } from 'src/utils/Shell.js' @@ -335,19 +334,6 @@ export async function setup( // overhead. NOT an early-return: the --dangerously-skip-permissions safety // gate, tengu_started beacon, and apiKeyHelper prefetch below must still run. if (!isBareMode()) { - if (isAntEmployee()) { - // Prime repo classification cache for auto-undercover mode. Default is - // undercover ON until proven internal; if this resolves to internal, clear - // the prompt cache so the next turn picks up the OFF state. - void import('./utils/commitAttribution.js').then(async m => { - if (await m.isInternalModelRepo()) { - const { clearSystemPromptSections } = await import( - './constants/systemPromptSections.js' - ) - clearSystemPromptSections() - } - }) - } if (feature('COMMIT_ATTRIBUTION')) { // Dynamic import to enable dead code elimination (module contains excluded strings). // Defer to next tick so the git subprocess spawn runs after first render @@ -414,32 +400,6 @@ export async function setup( process.exit(1) } - if ( - isAntEmployee() && - // Skip for Desktop's local agent mode — same trust model as CCR/BYOC - // (trusted Anthropic-managed launcher intentionally pre-approving everything). - // Precedent: permissionSetup.ts:861, applySettingsChange.ts:55 (PR #19116) - process.env.CLAUDE_CODE_ENTRYPOINT !== 'local-agent' && - // Same for CCD (Claude Code in Desktop) — apps#29127 passes the flag - // unconditionally to unlock mid-session bypass switching - process.env.CLAUDE_CODE_ENTRYPOINT !== 'claude-desktop' - ) { - // Only await if permission mode is set to bypass - const [isDocker, hasInternet] = await Promise.all([ - envDynamic.getIsDocker(), - env.hasInternetAccess(), - ]) - const isBubblewrap = envDynamic.getIsBubblewrapSandbox() - const isSandbox = process.env.IS_SANDBOX === '1' - const isSandboxed = isDocker || isBubblewrap || isSandbox - if (!isSandboxed || hasInternet) { - // biome-ignore lint/suspicious/noConsole:: intentional console output - console.error( - `--dangerously-skip-permissions can only be used in Docker/sandbox containers with no internet access but got Docker: ${isDocker}, Bubblewrap: ${isBubblewrap}, IS_SANDBOX: ${isSandbox}, hasInternet: ${hasInternet}`, - ) - process.exit(1) - } - } } if (process.env.NODE_ENV === 'test') { diff --git a/src/utils/fastMode.test.ts b/src/utils/fastMode.test.ts new file mode 100644 index 00000000..e7d3130a --- /dev/null +++ b/src/utils/fastMode.test.ts @@ -0,0 +1,162 @@ +import { afterEach, describe, expect, mock, test } from 'bun:test' + +const originalEnv = { ...process.env } + +async function importFreshFastModeModule() { + return import(`./fastMode.ts?ts=${Date.now()}-${Math.random()}`) +} + +function installCommonMocks(options?: { + cachedEnabled?: boolean + apiKey?: string | null + oauthToken?: string | null + hasProfileScope?: boolean + axiosReject?: boolean +}) { + mock.module('axios', () => ({ + default: { + get: options?.axiosReject + ? async () => { + throw new Error('network fail') + } + : async () => ({ data: { enabled: false, disabled_reason: 'preference' } }), + isAxiosError: () => false, + }, + })) + + mock.module('src/constants/oauth.js', () => ({ + getOauthConfig: () => ({ BASE_API_URL: 'https://api.anthropic.com' }), + OAUTH_BETA_HEADER: 'test-beta', + })) + + mock.module('src/services/analytics/growthbook.js', () => ({ + getFeatureValue_CACHED_MAY_BE_STALE: (_name: string, defaultValue: unknown) => + defaultValue, + })) + + mock.module('../bootstrap/state.js', () => ({ + getIsNonInteractiveSession: () => false, + getKairosActive: () => false, + preferThirdPartyAuthentication: () => false, + })) + + mock.module('../services/analytics/index.js', () => ({ + logEvent: () => {}, + })) + + mock.module('./auth.js', () => ({ + getAnthropicApiKey: () => options?.apiKey ?? null, + getClaudeAIOAuthTokens: () => + options?.oauthToken ? { accessToken: options.oauthToken } : null, + handleOAuth401Error: async () => {}, + hasProfileScope: () => options?.hasProfileScope ?? false, + })) + + mock.module('./bundledMode.js', () => ({ + isInBundledMode: () => true, + })) + + mock.module('./config.js', () => ({ + getGlobalConfig: () => ({ + penguinModeOrgEnabled: options?.cachedEnabled === true, + }), + saveGlobalConfig: (updater: (current: Record) => Record) => + updater({ penguinModeOrgEnabled: options?.cachedEnabled === true }), + })) + + mock.module('./debug.js', () => ({ + logForDebugging: () => {}, + })) + + mock.module('./envUtils.js', () => ({ + isEnvTruthy: (value: string | undefined) => + !!value && value !== '0' && value.toLowerCase() !== 'false', + })) + + mock.module('./model/model.js', () => ({ + getDefaultMainLoopModelSetting: () => 'claude-sonnet-4-6', + isOpus1mMergeEnabled: () => false, + parseUserSpecifiedModel: (model: string) => model, + })) + + mock.module('./model/providers.js', () => ({ + getAPIProvider: () => 'firstParty', + })) + + mock.module('./privacyLevel.js', () => ({ + isEssentialTrafficOnly: () => false, + })) + + mock.module('./settings/settings.js', () => ({ + getInitialSettings: () => ({ fastMode: true }), + getSettingsForSource: () => ({}), + updateSettingsForSource: () => {}, + })) + + mock.module('./signal.js', () => ({ + createSignal: () => { + const subscribe = () => () => {} + const emit = () => {} + return { subscribe, emit } + }, + })) +} + +afterEach(() => { + mock.restore() + process.env = { ...originalEnv } +}) + +describe('fastMode ant-only fallback cleanup', () => { + test('resolveFastModeStatusFromCache does not force-enable from USER_TYPE=ant', async () => { + process.env.USER_TYPE = 'ant' + installCommonMocks({ cachedEnabled: false }) + + const { + resolveFastModeStatusFromCache, + getFastModeUnavailableReason, + } = await importFreshFastModeModule() + + resolveFastModeStatusFromCache() + + expect(getFastModeUnavailableReason()).toBe( + 'Fast mode is currently unavailable', + ) + }) + + test('prefetchFastModeStatus without auth does not force-enable from USER_TYPE=ant', async () => { + process.env.USER_TYPE = 'ant' + installCommonMocks({ cachedEnabled: false, apiKey: null, oauthToken: null }) + + const { + prefetchFastModeStatus, + getFastModeUnavailableReason, + } = await importFreshFastModeModule() + + await prefetchFastModeStatus() + + expect(getFastModeUnavailableReason()).toBe( + 'Fast mode has been disabled by your organization', + ) + }) + + test('prefetchFastModeStatus network failure does not force-enable from USER_TYPE=ant', async () => { + process.env.USER_TYPE = 'ant' + installCommonMocks({ + cachedEnabled: false, + apiKey: 'test-key', + axiosReject: true, + }) + + const { + prefetchFastModeStatus, + getFastModeUnavailableReason, + } = await importFreshFastModeModule() + + await prefetchFastModeStatus() + + expect(getFastModeUnavailableReason()).toBe( + 'Fast mode unavailable due to network connectivity issues', + ) + }) +}) diff --git a/src/utils/fastMode.ts b/src/utils/fastMode.ts index a0b2d1a7..33bba143 100644 --- a/src/utils/fastMode.ts +++ b/src/utils/fastMode.ts @@ -396,10 +396,9 @@ export function resolveFastModeStatusFromCache(): void { if (orgStatus.status !== 'pending') { return } - const isAnt = process.env.USER_TYPE === 'ant' const cachedEnabled = getGlobalConfig().penguinModeOrgEnabled === true orgStatus = - isAnt || cachedEnabled + cachedEnabled ? { status: 'enabled' } : { status: 'disabled', reason: 'unknown' } } @@ -428,10 +427,9 @@ export async function prefetchFastModeStatus(): Promise { const hasUsableOAuth = getClaudeAIOAuthTokens()?.accessToken && hasProfileScope() if (!hasUsableOAuth && !apiKey) { - const isAnt = process.env.USER_TYPE === 'ant' const cachedEnabled = getGlobalConfig().penguinModeOrgEnabled === true orgStatus = - isAnt || cachedEnabled + cachedEnabled ? { status: 'enabled' } : { status: 'disabled', reason: 'preference' } return @@ -511,10 +509,9 @@ export async function prefetchFastModeStatus(): Promise { // On failure: ants default to enabled (don't block internal users). // External users: fall back to the cached penguinModeOrgEnabled value; // if no positive cache, disable with network_error reason. - const isAnt = process.env.USER_TYPE === 'ant' const cachedEnabled = getGlobalConfig().penguinModeOrgEnabled === true orgStatus = - isAnt || cachedEnabled + cachedEnabled ? { status: 'enabled' } : { status: 'disabled', reason: 'network_error' } logForDebugging(