starmux 0.13.0 → 0.14.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 +1 -1
- package/README.md +20 -30
- 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 +6 -175
- package/dist/src/cli/oauth.js +0 -1
- package/dist/src/cli/starmux-status.js +10 -14
- package/dist/src/cli/starmux-supervisor.js +0 -1
- package/dist/src/cli/starmux.js +13 -25
- package/dist/src/config-schemas.js +0 -9
- 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 +2 -3
- package/dist/src/home-write-sandbox.js +0 -1
- package/dist/src/http-host-policy.js +20 -0
- package/dist/src/instance-runtime.js +3 -3
- 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 +3 -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 +13 -17
- 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/docs/assets/starmux-app-en.png +0 -0
- package/docs/assets/starmux-architecture.svg +0 -315
- package/src/dc-workspace-http.mjs +0 -3
- package/test/fixtures/p1-fixture.txt +0 -2
package/dist/src/cli/starmux.js
CHANGED
|
@@ -9,12 +9,12 @@ 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
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';
|
|
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,6 @@ 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 (!instance && await exists(path.join(homedir(), '.config', 'mac-mcp-gateway')) && !registry.instances.length)
|
|
131
|
-
fail('Existing installation found. Run starmux migrate first.');
|
|
132
128
|
const loaded = instance ? await loadInstanceConfig(instance) : undefined;
|
|
133
129
|
const writing = flags.write || flags.code;
|
|
134
130
|
if (flags.read && writing)
|
|
@@ -364,7 +360,7 @@ async function auth(instance) {
|
|
|
364
360
|
return;
|
|
365
361
|
}
|
|
366
362
|
if (op === 'oauth') {
|
|
367
|
-
process.env.
|
|
363
|
+
process.env.STARMUX_ACCESS_FILE = instancePaths(instance).access;
|
|
368
364
|
const action = positional[1] ?? 'status';
|
|
369
365
|
const map = { 'init-keycloak': 'keycloak-realm', setup: 'configure', doctor: 'doctor', status: 'status' };
|
|
370
366
|
if (!map[action])
|
|
@@ -593,7 +589,7 @@ async function main() {
|
|
|
593
589
|
}
|
|
594
590
|
const allowed = {
|
|
595
591
|
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'],
|
|
592
|
+
status: ['all'], ls: ['archived'], rm: [], down: [], stop: [], logs: ['lines', 'follow'], doctor: [], verify: ['token-stdin'], dashboard: ['token', 'no-open'],
|
|
597
593
|
auth: ['expires', 'issuer', 'resource', 'resource-aliases', 'client-id', 'subject', 'subjects', 'registration', 'token-auth', 'callback', 'allow-loopback-http'],
|
|
598
594
|
config: ['yes', 'archived', 'effective', 'link', 'name', 'revision', 'patch', 'file', 'output'], permissions: ['target', 'mode'], privacy: ['all', 'target']
|
|
599
595
|
};
|
|
@@ -609,15 +605,6 @@ async function main() {
|
|
|
609
605
|
fail('Usage: starmux rm [NAME]');
|
|
610
606
|
if (command === 'auth' && ['key', 'mode', 'revoke'].includes(positional[0] ?? '') && positional.length !== 2)
|
|
611
607
|
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
608
|
if (command === 'up' || command === 'init') {
|
|
622
609
|
const instance = await configure();
|
|
623
610
|
if (command === 'up')
|
|
@@ -630,10 +617,6 @@ async function main() {
|
|
|
630
617
|
}
|
|
631
618
|
return;
|
|
632
619
|
}
|
|
633
|
-
if (command === 'migrate') {
|
|
634
|
-
await (await import('../starmux-migration.js')).migrate(positional[0], yes(), json);
|
|
635
|
-
return;
|
|
636
|
-
}
|
|
637
620
|
if (command === 'ls' || command === 'status' && flags.all) {
|
|
638
621
|
await handle('instance', ['list', ...(flags.archived ? ['--archived'] : []), ...(json ? ['--json'] : [])]);
|
|
639
622
|
return;
|
|
@@ -702,6 +685,12 @@ async function main() {
|
|
|
702
685
|
}
|
|
703
686
|
}
|
|
704
687
|
else {
|
|
688
|
+
if (command === 'permissions' && usesGlobalPermissions((await loadInstanceConfig(instance)).config)) {
|
|
689
|
+
if (positional.length && positional[0] !== 'status')
|
|
690
|
+
fail('Directory grants and tool filters do not apply to global modes. Use starmux up --read or starmux up --code.');
|
|
691
|
+
await show(instance);
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
705
694
|
const args = [...positional, '--instance', instance.id];
|
|
706
695
|
for (const [key, value] of Object.entries(flags))
|
|
707
696
|
if (key !== 'instance' && value !== undefined)
|
|
@@ -720,4 +709,3 @@ catch (error) {
|
|
|
720
709
|
process.stderr.write(message + '\n');
|
|
721
710
|
process.exitCode = 1;
|
|
722
711
|
}
|
|
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
5
|
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
|
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
|
|
@@ -17,7 +17,7 @@ export function controlPaths(port, root = controlRoot()) {
|
|
|
17
17
|
deny("CORE_INVALID_PORT");
|
|
18
18
|
const lock = path.join(root, `core-${port}.lease`);
|
|
19
19
|
return { root, lock, recovery: lock + ".recovery",
|
|
20
|
-
meta: path.join(lock, "owner.json"), socket: windows ? `\\\\.\\pipe\\
|
|
20
|
+
meta: path.join(lock, "owner.json"), socket: windows ? `\\\\.\\pipe\\starmux-${crypto.createHash("sha256").update(path.resolve(lock).toLowerCase()).digest("hex").slice(0, 32)}` : path.join(lock, "control.sock") };
|
|
21
21
|
}
|
|
22
22
|
function secureDir(p, create = false) {
|
|
23
23
|
if (create)
|
|
@@ -335,7 +335,7 @@ export async function acquireControl(port, { root, launcher = "cli" } = {}) {
|
|
|
335
335
|
};
|
|
336
336
|
}
|
|
337
337
|
// Private worker handoff for the setup profile. This is a same-UID
|
|
338
|
-
// supervisor identity check, not an OS boundary against the
|
|
338
|
+
// supervisor identity check, not an OS boundary against the local owner.
|
|
339
339
|
export function verifyInternalSetupWorker({ port, ownerPid, runId, token }) {
|
|
340
340
|
const paths = controlPaths(port);
|
|
341
341
|
const owner = readOwner(paths);
|
|
@@ -345,4 +345,3 @@ export function verifyInternalSetupWorker({ port, ownerPid, runId, token }) {
|
|
|
345
345
|
deny("CORE_SETUP_WORKER_NOT_SUPERVISED");
|
|
346
346
|
return true;
|
|
347
347
|
}
|
|
348
|
-
//# sourceMappingURL=global-core-control.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Host is an HTTP authority, not a URL; do not normalize alternate IP spellings or userinfo. */
|
|
2
|
+
export function isAllowedGatewayHost(req, listenerPort, route) {
|
|
3
|
+
// Node keeps only one Host in headers; inspect rawHeaders to reject duplicates too.
|
|
4
|
+
let hostCount = 0;
|
|
5
|
+
for (let i = 0; i < req.rawHeaders.length; i += 2)
|
|
6
|
+
if (req.rawHeaders[i]?.toLowerCase() === 'host')
|
|
7
|
+
hostCount++;
|
|
8
|
+
const host = req.headers.host;
|
|
9
|
+
if (hostCount !== 1 || !host || listenerPort === null)
|
|
10
|
+
return false;
|
|
11
|
+
// Owner pages and discovery retain their existing listener-specific boundary.
|
|
12
|
+
if (route !== '/mcp')
|
|
13
|
+
return host === `127.0.0.1:${listenerPort}` || host === `localhost:${listenerPort}`;
|
|
14
|
+
// SSH forwards TCP bytes unchanged, including the client's local Host port.
|
|
15
|
+
// Keep the hostname allowlist and validate the entire authority, including its port.
|
|
16
|
+
const match = /^(?:127\.0\.0\.1|localhost)(?::([0-9]{1,5}))?$/i.exec(host);
|
|
17
|
+
if (!match || match[0] !== host)
|
|
18
|
+
return false;
|
|
19
|
+
return match[1] === undefined || (Number(match[1]) >= 1 && Number(match[1]) <= 65535);
|
|
20
|
+
}
|
|
@@ -228,10 +228,10 @@ export async function runInstance(instance, options = {}) {
|
|
|
228
228
|
for (const key of childEnvKeys)
|
|
229
229
|
if (process.env[key])
|
|
230
230
|
env[key] = process.env[key];
|
|
231
|
-
Object.assign(env, instanceEnv(instance, config.config, home), {
|
|
231
|
+
Object.assign(env, instanceEnv(instance, config.config, home), { STARMUX_INTERNAL_SETUP_OWNER_PID: String(process.pid), STARMUX_INTERNAL_SETUP_RUN_ID: lease.owner.runId, STARMUX_INTERNAL_SETUP_TOKEN: lease.owner.token });
|
|
232
232
|
const upstreamFile = await prepareUpstreams(instance, config.config, home);
|
|
233
233
|
if (upstreamFile)
|
|
234
|
-
env.
|
|
234
|
+
env.STARMUX_UPSTREAMS_FILE = upstreamFile;
|
|
235
235
|
const log = path.join(instancePaths(instance, home).logs, 'core.log');
|
|
236
236
|
if (await exists(log))
|
|
237
237
|
await readTrusted(log, true, 32 * 1024 * 1024);
|
|
@@ -254,6 +254,7 @@ export async function runInstance(instance, options = {}) {
|
|
|
254
254
|
child.on('message', ready);
|
|
255
255
|
});
|
|
256
256
|
state.active = config;
|
|
257
|
+
state.permissionMode = env.STARMUX_PERMISSION_MODE === 'global' ? 'global' : 'workspace';
|
|
257
258
|
state.port = config.config.port;
|
|
258
259
|
state.phase = 'running';
|
|
259
260
|
lease.markRunning();
|
|
@@ -393,4 +394,3 @@ export async function runInstance(instance, options = {}) {
|
|
|
393
394
|
await rm(p.root, { recursive: true, force: true });
|
|
394
395
|
}
|
|
395
396
|
}
|
|
396
|
-
//# sourceMappingURL=instance-runtime.js.map
|
|
@@ -10,8 +10,10 @@ import { z } from 'zod';
|
|
|
10
10
|
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
11
11
|
import { validateConfiguredUpstreams } from './configured-mcp-upstreams.js';
|
|
12
12
|
import { createPrivateDirectory, ownedByUser } from './platform.js';
|
|
13
|
-
import {
|
|
13
|
+
import { supportedHost } from './platform.js';
|
|
14
|
+
import { createProjectPolicy } from './project-workspace-policy.js';
|
|
14
15
|
import { createAccessControl } from './access-control.js';
|
|
16
|
+
import { createAccountAccessPolicy } from './account-access-policy.js';
|
|
15
17
|
const nameSchema = z.string().trim().min(1).max(80).regex(/^[^\x00-\x1f\x7f/\\]+$/);
|
|
16
18
|
const sourceSchema = z.object({ kind: z.enum(['managed', 'linked']), path: z.string() }).strict();
|
|
17
19
|
export const instanceSchema = z.object({ id: z.string().uuid(), name: nameSchema, source: sourceSchema, archived: z.boolean().default(false),
|
|
@@ -118,17 +120,6 @@ export function parseConfig(text) {
|
|
|
118
120
|
const prefix = text.slice(0, e.offset);
|
|
119
121
|
throw Error(`CONFIG_JSON_INVALID line ${prefix.split('\n').length}, column ${e.offset - (prefix.lastIndexOf('\n') + 1) + 1}: ${printParseErrorCode(e.error)}`);
|
|
120
122
|
}
|
|
121
|
-
// Import the three historical onboarding formats without changing their authority.
|
|
122
|
-
if (value && typeof value === 'object' && 'version' in value) {
|
|
123
|
-
const legacy = value;
|
|
124
|
-
if (![1, 2, 3].includes(Number(legacy.version)))
|
|
125
|
-
throw Error('CONFIG_VERSION_UNSUPPORTED');
|
|
126
|
-
const allowed = new Set(['version', 'projectRoot', 'mode', 'connection', 'port', 'accessRole', 'writeScope']);
|
|
127
|
-
if (Object.keys(legacy).some(k => !allowed.has(k)))
|
|
128
|
-
throw Error('CONFIG_UNKNOWN_LEGACY_FIELD');
|
|
129
|
-
value = { schemaVersion: 1, projectRoot: legacy.projectRoot, mode: legacy.mode, connection: legacy.connection, port: legacy.port,
|
|
130
|
-
accessRole: legacy.version === 1 ? 'restricted' : legacy.accessRole, writeScope: legacy.version === 3 ? 'home-only' : 'unrestricted' };
|
|
131
|
-
}
|
|
132
123
|
const result = instanceConfigSchema.safeParse(value);
|
|
133
124
|
if (!result.success)
|
|
134
125
|
throw Error('CONFIG_INVALID: ' + result.error.issues.map(i => `${i.path.join('.') || 'config'}: ${i.message}`).join('; '));
|
|
@@ -138,8 +129,19 @@ export async function validateConfig(config) {
|
|
|
138
129
|
const { accessRole, writeScope } = config;
|
|
139
130
|
if (writeScope === 'home-only' && (accessRole !== 'trusted' || config.upstreams.length || config.gitMode !== 'off'))
|
|
140
131
|
throw Error('CONFIG_INVALID writeScope: home-only requires trusted mode without extra upstreams or Gateway Git');
|
|
141
|
-
|
|
142
|
-
|
|
132
|
+
supportedHost();
|
|
133
|
+
instanceConfigSchema.parse(config);
|
|
134
|
+
if (!path.isAbsolute(config.projectRoot))
|
|
135
|
+
throw Error('CONFIG_INVALID projectRoot: absolute path required');
|
|
136
|
+
if (accessRole === 'trusted' && config.mode !== 'read-write')
|
|
137
|
+
throw Error('CONFIG_ACCESS_ROLE_MISMATCH');
|
|
138
|
+
if (writeScope === 'home-only' && process.platform !== 'darwin')
|
|
139
|
+
throw Error('HOME_WRITE_SANDBOX_PLATFORM_UNSUPPORTED');
|
|
140
|
+
if (usesGlobalPermissions(config))
|
|
141
|
+
await createAccountAccessPolicy(config.projectRoot, config.mode);
|
|
142
|
+
else
|
|
143
|
+
await createProjectPolicy({ root: config.projectRoot, mode: config.mode,
|
|
144
|
+
developerCommands: accessRole === 'trusted' ? 'trusted-project' : 'presets', allowTrustedHomeRoot: accessRole === 'trusted' });
|
|
143
145
|
validateConfiguredUpstreams({ version: 1, upstreams: config.upstreams.map(({ envRefs, ...u }) => ({ ...u, env: Object.fromEntries(Object.keys(envRefs).map(k => [k, 'reference'])) })) });
|
|
144
146
|
return config;
|
|
145
147
|
}
|
|
@@ -185,7 +187,7 @@ export async function createInstance(name, options = {}, home = homedir()) {
|
|
|
185
187
|
const id = randomUUID();
|
|
186
188
|
const root = path.join(instancesRoot(home), 'instances', id);
|
|
187
189
|
const instance = instanceSchema.parse({ id, name, archived: false, requiredAccess: options.requiredAccess ?? 'ingress',
|
|
188
|
-
source: { kind: options.link ? 'linked' : 'managed', path: options.link ? input : path.join(root, 'config.jsonc') }
|
|
190
|
+
source: { kind: options.link ? 'linked' : 'managed', path: options.link ? input : path.join(root, 'config.jsonc') } });
|
|
189
191
|
createPrivateDirectory(path.join(instancesRoot(home), 'instances'));
|
|
190
192
|
createPrivateDirectory(root);
|
|
191
193
|
for (const dir of ['state', 'logs', 'snapshots'])
|
|
@@ -193,7 +195,7 @@ export async function createInstance(name, options = {}, home = homedir()) {
|
|
|
193
195
|
try {
|
|
194
196
|
await atomicPrivate(path.join(instancesRoot(home), 'gateway.schema.json'), JSON.stringify(configJSONSchema(), null, 2) + '\n');
|
|
195
197
|
if (!options.link) {
|
|
196
|
-
const original = text
|
|
198
|
+
const original = text ?? configTemplate(config);
|
|
197
199
|
await atomicPrivate(instance.source.path, applyEdits(original, modify(original, ['$schema'], '../../gateway.schema.json', { formattingOptions: { insertSpaces: true, tabSize: 2 } })));
|
|
198
200
|
}
|
|
199
201
|
registry.instances.push(instance);
|
|
@@ -234,13 +236,18 @@ export async function patchInstanceConfig(instance, patch, revision, home = home
|
|
|
234
236
|
text = applyEdits(text, modify(text, [key], value, { formattingOptions: { insertSpaces: true, tabSize: 2 } }));
|
|
235
237
|
return saveInstanceConfig(instance, text, revision, home);
|
|
236
238
|
}
|
|
239
|
+
export function usesGlobalPermissions(config) {
|
|
240
|
+
return config.writeScope === 'unrestricted' &&
|
|
241
|
+
(config.accessRole === 'restricted' && config.mode === 'read-only' || config.accessRole === 'trusted' && config.mode === 'read-write');
|
|
242
|
+
}
|
|
237
243
|
export function instanceEnv(instance, config, home = homedir()) {
|
|
238
|
-
const legacy = { projectRoot: config.projectRoot, mode: config.mode, port: config.port, connection: config.connection === 'local' ? 'https' : config.connection,
|
|
239
|
-
...(config.accessRole === 'restricted' && config.mode === 'read-write' ? { version: 1 } : config.writeScope === 'home-only' ? { version: 3, accessRole: config.accessRole, writeScope: 'home-only' } : { version: 2, accessRole: config.accessRole }) };
|
|
240
244
|
const p = instancePaths(instance, home);
|
|
241
|
-
return {
|
|
242
|
-
|
|
243
|
-
|
|
245
|
+
return { STARMUX_PROJECT_ROOT: config.projectRoot, STARMUX_PROJECT_PORT: String(config.port),
|
|
246
|
+
STARMUX_WORKSPACE_MODE: config.mode, STARMUX_DEV_COMMANDS: config.accessRole === 'trusted' ? 'trusted-project' : 'presets',
|
|
247
|
+
STARMUX_DC_ACCESS_MODE: config.accessRole === 'trusted' ? 'native' : 'workspace', STARMUX_DC_WRITE_SCOPE: config.writeScope,
|
|
248
|
+
STARMUX_ACCESS_FILE: p.access, STARMUX_GRANTS_FILE: p.grants, STARMUX_DENY_FILE: p.deny,
|
|
249
|
+
STARMUX_GIT_MODE: config.gitMode, STARMUX_WORKTREE_MODE: config.worktreeMode, STARMUX_INSTANCE_ID: instance.id, STARMUX_INSTANCE_NAME: instance.name,
|
|
250
|
+
STARMUX_PRIVATE_ACTIVITY_OBSERVER: '1', STARMUX_PERMISSION_MODE: usesGlobalPermissions(config) ? 'global' : 'workspace' };
|
|
244
251
|
}
|
|
245
252
|
export async function ensureAccessReady(instance, home = homedir()) {
|
|
246
253
|
const access = await createAccessControl(instancePaths(instance, home).access);
|
|
@@ -266,4 +273,3 @@ export async function prepareUpstreams(instance, config, home = homedir()) {
|
|
|
266
273
|
await atomicPrivate(file, JSON.stringify({ version: 1, upstreams: definitions }));
|
|
267
274
|
return file;
|
|
268
275
|
}
|
|
269
|
-
//# sourceMappingURL=instance-store.js.map
|
|
@@ -2,8 +2,7 @@ import { verifyInternalSetupWorker } from './global-core-control.js';
|
|
|
2
2
|
import { runGateway } from './dc-workspace-http.js';
|
|
3
3
|
if (!process.send)
|
|
4
4
|
throw Error('INSTANCE_WORKER_REQUIRES_SUPERVISOR');
|
|
5
|
-
verifyInternalSetupWorker({ port: Number(process.env.
|
|
6
|
-
runId: process.env.
|
|
5
|
+
verifyInternalSetupWorker({ port: Number(process.env.STARMUX_PROJECT_PORT), ownerPid: Number(process.env.STARMUX_INTERNAL_SETUP_OWNER_PID),
|
|
6
|
+
runId: process.env.STARMUX_INTERNAL_SETUP_RUN_ID, token: process.env.STARMUX_INTERNAL_SETUP_TOKEN });
|
|
7
7
|
await runGateway();
|
|
8
8
|
process.send({ type: 'gateway-ready' });
|
|
9
|
-
//# sourceMappingURL=instance-worker.js.map
|