Strip bridge metadata and trusted-device egress
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<void> {
|
||||
? 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<void> {
|
||||
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<void> {
|
||||
|
||||
const config: BridgeConfig = {
|
||||
dir,
|
||||
machineName,
|
||||
branch,
|
||||
gitRepoUrl,
|
||||
maxSessions,
|
||||
spawnMode,
|
||||
verbose,
|
||||
@@ -2435,7 +2427,7 @@ export async function bridgeMain(args: string[]): Promise<void> {
|
||||
}
|
||||
|
||||
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<void> {
|
||||
})
|
||||
|
||||
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<void> {
|
||||
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,
|
||||
|
||||
@@ -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: <sdk_message> } 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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<string | null>
|
||||
/**
|
||||
@@ -262,9 +257,6 @@ export async function initBridgeCore(
|
||||
): Promise<BridgeCoreHandle | null> {
|
||||
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),
|
||||
})
|
||||
|
||||
|
||||
@@ -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<void> {
|
||||
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',
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user