mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-10-19 20:53:34 +00:00
* feat(managed-agent): add SSH Tool Runtime support Add administrator-configured SSH placement, remote Workspace registration, Session-isolated workers and strict retirement over the existing HTTP protocol. Keep new admission disabled by default and preserve original-worker identity. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(managed-agent): clean up SSH runtime transport lifecycle Keep bootstrap work outside the endpoint lock and release unused forwards and stopped direct routes without disconnecting durable observers. Bound control work and complete queued futures during shutdown. Align SSH registration with the shared SQL clock, reuse storage identity and dependencies, and fix the environment inventory and transcript budget. Normalize inherited CSI test formatting for Java verification. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(managed-agent): align lifecycle and timestamp test fixtures Use canonical SQL timestamp columns and database milliseconds in fault-gate evidence, and require parent close to cancel a held child Turn as current main specifies. Preserve production behavior and capability boundaries. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> * fix(managed-agent): Bind environment query IDs as binary Preserve canonical Session and Turn identity types when H2 recompiles the latest environment query. Force recompilation in the existing regression without changing its admission-order or environment assertions. Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com> --------- Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
507 lines
18 KiB
JavaScript
Executable file
507 lines
18 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
|
|
/**
|
|
* @license
|
|
* Copyright 2025 Qwen Code
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/**
|
|
* Production bin entry wrapper.
|
|
*
|
|
* For most commands, global.gc() must be available for the memory-pressure
|
|
* monitor's critical-tier cleanup. On POSIX under Node the entry exposes gc at
|
|
* runtime and imports cli.js in this process; on Windows and under Bun it
|
|
* launches the bundled CLI as a child with --expose-gc.
|
|
*
|
|
* For bootstrap fast paths: imports cli.js directly in-process, skipping the
|
|
* spawnSync overhead. These paths do not need global.gc().
|
|
*/
|
|
|
|
const relaunchArgs = process.env['QWEN_CODE_RELAUNCH_ARGS'];
|
|
let cliArgs = process.argv.slice(2);
|
|
try {
|
|
cliArgs = relaunchArgs ? JSON.parse(relaunchArgs) : cliArgs;
|
|
} catch {
|
|
// Ignore stale or user-provided junk; normal argv is still usable.
|
|
}
|
|
delete process.env['QWEN_CODE_RELAUNCH_ARGS'];
|
|
delete process.env['QWEN_CODE_PENDING_COMPILE_CACHE'];
|
|
|
|
function hasFlag(flag, alias) {
|
|
for (const arg of cliArgs) {
|
|
if (arg === '--') {
|
|
return false;
|
|
}
|
|
if (arg === flag || arg === alias) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function isInProcessFastPath() {
|
|
const first = cliArgs[0];
|
|
if (
|
|
first === 'serve' ||
|
|
first === 'mcp' ||
|
|
first === 'managed-runtime-bootstrap' ||
|
|
first === 'managed-runtime-bootstrap-worker'
|
|
) {
|
|
return true;
|
|
}
|
|
if (first === undefined || first.startsWith('-')) {
|
|
return hasFlag('--help', '-h') || hasFlag('--version', '-v');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const isTopLevelVersion =
|
|
(cliArgs[0] === undefined || cliArgs[0].startsWith('-')) &&
|
|
hasFlag('--version', '-v');
|
|
|
|
if (isTopLevelVersion && process.env['CLI_VERSION']) {
|
|
process.stdout.write(`${process.env['CLI_VERSION']}\n`);
|
|
process.exit(0);
|
|
}
|
|
|
|
const { existsSync, readFileSync, realpathSync, statSync } = await import(
|
|
'node:fs'
|
|
);
|
|
const { createHash } = await import('node:crypto');
|
|
const { homedir, tmpdir } = await import('node:os');
|
|
const { parseEnv } = await import('node:util');
|
|
const { fileURLToPath, pathToFileURL } = await import('node:url');
|
|
const { delimiter, dirname, join, parse, resolve, sep } = await import(
|
|
'node:path'
|
|
);
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const currentEntryPath = realpathSync(fileURLToPath(import.meta.url));
|
|
|
|
function getHomeDir() {
|
|
try {
|
|
return homedir() || tmpdir();
|
|
} catch {
|
|
return tmpdir();
|
|
}
|
|
}
|
|
|
|
function resolveQwenHome() {
|
|
const configured = process.env['QWEN_HOME'];
|
|
if (!configured) return join(getHomeDir(), '.qwen');
|
|
if (configured === '~') return getHomeDir();
|
|
if (configured.startsWith('~/') || configured.startsWith('~\\')) {
|
|
return join(
|
|
getHomeDir(),
|
|
...configured
|
|
.slice(2)
|
|
.split(/[/\\]+/)
|
|
.filter(Boolean),
|
|
);
|
|
}
|
|
return resolve(configured);
|
|
}
|
|
|
|
function preResolveQwenHome() {
|
|
if (Object.hasOwn(process.env, 'QWEN_HOME')) return;
|
|
const home = getHomeDir();
|
|
for (const candidate of [join(home, '.qwen', '.env'), join(home, '.env')]) {
|
|
try {
|
|
const source = readFileSync(candidate, 'utf8')
|
|
.replace(/^\uFEFF/, '')
|
|
.replace(/^(\s*(?:export\s+)?QWEN_HOME):[^\S\r\n]+/gm, '$1=');
|
|
const configured = parseEnv(source)['QWEN_HOME'];
|
|
if (configured) {
|
|
process.env['QWEN_HOME'] = configured;
|
|
return;
|
|
}
|
|
} catch {
|
|
// Match the CLI's quiet handling of missing or invalid home env files.
|
|
}
|
|
}
|
|
}
|
|
|
|
delete process.env['QWEN_CODE_MANAGED_NPM_UPDATE'];
|
|
preResolveQwenHome();
|
|
|
|
function getManagedNpmPin() {
|
|
try {
|
|
const pin = JSON.parse(process.env['QWEN_CODE_MANAGED_NPM_PIN'] ?? '');
|
|
if (pin.bootstrap !== currentEntryPath) return undefined;
|
|
if (
|
|
typeof pin.updateRoot !== 'string' ||
|
|
resolve(pin.updateRoot) !== pin.updateRoot
|
|
) {
|
|
return undefined;
|
|
}
|
|
if (pin.version === null) {
|
|
return { version: null, updateRoot: pin.updateRoot };
|
|
}
|
|
return typeof pin.version === 'string' &&
|
|
/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(
|
|
pin.version,
|
|
)
|
|
? { version: pin.version, updateRoot: pin.updateRoot }
|
|
: undefined;
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
const managedNpmPin = getManagedNpmPin();
|
|
const managedNpmUpdateRoot =
|
|
managedNpmPin?.updateRoot ?? join(resolveQwenHome(), 'updates', 'npm');
|
|
|
|
function getManagedNpmInstallation() {
|
|
try {
|
|
if (managedNpmPin?.version === null) return undefined;
|
|
const launcherRoot = join(
|
|
managedNpmUpdateRoot,
|
|
createHash('sha256').update(currentEntryPath).digest('hex').slice(0, 16),
|
|
);
|
|
let version = managedNpmPin?.version;
|
|
if (version === undefined) {
|
|
const active = JSON.parse(
|
|
readFileSync(join(launcherRoot, 'active.json'), 'utf8'),
|
|
);
|
|
const basePackageJsonPath = [
|
|
join(__dirname, 'package.json'),
|
|
join(__dirname, '..', 'package.json'),
|
|
].find((candidate) => existsSync(candidate));
|
|
if (!basePackageJsonPath) return undefined;
|
|
const basePackage = JSON.parse(readFileSync(basePackageJsonPath, 'utf8'));
|
|
if (
|
|
typeof active.version !== 'string' ||
|
|
!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(
|
|
active.version,
|
|
) ||
|
|
typeof active.bootstrap !== 'string' ||
|
|
realpathSync(active.bootstrap) !== currentEntryPath ||
|
|
active.baseVersion !== basePackage.version ||
|
|
active.bootstrapCtimeMs !== statSync(currentEntryPath).ctimeMs
|
|
) {
|
|
return undefined;
|
|
}
|
|
version = active.version;
|
|
}
|
|
const packageRoot = join(
|
|
launcherRoot,
|
|
'versions',
|
|
version,
|
|
'node_modules',
|
|
'@qwen-code',
|
|
'qwen-code',
|
|
);
|
|
const packageJsonPath = join(packageRoot, 'package.json');
|
|
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
const cliPath = join(packageRoot, 'cli.js');
|
|
if (
|
|
pkg.name !== '@qwen-code/qwen-code' ||
|
|
pkg.version !== version ||
|
|
!existsSync(cliPath)
|
|
) {
|
|
return undefined;
|
|
}
|
|
process.env['QWEN_CODE_MANAGED_NPM_UPDATE'] = 'true';
|
|
return { cliPath, packageJsonPath, version };
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
// The entry a subprocess should call to reach THIS build.
|
|
//
|
|
// A skill that shells out to `qwen …` gets whatever `qwen` PATH resolves to, which
|
|
// is not necessarily the code that launched it: with an older global install on the
|
|
// machine, a current-source daemon's `qwen review agent-prompt --role 0` landed in a
|
|
// v0.19.10 binary whose `agent-prompt` predates `--role`, and the run died on
|
|
// "Missing required argument: chunk". This file is the executable entry and the one
|
|
// thing that knows its own path, so it publishes it; `getShellContextEnvVars` passes
|
|
// it to every shell subprocess, and a caller prefers it over a bare `qwen`.
|
|
//
|
|
// Assignment, not `||=`: an inherited value is another session's CLI — an outer
|
|
// qwen shelling out to this one — and honouring it re-creates the exact skew above,
|
|
// one level up. Each entry stamps itself, so nested sessions each call their own
|
|
// build. The managed-version pin keeps nested calls on the running session's build
|
|
// even if a background update changes the active pointer. A post-update relaunch
|
|
// clears that pin so the launcher's wrapper selects the newly active build.
|
|
//
|
|
// One exception, and it points the SAME way: the standalone package launches this
|
|
// file through a shim (`bin/qwen`) that selects the BUNDLED Node — the host may
|
|
// have none — and announces itself via QWEN_CODE_LAUNCHER_PATH. There, "the entry
|
|
// that reaches this build" is the shim: stamping this file instead would hand
|
|
// subprocesses a `#!/usr/bin/env node` script on a machine where that resolves to
|
|
// nothing. Read before the spawn path deletes the variable below.
|
|
// Captured AND deleted here, not just read: the serve/mcp fast path below never
|
|
// reaches the spawn branch that used to delete it, so the hint leaked into every
|
|
// child of a standalone daemon — and a child qwen from a DIFFERENT checkout
|
|
// would read the outer shim and republish it as its own entry: the wrong build,
|
|
// wearing this one's stamp.
|
|
const standaloneShim = process.env['QWEN_CODE_LAUNCHER_PATH'];
|
|
delete process.env['QWEN_CODE_LAUNCHER_PATH'];
|
|
|
|
const managedRuntimeBootstrap =
|
|
cliArgs[0] === 'managed-runtime-bootstrap' ||
|
|
cliArgs[0] === 'managed-runtime-bootstrap-worker';
|
|
const managedNpmInstallation = managedRuntimeBootstrap
|
|
? undefined
|
|
: getManagedNpmInstallation();
|
|
const validStandaloneShim =
|
|
standaloneShim && existsSync(standaloneShim) ? standaloneShim : undefined;
|
|
process.env['QWEN_CODE_CLI'] =
|
|
validStandaloneShim ?? fileURLToPath(import.meta.url);
|
|
if (validStandaloneShim) {
|
|
delete process.env['QWEN_CODE_MANAGED_NPM_PIN'];
|
|
delete process.env['QWEN_CODE_MANAGED_NPM_ROOT'];
|
|
} else {
|
|
process.env['QWEN_CODE_MANAGED_NPM_ROOT'] = managedNpmUpdateRoot;
|
|
process.env['QWEN_CODE_MANAGED_NPM_PIN'] = JSON.stringify({
|
|
bootstrap: currentEntryPath,
|
|
version: managedNpmInstallation?.version ?? null,
|
|
updateRoot: managedNpmUpdateRoot,
|
|
});
|
|
}
|
|
const cliPathCandidates = [
|
|
managedNpmInstallation?.cliPath,
|
|
join(__dirname, 'cli.js'),
|
|
join(__dirname, '..', 'dist', 'cli.js'),
|
|
].filter(Boolean);
|
|
const packageJsonPathCandidates = [
|
|
managedNpmInstallation?.packageJsonPath,
|
|
join(__dirname, 'package.json'),
|
|
join(__dirname, '..', 'package.json'),
|
|
].filter(Boolean);
|
|
const cliPath =
|
|
cliPathCandidates.find((candidate) => existsSync(candidate)) ??
|
|
cliPathCandidates[0];
|
|
const packageJsonPath =
|
|
packageJsonPathCandidates.find((candidate) => existsSync(candidate)) ??
|
|
packageJsonPathCandidates[0];
|
|
|
|
// Keep this separate from CLI_VERSION: esbuild replaces CLI_VERSION with the
|
|
// build package version, while this value must survive nested wrapper calls.
|
|
if (!process.env['QWEN_CODE_STARTUP_VERSION']?.trim()) {
|
|
try {
|
|
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
process.env['QWEN_CODE_STARTUP_VERSION'] = pkg.version || 'unknown';
|
|
} catch {
|
|
// The CLI has its own version fallback when package metadata is unavailable.
|
|
}
|
|
}
|
|
|
|
if (isTopLevelVersion) {
|
|
try {
|
|
const { readFileSync } = await import('node:fs');
|
|
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
process.stdout.write(`${pkg.version || 'unknown'}\n`);
|
|
process.exit(0);
|
|
} catch {
|
|
// Fall through to cli.js, which has its own version fallback.
|
|
}
|
|
}
|
|
|
|
if (isInProcessFastPath()) {
|
|
const { default: module } = await import('node:module');
|
|
const compileCache = module.enableCompileCache?.();
|
|
if (
|
|
cliArgs[0] === 'serve' &&
|
|
!process.env['NODE_COMPILE_CACHE'] &&
|
|
compileCache?.status === module.constants?.compileCacheStatus?.ENABLED &&
|
|
compileCache?.directory
|
|
) {
|
|
process.env['QWEN_CODE_PENDING_COMPILE_CACHE'] = compileCache.directory;
|
|
}
|
|
process.argv[1] = cliPath;
|
|
await import(pathToFileURL(cliPath).href);
|
|
} else {
|
|
const { spawnSync } = await import('node:child_process');
|
|
const UPDATE_COMPLETE_EXIT_CODE = 44;
|
|
// A child that never started comes back from spawnSync as
|
|
// { status: null, signal: null, error }; say why instead of exiting silently.
|
|
const SPAWN_FAILURE_EXIT_CODE = 126;
|
|
const exitOnSpawnError = (command, error) => {
|
|
// Node's message already reads "<syscall> <path> <code>"; add only what it
|
|
// does not say.
|
|
const detail = [
|
|
...[error.code, error.syscall].filter(
|
|
(part) => typeof part === 'string' && !error.message.includes(part),
|
|
),
|
|
...(error.errno === undefined ? [] : [`errno ${error.errno}`]),
|
|
].join(' ');
|
|
process.stderr.write(
|
|
`Failed to start ${command}: ${error.message}${detail ? ` (${detail})` : ''}\n`,
|
|
);
|
|
process.exit(SPAWN_FAILURE_EXIT_CODE);
|
|
};
|
|
// cmd.exe metacharacters. Mirrors UNSAFE_CMD_CHARS in
|
|
// packages/cli/src/ui/standalone-update.ts — kept local because this
|
|
// plain-ESM entry ships in the standalone package and cannot import the
|
|
// TypeScript sources.
|
|
const UNSAFE_CMD_CHARS = /[&|<>^%!"`\n\r]/;
|
|
const entryPath = resolve(process.argv[1]);
|
|
delete process.env['QWEN_CODE_LAUNCHER_PID'];
|
|
const launchEnv = { ...process.env };
|
|
|
|
const findLauncher = () => {
|
|
const launcherNames =
|
|
process.platform === 'win32'
|
|
? ['qwen.cmd', 'qwen.exe', 'qwen']
|
|
: ['qwen'];
|
|
const entryRootLength = parse(entryPath).root.length;
|
|
const launcherFromEnv = standaloneShim;
|
|
const launcherCandidates = process.env['PATH']
|
|
?.split(delimiter)
|
|
.flatMap((dir) => launcherNames.map((name) => join(dir, name)))
|
|
.filter((candidate) => existsSync(candidate));
|
|
return launcherFromEnv && existsSync(launcherFromEnv)
|
|
? launcherFromEnv
|
|
: launcherCandidates
|
|
?.map((candidate) => {
|
|
if (resolve(candidate) === entryPath) {
|
|
return { candidate, score: Number.MAX_SAFE_INTEGER };
|
|
}
|
|
try {
|
|
if (realpathSync(candidate) === realpathSync(entryPath)) {
|
|
return { candidate, score: Number.MAX_SAFE_INTEGER };
|
|
}
|
|
} catch {
|
|
// Fall back to matching the installation prefix.
|
|
}
|
|
let parent = resolve(dirname(candidate));
|
|
while (
|
|
parent.length > entryRootLength &&
|
|
entryPath !== parent &&
|
|
!entryPath.startsWith(`${parent}${sep}`)
|
|
) {
|
|
const next = dirname(parent);
|
|
if (next === parent) break;
|
|
parent = next;
|
|
}
|
|
return { candidate, score: parent.length };
|
|
})
|
|
.filter(({ score }) => score > entryRootLength)
|
|
.sort((a, b) => b.score - a.score)[0]?.candidate;
|
|
};
|
|
|
|
const relaunchAfterUpdate = () => {
|
|
const launcher = findLauncher();
|
|
if (!launcher) {
|
|
process.stderr.write(
|
|
'Update successful! The new version will be used on your next run.\n',
|
|
);
|
|
process.exit(0);
|
|
}
|
|
const relaunchEnv = {
|
|
...launchEnv,
|
|
QWEN_CODE_RELAUNCH_ARGS: JSON.stringify(cliArgs),
|
|
QWEN_CODE_SKIP_UPDATE_CHECK_ONCE: 'true',
|
|
};
|
|
// This is a new startup after the managed update; let the new package stamp
|
|
// its own version instead of inheriting the old process's session version.
|
|
delete relaunchEnv['QWEN_CODE_STARTUP_VERSION'];
|
|
delete relaunchEnv['QWEN_CODE_MANAGED_NPM_PIN'];
|
|
if (
|
|
process.platform === 'win32' &&
|
|
launcher.endsWith('.cmd') &&
|
|
UNSAFE_CMD_CHARS.test(launcher)
|
|
) {
|
|
// The cmd.exe relaunch below hands its command line over verbatim
|
|
// (windowsVerbatimArguments), which drops Node's escaping safety net:
|
|
// a launcher path with cmd metacharacters would be re-tokenized into
|
|
// extra commands. The update itself already landed, so skip the
|
|
// relaunch instead of spawning a malformed command line.
|
|
process.stderr.write(
|
|
'Update successful! The new version will be used on your next run.\n',
|
|
);
|
|
process.exit(0);
|
|
// Unreachable in production; keeps tests with a mocked exit honest.
|
|
return;
|
|
}
|
|
const relaunchResult =
|
|
process.platform === 'win32' && launcher.endsWith('.cmd')
|
|
? spawnSync(
|
|
process.env['ComSpec'] ?? 'cmd.exe',
|
|
['/d', '/s', '/c', `""${launcher}""`],
|
|
// cmd.exe does not understand Node's MSVCRT argv escaping: it
|
|
// rewrites every embedded quote of the ""…"" idiom as \", and
|
|
// cmd /s rule 2 then strips the line down to a literal
|
|
// \"\"path\"\" program name (#12687). Hand the line over
|
|
// verbatim, as shellExecutionService already does for cmd.
|
|
{
|
|
stdio: 'inherit',
|
|
env: relaunchEnv,
|
|
windowsVerbatimArguments: true,
|
|
},
|
|
)
|
|
: spawnSync(launcher, [], {
|
|
stdio: 'inherit',
|
|
env: relaunchEnv,
|
|
});
|
|
if (relaunchResult.error) {
|
|
process.stderr.write(
|
|
'Update successful! The new version will be used on your next run.\n',
|
|
);
|
|
exitOnSpawnError(launcher, relaunchResult.error);
|
|
} else if (relaunchResult.signal) {
|
|
process.kill(process.pid, relaunchResult.signal);
|
|
} else {
|
|
process.exit(relaunchResult.status ?? 1);
|
|
}
|
|
};
|
|
|
|
// A fresh Node image (the spawned or relaunched CLI) reaches the cache only
|
|
// through NODE_COMPILE_CACHE; enabling it here also resolves the default
|
|
// directory and honours NODE_DISABLE_COMPILE_CACHE. Like the serve fast
|
|
// path, the value then reaches tool subprocesses too.
|
|
const { default: module } = await import('node:module');
|
|
const compileCache = module.enableCompileCache?.();
|
|
const compileCacheEnv =
|
|
!process.env['NODE_COMPILE_CACHE'] &&
|
|
compileCache?.status === module.constants?.compileCacheStatus?.ENABLED &&
|
|
compileCache?.directory
|
|
? { NODE_COMPILE_CACHE: compileCache.directory }
|
|
: {};
|
|
|
|
// Bun cannot expose gc at runtime (no v8.setFlagsFromString), so it keeps
|
|
// the child with --expose-gc in argv, as Windows does.
|
|
if (process.platform !== 'win32' && !process.versions.bun) {
|
|
// Running the CLI in this process instead of a child saves a Node boot and
|
|
// the idle launcher's memory for the whole session. The memory-pressure
|
|
// monitor's global.gc() comes from the runtime flag instead of argv.
|
|
const [{ setFlagsFromString }, { runInNewContext }] = await Promise.all([
|
|
import('node:v8'),
|
|
import('node:vm'),
|
|
]);
|
|
setFlagsFromString('--expose-gc');
|
|
globalThis.gc ??= runInNewContext('gc');
|
|
process.on('exit', (code) => {
|
|
if (code === UPDATE_COMPLETE_EXIT_CODE) relaunchAfterUpdate();
|
|
});
|
|
Object.assign(process.env, compileCacheEnv);
|
|
process.argv.splice(1, Infinity, cliPath, ...cliArgs);
|
|
await import(pathToFileURL(cliPath).href);
|
|
} else {
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
['--expose-gc', cliPath, ...cliArgs],
|
|
{
|
|
stdio: 'inherit',
|
|
env: {
|
|
...process.env,
|
|
...compileCacheEnv,
|
|
QWEN_CODE_LAUNCHER_PID: String(process.pid),
|
|
},
|
|
},
|
|
);
|
|
if (result.error) {
|
|
exitOnSpawnError(`${process.execPath} ${cliPath}`, result.error);
|
|
} else if (result.signal) {
|
|
process.kill(process.pid, result.signal);
|
|
} else if (result.status !== UPDATE_COMPLETE_EXIT_CODE) {
|
|
process.exit(result.status ?? 1);
|
|
} else {
|
|
relaunchAfterUpdate();
|
|
}
|
|
}
|
|
}
|