File size: 7,173 Bytes
064bfd6 9086e0a 064bfd6 9086e0a 96f34e3 9086e0a 064bfd6 9086e0a 064bfd6 6bb6a3c 064bfd6 6bb6a3c 064bfd6 9086e0a 064bfd6 9086e0a 064bfd6 9086e0a 064bfd6 9086e0a 064bfd6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | /**
* Shared utilities for spawning teammates across different backends.
*/
import {
getChromeFlagOverride,
getFlagSettingsPath,
getInlinePlugins,
getMainLoopModelOverride,
getSessionBypassPermissionsMode,
} from '../../bootstrap/state.js'
import { readlinkSync } from 'fs'
import { quote } from '../bash/shellQuote.js'
import { isInBundledMode } from '../bundledMode.js'
import type { PermissionMode } from '../permissions/PermissionMode.js'
import { getTeammateModeFromSnapshot } from './backends/teammateModeSnapshot.js'
import { TEAMMATE_COMMAND_ENV_VAR } from './constants.js'
/**
* Resolves the real executable path for the current process.
*
* In bun-compiled mode, process.execPath may return a bunfs virtual path
* (e.g., /$bunfs/root/Codev) that only exists within the bun runtime.
* When a tmux pane tries to execute this path in a regular shell, it fails
* with "No such file or directory".
*
* Fix: use /proc/self/exe on Linux to resolve the real binary path.
* On unsupported platforms, falls back to process.execPath.
*/
function resolveRealExecPath(): string {
try {
if (process.platform === 'linux') {
return readlinkSync('/proc/self/exe')
}
} catch {
// Not on Linux or /proc not available — fall through
}
return process.execPath
}
/**
* Gets the command to use for spawning teammate processes.
* Uses TEAMMATE_COMMAND_ENV_VAR if set, otherwise falls back to the
* current process executable path.
*/
export function getTeammateCommand(): string {
if (process.env[TEAMMATE_COMMAND_ENV_VAR]) {
return process.env[TEAMMATE_COMMAND_ENV_VAR]
}
return isInBundledMode() ? resolveRealExecPath() : process.argv[1]!
}
/**
* Builds CLI flags to propagate from the current session to spawned teammates.
* This ensures teammates inherit important settings like permission mode,
* model selection, and plugin configuration from their parent.
*
* @param options.planModeRequired - If true, don't inherit bypass permissions (plan mode takes precedence)
* @param options.permissionMode - Permission mode to propagate
*/
export function buildInheritedCliFlags(options?: {
planModeRequired?: boolean
permissionMode?: PermissionMode
}): string {
const flags: string[] = []
const { planModeRequired, permissionMode } = options || {}
// Propagate permission mode to teammates, but NOT if plan mode is required
// Plan mode takes precedence over bypass permissions for safety
if (planModeRequired) {
// Don't inherit bypass permissions when plan mode is required
} else if (
permissionMode === 'bypassPermissions' ||
getSessionBypassPermissionsMode()
) {
flags.push('--dangerously-skip-permissions')
} else if (permissionMode === 'acceptEdits') {
flags.push('--permission-mode acceptEdits')
}
// Propagate --model if explicitly set via CLI
const modelOverride = getMainLoopModelOverride()
if (modelOverride) {
flags.push(`--model ${quote([modelOverride])}`)
}
// Propagate --settings if set via CLI
const settingsPath = getFlagSettingsPath()
if (settingsPath) {
flags.push(`--settings ${quote([settingsPath])}`)
}
// Propagate --plugin-dir for each inline plugin
const inlinePlugins = getInlinePlugins()
for (const pluginDir of inlinePlugins) {
flags.push(`--plugin-dir ${quote([pluginDir])}`)
}
// Propagate --teammate-mode so tmux teammates use the same mode as leader
const sessionMode = getTeammateModeFromSnapshot()
flags.push(`--teammate-mode ${sessionMode}`)
// Propagate --chrome / --no-chrome if explicitly set on the CLI
const chromeFlagOverride = getChromeFlagOverride()
if (chromeFlagOverride === true) {
flags.push('--chrome')
} else if (chromeFlagOverride === false) {
flags.push('--no-chrome')
}
return flags.join(' ')
}
/**
* Environment variables that must be explicitly forwarded to tmux-spawned
* teammates. Tmux may start a new login shell that doesn't inherit the
* parent's env, so we forward any that are set in the current process.
*/
const TEAMMATE_ENV_VARS = [
// Explicit provider override — without these, spawned teammates can fall
// back to firstParty even when the leader is pinned to OpenCode/OpenAI/etc.
'BETTER_CLAWD_API_PROVIDER',
'CLAUDE_CODE_API_PROVIDER',
// API provider selection — without these, teammates default to firstParty
// and send requests to the wrong endpoint (GitHub issue #23561)
'CLAUDE_CODE_USE_BEDROCK',
'CLAUDE_CODE_USE_VERTEX',
'CLAUDE_CODE_USE_FOUNDRY',
// Custom API endpoint
'ANTHROPIC_BASE_URL',
// Config directory override
'CLAUDE_CONFIG_DIR',
// CCR marker — teammates need this for CCR-aware code paths. Auth finds
// its own way via /home/claude/.claude/remote/.oauth_token regardless;
// the FD env var wouldn't help (pipe FDs don't cross tmux).
'CLAUDE_CODE_REMOTE',
// Auto-memory gate (memdir/paths.ts) checks REMOTE && !MEMORY_DIR to
// disable memory on ephemeral CCR filesystems. Forwarding REMOTE alone
// would flip teammates to memory-off when the parent has it on.
'CLAUDE_CODE_REMOTE_MEMORY_DIR',
// Upstream proxy — the parent's MITM relay is reachable from teammates
// (same container network). Forward the proxy vars so teammates route
// customer-configured upstream traffic through the relay for credential
// injection. Without these, teammates bypass the proxy entirely.
'HTTPS_PROXY',
'https_proxy',
'HTTP_PROXY',
'http_proxy',
'NO_PROXY',
'no_proxy',
// Third-party provider endpoints / auth overrides that may be configured
// only in the parent shell environment.
'OPENAI_API_KEY',
'OPENAI_ACCESS_TOKEN',
'CODEX_ACCESS_TOKEN',
'OPENROUTER_API_KEY',
'OPENAI_BASE_URL',
'OPENROUTER_BASE_URL',
'OPENCODE_BASE_URL',
'SSL_CERT_FILE',
'NODE_EXTRA_CA_CERTS',
'REQUESTS_CA_BUNDLE',
'CURL_CA_BUNDLE',
] as const
/**
* Builds a map of environment variables for teammate spawn commands.
* Skips CLAUDECODE and CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS if already set
* in the current environment (e.g., from settings.json env or tmux global env).
*
* Returns a Record<string, string> suitable for tmux -e flags or manual
* env KEY=VALUE construction.
*/
export function buildInheritedEnvMap(): Record<string, string> {
const envMap: Record<string, string> = {}
// Only hardcode these if NOT already set in process env — the user may
// have them in ~/.claude/settings.json env, tmux set-environment, etc.
if (!process.env.CLAUDECODE) {
envMap.CLAUDECODE = '1'
}
if (!process.env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS) {
envMap.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS = '1'
}
for (const key of TEAMMATE_ENV_VARS) {
const value = process.env[key]
if (value !== undefined && value !== '') {
envMap[key] = value
}
}
return envMap
}
/**
* Builds the `env KEY=VALUE ...` string for teammate spawn commands.
* Delegates to buildInheritedEnvMap() for the values.
*/
export function buildInheritedEnvVars(): string {
const envMap = buildInheritedEnvMap()
return Object.entries(envMap)
.map(([key, value]) => `${key}=${quote([value])}`)
.join(' ')
}
|