From d04ef7e701a6260506c2be4745c2911f9eda3222 Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Fri, 3 Apr 2026 14:34:07 +0800 Subject: [PATCH] Strip bridge metadata and trusted-device egress --- src/bridge/bridgeApi.ts | 6 +- src/bridge/bridgeMain.ts | 34 ++------- src/bridge/createSession.ts | 69 +---------------- src/bridge/initReplBridge.ts | 9 --- src/bridge/replBridge.ts | 21 +----- src/bridge/trustedDevice.ts | 142 +++-------------------------------- src/bridge/types.ts | 3 - 7 files changed, 20 insertions(+), 264 deletions(-) diff --git a/src/bridge/bridgeApi.ts b/src/bridge/bridgeApi.ts index 052bd4f..c5f6c2a 100644 --- a/src/bridge/bridgeApi.ts +++ b/src/bridge/bridgeApi.ts @@ -154,10 +154,6 @@ export function createBridgeApiClient(deps: BridgeApiDeps): BridgeApiClient { }>( `${deps.baseUrl}/v1/environments/bridge`, { - machine_name: config.machineName, - directory: config.dir, - branch: config.branch, - git_repo_url: config.gitRepoUrl, // Advertise session capacity so claude.ai/code can show // "2/4 sessions" badges and only block the picker when // actually at capacity. Backends that don't yet accept @@ -190,7 +186,7 @@ export function createBridgeApiClient(deps: BridgeApiDeps): BridgeApiClient { `[bridge:api] POST /v1/environments/bridge -> ${response.status} environment_id=${response.data.environment_id}`, ) debug( - `[bridge:api] >>> ${debugBody({ machine_name: config.machineName, directory: config.dir, branch: config.branch, git_repo_url: config.gitRepoUrl, max_sessions: config.maxSessions, metadata: { worker_type: config.workerType } })}`, + `[bridge:api] >>> ${debugBody({ max_sessions: config.maxSessions, metadata: { worker_type: config.workerType } })}`, ) debug(`[bridge:api] <<< ${debugBody(response.data)}`) return response.data diff --git a/src/bridge/bridgeMain.ts b/src/bridge/bridgeMain.ts index 7aeacaf..4423713 100644 --- a/src/bridge/bridgeMain.ts +++ b/src/bridge/bridgeMain.ts @@ -1,6 +1,6 @@ import { feature } from 'bun:bundle' import { randomUUID } from 'crypto' -import { hostname, tmpdir } from 'os' +import { tmpdir } from 'os' import { basename, join, resolve } from 'path' import { getRemoteSessionUrl } from '../constants/product.js' import { shutdownDatadog } from '../services/analytics/datadog.js' @@ -2203,9 +2203,7 @@ export async function bridgeMain(args: string[]): Promise { ? process.env.CLAUDE_BRIDGE_SESSION_INGRESS_URL : baseUrl - const { getBranch, getRemoteUrl, findGitRoot } = await import( - '../utils/git.js' - ) + const { findGitRoot } = await import('../utils/git.js') // Precheck worktree availability for the first-run dialog and the `w` // toggle. Unconditional so we know upfront whether worktree is an option. @@ -2337,9 +2335,6 @@ export async function bridgeMain(args: string[]): Promise { process.exit(1) } - const branch = await getBranch() - const gitRepoUrl = await getRemoteUrl() - const machineName = hostname() const bridgeId = randomUUID() const { handleOAuth401Error } = await import('../utils/auth.js') @@ -2417,9 +2412,6 @@ export async function bridgeMain(args: string[]): Promise { const config: BridgeConfig = { dir, - machineName, - branch, - gitRepoUrl, maxSessions, spawnMode, verbose, @@ -2435,7 +2427,7 @@ export async function bridgeMain(args: string[]): Promise { } logForDebugging( - `[bridge:init] bridgeId=${bridgeId}${reuseEnvironmentId ? ` reuseEnvironmentId=${reuseEnvironmentId}` : ''} dir=${dir} branch=${branch} gitRepoUrl=${gitRepoUrl} machine=${machineName}`, + `[bridge:init] bridgeId=${bridgeId}${reuseEnvironmentId ? ` reuseEnvironmentId=${reuseEnvironmentId}` : ''} dir=${dir}`, ) logForDebugging( `[bridge:init] apiBaseUrl=${baseUrl} sessionIngressUrl=${sessionIngressUrl}`, @@ -2591,11 +2583,7 @@ export async function bridgeMain(args: string[]): Promise { }) const logger = createBridgeLogger({ verbose }) - const { parseGitHubRepository } = await import('../utils/detectRepository.js') - const ownerRepo = gitRepoUrl ? parseGitHubRepository(gitRepoUrl) : null - // Use the repo name from the parsed owner/repo, or fall back to the dir basename - const repoName = ownerRepo ? ownerRepo.split('/').pop()! : basename(dir) - logger.setRepoInfo(repoName, branch) + logger.setRepoInfo(basename(dir), '') // `w` toggle is available iff we're in a multi-session mode AND worktree // is a valid option. When unavailable, the mode suffix and hint are hidden. @@ -2678,8 +2666,6 @@ export async function bridgeMain(args: string[]): Promise { environmentId, title: name, events: [], - gitRepoUrl, - branch, signal: controller.signal, baseUrl, getAccessToken: getBridgeAccessToken, @@ -2856,9 +2842,7 @@ export async function runBridgeHeadless( ? process.env.CLAUDE_BRIDGE_SESSION_INGRESS_URL : baseUrl - const { getBranch, getRemoteUrl, findGitRoot } = await import( - '../utils/git.js' - ) + const { findGitRoot } = await import('../utils/git.js') const { hasWorktreeCreateHook } = await import('../utils/hooks.js') if (opts.spawnMode === 'worktree') { @@ -2871,16 +2855,10 @@ export async function runBridgeHeadless( } } - const branch = await getBranch() - const gitRepoUrl = await getRemoteUrl() - const machineName = hostname() const bridgeId = randomUUID() const config: BridgeConfig = { dir, - machineName, - branch, - gitRepoUrl, maxSessions: opts.capacity, spawnMode: opts.spawnMode, verbose: false, @@ -2934,8 +2912,6 @@ export async function runBridgeHeadless( environmentId, title: opts.name, events: [], - gitRepoUrl, - branch, signal, baseUrl, getAccessToken: opts.getAccessToken, diff --git a/src/bridge/createSession.ts b/src/bridge/createSession.ts index ae17f7b..6011208 100644 --- a/src/bridge/createSession.ts +++ b/src/bridge/createSession.ts @@ -4,17 +4,6 @@ import { errorMessage } from '../utils/errors.js' import { extractErrorDetail } from './debugUtils.js' import { toCompatSessionId } from './sessionIdCompat.js' -type GitSource = { - type: 'git_repository' - url: string - revision?: string -} - -type GitOutcome = { - type: 'git_repository' - git_info: { type: 'github'; repo: string; branches: string[] } -} - // Events must be wrapped in { type: 'event', data: } for the // POST /v1/sessions endpoint (discriminated union format). type SessionEvent = { @@ -35,8 +24,6 @@ export async function createBridgeSession({ environmentId, title, events, - gitRepoUrl, - branch, signal, baseUrl: baseUrlOverride, getAccessToken, @@ -45,8 +32,6 @@ export async function createBridgeSession({ environmentId: string title?: string events: SessionEvent[] - gitRepoUrl: string | null - branch: string signal: AbortSignal baseUrl?: string getAccessToken?: () => string | undefined @@ -56,8 +41,6 @@ export async function createBridgeSession({ const { getOrganizationUUID } = await import('../services/oauth/client.js') const { getOauthConfig } = await import('../constants/oauth.js') const { getOAuthHeaders } = await import('../utils/teleport/api.js') - const { parseGitHubRepository } = await import('../utils/detectRepository.js') - const { getDefaultBranch } = await import('../utils/git.js') const { getMainLoopModel } = await import('../utils/model/model.js') const { default: axios } = await import('axios') @@ -74,60 +57,12 @@ export async function createBridgeSession({ return null } - // Build git source and outcome context - let gitSource: GitSource | null = null - let gitOutcome: GitOutcome | null = null - - if (gitRepoUrl) { - const { parseGitRemote } = await import('../utils/detectRepository.js') - const parsed = parseGitRemote(gitRepoUrl) - if (parsed) { - const { host, owner, name } = parsed - const revision = branch || (await getDefaultBranch()) || undefined - gitSource = { - type: 'git_repository', - url: `https://${host}/${owner}/${name}`, - revision, - } - gitOutcome = { - type: 'git_repository', - git_info: { - type: 'github', - repo: `${owner}/${name}`, - branches: [`claude/${branch || 'task'}`], - }, - } - } else { - // Fallback: try parseGitHubRepository for owner/repo format - const ownerRepo = parseGitHubRepository(gitRepoUrl) - if (ownerRepo) { - const [owner, name] = ownerRepo.split('/') - if (owner && name) { - const revision = branch || (await getDefaultBranch()) || undefined - gitSource = { - type: 'git_repository', - url: `https://github.com/${owner}/${name}`, - revision, - } - gitOutcome = { - type: 'git_repository', - git_info: { - type: 'github', - repo: `${owner}/${name}`, - branches: [`claude/${branch || 'task'}`], - }, - } - } - } - } - } - const requestBody = { ...(title !== undefined && { title }), events, session_context: { - sources: gitSource ? [gitSource] : [], - outcomes: gitOutcome ? [gitOutcome] : [], + sources: [], + outcomes: [], model: getMainLoopModel(), }, environment_id: environmentId, diff --git a/src/bridge/initReplBridge.ts b/src/bridge/initReplBridge.ts index 9bae291..8d8f358 100644 --- a/src/bridge/initReplBridge.ts +++ b/src/bridge/initReplBridge.ts @@ -14,7 +14,6 @@ */ import { feature } from 'bun:bundle' -import { hostname } from 'os' import { getOriginalCwd, getSessionId } from '../bootstrap/state.js' import type { SDKMessage } from '../entrypoints/agentSdkTypes.ts' import type { SDKControlResponse } from '../entrypoints/sdk/controlTypes.ts' @@ -34,7 +33,6 @@ import { getGlobalConfig, saveGlobalConfig } from '../utils/config.js' import { logForDebugging } from '../utils/debug.js' import { stripDisplayTagsAllowEmpty } from '../utils/displayTags.js' import { errorMessage } from '../utils/errors.js' -import { getBranch, getRemoteUrl } from '../utils/git.js' import { toSDKMessages } from '../utils/messages/mappers.js' import { getContentText, @@ -460,10 +458,6 @@ export async function initReplBridge( return null } - // Gather git context — this is the bootstrap-read boundary. - // Everything from here down is passed explicitly to bridgeCore. - const branch = await getBranch() - const gitRepoUrl = await getRemoteUrl() const sessionIngressUrl = process.env.USER_TYPE === 'ant' && process.env.CLAUDE_BRIDGE_SESSION_INGRESS_URL @@ -489,9 +483,6 @@ export async function initReplBridge( // so no adapter needed — just the narrower type on the way out. return initBridgeCore({ dir: getOriginalCwd(), - machineName: hostname(), - branch, - gitRepoUrl, title, baseUrl, sessionIngressUrl, diff --git a/src/bridge/replBridge.ts b/src/bridge/replBridge.ts index d3fd959..0674ef7 100644 --- a/src/bridge/replBridge.ts +++ b/src/bridge/replBridge.ts @@ -84,15 +84,12 @@ export type BridgeState = 'ready' | 'connected' | 'reconnecting' | 'failed' /** * Explicit-param input to initBridgeCore. Everything initReplBridge reads - * from bootstrap state (cwd, session ID, git, OAuth) becomes a field here. + * from bootstrap state (cwd, session ID, OAuth) becomes a field here. * A daemon caller (Agent SDK, PR 4) that never runs main.tsx fills these * in itself. */ export type BridgeCoreParams = { dir: string - machineName: string - branch: string - gitRepoUrl: string | null title: string baseUrl: string sessionIngressUrl: string @@ -113,14 +110,12 @@ export type BridgeCoreParams = { * Daemon wrapper passes `createBridgeSessionLean` from `sessionApi.ts` * (HTTP-only, orgUUID+model supplied by the daemon caller). * - * Receives `gitRepoUrl`+`branch` so the REPL wrapper can build the git - * source/outcome for claude.ai's session card. Daemon ignores them. + * Receives the registered environment ID and session title. Daemon callers + * may supply their own lean session-creation implementation. */ createSession: (opts: { environmentId: string title: string - gitRepoUrl: string | null - branch: string signal: AbortSignal }) => Promise /** @@ -262,9 +257,6 @@ export async function initBridgeCore( ): Promise { const { dir, - machineName, - branch, - gitRepoUrl, title, baseUrl, sessionIngressUrl, @@ -331,9 +323,6 @@ export async function initBridgeCore( const bridgeConfig: BridgeConfig = { dir, - machineName, - branch, - gitRepoUrl, maxSessions: 1, spawnMode: 'single-session', verbose: false, @@ -457,8 +446,6 @@ export async function initBridgeCore( const createdSessionId = await createSession({ environmentId, title, - gitRepoUrl, - branch, signal: AbortSignal.timeout(15_000), }) @@ -764,8 +751,6 @@ export async function initBridgeCore( const newSessionId = await createSession({ environmentId, title: currentTitle, - gitRepoUrl, - branch, signal: AbortSignal.timeout(15_000), }) diff --git a/src/bridge/trustedDevice.ts b/src/bridge/trustedDevice.ts index a4bcf35..a74c00b 100644 --- a/src/bridge/trustedDevice.ts +++ b/src/bridge/trustedDevice.ts @@ -1,16 +1,7 @@ -import axios from 'axios' import memoize from 'lodash-es/memoize.js' -import { hostname } from 'os' -import { getOauthConfig } from '../constants/oauth.js' -import { - checkGate_CACHED_OR_BLOCKING, - getFeatureValue_CACHED_MAY_BE_STALE, -} from '../services/analytics/growthbook.js' +import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' import { logForDebugging } from '../utils/debug.js' -import { errorMessage } from '../utils/errors.js' -import { isEssentialTrafficOnly } from '../utils/privacyLevel.js' import { getSecureStorage } from '../utils/secureStorage/index.js' -import { jsonStringify } from '../utils/slowOperations.js' /** * Trusted device token source for bridge (remote-control) sessions. @@ -38,7 +29,7 @@ function isGateEnabled(): boolean { // Memoized — secureStorage.read() spawns a macOS `security` subprocess (~40ms). // bridgeApi.ts calls this from getHeaders() on every poll/heartbeat/ack. -// Cache cleared after enrollment (below) and on logout (clearAuthRelatedCaches). +// Cache cleared on logout (clearAuthRelatedCaches) and after any local update. // // Only the storage read is memoized — the GrowthBook gate is checked live so // that a gate flip after GrowthBook refresh takes effect without a restart. @@ -64,10 +55,8 @@ export function clearTrustedDeviceTokenCache(): void { /** * Clear the stored trusted device token from secure storage and the memo cache. - * Called before enrollTrustedDevice() during /login so a stale token from the - * previous account isn't sent as X-Trusted-Device-Token while enrollment is - * in-flight (enrollTrustedDevice is async — bridge API calls between login and - * enrollment completion would otherwise still read the old cached token). + * Called during /login so a stale token from the previous account isn't sent + * as X-Trusted-Device-Token after account switches. */ export function clearTrustedDeviceToken(): void { if (!isGateEnabled()) { @@ -87,124 +76,11 @@ export function clearTrustedDeviceToken(): void { } /** - * Enroll this device via POST /auth/trusted_devices and persist the token - * to keychain. Best-effort — logs and returns on failure so callers - * (post-login hooks) don't block the login flow. - * - * The server gates enrollment on account_session.created_at < 10min, so - * this must be called immediately after a fresh /login. Calling it later - * (e.g. lazy enrollment on /bridge 403) will fail with 403 stale_session. + * Trusted-device enrollment is disabled in this build. Keep the no-op entry + * point so callers can continue to invoke it without branching. */ export async function enrollTrustedDevice(): Promise { - try { - // checkGate_CACHED_OR_BLOCKING awaits any in-flight GrowthBook re-init - // (triggered by refreshGrowthBookAfterAuthChange in login.tsx) before - // reading the gate, so we get the post-refresh value. - if (!(await checkGate_CACHED_OR_BLOCKING(TRUSTED_DEVICE_GATE))) { - logForDebugging( - `[trusted-device] Gate ${TRUSTED_DEVICE_GATE} is off, skipping enrollment`, - ) - return - } - // If CLAUDE_TRUSTED_DEVICE_TOKEN is set (e.g. by an enterprise wrapper), - // skip enrollment — the env var takes precedence in readStoredToken() so - // any enrolled token would be shadowed and never used. - if (process.env.CLAUDE_TRUSTED_DEVICE_TOKEN) { - logForDebugging( - '[trusted-device] CLAUDE_TRUSTED_DEVICE_TOKEN env var is set, skipping enrollment (env var takes precedence)', - ) - return - } - // Lazy require — utils/auth.ts transitively pulls ~1300 modules - // (config → file → permissions → sessionStorage → commands). Daemon callers - // of getTrustedDeviceToken() don't need this; only /login does. - /* eslint-disable @typescript-eslint/no-require-imports */ - const { getClaudeAIOAuthTokens } = - require('../utils/auth.js') as typeof import('../utils/auth.js') - /* eslint-enable @typescript-eslint/no-require-imports */ - const accessToken = getClaudeAIOAuthTokens()?.accessToken - if (!accessToken) { - logForDebugging('[trusted-device] No OAuth token, skipping enrollment') - return - } - // Always re-enroll on /login — the existing token may belong to a - // different account (account-switch without /logout). Skipping enrollment - // would send the old account's token on the new account's bridge calls. - const secureStorage = getSecureStorage() - - if (isEssentialTrafficOnly()) { - logForDebugging( - '[trusted-device] Essential traffic only, skipping enrollment', - ) - return - } - - const baseUrl = getOauthConfig().BASE_API_URL - let response - try { - response = await axios.post<{ - device_token?: string - device_id?: string - }>( - `${baseUrl}/api/auth/trusted_devices`, - { display_name: `Claude Code on ${hostname()} · ${process.platform}` }, - { - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - }, - timeout: 10_000, - validateStatus: s => s < 500, - }, - ) - } catch (err: unknown) { - logForDebugging( - `[trusted-device] Enrollment request failed: ${errorMessage(err)}`, - ) - return - } - - if (response.status !== 200 && response.status !== 201) { - logForDebugging( - `[trusted-device] Enrollment failed ${response.status}: ${jsonStringify(response.data).slice(0, 200)}`, - ) - return - } - - const token = response.data?.device_token - if (!token || typeof token !== 'string') { - logForDebugging( - '[trusted-device] Enrollment response missing device_token field', - ) - return - } - - try { - const storageData = secureStorage.read() - if (!storageData) { - logForDebugging( - '[trusted-device] Cannot read storage, skipping token persist', - ) - return - } - storageData.trustedDeviceToken = token - const result = secureStorage.update(storageData) - if (!result.success) { - logForDebugging( - `[trusted-device] Failed to persist token: ${result.warning ?? 'unknown'}`, - ) - return - } - readStoredToken.cache?.clear?.() - logForDebugging( - `[trusted-device] Enrolled device_id=${response.data.device_id ?? 'unknown'}`, - ) - } catch (err: unknown) { - logForDebugging( - `[trusted-device] Storage write failed: ${errorMessage(err)}`, - ) - } - } catch (err: unknown) { - logForDebugging(`[trusted-device] Enrollment error: ${errorMessage(err)}`) - } + logForDebugging( + '[trusted-device] Enrollment disabled in this build; skipping trusted device registration', + ) } diff --git a/src/bridge/types.ts b/src/bridge/types.ts index 210a3bb..c67c3d6 100644 --- a/src/bridge/types.ts +++ b/src/bridge/types.ts @@ -80,9 +80,6 @@ export type BridgeWorkerType = 'claude_code' | 'claude_code_assistant' export type BridgeConfig = { dir: string - machineName: string - branch: string - gitRepoUrl: string | null maxSessions: number spawnMode: SpawnMode verbose: boolean