@commonlyai/cli 0.1.100 → 0.1.101
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/package.json +1 -1
- package/src/lib/adapters/claude.js +26 -5
- package/src/lib/adapters/codex.js +23 -6
- package/src/lib/adapters/opencode.js +21 -4
- package/src/lib/adapters/qwen.js +19 -5
- package/src/lib/mcp-home.js +21 -4
- package/src/lib/mcp-warm-child.mjs +11 -5
package/package.json
CHANGED
|
@@ -75,7 +75,7 @@ import {
|
|
|
75
75
|
import { CREDENTIAL_FILE_VAR, CREDENTIAL_KEY, writeCredentialFile } from '../credential-file.js';
|
|
76
76
|
import { supportedGitHooks } from '../commit-attribution.js';
|
|
77
77
|
import { deliverSeatCredential, withholdRuntimeCredential } from '../mcp-credential-delivery.js';
|
|
78
|
-
import { prepareMcpSpawn } from '../mcp-home.js';
|
|
78
|
+
import { prepareMcpSpawn, PUBLIC_MCP_UNAVAILABLE_NOTICE, selectedMcpVersionDir } from '../mcp-home.js';
|
|
79
79
|
import { buildMemoryPreamble } from '../memory-bridge.js';
|
|
80
80
|
import { adapterFailure, spawnCredentials } from '../upstream-refusal.js';
|
|
81
81
|
|
|
@@ -412,8 +412,14 @@ const buildMcpConfig = (mcpServers, ctx = {}) => {
|
|
|
412
412
|
// credential channel below is decided on that same command, so the two can
|
|
413
413
|
// never disagree about which release is running.
|
|
414
414
|
const spawnCommand = server.command
|
|
415
|
-
? prepareMcpSpawn(server.command, {
|
|
415
|
+
? prepareMcpSpawn(server.command, {
|
|
416
|
+
apiUrl: ctx.instanceUrl, publicSeat: ctx.publicSeat,
|
|
417
|
+
})
|
|
416
418
|
: null;
|
|
419
|
+
if (server.command && !spawnCommand) {
|
|
420
|
+
ctx.unavailableMcp?.push(server.name);
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
417
423
|
const executed = spawnCommand ? { ...server, command: spawnCommand } : server;
|
|
418
424
|
const entry = { type: server.transport || 'stdio' };
|
|
419
425
|
if (server.url) entry.url = server.url;
|
|
@@ -439,6 +445,8 @@ const buildMcpConfig = (mcpServers, ctx = {}) => {
|
|
|
439
445
|
}
|
|
440
446
|
if (headerHelper) entry.headersHelper = headerHelper.command;
|
|
441
447
|
mcpServersMap[server.name] = entry;
|
|
448
|
+
const versionDir = ctx.publicSeat && selectedMcpVersionDir(spawnCommand);
|
|
449
|
+
if (versionDir) ctx.mcpReadDirectories?.push(versionDir);
|
|
442
450
|
}
|
|
443
451
|
return { mcpServers: mcpServersMap };
|
|
444
452
|
};
|
|
@@ -462,10 +470,14 @@ const createMcpConfig = async (mcpServers, ctx = {}) => {
|
|
|
462
470
|
const dynamicHeaderHelpers = credential?.path
|
|
463
471
|
? await createTokenHeaderHelpers(mcpServers, dir)
|
|
464
472
|
: new Map();
|
|
473
|
+
const unavailableMcp = [];
|
|
474
|
+
const mcpReadDirectories = [];
|
|
465
475
|
const config = buildMcpConfig(mcpServers, {
|
|
466
476
|
...ctx,
|
|
467
477
|
credentialFile: credential?.path || null,
|
|
468
478
|
dynamicHeaderHelpers,
|
|
479
|
+
unavailableMcp,
|
|
480
|
+
mcpReadDirectories,
|
|
469
481
|
});
|
|
470
482
|
await writeFile(
|
|
471
483
|
file,
|
|
@@ -489,7 +501,7 @@ const createMcpConfig = async (mcpServers, ctx = {}) => {
|
|
|
489
501
|
+ `inherits it. Put the credential on the entry's env to get the file channel.`,
|
|
490
502
|
);
|
|
491
503
|
}
|
|
492
|
-
return { dir, file, credential, expansionEnv };
|
|
504
|
+
return { dir, file, credential, expansionEnv, unavailableMcp, mcpReadDirectories };
|
|
493
505
|
} catch (err) {
|
|
494
506
|
try { await rm(dir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
495
507
|
throw err;
|
|
@@ -636,6 +648,7 @@ const prepareArgv = async (innerArgv, ctx) => {
|
|
|
636
648
|
mcpConfigDir: ctx.mcpConfigDir,
|
|
637
649
|
executablePaths: [...mcpExecutables, ...commitHookExecutables, ...originalHookExecutables],
|
|
638
650
|
allowClaudeRuntimeAccess: true,
|
|
651
|
+
readOnlyDirectories: ctx.mcpReadDirectories || [],
|
|
639
652
|
});
|
|
640
653
|
return {
|
|
641
654
|
cmd: wrapped[0],
|
|
@@ -664,7 +677,7 @@ const prepareArgv = async (innerArgv, ctx) => {
|
|
|
664
677
|
const claudeBin = resolveClaudePath(claudeEnv);
|
|
665
678
|
const wrapped = wrapArgvWithBwrap([claudeBin, ...innerArgv], env, {
|
|
666
679
|
workspacePath: ctx.cwd,
|
|
667
|
-
readOnlyPaths: [ctx.mcpConfigDir, commitHooksPath, originalHooksPath].filter(Boolean),
|
|
680
|
+
readOnlyPaths: [ctx.mcpConfigDir, commitHooksPath, originalHooksPath, ...(ctx.mcpReadDirectories || [])].filter(Boolean),
|
|
668
681
|
});
|
|
669
682
|
return { cmd: wrapped[0], args: wrapped.slice(1), env: claudeEnv };
|
|
670
683
|
}
|
|
@@ -785,8 +798,12 @@ export default {
|
|
|
785
798
|
runtimeToken: ctx.runtimeToken,
|
|
786
799
|
instanceUrl: ctx.instanceUrl,
|
|
787
800
|
agentName: ctx.agentName,
|
|
801
|
+
publicSeat: publicNativeSandbox,
|
|
788
802
|
});
|
|
789
803
|
}
|
|
804
|
+
if (mcpConfig?.unavailableMcp.length) {
|
|
805
|
+
baseArgs[1] = `${fullPrompt}\n\n${PUBLIC_MCP_UNAVAILABLE_NOTICE}`;
|
|
806
|
+
}
|
|
790
807
|
const claudeEnv = buildClaudeEnv(
|
|
791
808
|
ctx.env,
|
|
792
809
|
publicClaudeState,
|
|
@@ -800,6 +817,7 @@ export default {
|
|
|
800
817
|
...ctx,
|
|
801
818
|
mcpConfigPath: mcpConfig?.file || null,
|
|
802
819
|
mcpConfigDir: mcpConfig?.dir || null,
|
|
820
|
+
mcpReadDirectories: mcpConfig?.mcpReadDirectories || [],
|
|
803
821
|
publicClaudeState,
|
|
804
822
|
claudeEnv,
|
|
805
823
|
};
|
|
@@ -829,11 +847,14 @@ export default {
|
|
|
829
847
|
// as fresh too: otherwise a session-recovery path is the one fresh
|
|
830
848
|
// session that misses the durable-state cue.
|
|
831
849
|
const freshPrompt = buildPrompt(prompt, ctx.memoryLongTerm, { freshSession: true });
|
|
832
|
-
const retryBase = ['-p',
|
|
850
|
+
const retryBase = ['-p', mcpConfig?.unavailableMcp.length
|
|
851
|
+
? `${freshPrompt}\n\n${PUBLIC_MCP_UNAVAILABLE_NOTICE}` : freshPrompt,
|
|
852
|
+
'--output-format', 'text', '--session-id', freshId, ...modelArgs];
|
|
833
853
|
const retryCtx = {
|
|
834
854
|
...ctx,
|
|
835
855
|
mcpConfigPath: mcpConfig?.file || null,
|
|
836
856
|
mcpConfigDir: mcpConfig?.dir || null,
|
|
857
|
+
mcpReadDirectories: mcpConfig?.mcpReadDirectories || [],
|
|
837
858
|
publicClaudeState,
|
|
838
859
|
claudeEnv: buildClaudeEnv(
|
|
839
860
|
ctx.env,
|
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
import { CREDENTIAL_KEY, writeCredentialFile } from '../credential-file.js';
|
|
61
61
|
import { supportedGitHooks } from '../commit-attribution.js';
|
|
62
62
|
import { deliverSeatCredential, withholdRuntimeCredential } from '../mcp-credential-delivery.js';
|
|
63
|
-
import { prepareMcpSpawn } from '../mcp-home.js';
|
|
63
|
+
import { prepareMcpSpawn, PUBLIC_MCP_UNAVAILABLE_NOTICE, selectedMcpVersionDir } from '../mcp-home.js';
|
|
64
64
|
import { buildMemoryPreamble } from '../memory-bridge.js';
|
|
65
65
|
import { isLegacySandboxTrust, normalizeSandboxTrust } from '../environment.js';
|
|
66
66
|
import { adapterFailure, spawnCredentials } from '../upstream-refusal.js';
|
|
@@ -140,6 +140,8 @@ const toml = (s) => JSON.stringify(String(s));
|
|
|
140
140
|
const buildMcpOverrideArgs = (mcpServers, ctx = {}) => {
|
|
141
141
|
const flags = [];
|
|
142
142
|
const forwardedEnv = {};
|
|
143
|
+
const unavailableMcp = [];
|
|
144
|
+
const mcpReadDirectories = [];
|
|
143
145
|
for (const server of mcpServers || []) {
|
|
144
146
|
const transport = typeof server?.transport === 'string' ? server.transport.trim().toLowerCase() : 'stdio';
|
|
145
147
|
if (!server?.name || transport !== 'stdio'
|
|
@@ -149,7 +151,13 @@ const buildMcpOverrideArgs = (mcpServers, ctx = {}) => {
|
|
|
149
151
|
// same command, so the two can never disagree about which release runs. The
|
|
150
152
|
// rewrite is before `substitutePlaceholders` because it introduces no
|
|
151
153
|
// placeholder: it is a path this host resolved.
|
|
152
|
-
const spawnCommand = prepareMcpSpawn(server.command, {
|
|
154
|
+
const spawnCommand = prepareMcpSpawn(server.command, {
|
|
155
|
+
apiUrl: ctx.instanceUrl, publicSeat: ctx.publicSeat,
|
|
156
|
+
});
|
|
157
|
+
if (!spawnCommand) {
|
|
158
|
+
unavailableMcp.push(server.name);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
153
161
|
// A declared credential is rewritten to the file channel before anything is
|
|
154
162
|
// substituted, so for our server the token is not token-bearing at all: it
|
|
155
163
|
// lands in `env={...}` as a path and never reaches `env_vars`, which is the
|
|
@@ -185,6 +193,8 @@ const buildMcpOverrideArgs = (mcpServers, ctx = {}) => {
|
|
|
185
193
|
continue;
|
|
186
194
|
}
|
|
187
195
|
flags.push('-c', `mcp_servers.${server.name}.command=${toml(command)}`);
|
|
196
|
+
const versionDir = ctx.publicSeat && selectedMcpVersionDir(spawnCommand);
|
|
197
|
+
if (versionDir) mcpReadDirectories.push(versionDir);
|
|
188
198
|
// The user opted into every server present in the environment spec.
|
|
189
199
|
// Public permission profiles + approval_policy=never otherwise auto-deny
|
|
190
200
|
// side-effecting MCP calls, silently removing the agent's Commonly tools.
|
|
@@ -237,7 +247,7 @@ const buildMcpOverrideArgs = (mcpServers, ctx = {}) => {
|
|
|
237
247
|
+ 'get the file channel.',
|
|
238
248
|
);
|
|
239
249
|
}
|
|
240
|
-
return { flags, forwardedEnv };
|
|
250
|
+
return { flags, forwardedEnv, unavailableMcp, mcpReadDirectories };
|
|
241
251
|
};
|
|
242
252
|
|
|
243
253
|
const PUBLIC_PERMISSION_PROFILE = 'commonly_public';
|
|
@@ -301,6 +311,7 @@ const preparePublicCodexHome = async (ctx) => {
|
|
|
301
311
|
const publicPermissionProfileFlags = (
|
|
302
312
|
mode,
|
|
303
313
|
commitAttributionEnv = null,
|
|
314
|
+
mcpReadDirectories = [],
|
|
304
315
|
) => {
|
|
305
316
|
if (!PUBLIC_SANDBOX_MODES.has(mode)) {
|
|
306
317
|
throw new Error(
|
|
@@ -339,9 +350,10 @@ const publicPermissionProfileFlags = (
|
|
|
339
350
|
'"~/.aws"="deny"',
|
|
340
351
|
'"~/.config"="deny"',
|
|
341
352
|
'"/private/tmp"="deny"',
|
|
342
|
-
`":workspace_roots"={"."="${workspaceAccess}",".commonly/**"="deny",".codex/**"="deny","*.env"="deny","*/*.env"="deny","*/*/*.env"="deny"}`,
|
|
353
|
+
`":workspace_roots"={"."="${workspaceAccess}",".commonly/**"="deny",".codex/**"="deny","*.env"="deny","*/*.env"="deny","*/*/*.env"="deny","**/.npmrc"="deny","**/.yarnrc*"="deny"}`,
|
|
343
354
|
...(commitHooksPath ? [`${toml(commitHooksPath)}="read"`] : []),
|
|
344
355
|
...originalHookPaths.map((hookPath) => `${toml(hookPath)}="read"`),
|
|
356
|
+
...mcpReadDirectories.map((path) => `${toml(path)}="read"`),
|
|
345
357
|
].join(',');
|
|
346
358
|
const shellEnvironment = [
|
|
347
359
|
'PATH', 'HOME', 'TMPDIR', 'LANG', 'LC_*',
|
|
@@ -391,6 +403,7 @@ const buildArgs = ({
|
|
|
391
403
|
model = null,
|
|
392
404
|
effort = null,
|
|
393
405
|
commitAttributionEnv = null,
|
|
406
|
+
mcpReadDirectories = [],
|
|
394
407
|
}) => {
|
|
395
408
|
const publicSandbox = publicSandboxMode !== null;
|
|
396
409
|
// `--dangerously-bypass-approvals-and-sandbox` disables codex CLI's
|
|
@@ -413,6 +426,7 @@ const buildArgs = ({
|
|
|
413
426
|
...publicPermissionProfileFlags(
|
|
414
427
|
publicSandboxMode,
|
|
415
428
|
commitAttributionEnv,
|
|
429
|
+
mcpReadDirectories,
|
|
416
430
|
),
|
|
417
431
|
]
|
|
418
432
|
: ['--dangerously-bypass-approvals-and-sandbox'];
|
|
@@ -588,10 +602,12 @@ export default {
|
|
|
588
602
|
});
|
|
589
603
|
|
|
590
604
|
try {
|
|
605
|
+
const sandbox = normalizeSandboxTrust(ctx.environment?.sandbox);
|
|
591
606
|
const mcp = buildMcpOverrideArgs(ctx.environment?.mcp, {
|
|
592
607
|
runtimeToken: ctx.runtimeToken,
|
|
593
608
|
instanceUrl: ctx.instanceUrl,
|
|
594
609
|
credentialFile: credential?.path || null,
|
|
610
|
+
publicSeat: sandbox?.trust === 'public',
|
|
595
611
|
});
|
|
596
612
|
// A derived record stores `trust: 'public'` and no mode (the block is
|
|
597
613
|
// platform-independent; see cli/src/lib/default-environment.js). Codex's
|
|
@@ -607,15 +623,16 @@ export default {
|
|
|
607
623
|
+ '(Wren 69585).',
|
|
608
624
|
);
|
|
609
625
|
}
|
|
610
|
-
const sandbox = normalizeSandboxTrust(ctx.environment?.sandbox);
|
|
611
626
|
const publicSandboxMode = sandbox?.trust === 'public'
|
|
612
627
|
? sandbox?.mode ?? 'workspace'
|
|
613
628
|
: null;
|
|
614
629
|
const args = buildArgs({
|
|
615
630
|
sessionId: ctx.sessionId || null,
|
|
616
|
-
prompt:
|
|
631
|
+
prompt: mcp.unavailableMcp.length
|
|
632
|
+
? `${fullPrompt}\n\n${PUBLIC_MCP_UNAVAILABLE_NOTICE}` : fullPrompt,
|
|
617
633
|
outputFile,
|
|
618
634
|
mcpFlags: mcp.flags,
|
|
635
|
+
mcpReadDirectories: mcp.mcpReadDirectories,
|
|
619
636
|
publicSandboxMode,
|
|
620
637
|
model: ctx.environment?.model,
|
|
621
638
|
effort: ctx.environment?.effort,
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
} from 'path';
|
|
40
40
|
import { writeCredentialFile } from '../credential-file.js';
|
|
41
41
|
import { deliverSeatCredential, withholdRuntimeCredential } from '../mcp-credential-delivery.js';
|
|
42
|
-
import { prepareMcpSpawn } from '../mcp-home.js';
|
|
42
|
+
import { prepareMcpSpawn, PUBLIC_MCP_UNAVAILABLE_NOTICE, selectedMcpVersionDir } from '../mcp-home.js';
|
|
43
43
|
import { buildMemoryPreamble } from '../memory-bridge.js';
|
|
44
44
|
import {
|
|
45
45
|
isLegacySandboxTrust,
|
|
@@ -82,6 +82,7 @@ const PUBLIC_READ_DENIES = [
|
|
|
82
82
|
'**/.env.*',
|
|
83
83
|
'**/*.env.*',
|
|
84
84
|
];
|
|
85
|
+
const PUBLIC_EDIT_DENIES = [...PUBLIC_READ_DENIES, '**/.npmrc', '**/.yarnrc*'];
|
|
85
86
|
const PUBLIC_SAFE_ENV = /^(?:PATH|LANG|LC_[A-Z_]+|TERM|USER|LOGNAME|NO_COLOR|CI|SSL_CERT_FILE|SSL_CERT_DIR)$/;
|
|
86
87
|
|
|
87
88
|
const statOrNull = async (path) => {
|
|
@@ -119,7 +120,13 @@ const buildMcpConfig = (servers, ctx = {}) => {
|
|
|
119
120
|
|
|
120
121
|
if (transport === 'stdio') {
|
|
121
122
|
if (!Array.isArray(server.command) || server.command.length === 0) continue;
|
|
122
|
-
const command = prepareMcpSpawn(server.command, {
|
|
123
|
+
const command = prepareMcpSpawn(server.command, {
|
|
124
|
+
apiUrl: ctx.instanceUrl, publicSeat: ctx.publicSeat,
|
|
125
|
+
});
|
|
126
|
+
if (!command) {
|
|
127
|
+
ctx.unavailableMcp?.push(server.name);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
123
130
|
if (runtimeToken && command.some((part) => typeof part === 'string' && part.includes(runtimeToken))) {
|
|
124
131
|
// A bearer token in argv is readable by same-user processes.
|
|
125
132
|
// eslint-disable-next-line no-console
|
|
@@ -158,6 +165,8 @@ const buildMcpConfig = (servers, ctx = {}) => {
|
|
|
158
165
|
...(Object.keys(env).length ? { environment: env } : {}),
|
|
159
166
|
...(typeof server.cwd === 'string' ? { cwd: server.cwd } : {}),
|
|
160
167
|
};
|
|
168
|
+
const versionDir = ctx.publicSeat && selectedMcpVersionDir(command);
|
|
169
|
+
if (versionDir) ctx.mcpReadDirectories?.push(versionDir);
|
|
161
170
|
continue;
|
|
162
171
|
}
|
|
163
172
|
|
|
@@ -207,8 +216,8 @@ const publicPermissions = (mode, mcpNames) => {
|
|
|
207
216
|
const edit = { '*': mode === 'read-only' ? 'deny' : 'allow' };
|
|
208
217
|
for (const pattern of PUBLIC_READ_DENIES) {
|
|
209
218
|
read[pattern] = 'deny';
|
|
210
|
-
edit[pattern] = 'deny';
|
|
211
219
|
}
|
|
220
|
+
for (const pattern of PUBLIC_EDIT_DENIES) edit[pattern] = 'deny';
|
|
212
221
|
return {
|
|
213
222
|
'*': 'deny',
|
|
214
223
|
read,
|
|
@@ -666,10 +675,15 @@ export default {
|
|
|
666
675
|
...(stateHome ? [mkdir(stateHome, { recursive: true, mode: 0o700 })] : []),
|
|
667
676
|
...(cacheHome ? [mkdir(cacheHome, { recursive: true, mode: 0o700 })] : []),
|
|
668
677
|
]);
|
|
678
|
+
const unavailableMcp = [];
|
|
679
|
+
const mcpReadDirectories = [];
|
|
669
680
|
const mcp = buildMcpConfig(ctx.environment?.mcp, {
|
|
670
681
|
runtimeToken: ctx.runtimeToken,
|
|
671
682
|
instanceUrl: ctx.instanceUrl,
|
|
672
683
|
credentialFile: credential?.path || null,
|
|
684
|
+
publicSeat: isPublic,
|
|
685
|
+
unavailableMcp,
|
|
686
|
+
mcpReadDirectories,
|
|
673
687
|
});
|
|
674
688
|
const config = {
|
|
675
689
|
permission: isPublic
|
|
@@ -703,7 +717,8 @@ export default {
|
|
|
703
717
|
credentialFile: credential?.path || null,
|
|
704
718
|
});
|
|
705
719
|
const args = buildArgs({
|
|
706
|
-
prompt:
|
|
720
|
+
prompt: unavailableMcp.length
|
|
721
|
+
? `${fullPrompt}\n\n${PUBLIC_MCP_UNAVAILABLE_NOTICE}` : fullPrompt,
|
|
707
722
|
sessionId: ctx.sessionId,
|
|
708
723
|
cwd: ctx.cwd,
|
|
709
724
|
model: qualifiedProviderModel(provider, ctx.environment?.model),
|
|
@@ -738,6 +753,7 @@ export default {
|
|
|
738
753
|
tempDir,
|
|
739
754
|
binary,
|
|
740
755
|
process.execPath,
|
|
756
|
+
...mcpReadDirectories,
|
|
741
757
|
],
|
|
742
758
|
});
|
|
743
759
|
[cmd, ...spawnArgs] = wrapped;
|
|
@@ -753,6 +769,7 @@ export default {
|
|
|
753
769
|
mcpConfigDir: tempDir,
|
|
754
770
|
loopbackNetworkPorts: providerProxy ? [providerProxy.port] : [],
|
|
755
771
|
readOnlyPaths: [],
|
|
772
|
+
readOnlyDirectories: mcpReadDirectories,
|
|
756
773
|
});
|
|
757
774
|
[cmd, ...spawnArgs] = wrapped;
|
|
758
775
|
}
|
package/src/lib/adapters/qwen.js
CHANGED
|
@@ -6,7 +6,7 @@ import { tmpdir } from 'os';
|
|
|
6
6
|
import { basename, dirname, isAbsolute, join } from 'path';
|
|
7
7
|
import { buildMemoryPreamble } from '../memory-bridge.js';
|
|
8
8
|
import { deliverSeatCredential } from '../mcp-credential-delivery.js';
|
|
9
|
-
import { prepareMcpSpawn } from '../mcp-home.js';
|
|
9
|
+
import { prepareMcpSpawn, PUBLIC_MCP_UNAVAILABLE_NOTICE, selectedMcpVersionDir } from '../mcp-home.js';
|
|
10
10
|
import { writeCredentialFile } from '../credential-file.js';
|
|
11
11
|
import { normalizeSandboxTrust, validateEnvironmentSpec } from '../environment.js';
|
|
12
12
|
import { resolvePublicSandboxMode } from '../sandbox/mode.js';
|
|
@@ -88,7 +88,13 @@ export const buildQwenMcpConfig = (servers, ctx) => {
|
|
|
88
88
|
const transport = server.transport || 'stdio';
|
|
89
89
|
if (transport === 'stdio') {
|
|
90
90
|
if (!Array.isArray(server.command) || !server.command.length) continue;
|
|
91
|
-
const command = prepareMcpSpawn(server.command, {
|
|
91
|
+
const command = prepareMcpSpawn(server.command, {
|
|
92
|
+
apiUrl: ctx.instanceUrl, publicSeat: ctx.publicSeat,
|
|
93
|
+
});
|
|
94
|
+
if (!command) {
|
|
95
|
+
ctx.unavailableMcp?.push(server.name);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
92
98
|
const delivery = deliverSeatCredential({ ...server, command }, {
|
|
93
99
|
credentialFile: ctx.credentialFile,
|
|
94
100
|
label: 'qwen',
|
|
@@ -105,6 +111,8 @@ export const buildQwenMcpConfig = (servers, ctx) => {
|
|
|
105
111
|
command: command[0], args, env,
|
|
106
112
|
...(server.cwd ? { cwd: server.cwd } : {}),
|
|
107
113
|
};
|
|
114
|
+
const versionDir = ctx.publicSeat && selectedMcpVersionDir(command);
|
|
115
|
+
if (versionDir) ctx.mcpReadDirectories?.push(versionDir);
|
|
108
116
|
continue;
|
|
109
117
|
}
|
|
110
118
|
if (transport !== 'http' || typeof server.url !== 'string') {
|
|
@@ -271,10 +279,15 @@ export default {
|
|
|
271
279
|
provider, keyFile, tokenFile: join(tempDir, 'provider-proxy-token'), adapterLabel: 'Qwen',
|
|
272
280
|
});
|
|
273
281
|
const proxyWithBearer = { ...proxy, bearer: (await readFile(proxy.tokenFile, 'utf8')).trim() };
|
|
282
|
+
const unavailableMcp = [];
|
|
283
|
+
const mcpReadDirectories = [];
|
|
274
284
|
const mcp = buildQwenMcpConfig(ctx.environment?.mcp, {
|
|
275
285
|
credentialFile: credential?.path || '',
|
|
276
286
|
instanceUrl: ctx.instanceUrl || '',
|
|
277
287
|
runtimeToken: ctx.runtimeToken || '',
|
|
288
|
+
publicSeat: isPublic,
|
|
289
|
+
unavailableMcp,
|
|
290
|
+
mcpReadDirectories,
|
|
278
291
|
});
|
|
279
292
|
await Promise.all([
|
|
280
293
|
writeFile(settingsPath, JSON.stringify({
|
|
@@ -294,7 +307,8 @@ export default {
|
|
|
294
307
|
'--mcp-config', mcpPath, '--exclude-tools', [
|
|
295
308
|
...EXCLUDED_TOOLS, ...(isPublic ? PUBLIC_READ_TOOLS : []),
|
|
296
309
|
].join(','),
|
|
297
|
-
'-o', 'stream-json', '-p',
|
|
310
|
+
'-o', 'stream-json', '-p', unavailableMcp.length
|
|
311
|
+
? `${fullPrompt}\n\n${PUBLIC_MCP_UNAVAILABLE_NOTICE}` : fullPrompt,
|
|
298
312
|
];
|
|
299
313
|
let cmd = executable;
|
|
300
314
|
let spawnArgs = args;
|
|
@@ -313,7 +327,7 @@ export default {
|
|
|
313
327
|
],
|
|
314
328
|
},
|
|
315
329
|
},
|
|
316
|
-
}, { workspacePath: ctx.cwd, homePath: home, readOnlyPaths: [tempDir, executable, packageDir, process.execPath].filter(Boolean) });
|
|
330
|
+
}, { workspacePath: ctx.cwd, homePath: home, readOnlyPaths: [tempDir, executable, packageDir, process.execPath, ...mcpReadDirectories].filter(Boolean) });
|
|
317
331
|
} else if (isPublic) {
|
|
318
332
|
if ((ctx._platform || process.platform) !== 'darwin') {
|
|
319
333
|
throw new Error(`public Qwen sandbox.mode=${mode} requires Seatbelt on macOS or bwrap on Linux`);
|
|
@@ -325,7 +339,7 @@ export default {
|
|
|
325
339
|
statePath: home,
|
|
326
340
|
mcpConfigDir: tempDir,
|
|
327
341
|
loopbackNetworkPorts: [proxy.port],
|
|
328
|
-
readOnlyDirectories: packageDir ? [packageDir] : [],
|
|
342
|
+
readOnlyDirectories: [...(packageDir ? [packageDir] : []), ...mcpReadDirectories],
|
|
329
343
|
});
|
|
330
344
|
}
|
|
331
345
|
const requestedTimeout = Number(ctx.timeoutMs);
|
package/src/lib/mcp-home.js
CHANGED
|
@@ -45,13 +45,14 @@ import {
|
|
|
45
45
|
existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync,
|
|
46
46
|
} from 'node:fs';
|
|
47
47
|
import { homedir } from 'node:os';
|
|
48
|
-
import { dirname, join, sep } from 'node:path';
|
|
48
|
+
import { dirname, join, relative, sep } from 'node:path';
|
|
49
49
|
import { fileURLToPath } from 'node:url';
|
|
50
50
|
import { isShippedCommonlyMcpCommand } from './declared-mcp-guard.js';
|
|
51
51
|
import { MCP_PACKAGE, exactVersion, parseVersion } from './mcp-server-version.js';
|
|
52
52
|
|
|
53
53
|
/** The env override, so tests (and an operator) can point the home elsewhere. */
|
|
54
54
|
export const MCP_HOME_ENV = 'COMMONLY_MCP_HOME';
|
|
55
|
+
export const PUBLIC_MCP_UNAVAILABLE_NOTICE = 'Commonly runtime notice: An MCP server was not started this turn because no host-prepared executable was ready. Do not claim to have used its tools.';
|
|
55
56
|
|
|
56
57
|
export const WARM_LOCK_NAME = '.warm.lock';
|
|
57
58
|
export const REGISTRY_CACHE_NAME = '.registry.json';
|
|
@@ -324,6 +325,14 @@ export const readBinPath = (home, version, options = {}) => {
|
|
|
324
325
|
return readBinInPackageDir(packageDirFor(home, version), options);
|
|
325
326
|
};
|
|
326
327
|
|
|
328
|
+
/** Read grant for only the verified warm build selected for this spawn. */
|
|
329
|
+
export const selectedMcpVersionDir = (command, home = mcpHomeDir()) => {
|
|
330
|
+
if (!Array.isArray(command) || command.length !== 2 || command[0] !== 'node') return null;
|
|
331
|
+
const version = relative(home, command[1]).split(sep)[0];
|
|
332
|
+
if (exactVersion(version) !== version || readBinPath(home, version) !== command[1]) return null;
|
|
333
|
+
return versionDirFor(home, version);
|
|
334
|
+
};
|
|
335
|
+
|
|
327
336
|
/**
|
|
328
337
|
* The bin of an npm PREFIX dir, which is the shape the warm installs into:
|
|
329
338
|
* `npm install --prefix <dir>` leaves `<dir>/node_modules/...`, with no version
|
|
@@ -532,11 +541,14 @@ const lockPresent = (home, { exists = defaultExists } = {}) => exists(lockPathFo
|
|
|
532
541
|
*
|
|
533
542
|
* Sync and network-free by construction — the registry check, the install and
|
|
534
543
|
* the probe all live in the child. A lock already held means another spawn is
|
|
535
|
-
* warming, so this spawn just runs the current version.
|
|
544
|
+
* warming, so this spawn just runs the current version. Public seats receive
|
|
545
|
+
* null if that version is unavailable and the fallback is a package runner;
|
|
546
|
+
* adapters omit that MCP declaration and tell the seat.
|
|
536
547
|
*/
|
|
537
548
|
export const prepareMcpSpawn = (command, {
|
|
538
549
|
home,
|
|
539
550
|
apiUrl = null,
|
|
551
|
+
publicSeat = false,
|
|
540
552
|
spawnImpl,
|
|
541
553
|
childPath,
|
|
542
554
|
now = Date.now(),
|
|
@@ -566,10 +578,15 @@ export const prepareMcpSpawn = (command, {
|
|
|
566
578
|
const due = !lockPresent(resolvedHome, { exists })
|
|
567
579
|
&& warmLooksWanted(resolvedHome, plan.version, { now, readFile });
|
|
568
580
|
if (due) kickWarm({ home: resolvedHome, apiUrl, spawnImpl, childPath });
|
|
581
|
+
if (publicSeat && resolved[0] === 'npx') {
|
|
582
|
+
warn('commonly: an MCP server is unavailable for this public turn; no host-prepared executable was ready');
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
569
585
|
return resolved;
|
|
570
586
|
} catch {
|
|
571
|
-
//
|
|
572
|
-
//
|
|
587
|
+
// Trusted seats retain the declared fallback. A public seat must not
|
|
588
|
+
// execute a package runner from the workspace even on this failure path.
|
|
589
|
+
if (publicSeat && command?.[0] === 'npx') return null;
|
|
573
590
|
return command;
|
|
574
591
|
}
|
|
575
592
|
};
|
|
@@ -107,7 +107,7 @@ export const acquireWarmLock = (home, {
|
|
|
107
107
|
};
|
|
108
108
|
|
|
109
109
|
/** Default `npm` runner. Resolves `{ ok, stdout }`; never rejects. */
|
|
110
|
-
export const runCommand = (bin, args, { timeout = REGISTRY_TIMEOUT_MS } = {}) => new Promise((resolve) => {
|
|
110
|
+
export const runCommand = (bin, args, { timeout = REGISTRY_TIMEOUT_MS, cwd } = {}) => new Promise((resolve) => {
|
|
111
111
|
let settled = false;
|
|
112
112
|
let child;
|
|
113
113
|
const done = (result) => {
|
|
@@ -116,7 +116,7 @@ export const runCommand = (bin, args, { timeout = REGISTRY_TIMEOUT_MS } = {}) =>
|
|
|
116
116
|
resolve(result);
|
|
117
117
|
};
|
|
118
118
|
try {
|
|
119
|
-
child = spawnChild(bin, args, { stdio: ['ignore', 'pipe', 'pipe'], env: process.env });
|
|
119
|
+
child = spawnChild(bin, args, { stdio: ['ignore', 'pipe', 'pipe'], env: process.env, cwd });
|
|
120
120
|
} catch (error) {
|
|
121
121
|
done({ ok: false, stdout: '', stderr: String(error && error.message) });
|
|
122
122
|
return;
|
|
@@ -153,7 +153,9 @@ export const resolveRegistryLatest = async (home, {
|
|
|
153
153
|
}
|
|
154
154
|
} catch { /* unreadable cache is just a miss */ }
|
|
155
155
|
}
|
|
156
|
-
const result = await exec('npm', ['view', MCP_PACKAGE, 'version', '--json'], {
|
|
156
|
+
const result = await exec('npm', ['view', MCP_PACKAGE, 'version', '--json'], {
|
|
157
|
+
timeout: REGISTRY_TIMEOUT_MS, cwd: home,
|
|
158
|
+
});
|
|
157
159
|
const raw = result && result.ok ? String(result.stdout || '').trim() : '';
|
|
158
160
|
let version = null;
|
|
159
161
|
if (raw) {
|
|
@@ -199,6 +201,7 @@ export const probeBin = (binPath, {
|
|
|
199
201
|
spawnImpl = spawnChild,
|
|
200
202
|
timeoutMs = PROBE_TIMEOUT_MS,
|
|
201
203
|
apiUrl = process.env.COMMONLY_WARM_API_URL || PROBE_API_URL_PLACEHOLDER,
|
|
204
|
+
cwd,
|
|
202
205
|
} = {}) => new Promise((resolve) => {
|
|
203
206
|
let settled = false;
|
|
204
207
|
let child;
|
|
@@ -213,6 +216,7 @@ export const probeBin = (binPath, {
|
|
|
213
216
|
try {
|
|
214
217
|
child = spawnImpl(process.execPath, [binPath], {
|
|
215
218
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
219
|
+
cwd,
|
|
216
220
|
env: {
|
|
217
221
|
PATH: process.env.PATH,
|
|
218
222
|
HOME: process.env.HOME,
|
|
@@ -343,7 +347,7 @@ export const warmMcpHome = async (home, {
|
|
|
343
347
|
mkdir(temp);
|
|
344
348
|
const install = await exec('npm', [
|
|
345
349
|
'install', '--prefix', temp, `${MCP_PACKAGE}@${target}`, '--no-save', '--no-audit', '--no-fund',
|
|
346
|
-
], { timeout: 5 * 60 * 1000 });
|
|
350
|
+
], { timeout: 5 * 60 * 1000, cwd: home });
|
|
347
351
|
if (!install || !install.ok) {
|
|
348
352
|
remove(temp);
|
|
349
353
|
return WARM_RESULTS.INSTALL_FAILED;
|
|
@@ -353,7 +357,9 @@ export const warmMcpHome = async (home, {
|
|
|
353
357
|
remove(temp);
|
|
354
358
|
return WARM_RESULTS.INSTALL_FAILED;
|
|
355
359
|
}
|
|
356
|
-
const started = await (spawnProbe || probe)(bin, {
|
|
360
|
+
const started = await (spawnProbe || probe)(bin, {
|
|
361
|
+
apiUrl: process.env.COMMONLY_WARM_API_URL, cwd: home,
|
|
362
|
+
});
|
|
357
363
|
if (!started) {
|
|
358
364
|
remove(temp);
|
|
359
365
|
return WARM_RESULTS.PROBE_FAILED;
|