mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-03 10:23:16 +00:00
Merge pull request #1059 from innocarpe/fix/agent-team-with-gpt
fix(settings-profile): preserve nested model intent for agent team launches
This commit is contained in:
+17
-4
@@ -80,7 +80,7 @@ import { handleError, runCleanup } from './errors';
|
||||
import { tryHandleRootCommand } from './commands/root-command-router';
|
||||
|
||||
// Import extracted utility functions
|
||||
import { execClaude } from './utils/shell-executor';
|
||||
import { execClaude, stripAnthropicRoutingEnv } from './utils/shell-executor';
|
||||
import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils/glmt-deprecation';
|
||||
import { maybeWarnAboutResumeLaneMismatch } from './auth/resume-lane-warning';
|
||||
import { createLogger } from './services/logging';
|
||||
@@ -1359,8 +1359,21 @@ async function main(): Promise<void> {
|
||||
console.error(info(`Global env: ${envNames}`));
|
||||
}
|
||||
|
||||
// Explicitly inject effective settings env vars so stale ANTHROPIC_*
|
||||
// values from prior sessions cannot leak into the active profile.
|
||||
// For Claude target launches that already pass `--settings`, keep runtime
|
||||
// env free of ANTHROPIC routing/auth while preserving non-routing profile
|
||||
// env so nested Team/subagent sessions can still inherit model intent and
|
||||
// other profile-scoped runtime flags.
|
||||
const claudeRuntimeEnvVars: NodeJS.ProcessEnv = {
|
||||
...stripAnthropicRoutingEnv({ ...globalEnv, ...settingsEnv }),
|
||||
...(inheritedClaudeConfigDir ? { CLAUDE_CONFIG_DIR: inheritedClaudeConfigDir } : {}),
|
||||
...webSearchEnv,
|
||||
...imageAnalysisEnv,
|
||||
...(browserRuntimeEnv || {}),
|
||||
CCS_PROFILE_TYPE: 'settings',
|
||||
CCS_STRIP_INHERITED_ANTHROPIC_ENV: '1',
|
||||
};
|
||||
|
||||
// Non-Claude targets still need effective credentials injected directly.
|
||||
const envVars: NodeJS.ProcessEnv = {
|
||||
...globalEnv,
|
||||
...settingsEnv,
|
||||
@@ -1472,7 +1485,7 @@ async function main(): Promise<void> {
|
||||
settingsPath: expandedSettingsPath,
|
||||
});
|
||||
|
||||
execClaude(claudeCli, launchArgs, { ...envVars, ...traceEnv });
|
||||
execClaude(claudeCli, launchArgs, { ...claudeRuntimeEnvVars, ...traceEnv });
|
||||
} else if (profileInfo.type === 'account') {
|
||||
// NEW FLOW: Account-based profile (work, personal)
|
||||
// All platforms: Use instance isolation with CLAUDE_CONFIG_DIR
|
||||
|
||||
@@ -16,8 +16,13 @@ import { type ExecutionOptions, type ExecutionResult, type StreamMessage } from
|
||||
import { StreamBuffer, formatToolVerbose } from './executor/stream-parser';
|
||||
import { buildExecutionResult } from './executor/result-aggregator';
|
||||
import { getCcsDir, getModelDisplayName, loadSettings } from '../utils/config-manager';
|
||||
import { getGlobalEnvConfig } from '../config/unified-config-loader';
|
||||
import { getProfileLookupCandidates } from '../utils/profile-compat';
|
||||
import { getClaudeLaunchEnvOverrides, stripClaudeCodeEnv } from '../utils/shell-executor';
|
||||
import {
|
||||
getClaudeLaunchEnvOverrides,
|
||||
stripAnthropicRoutingEnv,
|
||||
stripClaudeCodeEnv,
|
||||
} from '../utils/shell-executor';
|
||||
import { resolveProfileContinuityInheritance } from '../auth/profile-continuity-inheritance';
|
||||
import {
|
||||
appendThirdPartyImageAnalysisToolArgs,
|
||||
@@ -38,6 +43,11 @@ import {
|
||||
import { resolveCliproxyBridgeMetadata } from '../api/services';
|
||||
import { ensureCliproxyService } from '../cliproxy';
|
||||
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager';
|
||||
import {
|
||||
buildOpenAICompatProxyEnv,
|
||||
resolveOpenAICompatProfileConfig,
|
||||
startOpenAICompatProxy,
|
||||
} from '../proxy';
|
||||
import {
|
||||
appendThirdPartyWebSearchToolArgs,
|
||||
appendWebSearchTrace,
|
||||
@@ -129,6 +139,14 @@ export class HeadlessExecutor {
|
||||
syncImageAnalysisMcpToConfigDir(inheritedClaudeConfigDir);
|
||||
|
||||
const settings = loadSettings(settingsPath);
|
||||
const globalEnvConfig = getGlobalEnvConfig();
|
||||
const globalEnv = globalEnvConfig.enabled ? globalEnvConfig.env : {};
|
||||
const settingsEnv = settings.env || {};
|
||||
const openAICompatProfile = resolveOpenAICompatProfileConfig(
|
||||
profile,
|
||||
settingsPath,
|
||||
settingsEnv
|
||||
);
|
||||
const cliproxyBridge = resolveCliproxyBridgeMetadata(settings);
|
||||
let imageAnalysisFallbackHookReady: boolean | undefined;
|
||||
if (imageAnalysisMcpReady) {
|
||||
@@ -207,6 +225,33 @@ export class HeadlessExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
let runtimeEnvVars: NodeJS.ProcessEnv = {
|
||||
...stripAnthropicRoutingEnv({ ...globalEnv, ...settingsEnv }),
|
||||
...(inheritedClaudeConfigDir ? { CLAUDE_CONFIG_DIR: inheritedClaudeConfigDir } : {}),
|
||||
CCS_PROFILE_TYPE: 'settings',
|
||||
CCS_STRIP_INHERITED_ANTHROPIC_ENV: '1',
|
||||
};
|
||||
|
||||
if (openAICompatProfile) {
|
||||
const proxyStart = await startOpenAICompatProxy(openAICompatProfile, {
|
||||
insecure: openAICompatProfile.insecure,
|
||||
});
|
||||
if (!proxyStart.success) {
|
||||
throw new Error(proxyStart.error || 'Failed to start local OpenAI-compatible proxy');
|
||||
}
|
||||
|
||||
runtimeEnvVars = {
|
||||
...runtimeEnvVars,
|
||||
...buildOpenAICompatProxyEnv(
|
||||
openAICompatProfile,
|
||||
proxyStart.port,
|
||||
proxyStart.authToken || '',
|
||||
inheritedClaudeConfigDir
|
||||
),
|
||||
};
|
||||
delete runtimeEnvVars.ANTHROPIC_API_KEY;
|
||||
}
|
||||
|
||||
// Smart slash command detection and preservation
|
||||
const processedPrompt = this._processSlashCommand(enhancedPrompt);
|
||||
|
||||
@@ -321,6 +366,7 @@ export class HeadlessExecutor {
|
||||
sessionMgr,
|
||||
claudeConfigDir: inheritedClaudeConfigDir,
|
||||
imageAnalysisEnv,
|
||||
runtimeEnvVars,
|
||||
traceEnv,
|
||||
});
|
||||
}
|
||||
@@ -340,6 +386,7 @@ export class HeadlessExecutor {
|
||||
sessionMgr: SessionManager;
|
||||
claudeConfigDir?: string;
|
||||
imageAnalysisEnv?: Record<string, string>;
|
||||
runtimeEnvVars?: NodeJS.ProcessEnv;
|
||||
traceEnv?: Record<string, string>;
|
||||
}
|
||||
): Promise<ExecutionResult> {
|
||||
@@ -352,6 +399,7 @@ export class HeadlessExecutor {
|
||||
sessionMgr,
|
||||
claudeConfigDir,
|
||||
imageAnalysisEnv = {},
|
||||
runtimeEnvVars = {},
|
||||
traceEnv = {},
|
||||
} = ctx;
|
||||
|
||||
@@ -368,9 +416,10 @@ export class HeadlessExecutor {
|
||||
// Strip Claude Code nested session guard env var to allow CCS delegation
|
||||
// (Claude Code v2.1.39+ sets CLAUDECODE to detect nested sessions)
|
||||
const cleanEnv = stripClaudeCodeEnv({
|
||||
...process.env,
|
||||
...stripAnthropicRoutingEnv(process.env),
|
||||
...getClaudeLaunchEnvOverrides(),
|
||||
...getWebSearchHookEnv(),
|
||||
...runtimeEnvVars,
|
||||
...imageAnalysisEnv,
|
||||
...traceEnv,
|
||||
...(claudeConfigDir ? { CLAUDE_CONFIG_DIR: claudeConfigDir } : {}),
|
||||
|
||||
+83
-22
@@ -26,6 +26,71 @@ export function stripAnthropicEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||
return result;
|
||||
}
|
||||
|
||||
const ANTHROPIC_ROUTING_ENV_KEYS = [
|
||||
'ANTHROPIC_BASE_URL',
|
||||
'ANTHROPIC_AUTH_TOKEN',
|
||||
'ANTHROPIC_API_KEY',
|
||||
];
|
||||
const ANTHROPIC_ROUTING_ENV_KEY_SET = new Set(ANTHROPIC_ROUTING_ENV_KEYS);
|
||||
const ANTHROPIC_MODEL_ENV_KEYS = [
|
||||
'ANTHROPIC_MODEL',
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
||||
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
||||
'ANTHROPIC_SMALL_FAST_MODEL',
|
||||
];
|
||||
const TMUX_SYNC_ENV_KEYS = [
|
||||
'CLAUDE_CONFIG_DIR',
|
||||
'CCS_PROFILE_TYPE',
|
||||
'CCS_WEBSEARCH_SKIP',
|
||||
'CCS_STRIP_INHERITED_ANTHROPIC_ENV',
|
||||
'CLAUDE_CODE_MAX_OUTPUT_TOKENS',
|
||||
...ANTHROPIC_MODEL_ENV_KEYS,
|
||||
...ANTHROPIC_ROUTING_ENV_KEYS,
|
||||
];
|
||||
|
||||
/**
|
||||
* Strip inherited Anthropic routing/auth env while preserving model intent.
|
||||
* Used for nested settings-profile Claude launches where `--settings` already
|
||||
* defines the provider transport and the parent process should only lend model
|
||||
* defaults or effort hints.
|
||||
*/
|
||||
export function stripAnthropicRoutingEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||
const result: NodeJS.ProcessEnv = {};
|
||||
for (const key of Object.keys(env)) {
|
||||
if (!ANTHROPIC_ROUTING_ENV_KEY_SET.has(key.toUpperCase())) {
|
||||
result[key] = env[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function syncTmuxNestedSessionEnv(env: NodeJS.ProcessEnv, profileType: string | undefined): void {
|
||||
if (!process.env.TMUX) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nestedSessionEnv =
|
||||
profileType === 'account' || profileType === 'default'
|
||||
? stripAnthropicEnv(env)
|
||||
: profileType === 'settings'
|
||||
? stripAnthropicRoutingEnv(env)
|
||||
: env;
|
||||
|
||||
for (const key of TMUX_SYNC_ENV_KEYS) {
|
||||
try {
|
||||
const value = nestedSessionEnv[key];
|
||||
if (value !== undefined) {
|
||||
spawnSync('tmux', ['setenv', key, value], { stdio: 'ignore' });
|
||||
} else {
|
||||
spawnSync('tmux', ['setenv', '-u', key], { stdio: 'ignore' });
|
||||
}
|
||||
} catch {
|
||||
// tmux setenv can fail if not in a tmux session; safe to ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip Claude Code nested-session guard env var from a process environment.
|
||||
*
|
||||
@@ -137,24 +202,31 @@ export function execClaude(
|
||||
const webSearchEnv = getWebSearchHookEnv();
|
||||
const claudeLaunchEnv = getClaudeLaunchEnvOverrides();
|
||||
|
||||
// For account/default profiles, strip ANTHROPIC_* from parent env to prevent
|
||||
// stale proxy config (e.g., from prior CLIProxy sessions) from interfering
|
||||
// with native Claude API routing. Settings-based profiles explicitly inject
|
||||
// their own ANTHROPIC_* values, so they don't need this protection.
|
||||
// Strip inherited ANTHROPIC_* when the launch should not reuse parent routing.
|
||||
// Account/default profiles need full isolation from prior proxy sessions.
|
||||
// Settings profiles can selectively strip only routing/auth when `--settings`
|
||||
// already carries the provider source of truth but the parent model intent
|
||||
// should still flow into nested Team/subagent launches.
|
||||
const profileType = envVars?.CCS_PROFILE_TYPE;
|
||||
const baseEnv =
|
||||
profileType === 'account' || profileType === 'default'
|
||||
? stripAnthropicEnv(process.env)
|
||||
const stripInheritedAnthropicEnv = profileType === 'account' || profileType === 'default';
|
||||
const stripInheritedAnthropicRoutingEnv = envVars?.CCS_STRIP_INHERITED_ANTHROPIC_ENV === '1';
|
||||
const baseEnv = stripInheritedAnthropicEnv
|
||||
? stripAnthropicEnv(process.env)
|
||||
: stripInheritedAnthropicRoutingEnv
|
||||
? stripAnthropicRoutingEnv(process.env)
|
||||
: process.env;
|
||||
|
||||
// Prepare environment (merge with base env if envVars provided)
|
||||
const mergedEnv = envVars
|
||||
? { ...baseEnv, ...claudeLaunchEnv, ...envVars, ...webSearchEnv }
|
||||
: { ...baseEnv, ...claudeLaunchEnv, ...webSearchEnv };
|
||||
const effectiveMergedEnv = stripInheritedAnthropicRoutingEnv
|
||||
? stripAnthropicRoutingEnv(mergedEnv)
|
||||
: mergedEnv;
|
||||
|
||||
// Strip Claude Code nested session guard env var to allow CCS delegation
|
||||
// (Claude Code v2.1.39+ sets CLAUDECODE to detect nested sessions)
|
||||
const env = stripClaudeCodeEnv(mergedEnv);
|
||||
const env = stripClaudeCodeEnv(effectiveMergedEnv);
|
||||
|
||||
if (profileType !== 'account') {
|
||||
try {
|
||||
@@ -164,20 +236,9 @@ export function execClaude(
|
||||
}
|
||||
}
|
||||
|
||||
// propagate key env vars to tmux session so agent team teammates
|
||||
// (spawned via tmux split-window) inherit the correct config dir
|
||||
if (process.env.TMUX && envVars) {
|
||||
const tmuxPropagateVars = ['CLAUDE_CONFIG_DIR', 'CCS_PROFILE_TYPE', 'CCS_WEBSEARCH_SKIP'];
|
||||
for (const key of tmuxPropagateVars) {
|
||||
if (envVars[key]) {
|
||||
try {
|
||||
spawnSync('tmux', ['setenv', key, envVars[key] ?? ''], { stdio: 'ignore' });
|
||||
} catch {
|
||||
// tmux setenv can fail if not in a tmux session; safe to ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Keep tmux teammate panes aligned with the nested-safe Claude runtime env
|
||||
// rather than the tmux server's original shell environment.
|
||||
syncTmuxNestedSessionEnv(env, profileType);
|
||||
|
||||
let child: ChildProcess;
|
||||
if (isPowerShellScript) {
|
||||
|
||||
Reference in New Issue
Block a user