starmux 0.13.1 → 0.15.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/CHANGELOG.md +27 -0
- package/LICENSE +1 -1
- package/README.md +20 -28
- package/dist/src/access-control.js +1 -2
- package/dist/src/access-dashboard.js +2 -3
- package/dist/src/account-access-policy.js +54 -0
- package/dist/src/authorized-workspace-roots.js +0 -1
- package/dist/src/batch-read-policy.js +30 -0
- package/dist/src/cli/instances.js +27 -187
- package/dist/src/cli/oauth.js +0 -1
- package/dist/src/cli/starmux-status.js +25 -18
- package/dist/src/cli/starmux-supervisor.js +0 -1
- package/dist/src/cli/starmux.js +47 -37
- package/dist/src/config-schemas.js +1 -10
- package/dist/src/configured-mcp-upstreams.js +0 -1
- package/dist/src/contracts.js +0 -1
- package/dist/src/current-permission-probe.js +0 -1
- package/dist/src/dc-workspace-http.js +57 -33
- package/dist/src/deny-dashboard.js +2 -3
- package/dist/src/desktop-commander-facade.js +4 -4
- package/dist/src/explicit-deny-rules.js +2 -3
- package/dist/src/global-core-control.js +108 -56
- package/dist/src/home-write-sandbox.js +0 -1
- package/dist/src/http-host-policy.js +20 -0
- package/dist/src/instance-control-protocol.js +27 -0
- package/dist/src/instance-runtime.js +215 -76
- package/dist/src/instance-store.js +28 -22
- package/dist/src/instance-worker.js +2 -3
- package/dist/src/invocation.js +1 -2
- package/dist/src/local-desktop-read-probe.js +5 -88
- package/dist/src/managed-workspace-policy.js +38 -4
- package/dist/src/mcp-results.js +0 -1
- package/dist/src/mcp-upstream-router.js +0 -1
- package/dist/src/oauth-keycloak.js +0 -1
- package/dist/src/oauth-resource.js +0 -1
- package/dist/src/ops-dashboard.js +2 -179
- package/dist/src/permissions-dashboard.js +3 -4
- package/dist/src/platform.js +6 -4
- package/dist/src/privacy-diagnostics.js +0 -1
- package/dist/src/private-mcp-activity.js +0 -1
- package/dist/src/private-store-lock.js +0 -1
- package/dist/src/project-dev-presets.js +0 -1
- package/dist/src/project-git-workflow.js +0 -1
- package/dist/src/project-workspace-policy.js +10 -6
- package/dist/src/runtime-identity.js +0 -14
- package/dist/src/runtime-paths.js +0 -3
- package/dist/src/tool-dispatch.js +45 -46
- package/dist/src/validation.js +0 -1
- package/dist/src/workspace-grants.js +1 -2
- package/package.json +7 -15
- package/scripts/prepare.mjs +1 -1
- package/CONTRIBUTING.md +0 -18
- package/SECURITY.md +0 -7
- package/app/README.md +0 -29
- package/dist/src/access-control.js.map +0 -1
- package/dist/src/access-dashboard.js.map +0 -1
- package/dist/src/authorized-workspace-roots.js.map +0 -1
- package/dist/src/cli/access.js +0 -53
- package/dist/src/cli/access.js.map +0 -1
- package/dist/src/cli/community-remote-revoke.js +0 -32
- package/dist/src/cli/community-remote-revoke.js.map +0 -1
- package/dist/src/cli/instances.js.map +0 -1
- package/dist/src/cli/live-home-settings.js +0 -35
- package/dist/src/cli/live-home-settings.js.map +0 -1
- package/dist/src/cli/mac-mcp-gateway.js +0 -257
- package/dist/src/cli/mac-mcp-gateway.js.map +0 -1
- package/dist/src/cli/oauth.js.map +0 -1
- package/dist/src/cli/permissions-owner.js +0 -27
- package/dist/src/cli/permissions-owner.js.map +0 -1
- package/dist/src/cli/service-setup.js +0 -26
- package/dist/src/cli/service-setup.js.map +0 -1
- package/dist/src/cli/setup-wizard.js +0 -149
- package/dist/src/cli/setup-wizard.js.map +0 -1
- package/dist/src/cli/starmux-status.js.map +0 -1
- package/dist/src/cli/starmux-supervisor.js.map +0 -1
- package/dist/src/cli/starmux.js.map +0 -1
- package/dist/src/cli/verify-setup.js +0 -53
- package/dist/src/cli/verify-setup.js.map +0 -1
- package/dist/src/community-remote-revocations.js +0 -135
- package/dist/src/community-remote-revocations.js.map +0 -1
- package/dist/src/config-schemas.js.map +0 -1
- package/dist/src/configured-mcp-upstreams.js.map +0 -1
- package/dist/src/contracts.js.map +0 -1
- package/dist/src/current-permission-probe.js.map +0 -1
- package/dist/src/dc-workspace-http.js.map +0 -1
- package/dist/src/deny-dashboard.js.map +0 -1
- package/dist/src/desktop-commander-facade.js.map +0 -1
- package/dist/src/explicit-deny-rules.js.map +0 -1
- package/dist/src/gateway-status-format.js +0 -66
- package/dist/src/gateway-status-format.js.map +0 -1
- package/dist/src/global-core-control.js.map +0 -1
- package/dist/src/home-write-sandbox.js.map +0 -1
- package/dist/src/instance-migration.js +0 -138
- package/dist/src/instance-migration.js.map +0 -1
- package/dist/src/instance-runtime.js.map +0 -1
- package/dist/src/instance-store.js.map +0 -1
- package/dist/src/instance-worker.js.map +0 -1
- package/dist/src/invocation.js.map +0 -1
- package/dist/src/live-profile-home-settings.js +0 -80
- package/dist/src/live-profile-home-settings.js.map +0 -1
- package/dist/src/local-desktop-read-probe.js.map +0 -1
- package/dist/src/managed-workspace-policy.js.map +0 -1
- package/dist/src/mcp-results.js.map +0 -1
- package/dist/src/mcp-upstream-router.js.map +0 -1
- package/dist/src/oauth-keycloak.js.map +0 -1
- package/dist/src/oauth-resource.js.map +0 -1
- package/dist/src/operator-cli.js +0 -559
- package/dist/src/operator-cli.js.map +0 -1
- package/dist/src/ops-dashboard.js.map +0 -1
- package/dist/src/permissions-dashboard.js.map +0 -1
- package/dist/src/platform.js.map +0 -1
- package/dist/src/privacy-diagnostics.js.map +0 -1
- package/dist/src/private-mcp-activity.js.map +0 -1
- package/dist/src/private-store-lock.js.map +0 -1
- package/dist/src/project-dev-presets.js.map +0 -1
- package/dist/src/project-git-workflow.js.map +0 -1
- package/dist/src/project-workspace-policy.js.map +0 -1
- package/dist/src/read-scope.js +0 -44
- package/dist/src/read-scope.js.map +0 -1
- package/dist/src/release1-onboarding.js +0 -120
- package/dist/src/release1-onboarding.js.map +0 -1
- package/dist/src/release1-service.js +0 -131
- package/dist/src/release1-service.js.map +0 -1
- package/dist/src/remote-auth.js +0 -47
- package/dist/src/remote-auth.js.map +0 -1
- package/dist/src/runtime-identity.js.map +0 -1
- package/dist/src/runtime-paths.js.map +0 -1
- package/dist/src/starmux-migration.js +0 -172
- package/dist/src/starmux-migration.js.map +0 -1
- package/dist/src/tool-dispatch.js.map +0 -1
- package/dist/src/validation.js.map +0 -1
- package/dist/src/workspace-grants.js.map +0 -1
- package/docs/ACCESS_CONTROL.md +0 -51
- package/docs/CLIENTS.md +0 -22
- package/docs/CONNECT_CHATGPT.md +0 -148
- package/docs/INSTALL.md +0 -62
- package/docs/INSTANCES.md +0 -83
- package/docs/OAUTH.md +0 -116
- package/docs/PLATFORMS.md +0 -29
- package/docs/README.md +0 -16
- package/docs/REMOTE_SETUP.md +0 -280
- package/docs/SECURITY.md +0 -15
- package/src/dc-workspace-http.mjs +0 -3
- package/test/fixtures/p1-fixture.txt +0 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createAccessControl } from '../access-control.js';
|
|
2
|
-
import { exists, instancePaths } from '../instance-store.js';
|
|
2
|
+
import { exists, instancePaths, usesGlobalPermissions } from '../instance-store.js';
|
|
3
3
|
import { shellQuote } from '../platform.js';
|
|
4
4
|
async function authentication(instance, status) {
|
|
5
5
|
const file = instancePaths(instance).access;
|
|
@@ -23,6 +23,8 @@ async function authentication(instance, status) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
export async function formatStarmuxStatus(instance, status, config, options = {}) {
|
|
26
|
+
// Only the live supervisor can confirm active permissions.
|
|
27
|
+
config = status.phase === 'stopped' ? status.saved?.config : status.active?.config;
|
|
26
28
|
const auth = await authentication(instance, status);
|
|
27
29
|
// Omit `-i default` for the default instance to keep everyday single-instance output clean.
|
|
28
30
|
const omitSelector = options.isDefault === true && instance.name === 'default';
|
|
@@ -37,29 +39,35 @@ export async function formatStarmuxStatus(instance, status, config, options = {}
|
|
|
37
39
|
'api-key': `API key (Bearer token; ${auth.activeKeys} active ${auth.activeKeys === 1 ? 'key' : 'keys'})`,
|
|
38
40
|
oauth: 'OAuth (browser sign-in)'
|
|
39
41
|
};
|
|
40
|
-
const saved = status.phase === '
|
|
42
|
+
const saved = status.phase === 'stopped' ? ' (saved for next start)' : ' (saved; live state unavailable)';
|
|
41
43
|
const row = (label, value) => ' ' + label.padEnd(16) + value;
|
|
42
44
|
const codingCmd = command('up --code');
|
|
43
|
-
const
|
|
45
|
+
const globalMode = status.phase === 'running' ? status.runtime?.permissionMode === 'global' : !!config && usesGlobalPermissions(config);
|
|
46
|
+
const describeMode = (value, global) => !value
|
|
44
47
|
? 'Unavailable'
|
|
45
|
-
:
|
|
46
|
-
?
|
|
47
|
-
:
|
|
48
|
+
: value.accessRole === 'trusted'
|
|
49
|
+
? value.writeScope === 'home-only' ? 'Coding (Home sandbox · edits, shell & git)' : 'Coding (OS account · edits, shell & git)'
|
|
50
|
+
: value.mode === 'read-write'
|
|
48
51
|
? 'File writes (restricted)'
|
|
49
|
-
: `Read-only
|
|
50
|
-
const
|
|
51
|
-
? '
|
|
52
|
-
:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
? `${indicator} Starmux is ${status.phase}`
|
|
57
|
-
: `${indicator} Starmux is ${status.phase} (instance: ${instance.name})`;
|
|
58
|
-
const lines = ['', title];
|
|
52
|
+
: `Read-only (${global ? 'global · OS account' : 'project scope'})`;
|
|
53
|
+
const switchHint = config && (status.phase === 'running' || status.phase === 'stopped')
|
|
54
|
+
? config.accessRole === 'trusted' ? ` (run '${command('up --read')}' to return to read-only)` : ` (run '${codingCmd}' to enable edits & shell)`
|
|
55
|
+
: '';
|
|
56
|
+
const mode = describeMode(config, globalMode) + switchHint;
|
|
57
|
+
const title = `${indicator} Starmux is ${status.phase} (instance: ${instance.name})`;
|
|
58
|
+
const lines = ['', title, row('Instance', instance.id)];
|
|
59
59
|
if (ready && status.port) {
|
|
60
60
|
lines.push(row('Listen', `http://127.0.0.1:${status.port}/mcp (this computer only)`));
|
|
61
61
|
}
|
|
62
|
-
lines.push(row('
|
|
62
|
+
lines.push(row(status.phase === 'stopped' ? 'Saved project' : 'Project', config?.projectRoot ?? 'Unavailable'), row(status.phase === 'stopped' ? 'Saved mode' : status.phase === 'unknown' || status.configState === 'pending' ? 'Active mode' : 'Mode', mode), row('Auth', auth.mode ? labels[auth.mode] + (ready ? '' : saved) : 'Unavailable'));
|
|
63
|
+
if (status.phase !== 'stopped' && (status.configState === 'pending' || !status.active)) {
|
|
64
|
+
const savedConfig = status.saved?.config;
|
|
65
|
+
lines.push(row('Saved mode', describeMode(savedConfig, !!savedConfig && usesGlobalPermissions(savedConfig)) + (status.configState === 'pending' ? ' (pending)' : '')));
|
|
66
|
+
if (!status.active)
|
|
67
|
+
lines.push(row('Saved project', savedConfig?.projectRoot ?? 'Unavailable'));
|
|
68
|
+
}
|
|
69
|
+
if (status.runtime)
|
|
70
|
+
lines.push(row('Runtime', status.runtime.buildVersion));
|
|
63
71
|
if (ready && status.port && auth.mode === 'oauth' && auth.oauth) {
|
|
64
72
|
lines.push(row('Client URL', auth.oauth.resource), row('Issuer', auth.oauth.issuer));
|
|
65
73
|
}
|
|
@@ -119,4 +127,3 @@ export async function formatStarmuxStatus(instance, status, config, options = {}
|
|
|
119
127
|
lines.push(row('Stop', command('down')));
|
|
120
128
|
return lines.join('\n') + '\n';
|
|
121
129
|
}
|
|
122
|
-
//# sourceMappingURL=starmux-status.js.map
|
package/dist/src/cli/starmux.js
CHANGED
|
@@ -8,13 +8,13 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
9
9
|
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
|
|
10
10
|
import { createAccessControl } from '../access-control.js';
|
|
11
|
-
import { instanceStatus, instanceRequest, runInstance } from '../instance-runtime.js';
|
|
12
|
-
import { atomicPrivate, availablePort, changeRegistry, configJSONSchema, createInstance, ensureAccessReady, exists, instanceConfigSchema, instancePaths, loadInstanceConfig, parseConfig, patchInstanceConfig, readRegistry, readTrusted, resolveInstance, saveInstanceConfig, suggestPort, validateConfig } from '../instance-store.js';
|
|
11
|
+
import { instanceStatus, instanceRequest, stopInstance, runInstance } from '../instance-runtime.js';
|
|
12
|
+
import { atomicPrivate, availablePort, changeRegistry, configJSONSchema, createInstance, ensureAccessReady, exists, instanceConfigSchema, instancePaths, loadInstanceConfig, parseConfig, patchInstanceConfig, readRegistry, readTrusted, resolveInstance, saveInstanceConfig, suggestPort, validateConfig, usesGlobalPermissions } from '../instance-store.js';
|
|
13
13
|
import { handle } from './instances.js';
|
|
14
|
-
import { desktopCommanderEntry } from '../runtime-paths.js';
|
|
14
|
+
import { packageRoot, desktopCommanderEntry } from '../runtime-paths.js';
|
|
15
15
|
import { shellQuote } from '../platform.js';
|
|
16
16
|
import { formatStarmuxStatus } from './starmux-status.js';
|
|
17
|
-
const version =
|
|
17
|
+
const { version } = JSON.parse(await readFile(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
18
18
|
const strings = ['instance', 'port', 'lines', 'expires', 'issuer', 'resource', 'resource-aliases', 'client-id', 'subject', 'subjects', 'registration', 'token-auth', 'callback', 'revision', 'patch', 'file', 'output', 'name', 'target', 'mode', 'app-parent-pid'];
|
|
19
19
|
const booleans = ['help', 'version', 'json', 'write', 'code', 'read', 'yes', 'foreground', 'follow', 'archived', 'all', 'effective', 'link', 'allow-loopback-http', 'token-stdin', 'no-open', 'token'];
|
|
20
20
|
const options = {};
|
|
@@ -52,12 +52,11 @@ function help() {
|
|
|
52
52
|
verify: 'verify [--token-stdin]\n Test MCP initialization, tool discovery and workspace read. Authenticated instances use STARMUX_TOKEN or stdin.',
|
|
53
53
|
doctor: 'doctor\n Check Node, bundled dependencies, workspace, configuration, port and authentication.',
|
|
54
54
|
logs: 'logs [-f | --follow] [--lines 1..200]\n Follow continues across restarts. Ctrl+C exits the follower only.',
|
|
55
|
-
migrate: 'migrate [registry|setup|live] [-y]\n No source: preview. Select a source to migrate, retaining originals.',
|
|
56
55
|
status: 'status [--all]\n Show the selected instance, its actual endpoint and connection guide.',
|
|
57
56
|
ls: 'ls [--archived]\n List local workspace instances.',
|
|
58
57
|
rm: 'rm [NAME] (alias for: config archive)\n Stop and archive an instance.',
|
|
59
58
|
down: 'down (alias: stop)\n Stop the selected instance.',
|
|
60
|
-
permissions: 'permissions [status
|
|
59
|
+
permissions: 'permissions [status]\n Show the selected instance’s mode. Switch with up --read or up --code.',
|
|
61
60
|
privacy: 'privacy [--all] [--target PATH]\n Check privacy access through the running instance.'
|
|
62
61
|
};
|
|
63
62
|
if (topic && topic !== 'help' && !sections[topic])
|
|
@@ -81,8 +80,7 @@ function help() {
|
|
|
81
80
|
' starmux ls List workspace instances',
|
|
82
81
|
' starmux rm [name] Stop and archive an instance',
|
|
83
82
|
' starmux init [path] Save instance settings without starting',
|
|
84
|
-
' starmux config Inspect, edit or apply configuration'
|
|
85
|
-
' starmux migrate Import settings from a legacy installation'
|
|
83
|
+
' starmux config Inspect, edit or apply configuration'
|
|
86
84
|
].join('\n');
|
|
87
85
|
process.stdout.write(`Starmux ${version}\n\n${sections[topic ?? ''] ?? overview}\n\nGlobal: -i, --instance NAME_OR_ID --json -h, --help\nUse starmux COMMAND --help for details.\n`);
|
|
88
86
|
}
|
|
@@ -127,8 +125,11 @@ async function configure() {
|
|
|
127
125
|
instance = await resolveInstance();
|
|
128
126
|
if (instance?.archived)
|
|
129
127
|
fail('Instance is archived. Use starmux config restore -i ' + instance.name);
|
|
130
|
-
if (
|
|
131
|
-
|
|
128
|
+
if (command === 'up' && instance) {
|
|
129
|
+
const state = await instanceStatus(instance);
|
|
130
|
+
if (state.phase !== 'running' && state.phase !== 'stopped')
|
|
131
|
+
fail(`Cannot start ${instance.name}: ${state.runtimeProblem ?? state.phase}. Run starmux doctor -i ${instance.id}.`);
|
|
132
|
+
}
|
|
132
133
|
const loaded = instance ? await loadInstanceConfig(instance) : undefined;
|
|
133
134
|
const writing = flags.write || flags.code;
|
|
134
135
|
if (flags.read && writing)
|
|
@@ -164,21 +165,29 @@ async function show(instance) {
|
|
|
164
165
|
print(status);
|
|
165
166
|
return;
|
|
166
167
|
}
|
|
167
|
-
const config = status.phase === '
|
|
168
|
+
const config = status.phase === 'stopped' ? status.saved?.config : status.active?.config;
|
|
168
169
|
process.stdout.write(await formatStarmuxStatus(instance, status, config, { isDefault: await isDefaultInstance(instance) }));
|
|
169
170
|
}
|
|
170
171
|
async function start(instance) {
|
|
171
172
|
const state = await instanceStatus(instance);
|
|
172
173
|
if (state.phase === 'running') {
|
|
173
|
-
if (state.configState === 'pending')
|
|
174
|
-
await instanceRequest(instance, 'apply', (await loadInstanceConfig(instance)).revision);
|
|
174
|
+
if (state.configState === 'pending') {
|
|
175
|
+
const applied = await instanceRequest(instance, 'apply', (await loadInstanceConfig(instance)).revision);
|
|
176
|
+
if (!applied || applied.phase !== 'running')
|
|
177
|
+
fail('Configuration was not applied: instance changed while starting. Run starmux status.');
|
|
178
|
+
}
|
|
179
|
+
const current = await instanceStatus(instance);
|
|
180
|
+
if (current.phase !== 'running' || current.health !== 'ok')
|
|
181
|
+
fail(`Instance is not ready: ${current.runtimeProblem ?? current.health}. Run starmux doctor -i ${instance.id}.`);
|
|
175
182
|
await show(instance);
|
|
176
183
|
return;
|
|
177
184
|
}
|
|
185
|
+
if (state.phase !== 'stopped')
|
|
186
|
+
fail(`Cannot start ${instance.name}: ${state.runtimeProblem ?? state.phase}. Run starmux doctor -i ${instance.id}.`);
|
|
178
187
|
await ensureAccessReady(instance);
|
|
179
188
|
const config = (await loadInstanceConfig(instance)).config;
|
|
180
189
|
if (!await availablePort(config.port))
|
|
181
|
-
fail(`Port ${config.port} is in use.
|
|
190
|
+
fail(`Port ${config.port} is in use. Inspect starmux doctor -i ${instance.id} before changing the port.`);
|
|
182
191
|
if (flags.foreground || string('app-parent-pid')) {
|
|
183
192
|
await runInstance(instance, { json, ...(!json ? { onReady: () => show(instance) } : {}), ...(string('app-parent-pid') ? { appParentPid: Number(string('app-parent-pid')) } : {}) });
|
|
184
193
|
return;
|
|
@@ -364,7 +373,7 @@ async function auth(instance) {
|
|
|
364
373
|
return;
|
|
365
374
|
}
|
|
366
375
|
if (op === 'oauth') {
|
|
367
|
-
process.env.
|
|
376
|
+
process.env.STARMUX_ACCESS_FILE = instancePaths(instance).access;
|
|
368
377
|
const action = positional[1] ?? 'status';
|
|
369
378
|
const map = { 'init-keycloak': 'keycloak-realm', setup: 'configure', doctor: 'doctor', status: 'status' };
|
|
370
379
|
if (!map[action])
|
|
@@ -593,7 +602,7 @@ async function main() {
|
|
|
593
602
|
}
|
|
594
603
|
const allowed = {
|
|
595
604
|
up: ['port', 'read', 'write', 'code', 'yes', 'foreground', 'app-parent-pid'], init: ['port', 'read', 'write', 'code', 'yes'],
|
|
596
|
-
status: ['all'], ls: ['archived'], rm: [], down: [], stop: [], logs: ['lines', 'follow'], doctor: [], verify: ['token-stdin'], dashboard: ['token', 'no-open'],
|
|
605
|
+
status: ['all'], ls: ['archived'], rm: [], down: [], stop: [], logs: ['lines', 'follow'], doctor: [], verify: ['token-stdin'], dashboard: ['token', 'no-open'],
|
|
597
606
|
auth: ['expires', 'issuer', 'resource', 'resource-aliases', 'client-id', 'subject', 'subjects', 'registration', 'token-auth', 'callback', 'allow-loopback-http'],
|
|
598
607
|
config: ['yes', 'archived', 'effective', 'link', 'name', 'revision', 'patch', 'file', 'output'], permissions: ['target', 'mode'], privacy: ['all', 'target']
|
|
599
608
|
};
|
|
@@ -609,15 +618,6 @@ async function main() {
|
|
|
609
618
|
fail('Usage: starmux rm [NAME]');
|
|
610
619
|
if (command === 'auth' && ['key', 'mode', 'revoke'].includes(positional[0] ?? '') && positional.length !== 2)
|
|
611
620
|
fail('Usage: starmux auth ' + positional[0] + ' VALUE');
|
|
612
|
-
if (command === 'migrate' && positional.length > 1)
|
|
613
|
-
fail('Usage: starmux migrate [registry|setup|live] [-y]');
|
|
614
|
-
// Public commands never inherit old environment overlays. Core's private protocol is retained.
|
|
615
|
-
for (const key of Object.keys(process.env))
|
|
616
|
-
if (key.startsWith('MAC_MCP_'))
|
|
617
|
-
delete process.env[key];
|
|
618
|
-
for (const [key, value] of Object.entries(process.env))
|
|
619
|
-
if (key.startsWith('STARMUX_') && key !== 'STARMUX_TOKEN')
|
|
620
|
-
process.env['MAC_MCP_' + key.slice(8)] = value;
|
|
621
621
|
if (command === 'up' || command === 'init') {
|
|
622
622
|
const instance = await configure();
|
|
623
623
|
if (command === 'up')
|
|
@@ -630,10 +630,6 @@ async function main() {
|
|
|
630
630
|
}
|
|
631
631
|
return;
|
|
632
632
|
}
|
|
633
|
-
if (command === 'migrate') {
|
|
634
|
-
await (await import('../starmux-migration.js')).migrate(positional[0], yes(), json);
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
633
|
if (command === 'ls' || command === 'status' && flags.all) {
|
|
638
634
|
await handle('instance', ['list', ...(flags.archived ? ['--archived'] : []), ...(json ? ['--json'] : [])]);
|
|
639
635
|
return;
|
|
@@ -662,11 +658,13 @@ async function main() {
|
|
|
662
658
|
if (command === 'status')
|
|
663
659
|
await show(instance);
|
|
664
660
|
else if (command === 'down' || command === 'stop') {
|
|
665
|
-
await
|
|
661
|
+
const result = await stopInstance(instance);
|
|
666
662
|
if (json)
|
|
667
|
-
print(
|
|
663
|
+
print(result);
|
|
664
|
+
else if (result.restarted)
|
|
665
|
+
process.stdout.write(`Stopped the previous run of ${instance.name}; another command has started a new run.\n`);
|
|
668
666
|
else
|
|
669
|
-
process.stdout.write('
|
|
667
|
+
process.stdout.write(`${result.result === 'ALREADY_STOPPED' ? 'Already stopped' : 'Stopped'} ${instance.name}\n`);
|
|
670
668
|
}
|
|
671
669
|
else if (command === 'auth')
|
|
672
670
|
await auth(instance);
|
|
@@ -679,29 +677,42 @@ async function main() {
|
|
|
679
677
|
else if (command === 'doctor') {
|
|
680
678
|
if (Number(process.versions.node.split('.')[0]) < 22 || (process.versions.node.startsWith('22.') && Number(process.versions.node.split('.')[1]) < 12))
|
|
681
679
|
fail('Node 22.12 or newer is required.');
|
|
680
|
+
const state = await instanceStatus(instance);
|
|
681
|
+
if (state.configProblem || state.runtimeProblem || state.runtime?.problem)
|
|
682
|
+
fail(state.configProblem ?? state.runtimeProblem ?? state.runtime.problem);
|
|
683
|
+
if (state.phase !== 'running' && state.phase !== 'stopped')
|
|
684
|
+
fail('Instance state is ' + state.phase + '. Inspect starmux logs -i ' + instance.id);
|
|
685
|
+
if (state.phase === 'running' && state.health !== 'ok')
|
|
686
|
+
fail('Instance health is ' + state.health + '. Inspect starmux logs -i ' + instance.id);
|
|
682
687
|
await stat(desktopCommanderEntry);
|
|
683
688
|
const loaded = await loadInstanceConfig(instance);
|
|
684
689
|
await ensureAccessReady(instance);
|
|
685
|
-
|
|
686
|
-
if (state.phase !== 'running' && !await availablePort(loaded.config.port))
|
|
690
|
+
if (state.phase === 'stopped' && !await availablePort(loaded.config.port))
|
|
687
691
|
fail('Configured port is in use: ' + loaded.config.port);
|
|
688
692
|
if (json)
|
|
689
693
|
print({ result: 'PASS', node: process.version, instanceId: instance.id, configState: state.configState });
|
|
690
694
|
else {
|
|
691
695
|
const isDef = await isDefaultInstance(instance);
|
|
692
696
|
const access = await (await createAccessControl(instancePaths(instance).access, { initialize: false })).snapshot();
|
|
693
|
-
const
|
|
697
|
+
const effective = state.phase === 'running' ? state.active?.config : loaded.config;
|
|
698
|
+
const modeLabel = !effective ? 'Mode unavailable' : effective.accessRole === 'trusted' ? 'Coding mode' : effective.mode === 'read-write' ? 'File writes' : 'Read-only mode';
|
|
694
699
|
const authLabel = access.mode === 'ingress' ? 'None (no token required)' : access.mode === 'api-key' ? `API key (${access.credentials.filter(c => c.status === 'active').length} active)` : 'OAuth';
|
|
695
700
|
const portState = state.phase === 'running' ? `Running on 127.0.0.1:${state.port}` : `Stopped · port ${loaded.config.port} available`;
|
|
696
701
|
process.stdout.write(`✓ Runtime Node ${process.version} & Desktop Commander engine ready\n` +
|
|
697
702
|
`✓ Service ${portState}${isDef ? '' : ` (instance: ${instance.name})`}\n` +
|
|
698
|
-
`✓ Workspace ${
|
|
703
|
+
`✓ Workspace ${effective?.projectRoot ?? 'Unavailable'} (${modeLabel})\n` +
|
|
699
704
|
`✓ Configuration ${instance.source.path} (${state.configState})\n` +
|
|
700
705
|
`✓ Authentication ${authLabel}\n` +
|
|
701
706
|
`PASS · Node, dependencies, workspace, configuration, port and authentication.\n`);
|
|
702
707
|
}
|
|
703
708
|
}
|
|
704
709
|
else {
|
|
710
|
+
if (command === 'permissions' && usesGlobalPermissions((await loadInstanceConfig(instance)).config)) {
|
|
711
|
+
if (positional.length && positional[0] !== 'status')
|
|
712
|
+
fail('Directory grants and tool filters do not apply to global modes. Use starmux up --read or starmux up --code.');
|
|
713
|
+
await show(instance);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
705
716
|
const args = [...positional, '--instance', instance.id];
|
|
706
717
|
for (const [key, value] of Object.entries(flags))
|
|
707
718
|
if (key !== 'instance' && value !== undefined)
|
|
@@ -720,4 +731,3 @@ catch (error) {
|
|
|
720
731
|
process.stderr.write(message + '\n');
|
|
721
732
|
process.exitCode = 1;
|
|
722
733
|
}
|
|
723
|
-
//# sourceMappingURL=starmux.js.map
|
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
const common = { projectRoot: z.string(), mode: z.enum(['read-only', 'read-write']), connection: z.enum(['tunnel', 'https']), port: z.number().int() };
|
|
3
|
-
export const setupSchema = z.discriminatedUnion('version', [
|
|
4
|
-
z.object({ ...common, version: z.literal(1) }).strict(),
|
|
5
|
-
z.object({ ...common, version: z.literal(2), accessRole: z.enum(['restricted', 'trusted']) }).strict(),
|
|
6
|
-
z.object({ ...common, version: z.literal(3), accessRole: z.literal('trusted'), writeScope: z.literal('home-only') }).strict()
|
|
7
|
-
]);
|
|
8
2
|
export const grantsSchema = z.object({ version: z.literal(1), homeRead: z.boolean(), outsideRead: z.boolean(), custom: z.array(z.object({ path: z.string(), mode: z.enum(['read-only', 'read-write']) }).strict()) }).strict();
|
|
9
3
|
export const denySchema = z.object({ version: z.literal(1), deniedTools: z.array(z.string()), deniedDirectories: z.array(z.string()) }).strict();
|
|
10
|
-
export const revocationsSchema = z.object({ version: z.literal(1), revokedSubjects: z.array(z.string()), revokedJtis: z.array(z.string()) }).strict();
|
|
11
4
|
export const upstreamDefinitionSchema = z.object({ id: z.string(), transport: z.literal('stdio'), command: z.string(), args: z.array(z.string()), env: z.record(z.string()).optional(), allowTools: z.array(z.string()) }).strict();
|
|
12
|
-
export const ownerSchema = z.object({ port: z.number().int(), pid: z.number().int().min(2), token: z.string().regex(/^[a-f0-9]{48}$/), launcher: z.enum(['cli', 'app']), runId: z.string().regex(/^[a-f0-9]{32}$/) }).passthrough();
|
|
13
|
-
export const privateAgentSchema = z.object({ Label: z.string(), ProgramArguments: z.tuple([z.string(), z.string()]), EnvironmentVariables: z.record(z.string()), WorkingDirectory: z.string(), StandardOutPath: z.string().optional(), StandardErrorPath: z.string().optional(), RunAtLoad: z.boolean().optional() }).passthrough();
|
|
14
|
-
//# sourceMappingURL=config-schemas.js.map
|
|
5
|
+
export const ownerSchema = z.object({ version: z.literal(1), port: z.number().int(), pid: z.number().int().min(2), token: z.string().regex(/^[a-f0-9]{48}$/), launcher: z.enum(['cli', 'app']), runId: z.string().regex(/^[a-f0-9]{32}$/) }).passthrough();
|
package/dist/src/contracts.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createAccessControl } from './access-control.js';
|
|
|
4
4
|
import { accessPage, accessPost, accessHeaders } from './access-dashboard.js';
|
|
5
5
|
import { isSafeInteger } from "./validation.js";
|
|
6
6
|
// Explicit project-root Desktop Commander MCP endpoint. Never exposed to public Caddy.
|
|
7
|
-
// Start only with owner-approved
|
|
7
|
+
// Start only with owner-approved STARMUX_PROJECT_ROOT; no default HOME traversal.
|
|
8
8
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
9
9
|
import { createServer } from "node:http";
|
|
10
10
|
import { createAuthorizedWorkspacePolicy } from "./authorized-workspace-roots.js";
|
|
@@ -22,6 +22,8 @@ import { createPrivateMCPActivity } from "./private-mcp-activity.js";
|
|
|
22
22
|
import { createGitWorkflow } from "./project-git-workflow.js";
|
|
23
23
|
import { createProjectPolicy } from "./project-workspace-policy.js";
|
|
24
24
|
import { createWorkspaceGrants } from "./workspace-grants.js";
|
|
25
|
+
import { createAccountAccessPolicy } from './account-access-policy.js';
|
|
26
|
+
import { isAllowedGatewayHost } from './http-host-policy.js';
|
|
25
27
|
/** Construction and imports are inert; only start acquires resources. */
|
|
26
28
|
export function createGatewayRuntime(options = {}) {
|
|
27
29
|
let running = null;
|
|
@@ -53,27 +55,31 @@ export function createGatewayRuntime(options = {}) {
|
|
|
53
55
|
};
|
|
54
56
|
}
|
|
55
57
|
async function initializeGateway({ env = process.env, audit: externalAudit }) {
|
|
56
|
-
const access = await createAccessControl(env.
|
|
57
|
-
const page = (html) => instancePage(html, env.
|
|
58
|
-
const root = env.
|
|
58
|
+
const access = await createAccessControl(env.STARMUX_ACCESS_FILE);
|
|
59
|
+
const page = (html) => instancePage(html, env.STARMUX_INSTANCE_NAME, env.STARMUX_INSTANCE_ID);
|
|
60
|
+
const root = env.STARMUX_PROJECT_ROOT;
|
|
59
61
|
if (!root)
|
|
60
62
|
throw new Error("OWNER_APPROVED_PROJECT_ROOT_REQUIRED");
|
|
61
|
-
const mode = env.
|
|
62
|
-
const developerCommands = env.
|
|
63
|
-
const nativeAccessMode = env.
|
|
64
|
-
const writeScope = env.
|
|
63
|
+
const mode = env.STARMUX_WORKSPACE_MODE || "read-only";
|
|
64
|
+
const developerCommands = env.STARMUX_DEV_COMMANDS || "presets";
|
|
65
|
+
const nativeAccessMode = env.STARMUX_DC_ACCESS_MODE || "workspace";
|
|
66
|
+
const writeScope = env.STARMUX_DC_WRITE_SCOPE || "unrestricted";
|
|
67
|
+
const globalPermissions = env.STARMUX_PERMISSION_MODE === 'global';
|
|
68
|
+
const globalReadOnly = globalPermissions && mode === 'read-only';
|
|
65
69
|
if (!["unrestricted", "home-only"].includes(writeScope) ||
|
|
66
70
|
(writeScope === "home-only" && nativeAccessMode !== "native"))
|
|
67
71
|
throw Error("HOME_WRITE_SCOPE_REQUIRES_NATIVE_ROLE");
|
|
68
|
-
if (writeScope === "home-only" && env.
|
|
72
|
+
if (writeScope === "home-only" && env.STARMUX_UPSTREAMS_FILE)
|
|
69
73
|
throw Error("HOME_WRITE_SCOPE_UNSANDBOXED_EXTRA_UPSTREAMS");
|
|
70
74
|
if (!["workspace", "native"].includes(nativeAccessMode) ||
|
|
71
75
|
(nativeAccessMode === "native" &&
|
|
72
76
|
(developerCommands !== "trusted-project" || mode !== "read-write")))
|
|
73
77
|
throw new Error("NATIVE_ACCESS_REQUIRES_OWNER_TRUSTED_DEVELOPER_PROFILE");
|
|
74
|
-
|
|
78
|
+
if (globalPermissions && (writeScope !== 'unrestricted' || (!globalReadOnly && nativeAccessMode !== 'native')))
|
|
79
|
+
throw Error('GLOBAL_ACCESS_MODE_INVALID');
|
|
80
|
+
const policy = globalPermissions ? await createAccountAccessPolicy(root, mode) : await createProjectPolicy({ root, mode, developerCommands,
|
|
75
81
|
allowTrustedHomeRoot: nativeAccessMode === "native" && developerCommands === "trusted-project" });
|
|
76
|
-
const desiredPort = Number(env.
|
|
82
|
+
const desiredPort = Number(env.STARMUX_PROJECT_PORT || 18893);
|
|
77
83
|
if (!isSafeInteger(desiredPort) || desiredPort < 0 || desiredPort > 65535)
|
|
78
84
|
throw new Error("INVALID_PORT");
|
|
79
85
|
let activity = null;
|
|
@@ -103,32 +109,31 @@ async function initializeGateway({ env = process.env, audit: externalAudit }) {
|
|
|
103
109
|
await upstream.connect();
|
|
104
110
|
// Default configuration remains the original Desktop Commander-only profile.
|
|
105
111
|
// Additional trusted operator-owned routes require an explicit private config.
|
|
106
|
-
configured = await bootstrapConfiguredUpstreams(env.
|
|
112
|
+
configured = await bootstrapConfiguredUpstreams(env.STARMUX_UPSTREAMS_FILE, audit);
|
|
107
113
|
const router = createMCPUpstreamRouter({ routes: [
|
|
108
114
|
{ id: "desktop", upstream, exposeNativeNames: true }, ...configured.routes
|
|
109
115
|
] });
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
if (env.MAC_MCP_PRIVATE_ACTIVITY_OBSERVER === "1")
|
|
116
|
+
// The instance supervisor enables this bounded, owner-local activity observer.
|
|
117
|
+
if (env.STARMUX_PRIVATE_ACTIVITY_OBSERVER === "1")
|
|
113
118
|
activity = createPrivateMCPActivity({ routeFor: name => router.routeFor(name) });
|
|
114
|
-
const worktreeMode = env.
|
|
119
|
+
const worktreeMode = env.STARMUX_WORKTREE_MODE || "off";
|
|
115
120
|
if (!["off", "linked"].includes(worktreeMode))
|
|
116
121
|
throw Error("INVALID_WORKTREE_MODE");
|
|
117
122
|
const worktreePolicy = createAuthorizedWorkspacePolicy({
|
|
118
|
-
basePolicy: policy, linkedGitWorktrees: worktreeMode
|
|
123
|
+
basePolicy: policy, linkedGitWorktrees: globalPermissions ? 'off' : worktreeMode
|
|
119
124
|
});
|
|
120
125
|
// Explicit opt-in: existing/private deployments remain unchanged without this file.
|
|
121
|
-
const grants = env.
|
|
122
|
-
await createWorkspaceGrants({ file: env.
|
|
126
|
+
const grants = !globalPermissions && env.STARMUX_GRANTS_FILE ?
|
|
127
|
+
await createWorkspaceGrants({ file: env.STARMUX_GRANTS_FILE, audit }) : null;
|
|
123
128
|
const workspacePolicy = grants ?
|
|
124
129
|
createManagedWorkspacePolicy({ basePolicy: worktreePolicy, grants }) : worktreePolicy;
|
|
125
130
|
// Optional explicit deny preferences: default empty, independent from DC tools.
|
|
126
131
|
// Owner-managed only via the same local token as Workspace Permissions.
|
|
127
|
-
if (env.
|
|
132
|
+
if (!globalPermissions && env.STARMUX_DENY_FILE && !grants)
|
|
128
133
|
throw Error("DENY_CONSOLE_REQUIRES_OWNER_GRANTS");
|
|
129
|
-
const denyRules = env.
|
|
130
|
-
await createDenyRules({ file: env.
|
|
131
|
-
const gitMode = env.
|
|
134
|
+
const denyRules = !globalPermissions && env.STARMUX_DENY_FILE ?
|
|
135
|
+
await createDenyRules({ file: env.STARMUX_DENY_FILE, audit }) : null;
|
|
136
|
+
const gitMode = globalPermissions ? 'off' : env.STARMUX_GIT_MODE || "off";
|
|
132
137
|
if (!["off", "review", "write", "push"].includes(gitMode))
|
|
133
138
|
throw new Error("INVALID_GIT_MODE");
|
|
134
139
|
if (writeScope === "home-only" && gitMode !== "off")
|
|
@@ -138,15 +143,33 @@ async function initializeGateway({ env = process.env, audit: externalAudit }) {
|
|
|
138
143
|
pushEnabled: gitMode === "push"
|
|
139
144
|
});
|
|
140
145
|
http = createServer(async (req, res) => {
|
|
141
|
-
const
|
|
146
|
+
const denyRequest = (reason) => {
|
|
147
|
+
audit({ event: 'HTTP_REQUEST_DENIED', reason });
|
|
148
|
+
res.writeHead(403, { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' }).end(reason);
|
|
149
|
+
};
|
|
150
|
+
if (req.socket.remoteAddress !== "127.0.0.1") {
|
|
151
|
+
denyRequest('LOOPBACK_REQUIRED');
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
let route;
|
|
155
|
+
try {
|
|
156
|
+
route = new URL(req.url || "/", "http://localhost").pathname;
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
res.writeHead(400).end('Invalid request target');
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
142
162
|
const address = http?.address();
|
|
143
163
|
const port = address && typeof address !== "string" ? address.port : null;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
164
|
+
if (!isAllowedGatewayHost(req, port, route)) {
|
|
165
|
+
denyRequest('HOST_NOT_ALLOWED');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (globalPermissions && (route === '/permissions' || route === '/filters')) {
|
|
169
|
+
res.writeHead(410, { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' });
|
|
170
|
+
res.end('Starmux uses global Read-only or Coding mode. Directory grants and tool filters do not apply. Use starmux up --read or starmux up --code.');
|
|
147
171
|
return;
|
|
148
172
|
}
|
|
149
|
-
const route = new URL(req.url || "/", "http://localhost").pathname;
|
|
150
173
|
if (route.startsWith('/.well-known/oauth-protected-resource') && (req.method === 'GET' || req.method === 'OPTIONS')) {
|
|
151
174
|
try {
|
|
152
175
|
const config = await access.oauthSettings();
|
|
@@ -175,10 +198,10 @@ async function initializeGateway({ env = process.env, audit: externalAudit }) {
|
|
|
175
198
|
}
|
|
176
199
|
else if (req.method === "POST") {
|
|
177
200
|
const result = await accessPost(req, access);
|
|
178
|
-
if (result.status === 200 && env.
|
|
201
|
+
if (result.status === 200 && env.STARMUX_INSTANCE_ID) {
|
|
179
202
|
const { changeRegistry, instancePaths } = await import('./instance-store.js');
|
|
180
203
|
await changeRegistry(async (registry) => {
|
|
181
|
-
const instance = registry.instances.find(i => i.id === env.
|
|
204
|
+
const instance = registry.instances.find(i => i.id === env.STARMUX_INSTANCE_ID);
|
|
182
205
|
if (instance && instancePaths(instance).access === access.file)
|
|
183
206
|
instance.requiredAccess = (await access.snapshot()).mode;
|
|
184
207
|
});
|
|
@@ -251,10 +274,11 @@ async function initializeGateway({ env = process.env, audit: externalAudit }) {
|
|
|
251
274
|
if ((route === "/dashboard" || route === "/dashboard.json") && req.method === "GET") {
|
|
252
275
|
try {
|
|
253
276
|
const snapshot = { ...await collectOperationsSnapshot(), configuredUpstreams: configured.status(),
|
|
254
|
-
...(env.
|
|
277
|
+
...(env.STARMUX_INSTANCE_ID ? { instance: { id: env.STARMUX_INSTANCE_ID, name: env.STARMUX_INSTANCE_NAME ?? env.STARMUX_INSTANCE_ID, port: desiredPort } } : {}),
|
|
255
278
|
accessControl: await access.snapshot(),
|
|
256
279
|
permissionManagementEnabled: Boolean(grants),
|
|
257
280
|
nativeAccessEnabled: nativeAccessMode === "native",
|
|
281
|
+
permissionMode: globalPermissions ? 'global' : 'workspace',
|
|
258
282
|
denyRuleManagementEnabled: Boolean(denyRules) };
|
|
259
283
|
const json = route.endsWith(".json");
|
|
260
284
|
res.writeHead(200, { ...dashboardHeaders,
|
|
@@ -291,7 +315,7 @@ async function initializeGateway({ env = process.env, audit: externalAudit }) {
|
|
|
291
315
|
res.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
|
|
292
316
|
const accessMode = await access.snapshot().then(s => s.mode).catch(() => "unavailable");
|
|
293
317
|
res.end(JSON.stringify({ status: "ok", service: "dc-project-workspace", mode,
|
|
294
|
-
writeScope, accessMode, ...(env.
|
|
318
|
+
writeScope, accessMode, ...(env.STARMUX_INSTANCE_ID ? { instanceId: env.STARMUX_INSTANCE_ID, instanceName: env.STARMUX_INSTANCE_NAME } : {}) }));
|
|
295
319
|
return;
|
|
296
320
|
}
|
|
297
321
|
if (route !== "/mcp") {
|
|
@@ -331,6 +355,7 @@ async function initializeGateway({ env = process.env, audit: externalAudit }) {
|
|
|
331
355
|
const gateway = createDesktopCommanderBridge(router, audit, {
|
|
332
356
|
request, ...(oauth ? { oauthScope: oauth.scope } : {}), workspacePolicy, upstreamRouter: router,
|
|
333
357
|
nativePassthrough: nativeAccessMode === "native", denyRules,
|
|
358
|
+
globalReadOnly, permissionMode: globalPermissions ? 'global' : 'workspace',
|
|
334
359
|
...(gitWorkflow ? { gitWorkflow } : {})
|
|
335
360
|
});
|
|
336
361
|
bridges.add(gateway);
|
|
@@ -385,4 +410,3 @@ export async function runGateway(options = {}) {
|
|
|
385
410
|
process.on('SIGTERM', stop);
|
|
386
411
|
return runtime;
|
|
387
412
|
}
|
|
388
|
-
//# sourceMappingURL=dc-workspace-http.js.map
|
|
@@ -43,7 +43,7 @@ export function renderDenyDashboard({ rules, toolCatalog = [] }) {
|
|
|
43
43
|
'<p>仅检查具有明确文件路径参数的直接工具调用。' +
|
|
44
44
|
'<strong>不限制 Shell、Python、脚本或上游工具间接访问文件。</strong> ' +
|
|
45
45
|
'也不是操作系统沙箱;对目录的递归枚举或搜索亦不能提供可靠隔离。' +
|
|
46
|
-
'需要真正隔离时,应由 Desktop Commander /
|
|
46
|
+
'需要真正隔离时,应由 Desktop Commander / 操作系统 / 容器负责。</p>' +
|
|
47
47
|
'<table><thead><tr><th>过滤目录</th><th>操作</th></tr></thead><tbody>' +
|
|
48
48
|
(paths || '<tr><td colspan="2">没有目录过滤规则</td></tr>') +
|
|
49
49
|
'</tbody></table><h3>添加禁止的目录</h3>' +
|
|
@@ -56,7 +56,7 @@ export function renderDenyDashboard({ rules, toolCatalog = [] }) {
|
|
|
56
56
|
'</section><p><a href="/dashboard">运行 Dashboard</a> · ' +
|
|
57
57
|
'<a href="/permissions">目录授权</a></p>' +
|
|
58
58
|
'<small>黑名单是防误操作护栏,不是抵御恶意 Shell 或提示注入的安全边界。' +
|
|
59
|
-
'
|
|
59
|
+
'这里的管理密钥只由设备所有者在本机使用,不得发送给远程 AI。</small>' +
|
|
60
60
|
'</body></html>';
|
|
61
61
|
}
|
|
62
62
|
export async function applyDenyPost(req, ruleStore, authorize, host) {
|
|
@@ -89,4 +89,3 @@ export async function applyDenyPost(req, ruleStore, authorize, host) {
|
|
|
89
89
|
return { status: 400, message: "Rule rejected: check name or directory" };
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
|
-
//# sourceMappingURL=deny-dashboard.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createInvocationPipeline, safeAuditSink } from "./invocation.js";
|
|
2
2
|
import { createToolDispatcher } from "./tool-dispatch.js";
|
|
3
|
+
import { isReadOnlyTool } from './account-access-policy.js';
|
|
3
4
|
// Desktop Commander tool-schema facade. Upstream catalog remains unmodified.
|
|
4
5
|
// Preserve all native upstream tools; owner-approved native mode forwards unchanged.
|
|
5
6
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
@@ -10,7 +11,7 @@ export function createDesktopCommanderBridge(upstream, audit = () => { }, option
|
|
|
10
11
|
const resourcesEnabled = typeof upstream.listResources === "function" &&
|
|
11
12
|
typeof upstream.readResource === "function";
|
|
12
13
|
const server = new Server({
|
|
13
|
-
name: "
|
|
14
|
+
name: "starmux-desktop-commander-bridge", version: "0.1.0"
|
|
14
15
|
}, { capabilities: { tools: {}, ...(resourcesEnabled ? { resources: {} } : {}) } });
|
|
15
16
|
if (resourcesEnabled) {
|
|
16
17
|
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
@@ -40,10 +41,10 @@ export function createDesktopCommanderBridge(upstream, audit = () => { }, option
|
|
|
40
41
|
annotations: { readOnlyHint: true, destructiveHint: false }
|
|
41
42
|
}] : []), {
|
|
42
43
|
name: "health_check",
|
|
43
|
-
description: "
|
|
44
|
+
description: "Starmux bridge connection, upstream availability and active mode check. Read-only blocks writes and arbitrary execution; Coding uses upstream tools with OS account permissions.",
|
|
44
45
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
45
46
|
annotations: { readOnlyHint: true, destructiveHint: false }
|
|
46
|
-
}].filter(tool => !state?.deniedTools.includes(tool.name)).map(tool => options.oauthScope ? { ...tool,
|
|
47
|
+
}].filter(tool => !state?.deniedTools.includes(tool.name) && (!options.globalReadOnly || isReadOnlyTool(tool.name))).map(tool => options.oauthScope ? { ...tool,
|
|
47
48
|
securitySchemes: [{ type: 'oauth2', scopes: [options.oauthScope] }],
|
|
48
49
|
_meta: { ...('_meta' in tool ? tool._meta : {}), securitySchemes: [{ type: 'oauth2', scopes: [options.oauthScope] }] } } : tool) };
|
|
49
50
|
});
|
|
@@ -57,4 +58,3 @@ export function createDesktopCommanderBridge(upstream, audit = () => { }, option
|
|
|
57
58
|
server.setRequestHandler(CallToolRequestSchema, request => invoke(request.params));
|
|
58
59
|
return server;
|
|
59
60
|
}
|
|
60
|
-
//# sourceMappingURL=desktop-commander-facade.js.map
|
|
@@ -3,7 +3,7 @@ import { ownedByUser, createPrivateDirectory, within } from "./platform.js";
|
|
|
3
3
|
import { denySchema } from "./config-schemas.js";
|
|
4
4
|
import { GatewayError, errorCode, parseJSON, record } from "./validation.js";
|
|
5
5
|
// Optional explicit-deny preferences. Never a sandbox; native tools and command
|
|
6
|
-
// semantics belong to upstream Desktop Commander and the
|
|
6
|
+
// semantics belong to upstream Desktop Commander and the OS execution user.
|
|
7
7
|
// Reads the owner-only 0600 profile on EVERY discovery/call, so revocation applies
|
|
8
8
|
// to stale-client tool calls without restarting the Gateway.
|
|
9
9
|
import { randomBytes } from "node:crypto";
|
|
@@ -15,7 +15,7 @@ function issue(code) { const e = new GatewayError(code); throw e; }
|
|
|
15
15
|
const toolName = /^[a-zA-Z_][a-zA-Z0-9_.:-]{0,127}$/;
|
|
16
16
|
const pathKeys = new Set(["path", "file_path", "paths", "source", "destination", "outputPath"]);
|
|
17
17
|
const defaults = () => ({ version: 1, deniedTools: [], deniedDirectories: [] });
|
|
18
|
-
export const defaultDenyRulesPath = (home = homedir()) => path.join(home, ".config", "
|
|
18
|
+
export const defaultDenyRulesPath = (home = homedir()) => path.join(home, ".config", "starmux", "deny-rules-v1.json");
|
|
19
19
|
function verify(value) {
|
|
20
20
|
const obj = record(value, "DENY_RULES_INVALID");
|
|
21
21
|
if (!obj || typeof obj !== "object" || Array.isArray(obj) ||
|
|
@@ -169,4 +169,3 @@ export async function createDenyRules({ file = defaultDenyRulesPath(), audit = (
|
|
|
169
169
|
await snapshot();
|
|
170
170
|
return Object.freeze({ file, snapshot, update, check });
|
|
171
171
|
}
|
|
172
|
-
//# sourceMappingURL=explicit-deny-rules.js.map
|