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(' ')
}