Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1074,6 +1074,12 @@ export class Config {

constructor(params: ConfigParameters) {
this.sessionId = params.sessionId ?? randomUUID();
// Only set the global env marker if no session has claimed it yet,
// so throwaway Config instances (e.g. telemetry-only) don't clobber
// the real interactive session's ID.
if (!process.env['QWEN_CODE_SESSION_ID']) {
process.env['QWEN_CODE_SESSION_ID'] = this.sessionId;
Comment thread
yiliang114 marked this conversation as resolved.
Resolved
}
this.sessionData = params.sessionData;
setDebugLogSession(this);
this.debugLogger = createDebugLogger();
Expand Down Expand Up @@ -1986,6 +1992,7 @@ export class Config {

const previousSessionId = this.sessionId;
this.sessionId = sessionId ?? randomUUID();
process.env['QWEN_CODE_SESSION_ID'] = this.sessionId;
this.sessionData = sessionData;
setDebugLogSession(this);
this.debugLogger = createDebugLogger();
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/hooks/hookRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { FunctionHookRunner } from './functionHookRunner.js';
import { PromptHookRunner } from './promptHookRunner.js';
import { AsyncHookRegistry, generateHookId } from './asyncHookRegistry.js';
import type { Config } from '../config/config.js';
import { getShellContextEnvVars } from '../utils/shellContextEnv.js';

const debugLogger = createDebugLogger('TRUSTED_HOOKS');

Expand Down Expand Up @@ -573,6 +574,7 @@ export class HookRunner {
GEMINI_PROJECT_DIR: input.cwd,
CLAUDE_PROJECT_DIR: input.cwd, // For compatibility
QWEN_PROJECT_DIR: input.cwd, // For Qwen Code compatibility
...getShellContextEnvVars(),
...hookConfig.env,
};

Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/services/shellExecutionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
type AnsiOutput,
} from '../utils/terminalSerializer.js';
import { normalizePathEnvForWindows } from '../utils/windowsPath.js';
import { getShellContextEnvVars } from '../utils/shellContextEnv.js';
import { createDebugLogger } from '../utils/debugLogger.js';
const { Terminal } = pkg;

Expand Down Expand Up @@ -550,6 +551,7 @@ export class ShellExecutionService {
QWEN_CODE: '1',
TERM: 'xterm-256color',
PAGER: 'cat',
...getShellContextEnvVars(),
},
});

Expand Down Expand Up @@ -1152,6 +1154,7 @@ export class ShellExecutionService {
TERM: 'xterm-256color',
PAGER: shellExecutionConfig.pager ?? 'cat',
GIT_PAGER: shellExecutionConfig.pager ?? 'cat',
...getShellContextEnvVars(),
},
handleFlowControl: true,
});
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/tools/monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import {
isShellCommandReadOnlyAST,
} from '../utils/shellAstParser.js';
import { getCurrentAgentId } from '../agents/runtime/agent-context.js';
import { getShellContextEnvVars } from '../utils/shellContextEnv.js';

const debugLogger = createDebugLogger('MONITOR');

Expand Down Expand Up @@ -366,6 +367,7 @@ class MonitorToolInvocation extends BaseToolInvocation<
QWEN_CODE: '1',
TERM: 'dumb', // no color codes for streaming
PAGER: 'cat',
...getShellContextEnvVars(),
},
});
} catch (err) {
Expand Down
69 changes: 69 additions & 0 deletions packages/core/src/utils/shellContextEnv.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/

import { describe, expect, it, beforeEach, afterEach } from 'vitest';
import { getShellContextEnvVars } from './shellContextEnv.js';
import { runWithAgentContext } from '../agents/runtime/agent-context.js';
import { promptIdContext } from './promptIdContext.js';

describe('getShellContextEnvVars', () => {
let originalSessionId: string | undefined;

beforeEach(() => {
originalSessionId = process.env['QWEN_CODE_SESSION_ID'];
delete process.env['QWEN_CODE_SESSION_ID'];
});

afterEach(() => {
if (originalSessionId !== undefined) {
process.env['QWEN_CODE_SESSION_ID'] = originalSessionId;
} else {
delete process.env['QWEN_CODE_SESSION_ID'];
}
});

it('returns empty strings for agent/prompt when no context is available', () => {
const env = getShellContextEnvVars();
expect(env).toEqual({
QWEN_CODE_AGENT_ID: '',
QWEN_CODE_PROMPT_ID: '',
});
});

it('returns QWEN_CODE_SESSION_ID when set in process.env', () => {
process.env['QWEN_CODE_SESSION_ID'] = 'test-session-123';
const env = getShellContextEnvVars();
expect(env.QWEN_CODE_SESSION_ID).toBe('test-session-123');
});

it('returns QWEN_CODE_AGENT_ID when called within agent context', async () => {
const env = await runWithAgentContext('my-agent-42', async () => getShellContextEnvVars());
expect(env.QWEN_CODE_AGENT_ID).toBe('my-agent-42');
});

it('returns QWEN_CODE_PROMPT_ID when called within prompt context', () => {
const env = promptIdContext.run('prompt-abc', () => getShellContextEnvVars());
expect(env.QWEN_CODE_PROMPT_ID).toBe('prompt-abc');
});

it('returns all vars when all contexts are active', async () => {
process.env['QWEN_CODE_SESSION_ID'] = 'sess-uuid';
const env = await runWithAgentContext('agent-xyz', async () => promptIdContext.run('prompt-456', () => getShellContextEnvVars()));
expect(env).toEqual({
QWEN_CODE_SESSION_ID: 'sess-uuid',
QWEN_CODE_AGENT_ID: 'agent-xyz',
QWEN_CODE_PROMPT_ID: 'prompt-456',
});
});

it('sets empty string for agent/prompt to override inherited env', () => {
// Simulates a nested qwen-code process where parent injected these
const env = getShellContextEnvVars();
expect(env.QWEN_CODE_AGENT_ID).toBe('');
expect(env.QWEN_CODE_PROMPT_ID).toBe('');
// Empty strings will overwrite any stale inherited values in process.env
});
});
41 changes: 41 additions & 0 deletions packages/core/src/utils/shellContextEnv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @license
* Copyright 2025 Qwen
* SPDX-License-Identifier: Apache-2.0
*/

/**
* Returns context environment variables to inject into shell subprocesses.
*
* Reads dynamic context (agent ID, prompt ID) from AsyncLocalStorage at
* call time, and session ID from process.env (set by Config at session
* start). This enables downstream scripts to identify which session,
* agent, and prompt triggered their execution — useful for tracing,
* audit logging, and business context correlation.
*
* Must be called at spawn time within the executing async context to
* capture the correct agent/prompt frame.
*/

import { getCurrentAgentId } from '../agents/runtime/agent-context.js';
import { promptIdContext } from './promptIdContext.js';

export function getShellContextEnvVars(): Record<string, string> {
const env: Record<string, string> = {};

const sessionId = process.env['QWEN_CODE_SESSION_ID'];
if (sessionId) {
env['QWEN_CODE_SESSION_ID'] = sessionId;
}

// For agent/prompt IDs: explicitly set empty string when no ALS context
// exists, so that stale values inherited from a parent qwen-code process
// (via process.env spread) are overwritten rather than leaked.
const agentId = getCurrentAgentId();
env['QWEN_CODE_AGENT_ID'] = agentId ?? '';
Comment thread
yiliang114 marked this conversation as resolved.
Resolved

const promptId = promptIdContext.getStore();
env['QWEN_CODE_PROMPT_ID'] = promptId ?? '';
Comment thread
yiliang114 marked this conversation as resolved.
Resolved

return env;
}
Loading