@ravefox/space 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -205
- package/README.md +80 -31
- package/bin/space.js +106 -0
- package/lib/agents.js +119 -0
- package/lib/credentials.js +95 -0
- package/lib/daemon.js +294 -0
- package/lib/harness.js +166 -0
- package/lib/http.js +101 -0
- package/lib/login.js +68 -0
- package/lib/mcp.js +34 -0
- package/lib/runtime.js +241 -0
- package/lib/store.js +96 -0
- package/lib/tools.js +93 -0
- package/package.json +27 -46
- package/NOTICE +0 -55
- package/assets/webhook-sdk/python/bot.py +0 -63
- package/assets/webhook-sdk/python/commonly.py +0 -232
- package/node_modules/@commonlyai/seats/README.md +0 -25
- package/node_modules/@commonlyai/seats/package.json +0 -22
- package/node_modules/@commonlyai/seats/src/adapters/hermes.js +0 -264
- package/node_modules/@commonlyai/seats/src/adapters/stub.js +0 -24
- package/node_modules/@commonlyai/seats/src/contract.js +0 -48
- package/node_modules/@commonlyai/seats/src/failures.js +0 -44
- package/node_modules/@commonlyai/seats/src/harness-plan.js +0 -329
- package/node_modules/@commonlyai/seats/src/index.js +0 -22
- package/node_modules/@commonlyai/seats/src/process-group.js +0 -269
- package/node_modules/@commonlyai/seats/src/record/format.js +0 -737
- package/node_modules/@commonlyai/seats/src/record/otel.js +0 -238
- package/node_modules/@commonlyai/seats/src/runtime-observation.js +0 -29
- package/node_modules/@commonlyai/seats/src/seat-model.js +0 -17
- package/node_modules/@commonlyai/seats/src/supervisor.js +0 -308
- package/node_modules/@commonlyai/seats/src/trace/events.js +0 -46
- package/node_modules/@commonlyai/seats/src/trace/store.js +0 -276
- package/node_modules/@commonlyai/seats/src/turn-processes.js +0 -226
- package/node_modules/@commonlyai/seats/src/upstream-refusal.js +0 -418
- package/node_modules/@commonlyai/seats/src/valet/env.js +0 -201
- package/node_modules/@commonlyai/seats/src/valet/grant.js +0 -123
- package/node_modules/@commonlyai/seats/src/valet/permissions.js +0 -213
- package/node_modules/@commonlyai/seats/src/wire/provider-proxy.js +0 -529
- package/skills/commonly/SKILL.md +0 -233
- package/src/commands/agent.js +0 -6334
- package/src/commands/daemon.js +0 -933
- package/src/commands/dev.js +0 -153
- package/src/commands/doctor.js +0 -141
- package/src/commands/inbox.js +0 -330
- package/src/commands/login.js +0 -194
- package/src/commands/mcp.js +0 -56
- package/src/commands/pod.js +0 -120
- package/src/commands/task.js +0 -279
- package/src/commands/verify.js +0 -48
- package/src/index.js +0 -99
- package/src/lib/adapter-login.js +0 -76
- package/src/lib/adapters/broker-bridge-entry.js +0 -76
- package/src/lib/adapters/claude.js +0 -2207
- package/src/lib/adapters/codex-broker-bridge.mjs +0 -298
- package/src/lib/adapters/codex.js +0 -1898
- package/src/lib/adapters/dsh.js +0 -513
- package/src/lib/adapters/goose.js +0 -510
- package/src/lib/adapters/hermes.js +0 -18
- package/src/lib/adapters/index.js +0 -68
- package/src/lib/adapters/opencode.js +0 -1681
- package/src/lib/adapters/pi-commonly-mcp.mjs +0 -71
- package/src/lib/adapters/pi-mcp-client.mjs +0 -617
- package/src/lib/adapters/pi-shell-env.mjs +0 -41
- package/src/lib/adapters/pi.js +0 -1025
- package/src/lib/adapters/qwen.js +0 -523
- package/src/lib/adapters/seat-model.js +0 -8
- package/src/lib/adapters/stub.js +0 -1
- package/src/lib/adapters/tested-version.js +0 -106
- package/src/lib/api.js +0 -205
- package/src/lib/claim-outcome.js +0 -128
- package/src/lib/claimed-work-gate.js +0 -73
- package/src/lib/cli-hint.js +0 -33
- package/src/lib/commit-attribution.js +0 -391
- package/src/lib/config.js +0 -155
- package/src/lib/content-refusal.js +0 -94
- package/src/lib/credential-file.js +0 -208
- package/src/lib/daemon-agent-states.js +0 -95
- package/src/lib/daemon-hint.js +0 -18
- package/src/lib/daemon-logs.js +0 -43
- package/src/lib/daemon-service-windows.js +0 -263
- package/src/lib/daemon-service.js +0 -486
- package/src/lib/daemon-state.js +0 -84
- package/src/lib/daemon-store.js +0 -119
- package/src/lib/daemon-supervisor.js +0 -629
- package/src/lib/daemon-watch.js +0 -115
- package/src/lib/declared-mcp-guard.js +0 -284
- package/src/lib/default-environment.js +0 -207
- package/src/lib/device-login.js +0 -126
- package/src/lib/digest-frame.js +0 -151
- package/src/lib/doctor.js +0 -291
- package/src/lib/egress-proxy.js +0 -160
- package/src/lib/enforcement.js +0 -946
- package/src/lib/environment.js +0 -681
- package/src/lib/first-output-watchdog.js +0 -191
- package/src/lib/git-readonly-server.js +0 -138
- package/src/lib/git-readonly.js +0 -463
- package/src/lib/git-remote-commonly.mjs +0 -100
- package/src/lib/grant-broker-guard.js +0 -345
- package/src/lib/hooks-config.js +0 -238
- package/src/lib/host-gh-broker.js +0 -152
- package/src/lib/host-gh-shim.mjs +0 -98
- package/src/lib/inbox.js +0 -483
- package/src/lib/instance-proxy.js +0 -76
- package/src/lib/machine-host.js +0 -29
- package/src/lib/mcp-credential-delivery.js +0 -121
- package/src/lib/mcp-first-run.js +0 -50
- package/src/lib/mcp-home.js +0 -644
- package/src/lib/mcp-ready.js +0 -68
- package/src/lib/mcp-server-version.js +0 -123
- package/src/lib/mcp-token.js +0 -36
- package/src/lib/mcp-warm-child.mjs +0 -412
- package/src/lib/memory-bridge.js +0 -191
- package/src/lib/memory-import.js +0 -156
- package/src/lib/missing-harness.js +0 -31
- package/src/lib/model-unavailable.js +0 -154
- package/src/lib/native-runtime-inventory.js +0 -40
- package/src/lib/outsider-grant-mcp.js +0 -82
- package/src/lib/own-key-launch.js +0 -42
- package/src/lib/pod-catch-up.js +0 -55
- package/src/lib/pod-focus.js +0 -224
- package/src/lib/pod-tail.js +0 -68
- package/src/lib/poll-liveness.js +0 -103
- package/src/lib/poll-retry.js +0 -81
- package/src/lib/poller.js +0 -200
- package/src/lib/provider-proxy.js +0 -42
- package/src/lib/public-seat-mcp-flag.js +0 -36
- package/src/lib/public-tool-error.js +0 -60
- package/src/lib/public-withheld-tools.js +0 -48
- package/src/lib/reply-receipt.js +0 -55
- package/src/lib/resolve-on-path.js +0 -22
- package/src/lib/runtime-inventory.js +0 -56
- package/src/lib/runtime-observation.js +0 -37
- package/src/lib/sandbox/bwrap.js +0 -275
- package/src/lib/sandbox/codex-perimeter.js +0 -16
- package/src/lib/sandbox/host-check.js +0 -268
- package/src/lib/sandbox/mode.js +0 -49
- package/src/lib/sandbox/opencode-loopback.mjs +0 -68
- package/src/lib/sandbox/seatbelt.js +0 -684
- package/src/lib/sandbox/turn-dir.js +0 -56
- package/src/lib/sandbox/windows-unsandboxed.js +0 -183
- package/src/lib/sandbox/workspace-git-config.js +0 -243
- package/src/lib/sandbox/workspace-pin.js +0 -158
- package/src/lib/seat-runtime-controls.js +0 -61
- package/src/lib/seat-stop.js +0 -97
- package/src/lib/server-skills.js +0 -32
- package/src/lib/session-recycle.js +0 -219
- package/src/lib/session-store.js +0 -215
- package/src/lib/skill-prompt.js +0 -74
- package/src/lib/skills-import.js +0 -113
- package/src/lib/spawn-credential.js +0 -402
- package/src/lib/spawn-retry.js +0 -311
- package/src/lib/stable-install.js +0 -176
- package/src/lib/supervised-turn.js +0 -352
- package/src/lib/task.js +0 -235
- package/src/lib/turn-failure-notice.js +0 -177
- package/src/lib/unconfined-seat-guard.js +0 -94
- package/src/lib/update-check.js +0 -192
- package/src/lib/upstream-refusal.js +0 -1
- package/src/lib/webhook-server.js +0 -109
- package/src/lib/windows-command.js +0 -393
- package/src/lib/workspace-git.js +0 -87
- package/src/lib/workspace-trust.js +0 -162
package/src/lib/adapters/qwen.js
DELETED
|
@@ -1,523 +0,0 @@
|
|
|
1
|
-
/** Qwen Code 0.25.1 and 0.26.2 wrapper. Public turns have fresh HOME, config and keys. */
|
|
2
|
-
import { requireSeatModel } from './seat-model.js';
|
|
3
|
-
import { isPatchAfterNewestTested, probeVersion } from './tested-version.js';
|
|
4
|
-
import * as childProcess from 'child_process';
|
|
5
|
-
import { spawn as childSpawn, spawnSync } from 'child_process';
|
|
6
|
-
import { lstatSync, realpathSync } from 'fs';
|
|
7
|
-
import {
|
|
8
|
-
chmod, mkdir, mkdtemp, readFile, rm, writeFile,
|
|
9
|
-
} from 'fs/promises';
|
|
10
|
-
import { tmpdir } from 'os';
|
|
11
|
-
import { basename, dirname, isAbsolute, join } from 'path';
|
|
12
|
-
import { buildMemoryPreamble } from '../memory-bridge.js';
|
|
13
|
-
import { deliverSeatCredential } from '../mcp-credential-delivery.js';
|
|
14
|
-
import { prepareMcpSpawn, publicMcpUnavailableError, selectedMcpVersionDir } from '../mcp-home.js';
|
|
15
|
-
import { armCredentialRelease, writeCredentialFile } from '../credential-file.js';
|
|
16
|
-
import { MCP_READY_FILE_ENV, bridgeReadyFile, commonlyMcpReadyFile } from '../mcp-ready.js';
|
|
17
|
-
import { normalizeSandboxTrust, refuseWorkspaceTrust, validateEnvironmentSpec } from '../environment.js';
|
|
18
|
-
import { resolvePublicSandboxMode } from '../sandbox/mode.js';
|
|
19
|
-
import { detectBwrap, wrapArgvWithBwrap } from '../sandbox/bwrap.js';
|
|
20
|
-
import { wrapArgvWithSeatbelt } from '../sandbox/seatbelt.js';
|
|
21
|
-
import { refuseWorkspaceHoldingTurnDirs } from '../sandbox/turn-dir.js';
|
|
22
|
-
import { resolveProviderKeyFile, startProviderProxy } from '../provider-proxy.js';
|
|
23
|
-
import { adapterFailure, spawnCredentials } from '../upstream-refusal.js';
|
|
24
|
-
import { publicSeatWithheldTools } from '../public-withheld-tools.js';
|
|
25
|
-
import { isOurGrantBroker } from '../grant-broker-guard.js';
|
|
26
|
-
import { brokerBridgeEntry } from './broker-bridge-entry.js';
|
|
27
|
-
import { describeMcpCommand } from '../mcp-server-version.js';
|
|
28
|
-
|
|
29
|
-
// Add a version only with a passing real clean-box sandbox smoke that names it.
|
|
30
|
-
const TESTED_QWEN_VERSIONS = Object.freeze(['0.25.1', '0.26.2']);
|
|
31
|
-
// What keeps a broker-holding seat off its credential is a list of denied
|
|
32
|
-
// built-ins, so a version with a file-reading tool the list doesn't name could
|
|
33
|
-
// reach it. A broker rides only a version that passed the grant proof
|
|
34
|
-
// (qwen.grant-broker.public-smoke.test.mjs), compared exactly: no later patch
|
|
35
|
-
// and no host override. Narrower than TESTED_QWEN_VERSIONS on purpose.
|
|
36
|
-
export const GRANT_TESTED_QWEN_VERSIONS = Object.freeze(['0.26.2']);
|
|
37
|
-
const ALLOW_UNTESTED_QWEN_VERSION_ENV = 'COMMONLY_QWEN_ALLOW_UNTESTED_VERSION';
|
|
38
|
-
const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
39
|
-
const TERMINATION_GRACE_MS = 5000;
|
|
40
|
-
const PUBLIC_SAFE_ENV = /^(?:PATH|LANG|LC_[A-Z_]+|TERM|USER|LOGNAME|NO_COLOR|CI|SSL_CERT_FILE|SSL_CERT_DIR)$/;
|
|
41
|
-
const PROXY_ENV = [
|
|
42
|
-
'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY', 'NO_PROXY',
|
|
43
|
-
'http_proxy', 'https_proxy', 'all_proxy', 'no_proxy',
|
|
44
|
-
];
|
|
45
|
-
const EXCLUDED_TOOLS = [
|
|
46
|
-
'run_shell_command', 'exec', 'edit', 'notebook_edit', 'write_file',
|
|
47
|
-
'agent', 'create_sub_session', 'task_create', 'team_create',
|
|
48
|
-
'workflow', 'advisor',
|
|
49
|
-
// Legacy aliases. Deny both names even if Qwen changes when it canonicalizes.
|
|
50
|
-
'replace', 'task',
|
|
51
|
-
'web_fetch', 'web_search',
|
|
52
|
-
];
|
|
53
|
-
// Qwen can read its own config directory. That directory also contains the
|
|
54
|
-
// per-spawn MCP credential file, so public turns cannot expose OS read tools.
|
|
55
|
-
const PUBLIC_READ_TOOLS = ['read_file', 'grep_search', 'search_file_content', 'glob', 'list_directory', 'lsp'];
|
|
56
|
-
|
|
57
|
-
// What a public turn adds to --exclude-tools. Both spellings of a Commonly MCP
|
|
58
|
-
// tool, under the usual server name and any this turn declares, as with the
|
|
59
|
-
// aliases above (public-withheld-tools.js; self-install and memory writes, F118,
|
|
60
|
-
// F145).
|
|
61
|
-
export const publicExcludedTools = (serverNames, ctx) => {
|
|
62
|
-
const names = [...new Set(['commonly', ...serverNames])];
|
|
63
|
-
return [
|
|
64
|
-
...PUBLIC_READ_TOOLS,
|
|
65
|
-
...publicSeatWithheldTools(ctx).flatMap((tool) => [tool, ...names.map((name) => `mcp__${name}__${tool}`)]),
|
|
66
|
-
];
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
const parseVersion = (output) => {
|
|
70
|
-
const match = String(output || '').trim().match(/^(?:qwen(?:\s+code|-code)?\s+)?v?(\d+\.\d+\.\d+)$/i);
|
|
71
|
-
return match ? match[1] : null;
|
|
72
|
-
};
|
|
73
|
-
|
|
74
|
-
// Async and cached per binary (tested-version.js probeVersion): a synchronous
|
|
75
|
-
// probe stalled every other seat's provider proxy on the daemon's loop. The
|
|
76
|
-
// namespace import is read at call time, so a suite that mocks child_process
|
|
77
|
-
// without execFile falls back to the real one.
|
|
78
|
-
const readVersion = (binary, env) => probeVersion(binary, {
|
|
79
|
-
env, timeoutMs: 5000, parse: parseVersion, execFileImpl: childProcess.execFile,
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
const resolvedBinary = (env) => {
|
|
83
|
-
const where = spawnSync('which', ['qwen'], { encoding: 'utf8', env });
|
|
84
|
-
return where.status === 0 ? (where.stdout || '').trim() || 'qwen' : 'qwen';
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const withProbeEnv = async (source, run) => {
|
|
88
|
-
const home = await mkdtemp(join(tmpdir(), 'commonly-qwen-version-'));
|
|
89
|
-
try {
|
|
90
|
-
const settingsPath = join(home, 'settings.json');
|
|
91
|
-
await writeFile(settingsPath, JSON.stringify({
|
|
92
|
-
privacy: { usageStatisticsEnabled: false },
|
|
93
|
-
}), { mode: 0o600 });
|
|
94
|
-
const env = Object.fromEntries(Object.entries(source)
|
|
95
|
-
.filter(([name]) => PUBLIC_SAFE_ENV.test(name)));
|
|
96
|
-
env.HOME = home;
|
|
97
|
-
env.QWEN_HOME = home;
|
|
98
|
-
env.QWEN_RUNTIME_DIR = home;
|
|
99
|
-
env.QWEN_CODE_SYSTEM_SETTINGS_PATH = settingsPath;
|
|
100
|
-
env.QWEN_USAGE_STATISTICS_ENABLED = 'false';
|
|
101
|
-
for (const name of PROXY_ENV) env[name] = '';
|
|
102
|
-
// Awaited here, so the probe home outlives an async probe.
|
|
103
|
-
return await run(env);
|
|
104
|
-
} finally {
|
|
105
|
-
await rm(home, { recursive: true, force: true });
|
|
106
|
-
}
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const substitute = (value, ctx) => String(value).replace(/\$\{(COMMONLY_[A-Z_]+)\}/g, (whole, name) => ({
|
|
110
|
-
COMMONLY_TOKEN_FILE: ctx.credentialFile,
|
|
111
|
-
COMMONLY_API_URL: ctx.instanceUrl,
|
|
112
|
-
COMMONLY_INSTANCE_URL: ctx.instanceUrl,
|
|
113
|
-
}[name] || whole));
|
|
114
|
-
|
|
115
|
-
const isCommonlyStdio = (server) => server?.name === 'commonly'
|
|
116
|
-
&& (server.transport || 'stdio') === 'stdio'
|
|
117
|
-
&& Boolean(describeMcpCommand(server.command)?.isCommonly);
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* The turn's servers. A public seat holds a broker only on a proven version,
|
|
121
|
-
* with the Commonly server as its one other entry, as Goose requires: a second
|
|
122
|
-
* server could read the credential file from the jail, or replace an entry by
|
|
123
|
-
* name and inherit its tool approval. Otherwise the broker goes and the turn runs.
|
|
124
|
-
*/
|
|
125
|
-
export const withoutUnprovenBroker = (servers, { version, isPublic, instanceUrl }) => {
|
|
126
|
-
if (!isPublic || !Array.isArray(servers)) return servers;
|
|
127
|
-
const brokers = servers.filter((server) => isOurGrantBroker(server, { instanceUrl }));
|
|
128
|
-
if (!brokers.length) return servers;
|
|
129
|
-
const others = servers.filter((server) => !brokers.includes(server));
|
|
130
|
-
const proven = GRANT_TESTED_QWEN_VERSIONS.includes(version);
|
|
131
|
-
const alone = others.every(isCommonlyStdio);
|
|
132
|
-
if (proven && alone) return servers;
|
|
133
|
-
const why = proven
|
|
134
|
-
? 'another MCP server is declared beside it'
|
|
135
|
-
: `not on Qwen ${version || 'unknown'}, which hasn't passed the grant proof (${GRANT_TESTED_QWEN_VERSIONS.join(', ')})`;
|
|
136
|
-
for (const broker of brokers) {
|
|
137
|
-
// eslint-disable-next-line no-console
|
|
138
|
-
console.warn(`[qwen] grant broker ${broker.name}: ${why}, so this turn runs without it.`);
|
|
139
|
-
}
|
|
140
|
-
return others;
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
export const buildQwenMcpConfig = (servers, ctx) => {
|
|
144
|
-
const mcpServers = {};
|
|
145
|
-
for (const server of servers || []) {
|
|
146
|
-
if (!/^[A-Za-z0-9_-]+$/.test(server?.name || '')) continue;
|
|
147
|
-
const transport = server.transport || 'stdio';
|
|
148
|
-
if (transport === 'stdio') {
|
|
149
|
-
if (!Array.isArray(server.command) || !server.command.length) continue;
|
|
150
|
-
const command = prepareMcpSpawn(server.command, {
|
|
151
|
-
apiUrl: ctx.instanceUrl, publicSeat: ctx.publicSeat,
|
|
152
|
-
});
|
|
153
|
-
if (!command) {
|
|
154
|
-
ctx.unavailableMcp?.push(server.name);
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
const delivery = deliverSeatCredential({ ...server, command }, {
|
|
158
|
-
credentialFile: ctx.credentialFile,
|
|
159
|
-
label: 'qwen',
|
|
160
|
-
});
|
|
161
|
-
const args = command.slice(1).map((part) => substitute(part, ctx));
|
|
162
|
-
const env = Object.fromEntries(Object.entries(delivery.env)
|
|
163
|
-
.map(([name, value]) => [name, substitute(value, ctx)]));
|
|
164
|
-
// F460: on a public seat our server names the file it writes once it has
|
|
165
|
-
// read the credential (a path, not a secret). An older warm build refuses.
|
|
166
|
-
const readyPath = commonlyMcpReadyFile(server, command, ctx, { env });
|
|
167
|
-
if (readyPath) env[MCP_READY_FILE_ENV] = readyPath;
|
|
168
|
-
const serialized = JSON.stringify({ command: command[0], args, env });
|
|
169
|
-
if (serialized.includes('${COMMONLY_AGENT_TOKEN}')
|
|
170
|
-
|| (ctx.runtimeToken && serialized.includes(ctx.runtimeToken))) {
|
|
171
|
-
throw new Error(`Qwen MCP server ${server.name} has no file-backed credential delivery`);
|
|
172
|
-
}
|
|
173
|
-
mcpServers[server.name] = {
|
|
174
|
-
command: command[0], args, env,
|
|
175
|
-
...(server.cwd ? { cwd: server.cwd } : {}),
|
|
176
|
-
// `--exclude-tools` refuses these at the call and still lists them for
|
|
177
|
-
// the model (measured on 0.26.2), so the server entry drops them too.
|
|
178
|
-
...(ctx.publicSeat ? { excludeTools: [...publicSeatWithheldTools(ctx)] } : {}),
|
|
179
|
-
};
|
|
180
|
-
const versionDir = ctx.publicSeat && selectedMcpVersionDir(command);
|
|
181
|
-
if (versionDir) ctx.mcpReadDirectories?.push(versionDir);
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
|
-
if (transport !== 'http' || typeof server.url !== 'string') {
|
|
185
|
-
throw new Error(`Qwen MCP server ${server.name} has unsupported transport`);
|
|
186
|
-
}
|
|
187
|
-
// F175d: Qwen sends these headers literally (no `${VAR}`, no file), so the
|
|
188
|
-
// broker runs behind the stdio bridge. Public seats only: a private seat
|
|
189
|
-
// has no jail, so the guard withholds its broker, and an entry that reaches
|
|
190
|
-
// here anyway falls through to the refusal below.
|
|
191
|
-
if (ctx.publicSeat && isOurGrantBroker(server, { instanceUrl: ctx.instanceUrl })) {
|
|
192
|
-
const entry = brokerBridgeEntry(server, ctx);
|
|
193
|
-
if (entry.skip) {
|
|
194
|
-
// eslint-disable-next-line no-console
|
|
195
|
-
console.warn(`[qwen] grant broker ${server.name}: ${entry.skip}, so it is not wired for this spawn.`);
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
const readyPath = bridgeReadyFile(server, ctx);
|
|
199
|
-
mcpServers[server.name] = {
|
|
200
|
-
command: entry.command,
|
|
201
|
-
args: entry.args,
|
|
202
|
-
env: readyPath ? { ...entry.env, [MCP_READY_FILE_ENV]: readyPath } : entry.env,
|
|
203
|
-
};
|
|
204
|
-
ctx.mcpReadDirectories?.push(entry.readPath);
|
|
205
|
-
ctx.bridgedBrokers?.push(server.name);
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
const httpUrl = substitute(server.url, ctx);
|
|
209
|
-
const headers = Object.fromEntries(Object.entries(server.headers || {})
|
|
210
|
-
.map(([name, value]) => [name, substitute(value, ctx)]));
|
|
211
|
-
const serialized = JSON.stringify({ httpUrl, headers });
|
|
212
|
-
if (serialized.includes('${COMMONLY_AGENT_TOKEN}')
|
|
213
|
-
|| (ctx.runtimeToken && serialized.includes(ctx.runtimeToken))) {
|
|
214
|
-
throw new Error(`Qwen MCP server ${server.name} requires a credential transport Qwen can keep out of config`);
|
|
215
|
-
}
|
|
216
|
-
mcpServers[server.name] = { httpUrl, headers };
|
|
217
|
-
}
|
|
218
|
-
return { mcpServers };
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
const makeChildEnv = (source, home, settingsPath, proxy) => {
|
|
222
|
-
const env = Object.fromEntries(Object.entries(source)
|
|
223
|
-
.filter(([name]) => PUBLIC_SAFE_ENV.test(name)));
|
|
224
|
-
for (const name of PROXY_ENV) env[name] = '';
|
|
225
|
-
env.HOME = home;
|
|
226
|
-
env.QWEN_HOME = home;
|
|
227
|
-
env.QWEN_RUNTIME_DIR = home;
|
|
228
|
-
env.QWEN_CODE_SYSTEM_SETTINGS_PATH = settingsPath;
|
|
229
|
-
env.QWEN_USAGE_STATISTICS_ENABLED = 'false';
|
|
230
|
-
env.OPENAI_BASE_URL = proxy.baseURL;
|
|
231
|
-
env.OPENAI_API_KEY = proxy.bearer;
|
|
232
|
-
return env;
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
const parseStream = (line, state) => {
|
|
236
|
-
let event;
|
|
237
|
-
try { event = JSON.parse(line); } catch { return; }
|
|
238
|
-
if (event.type === 'assistant' && Array.isArray(event.message?.content)) {
|
|
239
|
-
state.text = event.message.content
|
|
240
|
-
.filter((part) => part.type === 'text' && typeof part.text === 'string')
|
|
241
|
-
.map((part) => part.text).join('');
|
|
242
|
-
}
|
|
243
|
-
if (event.type === 'result') {
|
|
244
|
-
if (typeof event.result === 'string') state.text = event.result;
|
|
245
|
-
if (event.is_error) state.error = event.result || 'Qwen returned an error result';
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
const runQwen = ({ cmd, args, cwd, env, timeoutMs, spawnImpl, credentials }) => new Promise((resolve, reject) => {
|
|
250
|
-
const child = (spawnImpl || childSpawn)(cmd, args, {
|
|
251
|
-
cwd, env, stdio: ['ignore', 'pipe', 'pipe'],
|
|
252
|
-
});
|
|
253
|
-
const state = { text: '', error: null, buffer: '' };
|
|
254
|
-
let stderr = '';
|
|
255
|
-
let timedOut = false;
|
|
256
|
-
let settled = false;
|
|
257
|
-
const finish = (error, result) => {
|
|
258
|
-
if (settled) return;
|
|
259
|
-
settled = true;
|
|
260
|
-
clearTimeout(timer);
|
|
261
|
-
clearTimeout(killTimer);
|
|
262
|
-
if (error) reject(error); else resolve(result);
|
|
263
|
-
};
|
|
264
|
-
const timer = setTimeout(() => {
|
|
265
|
-
timedOut = true;
|
|
266
|
-
child.kill('SIGTERM');
|
|
267
|
-
killTimer = setTimeout(() => child.kill('SIGKILL'), TERMINATION_GRACE_MS);
|
|
268
|
-
}, timeoutMs);
|
|
269
|
-
let killTimer;
|
|
270
|
-
child.stdout?.on('data', (chunk) => {
|
|
271
|
-
state.buffer += chunk.toString();
|
|
272
|
-
if (state.buffer.length > 1024 * 1024) {
|
|
273
|
-
finish(new Error('Qwen stream line exceeded 1 MiB'));
|
|
274
|
-
child.kill('SIGTERM');
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
while (state.buffer.includes('\n')) {
|
|
278
|
-
const end = state.buffer.indexOf('\n');
|
|
279
|
-
parseStream(state.buffer.slice(0, end), state);
|
|
280
|
-
state.buffer = state.buffer.slice(end + 1);
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
child.stderr?.on('data', (chunk) => { stderr = (stderr + chunk.toString()).slice(-8192); });
|
|
284
|
-
child.on('error', (error) => finish(error));
|
|
285
|
-
child.on('close', (code) => {
|
|
286
|
-
if (state.buffer) parseStream(state.buffer, state);
|
|
287
|
-
if (timedOut) return finish(new Error(`qwen timed out after ${timeoutMs}ms`));
|
|
288
|
-
if (code !== 0 || state.error) {
|
|
289
|
-
return finish(adapterFailure('qwen', state.error || stderr.trim() || `exit ${code}`, {
|
|
290
|
-
credentials, exitCode: code, limit: 500,
|
|
291
|
-
}));
|
|
292
|
-
}
|
|
293
|
-
return finish(null, { text: state.text.trim() });
|
|
294
|
-
});
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
export default {
|
|
298
|
-
requiresModel: true,
|
|
299
|
-
name: 'qwen',
|
|
300
|
-
runtimeType: 'qwen',
|
|
301
|
-
supportsProvider: true,
|
|
302
|
-
|
|
303
|
-
async validateEnvironment(environment, workspacePath) {
|
|
304
|
-
const validation = validateEnvironmentSpec(environment);
|
|
305
|
-
if (!validation.ok) throw new Error(validation.errors.join('; '));
|
|
306
|
-
if (!environment?.provider || !environment?.model) {
|
|
307
|
-
throw new Error('Qwen requires environment.provider and environment.model');
|
|
308
|
-
}
|
|
309
|
-
return resolveProviderKeyFile(environment.provider, workspacePath, 'Qwen', { environment });
|
|
310
|
-
},
|
|
311
|
-
|
|
312
|
-
async detect() {
|
|
313
|
-
return withProbeEnv(process.env, (env) => {
|
|
314
|
-
const result = spawnSync('qwen', ['--version'], { encoding: 'utf8', timeout: 5000, env });
|
|
315
|
-
if (result.status !== 0 || result.error) return null;
|
|
316
|
-
return { path: resolvedBinary(env), version: parseVersion(result.stdout) || 'unknown' };
|
|
317
|
-
});
|
|
318
|
-
},
|
|
319
|
-
|
|
320
|
-
async spawn(prompt, ctx = {}) {
|
|
321
|
-
const seatModel = requireSeatModel(ctx.environment, 'qwen', true);
|
|
322
|
-
const sandbox = normalizeSandboxTrust(ctx.environment?.sandbox);
|
|
323
|
-
refuseWorkspaceTrust(sandbox, 'qwen');
|
|
324
|
-
const isPublic = sandbox?.trust === 'public';
|
|
325
|
-
const mode = isPublic ? resolvePublicSandboxMode(sandbox) : null;
|
|
326
|
-
if (isPublic && !['workspace', 'read-only', 'bwrap'].includes(mode)) {
|
|
327
|
-
throw new Error('public Qwen seats require an enforced sandbox');
|
|
328
|
-
}
|
|
329
|
-
const provider = ctx.environment?.provider;
|
|
330
|
-
if (!provider || !ctx.environment?.model) {
|
|
331
|
-
throw new Error('Qwen seats require environment.provider and environment.model');
|
|
332
|
-
}
|
|
333
|
-
if (isPublic && mode === 'bwrap' && sandbox?.network?.policy === 'restricted') {
|
|
334
|
-
throw new Error('public Qwen provider proxy requires shared bwrap networking');
|
|
335
|
-
}
|
|
336
|
-
const keyFile = await this.validateEnvironment(ctx.environment, ctx.cwd);
|
|
337
|
-
const sourceEnv = ctx.env || process.env;
|
|
338
|
-
const { binary, version } = await withProbeEnv(sourceEnv, async (probeEnv) => {
|
|
339
|
-
const path = ctx._binaryPath || resolvedBinary(probeEnv);
|
|
340
|
-
return { binary: path, version: await readVersion(path, probeEnv) };
|
|
341
|
-
});
|
|
342
|
-
if (!TESTED_QWEN_VERSIONS.includes(version)) {
|
|
343
|
-
const versions = TESTED_QWEN_VERSIONS.join(', ');
|
|
344
|
-
const allowOverride = process.env[ALLOW_UNTESTED_QWEN_VERSION_ENV] === '1';
|
|
345
|
-
const patchAfterTested = isPatchAfterNewestTested(version, TESTED_QWEN_VERSIONS);
|
|
346
|
-
if (isPublic && !allowOverride && !patchAfterTested) {
|
|
347
|
-
throw new Error(`Public Qwen seats refuse version ${version || 'unknown'}; smoke-tested: ${versions}. Set ${ALLOW_UNTESTED_QWEN_VERSION_ENV}=1 in the host process to override.`);
|
|
348
|
-
}
|
|
349
|
-
let outcome = 'trusted seat continues';
|
|
350
|
-
if (isPublic) outcome = allowOverride ? 'host override accepted' : 'a patch release on the newest smoke-tested line, so the public seat continues';
|
|
351
|
-
// eslint-disable-next-line no-console
|
|
352
|
-
console.warn(`[qwen] installed ${version || 'unknown'} differs from smoke-tested ${versions}; ${outcome}`);
|
|
353
|
-
}
|
|
354
|
-
// npm launches through a symlink into the package. Qwen loads chunks next
|
|
355
|
-
// to that entry file, so public sandboxes need that exact read-only tree.
|
|
356
|
-
const symlinkedBinary = isPublic && lstatSync(binary).isSymbolicLink();
|
|
357
|
-
const executable = symlinkedBinary
|
|
358
|
-
? join(realpathSync(dirname(binary)), basename(binary)) : binary;
|
|
359
|
-
const packageDir = symlinkedBinary ? dirname(realpathSync(executable)) : null;
|
|
360
|
-
if (isPublic) await refuseWorkspaceHoldingTurnDirs({ workspace: ctx.cwd, seat: 'public Qwen agents' });
|
|
361
|
-
const tempDir = await mkdtemp(join(tmpdir(), 'commonly-qwen-'));
|
|
362
|
-
await chmod(tempDir, 0o700);
|
|
363
|
-
// Keep child-writable state separate from adapter-owned config and keys.
|
|
364
|
-
const home = await mkdtemp(join(tmpdir(), 'commonly-qwen-home-'));
|
|
365
|
-
await chmod(home, 0o700);
|
|
366
|
-
const settingsPath = join(tempDir, 'settings.json');
|
|
367
|
-
const mcpPath = join(tempDir, 'mcp.json');
|
|
368
|
-
let proxy;
|
|
369
|
-
let credentialRelease = null;
|
|
370
|
-
let reply = null;
|
|
371
|
-
let cleanupFailure = null;
|
|
372
|
-
try {
|
|
373
|
-
const credential = writeCredentialFile(ctx.runtimeToken, {
|
|
374
|
-
agentName: ctx.agentName || 'agent', root: tempDir,
|
|
375
|
-
});
|
|
376
|
-
// F460: the jail writes only the seat's home, so a public seat's MCP
|
|
377
|
-
// servers write their ready files there.
|
|
378
|
-
const readyFileDir = isPublic && credential ? join(home, 'mcp-ready') : null;
|
|
379
|
-
if (readyFileDir) await mkdir(readyFileDir, { recursive: true, mode: 0o700 });
|
|
380
|
-
const readyFiles = [];
|
|
381
|
-
proxy = await startProviderProxy({
|
|
382
|
-
provider, keyFile, tokenFile: join(tempDir, 'provider-proxy-token'), adapterLabel: 'Qwen',
|
|
383
|
-
});
|
|
384
|
-
const proxyWithBearer = { ...proxy, bearer: (await readFile(proxy.tokenFile, 'utf8')).trim() };
|
|
385
|
-
const unavailableMcp = [];
|
|
386
|
-
const mcpReadDirectories = [];
|
|
387
|
-
const bridgedBrokers = [];
|
|
388
|
-
const turnServers = withoutUnprovenBroker(ctx.environment?.mcp, {
|
|
389
|
-
version, isPublic, instanceUrl: ctx.instanceUrl || '',
|
|
390
|
-
});
|
|
391
|
-
const mcp = buildQwenMcpConfig(turnServers, {
|
|
392
|
-
credentialFile: credential?.path || '',
|
|
393
|
-
instanceUrl: ctx.instanceUrl || '',
|
|
394
|
-
runtimeToken: ctx.runtimeToken || '',
|
|
395
|
-
publicSeat: isPublic,
|
|
396
|
-
ownerWoken: ctx.ownerWoken,
|
|
397
|
-
unavailableMcp,
|
|
398
|
-
mcpReadDirectories,
|
|
399
|
-
bridgedBrokers,
|
|
400
|
-
readyFileDir,
|
|
401
|
-
readyFiles,
|
|
402
|
-
});
|
|
403
|
-
if (unavailableMcp.length) throw publicMcpUnavailableError(unavailableMcp);
|
|
404
|
-
await Promise.all([
|
|
405
|
-
writeFile(settingsPath, JSON.stringify({
|
|
406
|
-
privacy: { usageStatisticsEnabled: false },
|
|
407
|
-
general: { enableAutoUpdate: false },
|
|
408
|
-
}), { mode: 0o600 }),
|
|
409
|
-
writeFile(mcpPath, JSON.stringify(mcp), { mode: 0o600 }),
|
|
410
|
-
]);
|
|
411
|
-
const env = makeChildEnv(sourceEnv, home, settingsPath, proxyWithBearer);
|
|
412
|
-
// QWEN_HOME is discarded after every turn, so Qwen session ids cannot
|
|
413
|
-
// be resumed. The kernel memory preamble supplies continuity instead.
|
|
414
|
-
const fullPrompt = buildMemoryPreamble(prompt, ctx.memoryLongTerm, {
|
|
415
|
-
freshSession: true,
|
|
416
|
-
// F118, F145: a public seat reads its memory, and is offered a write
|
|
417
|
-
// only on a turn its owner woke.
|
|
418
|
-
memoryReadOnly: isPublic && ctx.ownerWoken !== true,
|
|
419
|
-
});
|
|
420
|
-
const allowedTools = [
|
|
421
|
-
...(Object.hasOwn(mcp.mcpServers, 'commonly') ? ['commonly'] : []), ...bridgedBrokers,
|
|
422
|
-
].map((name) => `mcp__${name}__*`);
|
|
423
|
-
const args = [
|
|
424
|
-
'--bare', '--safe-mode', '--auth-type', 'openai', '-m', seatModel,
|
|
425
|
-
'--mcp-config', mcpPath, '--exclude-tools', [
|
|
426
|
-
...EXCLUDED_TOOLS, ...(isPublic ? publicExcludedTools(Object.keys(mcp.mcpServers), ctx) : []),
|
|
427
|
-
].join(','),
|
|
428
|
-
// Qwen ignores settings permissions in bare/safe mode, but still
|
|
429
|
-
// honours CLI allow rules. Any seat needs its declared Commonly tools
|
|
430
|
-
// approved before non-interactive execution begins, public included:
|
|
431
|
-
// without it a public seat's commonly_post_message was declined
|
|
432
|
-
// (D247 walk, 11-04). Public parity with Claude's --allowedTools
|
|
433
|
-
// mcp__commonly__*; the withheld tools stay in --exclude-tools above.
|
|
434
|
-
// The broker parks every write for the owner itself (F175d).
|
|
435
|
-
...(allowedTools.length ? ['--allowed-tools', allowedTools.join(',')] : []),
|
|
436
|
-
'-o', 'stream-json', '-p', fullPrompt,
|
|
437
|
-
];
|
|
438
|
-
let cmd = executable;
|
|
439
|
-
let spawnArgs = args;
|
|
440
|
-
if (isPublic && mode === 'bwrap') {
|
|
441
|
-
if (!isAbsolute(executable)) throw new Error('public Qwen seats require an absolute qwen executable path');
|
|
442
|
-
// bwrap binds a symlink's target at the link's path, so npm's `.bin/qwen`
|
|
443
|
-
// would lose the `./cli.js` beside it. The real entry keeps its package.
|
|
444
|
-
const bwrapEntry = symlinkedBinary ? realpathSync(executable) : executable;
|
|
445
|
-
const bwrap = (ctx._detectBwrap || detectBwrap)();
|
|
446
|
-
if (!bwrap.available) throw new Error(`public Qwen seats require bwrap: ${bwrap.error}`);
|
|
447
|
-
[cmd, ...spawnArgs] = (ctx._wrapArgvWithBwrap || wrapArgvWithBwrap)([bwrapEntry, ...args], {
|
|
448
|
-
...ctx.environment,
|
|
449
|
-
sandbox: {
|
|
450
|
-
...sandbox,
|
|
451
|
-
filesystem: {
|
|
452
|
-
...(sandbox?.filesystem || {}),
|
|
453
|
-
'write-outside': [
|
|
454
|
-
...(sandbox?.filesystem?.['write-outside'] || []), home,
|
|
455
|
-
],
|
|
456
|
-
},
|
|
457
|
-
},
|
|
458
|
-
}, {
|
|
459
|
-
workspacePath: ctx.cwd,
|
|
460
|
-
homePath: home,
|
|
461
|
-
readOnlyPaths: [tempDir, bwrapEntry, packageDir, process.execPath, ...mcpReadDirectories].filter(Boolean),
|
|
462
|
-
});
|
|
463
|
-
} else if (isPublic) {
|
|
464
|
-
if ((ctx._platform || process.platform) !== 'darwin') {
|
|
465
|
-
throw new Error(`public Qwen sandbox.mode=${mode} requires Seatbelt on macOS or bwrap on Linux`);
|
|
466
|
-
}
|
|
467
|
-
[cmd, ...spawnArgs] = (ctx._wrapArgvWithSeatbelt || wrapArgvWithSeatbelt)([executable, ...args], {
|
|
468
|
-
workspacePath: ctx.cwd,
|
|
469
|
-
workspaceAccess: mode === 'read-only' ? 'read' : 'write',
|
|
470
|
-
executablePath: executable,
|
|
471
|
-
statePath: home,
|
|
472
|
-
// Public seat: the CLI made this workspace, so it is pinned (F197).
|
|
473
|
-
pinWorkspace: true,
|
|
474
|
-
mcpConfigDir: tempDir,
|
|
475
|
-
loopbackNetworkPorts: [proxy.port],
|
|
476
|
-
readOnlyDirectories: [...(packageDir ? [packageDir] : []), ...mcpReadDirectories],
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
const requestedTimeout = Number(ctx.timeoutMs);
|
|
480
|
-
const timeoutMs = Number.isFinite(requestedTimeout) && requestedTimeout > 0
|
|
481
|
-
? Math.min(requestedTimeout, DEFAULT_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
|
|
482
|
-
// F460: on a public seat the credential goes once every MCP reader has it
|
|
483
|
-
// (their ready files), or at the deadline. A delete that fails ends the turn.
|
|
484
|
-
let releaseFailure = null;
|
|
485
|
-
let child = null;
|
|
486
|
-
const spawnImpl = (...spawnArgs) => {
|
|
487
|
-
child = (ctx._spawnImpl || childSpawn)(...spawnArgs);
|
|
488
|
-
return child;
|
|
489
|
-
};
|
|
490
|
-
if (readyFileDir) {
|
|
491
|
-
credentialRelease = armCredentialRelease(credential, {
|
|
492
|
-
readyFiles: readyFiles.map(({ path }) => path),
|
|
493
|
-
label: 'public Qwen seat',
|
|
494
|
-
// eslint-disable-next-line no-console
|
|
495
|
-
warn: (text) => console.warn(`[qwen] ${text}`),
|
|
496
|
-
onFailure: (err) => { releaseFailure = err; child?.kill?.('SIGKILL'); },
|
|
497
|
-
...ctx._credentialRelease,
|
|
498
|
-
});
|
|
499
|
-
if (credentialRelease.error) throw credentialRelease.error;
|
|
500
|
-
}
|
|
501
|
-
const result = await runQwen({
|
|
502
|
-
cmd, args: spawnArgs, cwd: ctx.cwd, env, timeoutMs,
|
|
503
|
-
spawnImpl,
|
|
504
|
-
credentials: [...spawnCredentials(ctx, []), proxyWithBearer.bearer],
|
|
505
|
-
}).catch((err) => { throw releaseFailure || err; });
|
|
506
|
-
if (releaseFailure) throw releaseFailure;
|
|
507
|
-
reply = { text: result.text, newSessionId: null };
|
|
508
|
-
} finally {
|
|
509
|
-
credentialRelease?.stop();
|
|
510
|
-
try { await proxy?.close(); } catch { /* best effort */ }
|
|
511
|
-
const removed = await Promise.allSettled([tempDir, home].map((path) => rm(path, { recursive: true, force: true })));
|
|
512
|
-
cleanupFailure = removed.find((outcome) => outcome.status === 'rejected')?.reason || null;
|
|
513
|
-
}
|
|
514
|
-
// Reached only when the turn worked. A cleanup failure is then the turn's
|
|
515
|
-
// error: on a private seat that rm is the one delete of the credential, so
|
|
516
|
-
// the turn must not report success over it. When another error ended the
|
|
517
|
-
// turn (a failed release, say), it propagates past this and is kept.
|
|
518
|
-
if (cleanupFailure) throw cleanupFailure;
|
|
519
|
-
return reply;
|
|
520
|
-
},
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
export { TESTED_QWEN_VERSIONS, ALLOW_UNTESTED_QWEN_VERSION_ENV, makeChildEnv, parseVersion };
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { requireSeatModel as requireDeclaredModel } from '@commonlyai/seats/seat-model';
|
|
2
|
-
|
|
3
|
-
/** Fleet runners opt in; BYO users retain their own CLI's model default. */
|
|
4
|
-
export const seatModelRequired = () => process.env.COMMONLY_REQUIRE_SEAT_MODEL === '1';
|
|
5
|
-
|
|
6
|
-
export const requireSeatModel = (environment, adapterName, required = seatModelRequired()) => (
|
|
7
|
-
requireDeclaredModel(environment, adapterName, required)
|
|
8
|
-
);
|
package/src/lib/adapters/stub.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { default } from '@commonlyai/seats/adapters/stub';
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
// A namespace import, read at call time: suites that mock child_process with
|
|
2
|
-
// only spawn/spawnSync still load this module.
|
|
3
|
-
import * as childProcess from 'node:child_process';
|
|
4
|
-
import { realpathSync, statSync } from 'node:fs';
|
|
5
|
-
import { basename } from 'node:path';
|
|
6
|
-
import { resolveOnPath } from '../resolve-on-path.js';
|
|
7
|
-
|
|
8
|
-
// Public seats run only on a harness version smoke-tested in its sandbox, with
|
|
9
|
-
// one tolerance: a stable patch release after the newest tested one, on that
|
|
10
|
-
// same major.minor line. Harnesses ship patches every few days, and refusing
|
|
11
|
-
// them refused every new BYO user on the current npm release (D245, 11-04).
|
|
12
|
-
// A newer minor or major, an older patch, an older line, or a prerelease still
|
|
13
|
-
// refuses until it is smoke-tested and added to the adapter's list.
|
|
14
|
-
const STABLE = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
15
|
-
const SEMVER = /^(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?$/;
|
|
16
|
-
|
|
17
|
-
export const isPatchAfterNewestTested = (version, tested) => {
|
|
18
|
-
const installed = STABLE.exec(String(version || ''));
|
|
19
|
-
const newest = STABLE.exec(String(tested[tested.length - 1] || ''));
|
|
20
|
-
if (!installed || !newest) return false;
|
|
21
|
-
return Number(installed[1]) === Number(newest[1])
|
|
22
|
-
&& Number(installed[2]) === Number(newest[2])
|
|
23
|
-
&& Number(installed[3]) > Number(newest[3]);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
// A floor rather than a list: the version, or anything later, prereleases of a
|
|
27
|
-
// later version included, so the list's prerelease rule above doesn't apply. A
|
|
28
|
-
// prerelease of the floor comes before it, and an unreadable version meets nothing.
|
|
29
|
-
export const meetsMinimumVersion = (version, minimum) => {
|
|
30
|
-
const installed = SEMVER.exec(String(version ?? ''));
|
|
31
|
-
const floor = STABLE.exec(String(minimum ?? ''));
|
|
32
|
-
if (!installed || !floor) return false;
|
|
33
|
-
for (let part = 1; part <= 3; part += 1) {
|
|
34
|
-
const difference = Number(installed[part]) - Number(floor[part]);
|
|
35
|
-
if (difference !== 0) return difference > 0;
|
|
36
|
-
}
|
|
37
|
-
return !installed[4];
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
export const PUBLIC_CODEX_VERSION_CODE = 'public_codex_version';
|
|
41
|
-
|
|
42
|
-
// `found` stays in this message, which only the seat's log shows. The line a pod
|
|
43
|
-
// sees is built from `requiredVersion` (turn-failure-notice.js).
|
|
44
|
-
// `seat` names which seat refused: a private seat under its permission profile
|
|
45
|
-
// (F387) needs the same tested version as a public one.
|
|
46
|
-
export const publicCodexVersionError = ({ found, required, seat = 'public seat' }) => {
|
|
47
|
-
const error = new Error(found
|
|
48
|
-
? `${seat} refused this turn: Codex ${found} is older than this seat needs. `
|
|
49
|
-
+ `Upgrade Codex to at least ${required} and start the agent again.`
|
|
50
|
-
: `${seat} refused this turn: could not read the Codex version. `
|
|
51
|
-
+ `Check that \`codex --version\` works for this agent, upgrade Codex to at least ${required} `
|
|
52
|
-
+ 'if it is older, and start the agent again.');
|
|
53
|
-
error.code = PUBLIC_CODEX_VERSION_CODE;
|
|
54
|
-
error.requiredVersion = required;
|
|
55
|
-
return error;
|
|
56
|
-
};
|
|
57
|
-
|
|
58
|
-
// The harness's own `--version`, asked without blocking the event loop and
|
|
59
|
-
// remembered per binary. A daemon runs every seat in one process, beside the
|
|
60
|
-
// loopback provider proxies of other seats' live turns, so a synchronous probe
|
|
61
|
-
// stalled all of them while a cold harness started (up to its timeout).
|
|
62
|
-
//
|
|
63
|
-
// The cache key is the binary's real path; its mtime decides freshness, so a
|
|
64
|
-
// harness upgraded in place is probed again. Only a parsed version is kept: a
|
|
65
|
-
// failed or unparseable probe is asked again next turn, never remembered.
|
|
66
|
-
//
|
|
67
|
-
// A bare name ('goose', 'qwen') is resolved through the probe env's PATH, as
|
|
68
|
-
// execFile would resolve it, never against the daemon's working directory: a
|
|
69
|
-
// cwd lookup missed the cache every turn and could key it on a same-named file
|
|
70
|
-
// there. What runs is that resolved real path, so the binary that answers is
|
|
71
|
-
// the one the key names. A name that does not resolve runs as given, uncached.
|
|
72
|
-
//
|
|
73
|
-
// `args` come before `--version`, for a launch that is an interpreter and a
|
|
74
|
-
// script (a Windows npm shim). The interpreter's path says nothing about which
|
|
75
|
-
// script it would run, so that probe is never cached.
|
|
76
|
-
const versionCache = new Map();
|
|
77
|
-
|
|
78
|
-
const resolveBinary = (binary, env) => {
|
|
79
|
-
const name = String(binary || '');
|
|
80
|
-
if (!name) return null;
|
|
81
|
-
const located = basename(name) === name ? resolveOnPath(name, env?.PATH) : name;
|
|
82
|
-
if (!located) return null;
|
|
83
|
-
try {
|
|
84
|
-
const key = realpathSync(located);
|
|
85
|
-
return { key, mtimeMs: statSync(key).mtimeMs };
|
|
86
|
-
} catch {
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
export const probeVersion = async (binary, {
|
|
92
|
-
args = [], env, timeoutMs = 30000, parse, execFileImpl = childProcess.execFile, cache = versionCache,
|
|
93
|
-
} = {}) => {
|
|
94
|
-
const resolved = args.length ? null : resolveBinary(binary, env);
|
|
95
|
-
const key = resolved?.key || null;
|
|
96
|
-
const hit = key ? cache.get(key) : null;
|
|
97
|
-
if (hit && hit.mtimeMs === resolved.mtimeMs) return hit.version;
|
|
98
|
-
const stdout = await new Promise((resolve) => {
|
|
99
|
-
execFileImpl(key || binary, [...args, '--version'], { encoding: 'utf8', env, timeout: timeoutMs }, (error, out) => {
|
|
100
|
-
resolve(error ? null : out);
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
const version = stdout === null ? null : parse(stdout);
|
|
104
|
-
if (version && key) cache.set(key, { mtimeMs: resolved.mtimeMs, version });
|
|
105
|
-
return version;
|
|
106
|
-
};
|