qwen-code/integration-tests/cli/daemon-invocation-context.test.ts
callmeYe b436855a40
feat(core): propagate trusted daemon invocation context (#7279)
* feat(core): propagate trusted daemon invocation context

* test(cli): update ACP startup expectation

* refactor(core): centralize ACP capability env key

* test(cli): update worktree ACP core mock

* test(integration): run daemon context smoke on PRs

* test(ci): update no-AK smoke expectation

* test(core): cover invocation context isolation

* fix(cli): compare ACP capability safely

* fix(docs): restore GitHub action input names

* fix(core): sanitize private ACP capability from child env

* fix(core): reuse private ACP capability env constant

* test(cli): cover malformed trusted invocation context

* test(acp-bridge): assert exact child environment

---------

Co-authored-by: qwen-code-dev-bot <qwen-code-dev-bot@users.noreply.github.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: 易良 <1204183885@qq.com>
2026-07-23 06:49:11 +00:00

216 lines
6.8 KiB
TypeScript

/**
* @license
* Copyright 2026 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { describe, expect, it } from 'vitest';
import { fakeToolCall, startFakeOpenAIServer } from '../fake-openai-server.js';
import {
approveWorkspaceMcpServers,
spawnDaemon,
type SpawnedDaemon,
writeWorkspaceSettings,
} from './_daemon-harness.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(__dirname, '../..');
const ECHO_SERVER = path.join(
REPO_ROOT,
'integration-tests',
'fixtures',
'invocation-context-echo.mjs',
);
const TSX_BIN = path.join(REPO_ROOT, 'node_modules', 'tsx', 'dist', 'cli.mjs');
const SERVE_BRIDGE_SOURCE = path.join(
REPO_ROOT,
'packages',
'sdk-typescript',
'src',
'daemon-mcp',
'serve-bridge',
'bin.ts',
);
const ECHO_TOOL = 'mcp__invocation-echo__capture_invocation_context';
const INVOCATION_META_KEY = 'qwen-code/invocation';
const PROMPT_SENTINEL = 'CAPTURE_DAEMON_INVOCATION_CONTEXT';
const FINAL_ASSISTANT_TEXT = 'INVOCATION_CONTEXT_CAPTURED';
const UUID_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
type CaptureRecord = {
arguments: Record<string, unknown>;
metadata: Record<string, unknown> | null;
privateCapabilityInEnv: boolean;
};
type BridgeSession = {
sessionId: string;
clientId?: string;
};
function stringEnv(env: NodeJS.ProcessEnv): Record<string, string> {
return Object.fromEntries(
Object.entries(env).filter(
(entry): entry is [string, string] => typeof entry[1] === 'string',
),
);
}
function pendingSentinel(body: Record<string, unknown>): boolean {
const messages = body['messages'];
if (!Array.isArray(messages)) return false;
const userIndex = messages.findLastIndex(
(message) =>
typeof message === 'object' &&
message !== null &&
(message as Record<string, unknown>)['role'] === 'user',
);
if (userIndex === -1) return false;
if (!JSON.stringify(messages[userIndex]).includes(PROMPT_SENTINEL)) {
return false;
}
return !messages
.slice(userIndex + 1)
.some(
(message) =>
typeof message === 'object' &&
message !== null &&
(message as Record<string, unknown>)['role'] === 'tool',
);
}
async function callBridgeTool<T>(
client: Client,
name: string,
args: Record<string, unknown>,
): Promise<T> {
const result = await client.callTool({ name, arguments: args });
const content = result.content as Array<{ type?: string; text?: string }>;
const text = content.find((item) => item.type === 'text')?.text;
if (!text || result.isError) {
throw new Error(`${name} failed: ${JSON.stringify(result)}`);
}
return JSON.parse(text) as T;
}
describe('trusted daemon invocation context', () => {
it('reaches a Qwen-launched local stdio MCP tool without leaking the private capability', async ({
skip,
}) => {
skip(process.platform === 'win32', 'requires the POSIX daemon harness');
const root = mkdtempSync(path.join(tmpdir(), 'qwen-daemon-invocation-'));
const workspace = path.join(root, 'workspace');
const home = path.join(root, 'home');
const captureFile = path.join(root, 'capture.jsonl');
mkdirSync(workspace, { recursive: true });
mkdirSync(home, { recursive: true });
const fakeModel = await startFakeOpenAIServer(({ body }) =>
pendingSentinel(body)
? { toolCalls: [fakeToolCall(ECHO_TOOL, { probe: PROMPT_SENTINEL })] }
: { content: FINAL_ASSISTANT_TEXT },
);
let daemon: SpawnedDaemon | undefined;
let bridgeClient: Client | undefined;
try {
const mcpServers = {
'invocation-echo': {
command: process.execPath,
args: [ECHO_SERVER],
env: { INVOCATION_CONTEXT_ECHO_FILE: captureFile },
trust: true,
alwaysLoadTools: true,
},
};
writeWorkspaceSettings(workspace, {
tools: { approvalMode: 'yolo' },
mcpServers,
});
const runtimeEnv = {
...process.env,
...approveWorkspaceMcpServers(workspace, mcpServers),
HOME: home,
QWEN_HOME: path.join(home, '.qwen'),
QWEN_SANDBOX: 'false',
QWEN_CODE_NO_RELAUNCH: 'true',
QWEN_CODE_LEGACY_MCP_BLOCKING: '1',
QWEN_CODE_SUPPRESS_YOLO_WARNING: '1',
OPENAI_API_KEY: 'fake-key',
OPENAI_BASE_URL: fakeModel.baseUrl,
OPENAI_MODEL: 'fake-model',
QWEN_MODEL: 'fake-model',
NO_PROXY: '127.0.0.1,localhost',
no_proxy: '127.0.0.1,localhost',
};
daemon = await spawnDaemon({
workspaceCwd: workspace,
bootTimeoutMs: 20_000,
env: runtimeEnv,
});
bridgeClient = new Client({
name: 'daemon-invocation-context-test',
version: '1.0.0',
});
await bridgeClient.connect(
new StdioClientTransport({
command: process.execPath,
args: [TSX_BIN, SERVE_BRIDGE_SOURCE],
env: stringEnv({
...runtimeEnv,
QWEN_DAEMON_URL: daemon.base,
QWEN_DAEMON_TOKEN: daemon.token,
QWEN_WORKSPACE_CWD: workspace,
}),
}),
);
const session = await callBridgeTool<BridgeSession>(
bridgeClient,
'session_create',
{ workspace_cwd: workspace, session_scope: 'thread' },
);
await callBridgeTool(bridgeClient, 'prompt', {
session_id: session.sessionId,
prompt: PROMPT_SENTINEL,
});
const records = readFileSync(captureFile, 'utf8')
.trim()
.split('\n')
.filter(Boolean)
.map((line) => JSON.parse(line) as CaptureRecord);
expect(records).toHaveLength(1);
expect(records[0]?.arguments['probe']).toBe(PROMPT_SENTINEL);
expect(records[0]?.privateCapabilityInEnv).toBe(false);
expect(records[0]?.metadata?.[INVOCATION_META_KEY]).toEqual({
version: 1,
sessionId: session.sessionId,
promptId: expect.stringMatching(UUID_PATTERN),
});
expect(
(
records[0]?.metadata?.[INVOCATION_META_KEY] as
| Record<string, unknown>
| undefined
)?.['originatorClientId'],
).toBeUndefined();
await callBridgeTool(bridgeClient, 'session_close', {
session_id: session.sessionId,
});
} finally {
await bridgeClient?.close().catch(() => undefined);
await daemon?.dispose();
await fakeModel.close();
rmSync(root, { recursive: true, force: true });
}
}, 120_000);
});