starmux 0.13.1 → 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 +18 -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 +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 +5 -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
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Cloud AI assistants (like **ChatGPT Web**, **Claude**, **Codex**, or **WorkBuddy
|
|
|
24
24
|
|
|
25
25
|
- **Batteries-Included Workspace Tools:** Powered by a built-in [Desktop Commander](https://github.com/wonderwhy-er/DesktopCommanderMCP) engine (installed automatically via npm). Out of the box, your AI can read/write files, search code, run Shell commands, and manage Git.
|
|
26
26
|
- **Code on Any Host (macOS, Linux, Windows & Edge Devices):** Many AI clients (like **ChatGPT Desktop** or **WorkBuddy**) have no native Linux or ARM64 client. Because Starmux runs anywhere **Node.js 22.12+** runs—including **macOS**, **Linux (`x64` / `arm64`)**, **NVIDIA Jetson**, headless servers, and **Windows**—you can use your favorite AI client on your main computer or browser to develop directly on any target machine.
|
|
27
|
-
- **
|
|
27
|
+
- **Two Clear Permission Modes:** Starts in global **Read-Only** mode (`starmux up`): AI can read files your OS account and the upstream server permit, while writes and arbitrary execution are blocked. Explicitly opt into **Coding Mode** (`starmux up --code`) to use native tools with your OS account permissions.
|
|
28
28
|
- **Your Network, Your Keys:** No vendor-hosted relay required. Protect your gateway with Bearer **API Keys**, browser-based **OAuth**, or your existing **Tunnel / Reverse Proxy** access controls. Revoke access instantly at any time.
|
|
29
29
|
|
|
30
30
|
---
|
|
@@ -44,7 +44,7 @@ npm install -g starmux
|
|
|
44
44
|
```bash
|
|
45
45
|
cd /path/to/your/project
|
|
46
46
|
|
|
47
|
-
# Option A:
|
|
47
|
+
# Option A: Global read-only mode (default — inspect files & code)
|
|
48
48
|
starmux up
|
|
49
49
|
|
|
50
50
|
# Option B: Full coding mode (edit files, run Shell & Git)
|
|
@@ -53,28 +53,18 @@ starmux up --code
|
|
|
53
53
|
|
|
54
54
|
Starmux initializes itself and runs in the background on port `18893` (coding mode asks for confirmation the first time):
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
Mode Read-only (run 'starmux up --code' to enable edits & shell)
|
|
61
|
-
Auth None (no token required)
|
|
62
|
-
Runtime Background · continues after this terminal closes
|
|
63
|
-
|
|
64
|
-
Connect an AI client (MCP settings)
|
|
65
|
-
Transport Streamable HTTP
|
|
66
|
-
URL (Local) http://127.0.0.1:18893/mcp (AI client on this computer)
|
|
67
|
-
URL (Remote) https://<your-domain-or-tunnel>/mcp (via protected HTTPS proxy or tunnel -> 127.0.0.1:18893)
|
|
68
|
-
Authentication None (optional Bearer API key: starmux auth key "My AI Client")
|
|
69
|
-
|
|
70
|
-
Commands
|
|
71
|
-
Verify starmux verify
|
|
72
|
-
Auth & keys starmux auth
|
|
73
|
-
Dashboard starmux dashboard (view & copy settings in browser)
|
|
74
|
-
Logs starmux logs -f
|
|
75
|
-
Stop starmux down
|
|
56
|
+
Switch back from Coding mode with
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
starmux up --read
|
|
76
60
|
```
|
|
77
61
|
|
|
62
|
+
Add `-i NAME` for a named instance. Both `up` and `status` show the mode-switch command.
|
|
63
|
+
|
|
64
|
+
`Project` provides context for relative file paths in Read-only mode; it does not restrict access to that directory. Read-only applies across the computer wherever the OS account and upstream server permit reading. Coding forwards native operations with the account's permissions. Neither mode uses Starmux directory grants or path exclusions.
|
|
65
|
+
|
|
66
|
+
In Read-only mode, `read_multiple_files` supports up to 20 files and 4 MiB of combined file sizes per batch. Missing files retain Desktop Commander's per-file errors alongside successful contents. Oversized batches return `BATCH_READ_COUNT_LIMIT` or `BATCH_READ_SIZE_LIMIT` with guidance to split the request.
|
|
67
|
+
|
|
78
68
|
### 3. Add Starmux to your AI client
|
|
79
69
|
|
|
80
70
|
Need a Bearer API key for remote access? Generate one first (optional for local use):
|
|
@@ -176,7 +166,7 @@ See [Client Compatibility](docs/CLIENTS.md) and [Remote Setup](docs/REMOTE_SETUP
|
|
|
176
166
|
|
|
177
167
|
---
|
|
178
168
|
|
|
179
|
-
## Everyday CLI &
|
|
169
|
+
## Everyday CLI & Multiple Instances
|
|
180
170
|
|
|
181
171
|
Manage your gateway with simple, human-friendly commands:
|
|
182
172
|
|
|
@@ -190,11 +180,11 @@ starmux logs -f # Stream live gateway logs
|
|
|
190
180
|
starmux down # Stop the gateway (alias: stop)
|
|
191
181
|
```
|
|
192
182
|
|
|
193
|
-
After initialization, `starmux up` reuses your saved
|
|
183
|
+
After initialization, `starmux up` reuses your instance's saved project context. Use `starmux up .` to change it to your current directory, or pass `--foreground` to keep the process in your terminal. Only a new instance defaults to the current directory when no path is supplied.
|
|
194
184
|
|
|
195
|
-
### Advanced: Running multiple
|
|
185
|
+
### Advanced: Running multiple instances (`-i`)
|
|
196
186
|
|
|
197
|
-
*(Optional)* By default, Starmux manages a single `default`
|
|
187
|
+
*(Optional)* By default, Starmux manages a single `default` instance so you never have to pass extra flags. To use different project contexts or permission modes on the same machine, pass `-i <name>` to create instances with their own ports, credentials, configuration, and logs. Instances share the OS account; they are not filesystem sandboxes:
|
|
198
188
|
|
|
199
189
|
```bash
|
|
200
190
|
starmux up ~/work/repo-a -i work --code
|
|
@@ -238,8 +228,8 @@ Explore the `docs/` guides by topic:
|
|
|
238
228
|
- [Access Control & API Keys](docs/ACCESS_CONTROL.md) — Creating/revoking Bearer API keys, expiration policies, and using the local Web Access Console.
|
|
239
229
|
- [ChatGPT Web & OAuth 2.0](docs/OAUTH.md) & [Connect ChatGPT](docs/CONNECT_CHATGPT.md) — Configuring Keycloak OAuth 2.0 + PKCE and OpenAI Secure MCP Tunnel.
|
|
240
230
|
- **⚙️ Advanced Configuration & Security**
|
|
241
|
-
- [Multi-Instance & JSONC Config](docs/INSTANCES.md) — Managing
|
|
242
|
-
- [Security &
|
|
231
|
+
- [Multi-Instance & JSONC Config](docs/INSTANCES.md) — Managing instances, hot-reloading `config.jsonc`, mounting extra `stdio` MCP upstreams, and secrets.
|
|
232
|
+
- [Security & Permissions](docs/SECURITY.md) — Understanding global Read-Only vs. Coding Mode, upstream permissions, and macOS privacy boundaries.
|
|
243
233
|
- [Client Compatibility](docs/CLIENTS.md) & [Platform Details](docs/PLATFORMS.md) — Tested AI client matrix and macOS / Linux / Windows runtime specifics.
|
|
244
234
|
- **🛠️ Reference & Project**
|
|
245
235
|
- [Documentation Index](docs/README.md) · [Installation](docs/INSTALL.md) · [Contributing](CONTRIBUTING.md)
|
|
@@ -12,7 +12,7 @@ const schema = z.object({ version: z.literal(1), mode: z.enum(['ingress', 'api-k
|
|
|
12
12
|
oauth: oauthConfigSchema.optional(), oauthConnections: z.array(oauthConnectionSchema).max(512).default([]) }).strict();
|
|
13
13
|
const digest = (value) => createHash('sha256').update(value).digest('hex');
|
|
14
14
|
const equal = (a, b) => a.length === b.length && timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
15
|
-
export const defaultAccessFile = () => process.env.
|
|
15
|
+
export const defaultAccessFile = () => process.env.STARMUX_ACCESS_FILE || path.join(homedir(), '.config', 'starmux', 'access-v1.json');
|
|
16
16
|
const active = (c) => !c.revokedAt && (!c.expiresAt || Date.parse(c.expiresAt) > Date.now());
|
|
17
17
|
export async function createAccessControl(file = defaultAccessFile(), options = {}) {
|
|
18
18
|
if (!path.isAbsolute(file))
|
|
@@ -222,4 +222,3 @@ export async function createAccessControl(file = defaultAccessFile(), options =
|
|
|
222
222
|
}
|
|
223
223
|
return { file, snapshot, create, setMode, revoke, authenticate, ownerToken, authorized, configureOAuth, revokeOAuth, oauthSettings };
|
|
224
224
|
}
|
|
225
|
-
//# sourceMappingURL=access-control.js.map
|
|
@@ -8,8 +8,8 @@ const e = (s) => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<':
|
|
|
8
8
|
export function accessPage(state, created) {
|
|
9
9
|
const form = (op, fields, label) => `<form method="post" action="/access"><input type="hidden" name="op" value="${op}">${fields}<label>Owner management token <input type="password" name="token" required autocomplete="off"></label><button>${label}</button></form>`;
|
|
10
10
|
return `<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>Starmux · Access Control</title><style>body{font:16px system-ui;max-width:960px;margin:40px auto;padding:0 24px;background:#f5f7fa;color:#243249}section{background:white;border:1px solid #dae0e8;border-radius:12px;padding:24px;margin:20px 0}input,select,button{font:inherit;padding:8px;margin:8px}label{display:block}code{overflow-wrap:anywhere}td,th{text-align:left;padding:10px;border-bottom:1px solid #ddd}table{width:100%}</style><h1>Starmux · Access Control</h1>
|
|
11
|
-
<p><a href="/dashboard">Dashboard</a
|
|
12
|
-
<section><h2>${state.mode === 'ingress' ? 'None (local or ingress-managed)' : state.mode === 'oauth' ? 'OAuth' : 'Bearer API Key'}</h2><p>
|
|
11
|
+
<p><a href="/dashboard">Dashboard</a></p>
|
|
12
|
+
<section><h2>${state.mode === 'ingress' ? 'None (local or ingress-managed)' : state.mode === 'oauth' ? 'OAuth' : 'Bearer API Key'}</h2><p>Credentials do not change the instance’s mode or operating-system permissions.</p><p>${state.mode === 'ingress' ? 'Starmux does not verify a token. Restrict remote access through your HTTPS proxy or tunnel.' : 'Every MCP request requires an active credential. Missing, invalid, expired or revoked credentials are rejected.'}</p>
|
|
13
13
|
<table><tr><th>MCP Client Field</th><th>Value to Copy</th></tr>
|
|
14
14
|
<tr><td>Transport</td><td><code>Streamable HTTP</code></td></tr>
|
|
15
15
|
<tr><td>URL (Local)</td><td><code>http://127.0.0.1:<PORT>/mcp</code></td></tr>
|
|
@@ -74,4 +74,3 @@ export async function accessPost(req, access) {
|
|
|
74
74
|
return { status: 400 };
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
-
//# sourceMappingURL=access-dashboard.js.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { GatewayError, errorCode } from './validation.js';
|
|
5
|
+
import { prepareBatchRead } from './batch-read-policy.js';
|
|
6
|
+
// Reviewed Desktop Commander operations. Tool annotations are hints, not an
|
|
7
|
+
// authorization boundary; unknown tools cannot opt themselves into read-only.
|
|
8
|
+
const readOnlyTools = new Set(['health_check', 'get_config', 'read_file', 'read_multiple_files',
|
|
9
|
+
'list_directory', 'get_file_info', 'start_search', 'get_more_search_results', 'stop_search',
|
|
10
|
+
'list_searches', 'read_process_output', 'list_sessions', 'list_processes', 'get_usage_stats', 'get_recent_tool_calls']);
|
|
11
|
+
export const isReadOnlyTool = (name) => readOnlyTools.has(name);
|
|
12
|
+
export async function createAccountAccessPolicy(root, mode = 'read-only') {
|
|
13
|
+
if (!path.isAbsolute(root))
|
|
14
|
+
throw new GatewayError('ROOT_REQUIRED');
|
|
15
|
+
const base = await realpath(root);
|
|
16
|
+
if (!(await stat(base)).isDirectory())
|
|
17
|
+
throw new GatewayError('NOT_DIRECTORY');
|
|
18
|
+
function absolute(raw) {
|
|
19
|
+
if (typeof raw !== 'string' || !raw || raw.includes('\0'))
|
|
20
|
+
throw new GatewayError('INVALID_PATH');
|
|
21
|
+
const expanded = raw === '~' ? homedir() : raw.startsWith('~/') ? path.join(homedir(), raw.slice(2)) : raw;
|
|
22
|
+
return path.resolve(base, expanded);
|
|
23
|
+
}
|
|
24
|
+
const policy = {
|
|
25
|
+
root: base, mode, developerCommands: 'off',
|
|
26
|
+
async validateGitPath() { throw new GatewayError('READ_ONLY_MODE'); },
|
|
27
|
+
async prepare(name, args = {}) {
|
|
28
|
+
if (!isReadOnlyTool(name))
|
|
29
|
+
throw new GatewayError('READ_ONLY_MODE');
|
|
30
|
+
if (name === 'read_multiple_files')
|
|
31
|
+
return prepareBatchRead(args, policy.prepare);
|
|
32
|
+
if (['read_file', 'list_directory', 'get_file_info', 'start_search'].includes(name)) {
|
|
33
|
+
let target = absolute(args.path);
|
|
34
|
+
if (name === 'read_file') {
|
|
35
|
+
// Canonicalize existing links for accurate batch byte accounting, while
|
|
36
|
+
// letting upstream retain its native missing-file errors.
|
|
37
|
+
try {
|
|
38
|
+
target = await realpath(target);
|
|
39
|
+
const info = await stat(target);
|
|
40
|
+
if (info.isFile() && info.size > 1024 * 1024)
|
|
41
|
+
throw new GatewayError('FILE_TOO_LARGE');
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (errorCode(error) !== 'ENOENT')
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { name, arguments: { ...args, path: target } };
|
|
49
|
+
}
|
|
50
|
+
return { name, arguments: args };
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
return Object.freeze(policy);
|
|
54
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { lstat } from 'node:fs/promises';
|
|
2
|
+
import { GatewayError, errorCode } from './validation.js';
|
|
3
|
+
// Validate every file before forwarding any part of a batch upstream.
|
|
4
|
+
export async function prepareBatchRead(args, prepare) {
|
|
5
|
+
if (!args || Object.keys(args).some(key => key !== 'paths') || !Array.isArray(args.paths) ||
|
|
6
|
+
args.paths.length < 1 || args.paths.some(p => typeof p !== 'string'))
|
|
7
|
+
throw new GatewayError('BATCH_READ_ARGUMENTS_DENIED');
|
|
8
|
+
if (args.paths.length > 20)
|
|
9
|
+
throw new GatewayError('BATCH_READ_COUNT_LIMIT');
|
|
10
|
+
const paths = [];
|
|
11
|
+
let bytes = 0;
|
|
12
|
+
for (const raw of args.paths) {
|
|
13
|
+
const invocation = await prepare('read_file', { path: raw });
|
|
14
|
+
const target = invocation.arguments?.path;
|
|
15
|
+
if (typeof target !== 'string')
|
|
16
|
+
throw new GatewayError('BATCH_READ_PATH_INVALID');
|
|
17
|
+
const st = await lstat(target).catch(error => {
|
|
18
|
+
// Authorization already succeeded. Keep missing paths in the upstream
|
|
19
|
+
// batch so Desktop Commander can report ENOENT beside successful reads.
|
|
20
|
+
if (errorCode(error) === 'ENOENT')
|
|
21
|
+
return null;
|
|
22
|
+
throw error;
|
|
23
|
+
});
|
|
24
|
+
bytes += st?.size ?? 0;
|
|
25
|
+
if (bytes > 4 * 1024 * 1024)
|
|
26
|
+
throw new GatewayError('BATCH_READ_SIZE_LIMIT');
|
|
27
|
+
paths.push(target);
|
|
28
|
+
}
|
|
29
|
+
return { name: 'read_multiple_files', arguments: { paths } };
|
|
30
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createInterface } from 'node:readline/promises';
|
|
2
1
|
import { spawn } from 'node:child_process';
|
|
3
2
|
import { readFile, writeFile, mkdtemp, rm, realpath } from 'node:fs/promises';
|
|
4
3
|
import { tmpdir, homedir } from 'node:os';
|
|
@@ -7,9 +6,9 @@ import { createAccessControl } from '../access-control.js';
|
|
|
7
6
|
import { createWorkspaceGrants } from '../workspace-grants.js';
|
|
8
7
|
import { createDenyRules } from '../explicit-deny-rules.js';
|
|
9
8
|
import { createPrivateDirectory } from '../platform.js';
|
|
10
|
-
import { instanceStatus, instanceRequest
|
|
11
|
-
import { atomicPrivate, changeRegistry, configJSONSchema, sourceRevision, configTemplate,
|
|
12
|
-
const booleans = new Set(['--json', '--all', '--link', '--archived', '--approve-trusted-developer', '--approve-unrestricted', '--
|
|
9
|
+
import { instanceStatus, instanceRequest } from '../instance-runtime.js';
|
|
10
|
+
import { atomicPrivate, changeRegistry, configJSONSchema, sourceRevision, configTemplate, validateConfig, withLock, createInstance, exists, fingerprint, instancePaths, loadInstanceConfig, parseConfig, patchInstanceConfig, readRegistry, readTrusted, resolveInstance, saveInstanceConfig, suggestPort } from '../instance-store.js';
|
|
11
|
+
const booleans = new Set(['--json', '--all', '--link', '--archived', '--approve-trusted-developer', '--approve-unrestricted', '--effective', '--allow-loopback-http']);
|
|
13
12
|
function options(args) {
|
|
14
13
|
const flags = new Map();
|
|
15
14
|
const positional = [];
|
|
@@ -44,75 +43,10 @@ function requireApprovals(config, flags) {
|
|
|
44
43
|
if (config.accessRole === 'trusted' && process.platform !== 'darwin' && !flags.has('--approve-unrestricted'))
|
|
45
44
|
throw Error('UNRESTRICTED_APPROVAL_REQUIRED: add --approve-unrestricted');
|
|
46
45
|
}
|
|
47
|
-
async function wizard(flags, current) {
|
|
48
|
-
let project = flags.get('--project') ?? current?.projectRoot;
|
|
49
|
-
let role = flags.get('--access-role') ?? current?.accessRole ?? 'restricted';
|
|
50
|
-
let port = Number(flags.get('--port') ?? current?.port ?? await suggestPort());
|
|
51
|
-
if (!project) {
|
|
52
|
-
if (!process.stdin.isTTY)
|
|
53
|
-
throw Error('PROJECT_REQUIRED: use --project /absolute/directory');
|
|
54
|
-
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
55
|
-
try {
|
|
56
|
-
project = await rl.question('Work directory: ');
|
|
57
|
-
role = (await rl.question('Capability (restricted / trusted) [restricted]: ')) || 'restricted';
|
|
58
|
-
if (role === 'trusted' && (await rl.question('Enable native tools and Shell as your OS account? Type yes: ')) === 'yes') {
|
|
59
|
-
flags.set('--approve-trusted-developer', 'true');
|
|
60
|
-
if (process.platform !== 'darwin')
|
|
61
|
-
flags.set('--approve-unrestricted', 'true');
|
|
62
|
-
}
|
|
63
|
-
port = Number((await rl.question(`Local port [${port}]: `)) || port);
|
|
64
|
-
}
|
|
65
|
-
finally {
|
|
66
|
-
rl.close();
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
const config = instanceConfigSchema.parse({ ...current, schemaVersion: 1, projectRoot: await realpath(path.resolve(project)), port,
|
|
70
|
-
accessRole: role, mode: current?.accessRole === role ? current.mode : role === 'trusted' ? 'read-write' : 'read-only', writeScope: current?.accessRole === role ? current.writeScope : role === 'trusted' && process.platform === 'darwin' ? 'home-only' : 'unrestricted', connection: flags.get('--connection') ?? current?.connection ?? 'local' });
|
|
71
|
-
requireApprovals(config, flags);
|
|
72
|
-
return validateConfig(config);
|
|
73
|
-
}
|
|
74
|
-
export async function maybeHandleInstances(argv) {
|
|
75
|
-
let command = argv[0] ?? 'help';
|
|
76
|
-
if (command === 'start-setup' && !process.env.MAC_MCP_INTERNAL_SETUP_OWNER_PID) {
|
|
77
|
-
command = 'run';
|
|
78
|
-
argv = ['run', '--profile', 'setup', ...argv.slice(1)];
|
|
79
|
-
}
|
|
80
|
-
if (command === 'doctor-setup') {
|
|
81
|
-
command = 'doctor';
|
|
82
|
-
argv = ['doctor', '--profile', 'setup', ...argv.slice(1)];
|
|
83
|
-
}
|
|
84
|
-
const supported = ['instance', 'config', 'run', 'status', 'stop', 'restart', 'logs', 'doctor', 'verify', 'access', 'permissions', 'privacy', 'setup'];
|
|
85
|
-
if (!supported.includes(command))
|
|
86
|
-
return false;
|
|
87
|
-
try {
|
|
88
|
-
const explicit = argv.includes('--instance');
|
|
89
|
-
const registry = await readRegistry();
|
|
90
|
-
const managed = registry.instances.length > 0;
|
|
91
|
-
const firstWizard = command === 'setup' && argv.length === 1 && process.stdin.isTTY;
|
|
92
|
-
if (!['instance', 'config'].includes(command) && !explicit && !managed && !firstWizard)
|
|
93
|
-
return false;
|
|
94
|
-
// A retained legacy profile remains available until the owner migrates it.
|
|
95
|
-
const profileIndex = argv.indexOf('--profile');
|
|
96
|
-
if (profileIndex >= 0 && !registry.instances.some(i => i.legacy === argv[profileIndex + 1]) && argv[profileIndex + 1] !== 'auto') {
|
|
97
|
-
if (registry.instances.some(i => i.legacy))
|
|
98
|
-
throw Error('LEGACY_PROFILE_NOT_MIGRATED_USE_INSTANCE: select --instance explicitly');
|
|
99
|
-
return false;
|
|
100
|
-
}
|
|
101
|
-
await handle(command, argv.slice(1));
|
|
102
|
-
}
|
|
103
|
-
catch (error) {
|
|
104
|
-
if (argv.includes('--json'))
|
|
105
|
-
output({ schemaVersion: 1, error: message(error) });
|
|
106
|
-
else
|
|
107
|
-
process.stderr.write(message(error) + '\n');
|
|
108
|
-
process.exitCode = 1;
|
|
109
|
-
}
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
46
|
export async function handle(command, args) {
|
|
113
47
|
const { flags, positional } = options(args);
|
|
114
48
|
const json = flags.has('--json');
|
|
115
|
-
const known = new Set([...booleans, '--instance', '--name', '--project', '--port', '--access-role', '--connection', '--
|
|
49
|
+
const known = new Set([...booleans, '--instance', '--name', '--project', '--port', '--access-role', '--connection', '--app-parent-pid', '--revision', '--patch', '--file', '--output', '--lines', '--expires', '--issuer', '--resource', '--resource-aliases', '--client-id', '--subjects', '--registration', '--token-auth', '--callback', '--target', '--mode']);
|
|
116
50
|
for (const flag of flags.keys())
|
|
117
51
|
if (!known.has(flag))
|
|
118
52
|
throw Error('UNKNOWN_OPTION: ' + flag);
|
|
@@ -152,12 +86,6 @@ export async function handle(command, args) {
|
|
|
152
86
|
}
|
|
153
87
|
return;
|
|
154
88
|
}
|
|
155
|
-
if (op === 'create') {
|
|
156
|
-
const config = await wizard(flags);
|
|
157
|
-
const instance = await createInstance(positional[1] ?? flags.get('--name') ?? 'default', { config });
|
|
158
|
-
output({ instance, message: 'Saved. Start with: starmux up -i ' + JSON.stringify(instance.name) });
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
89
|
if (op === 'import') {
|
|
162
90
|
const file = positional[1];
|
|
163
91
|
if (!file)
|
|
@@ -168,11 +96,6 @@ export async function handle(command, args) {
|
|
|
168
96
|
message: flags.has('--link') ? 'Bound to original file. Changes require apply.' : 'Imported a private copy. The original file is no longer used.' });
|
|
169
97
|
return;
|
|
170
98
|
}
|
|
171
|
-
if (op === 'migrate') {
|
|
172
|
-
const { migrateLegacy } = await import('../instance-migration.js');
|
|
173
|
-
output(await migrateLegacy(positional[1], flags.has('--confirm'), flags.get('--name')));
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
99
|
const instance = await resolveInstance(positional[1] ?? flags.get('--instance'), homedir(), op === 'restore');
|
|
177
100
|
if (op === 'copy') {
|
|
178
101
|
const config = { ...(await loadInstanceConfig(instance)).config, port: await suggestPort() };
|
|
@@ -222,47 +145,9 @@ export async function handle(command, args) {
|
|
|
222
145
|
process.stdout.write(`Restored instance '${instance.name}'.\n`);
|
|
223
146
|
return;
|
|
224
147
|
}
|
|
225
|
-
|
|
226
|
-
return handle('instance', ['list', ...(json ? ['--json'] : [])]);
|
|
227
|
-
const profile = flags.get('--profile');
|
|
228
|
-
let selector = flags.get('--instance');
|
|
229
|
-
if (profile && profile !== 'auto') {
|
|
230
|
-
const legacy = (await readRegistry()).instances.find(i => i.legacy === profile);
|
|
231
|
-
if (!legacy)
|
|
232
|
-
throw Error('LEGACY_PROFILE_NOT_MIGRATED');
|
|
233
|
-
selector = legacy.id;
|
|
234
|
-
}
|
|
235
|
-
if (command === 'setup' && !(await readRegistry()).instances.length) {
|
|
236
|
-
await handle('instance', ['create', 'default', ...args]);
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
148
|
+
const selector = flags.get('--instance');
|
|
239
149
|
const instance = await resolveInstance(selector);
|
|
240
150
|
const p = instancePaths(instance);
|
|
241
|
-
if (command === 'status') {
|
|
242
|
-
output(await instanceStatus(instance));
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
if (command === 'run') {
|
|
246
|
-
if (flags.has('--port'))
|
|
247
|
-
throw Error('INSTANCE_PORT_OVERRIDE_DENIED: edit configuration and apply');
|
|
248
|
-
await runInstance(instance, { json, ...(flags.has('--app-parent-pid') ? { appParentPid: Number(flags.get('--app-parent-pid')) } : {}) });
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
if (command === 'stop') {
|
|
252
|
-
output({ result: (await instanceRequest(instance, 'stop')) ? 'STOPPED' : 'ALREADY_STOPPED', instanceId: instance.id });
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
if (command === 'restart') {
|
|
256
|
-
await handle('config', ['apply', '--instance', instance.id, ...(json ? ['--json'] : [])]);
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
if (command === 'setup') {
|
|
260
|
-
const current = await loadInstanceConfig(instance);
|
|
261
|
-
const config = await wizard(flags, current.config);
|
|
262
|
-
await patchInstanceConfig(instance, config, current.revision);
|
|
263
|
-
output({ result: 'SAVED', instanceId: instance.id, message: 'Apply changes to restart the running instance.' });
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
151
|
if (command === 'config') {
|
|
267
152
|
const op = positional[0] ?? 'show';
|
|
268
153
|
if (op === 'path') {
|
|
@@ -444,29 +329,6 @@ export async function handle(command, args) {
|
|
|
444
329
|
}
|
|
445
330
|
throw Error('CONFIG_ACTION_INVALID');
|
|
446
331
|
}
|
|
447
|
-
if (command === 'logs') {
|
|
448
|
-
const file = path.join(p.logs, 'core.log');
|
|
449
|
-
const lines = Math.min(200, Math.max(1, Number(flags.get('--lines') ?? 60)));
|
|
450
|
-
if (!Number.isSafeInteger(Number(flags.get('--lines') ?? 60)) || Number(flags.get('--lines') ?? 60) < 1 || Number(flags.get('--lines') ?? 60) > 200)
|
|
451
|
-
throw Error('INVALID_LINES: expected 1..200');
|
|
452
|
-
const text = await exists(file) ? (await readTrusted(file, true, 32 * 1024 * 1024)).split('\n').slice(-lines).join('\n') : 'No logs yet.';
|
|
453
|
-
if (json)
|
|
454
|
-
output({ instanceId: instance.id, output: text });
|
|
455
|
-
else
|
|
456
|
-
process.stdout.write(text + '\n');
|
|
457
|
-
return;
|
|
458
|
-
}
|
|
459
|
-
if (command === 'access') {
|
|
460
|
-
process.env.MAC_MCP_ACCESS_FILE = p.access;
|
|
461
|
-
const clean = args.filter((_, i) => args[i] !== '--instance' && args[i - 1] !== '--instance' && args[i] !== '--profile' && args[i - 1] !== '--profile');
|
|
462
|
-
await (await import('./access.js')).accessCommand(clean);
|
|
463
|
-
if (positional[0] === 'mode') {
|
|
464
|
-
const mode = positional[1];
|
|
465
|
-
if (mode === 'ingress' || mode === 'api-key' || mode === 'oauth')
|
|
466
|
-
await changeRegistry(async (r) => { r.instances.find(i => i.id === instance.id).requiredAccess = mode; });
|
|
467
|
-
}
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
332
|
if (command === 'permissions') {
|
|
471
333
|
const grants = await createWorkspaceGrants({ file: p.grants });
|
|
472
334
|
const deny = await createDenyRules({ file: p.deny });
|
|
@@ -482,15 +344,6 @@ export async function handle(command, args) {
|
|
|
482
344
|
}
|
|
483
345
|
return;
|
|
484
346
|
}
|
|
485
|
-
const loaded = await loadInstanceConfig(instance);
|
|
486
|
-
if (command === 'doctor') {
|
|
487
|
-
const { ensureAccessReady } = await import('../instance-store.js');
|
|
488
|
-
await ensureAccessReady(instance);
|
|
489
|
-
const status = await instanceStatus(instance);
|
|
490
|
-
output({ result: 'PASS', instanceId: instance.id, source: instance.source, config: loaded.config, configState: status.configState,
|
|
491
|
-
message: 'Configuration and access state validated. Run verify after starting to check real tools.' });
|
|
492
|
-
return;
|
|
493
|
-
}
|
|
494
347
|
if (command === 'privacy') {
|
|
495
348
|
const status = await instanceStatus(instance);
|
|
496
349
|
if (!status.runtime?.port)
|
|
@@ -498,27 +351,5 @@ export async function handle(command, args) {
|
|
|
498
351
|
output(await (await import('../privacy-diagnostics.js')).privacyReport({ port: status.runtime.port, home: homedir(), all: flags.has('--all'), target: flags.get('--target') ?? null }));
|
|
499
352
|
return;
|
|
500
353
|
}
|
|
501
|
-
|
|
502
|
-
const state = await instanceRequest(instance, 'status');
|
|
503
|
-
if (state?.phase !== 'running' || !state.port)
|
|
504
|
-
throw Error('INSTANCE_NOT_RUNNING');
|
|
505
|
-
const endpoint = `http://127.0.0.1:${state.port}`;
|
|
506
|
-
const health = await fetch(endpoint + '/healthz', { signal: AbortSignal.timeout(3000) }).then(r => r.json());
|
|
507
|
-
if (health.instanceId !== instance.id)
|
|
508
|
-
throw Error('INSTANCE_HEALTH_IDENTITY_MISMATCH');
|
|
509
|
-
const request = async (method, params) => {
|
|
510
|
-
const reply = await fetch(endpoint + '/mcp', { method: 'POST', headers: { 'content-type': 'application/json', accept: 'application/json, text/event-stream', ...(process.env.MAC_MCP_API_KEY ? { authorization: 'Bearer ' + process.env.MAC_MCP_API_KEY } : {}) }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }), signal: AbortSignal.timeout(15000) });
|
|
511
|
-
if (!reply.ok)
|
|
512
|
-
throw Error(reply.status === 401 ? 'VERIFICATION_CREDENTIAL_REQUIRED: set MAC_MCP_API_KEY' : 'MCP_VERIFICATION_FAILED');
|
|
513
|
-
const data = await reply.json();
|
|
514
|
-
if (data.error || data.result?.isError)
|
|
515
|
-
throw Error('MCP_VERIFICATION_FAILED');
|
|
516
|
-
return data.result;
|
|
517
|
-
};
|
|
518
|
-
await request('tools/list', {});
|
|
519
|
-
await request('tools/call', { name: 'list_directory', arguments: { path: '.', depth: 0 } });
|
|
520
|
-
output({ result: 'PASS', instanceId: instance.id, port: state.port, remoteClientVerified: false });
|
|
521
|
-
return;
|
|
522
|
-
}
|
|
354
|
+
throw Error('INSTANCE_COMMAND_UNSUPPORTED');
|
|
523
355
|
}
|
|
524
|
-
//# sourceMappingURL=instances.js.map
|
package/dist/src/cli/oauth.js
CHANGED
|
@@ -71,4 +71,3 @@ export async function oauthCommand(args) {
|
|
|
71
71
|
process.stdout.write(' Configuration saved. Enable explicitly: starmux auth mode oauth\n');
|
|
72
72
|
process.stdout.write(' Remote ChatGPT authorization is not verified by this command.\n\n');
|
|
73
73
|
}
|
|
74
|
-
//# sourceMappingURL=oauth.js.map
|
|
@@ -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,9 @@ async function authentication(instance, status) {
|
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
export async function formatStarmuxStatus(instance, status, config, options = {}) {
|
|
26
|
+
// Pending edits must never be shown as permissions of the running instance.
|
|
27
|
+
if (status.phase === 'running')
|
|
28
|
+
config = status.active?.config;
|
|
26
29
|
const auth = await authentication(instance, status);
|
|
27
30
|
// Omit `-i default` for the default instance to keep everyday single-instance output clean.
|
|
28
31
|
const omitSelector = options.isDefault === true && instance.name === 'default';
|
|
@@ -40,26 +43,20 @@ export async function formatStarmuxStatus(instance, status, config, options = {}
|
|
|
40
43
|
const saved = status.phase === 'running' ? ' (saved; live state unavailable)' : ' (saved for next start)';
|
|
41
44
|
const row = (label, value) => ' ' + label.padEnd(16) + value;
|
|
42
45
|
const codingCmd = command('up --code');
|
|
46
|
+
const globalMode = status.phase === 'running' ? status.runtime?.permissionMode === 'global' : !!config && usesGlobalPermissions(config);
|
|
43
47
|
const mode = !config
|
|
44
48
|
? 'Unavailable'
|
|
45
49
|
: config.accessRole === 'trusted'
|
|
46
|
-
?
|
|
50
|
+
? `${config.writeScope === 'home-only' ? 'Coding (Home sandbox · edits, shell & git)' : 'Coding (OS account · edits, shell & git)'} (run '${command('up --read')}' to return to read-only)`
|
|
47
51
|
: config.mode === 'read-write'
|
|
48
52
|
? 'File writes (restricted)'
|
|
49
|
-
: `Read-only (run '${codingCmd}' to enable edits & shell)`;
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
: status.executionMode === 'foreground-owned'
|
|
53
|
-
? (status.runtime?.launcher === 'app' ? 'App-owned · stops when the app closes' : 'Foreground · Ctrl+C stops this instance')
|
|
54
|
-
: 'Stopped';
|
|
55
|
-
const title = omitSelector
|
|
56
|
-
? `${indicator} Starmux is ${status.phase}`
|
|
57
|
-
: `${indicator} Starmux is ${status.phase} (instance: ${instance.name})`;
|
|
58
|
-
const lines = ['', title];
|
|
53
|
+
: `Read-only (${globalMode ? 'global · OS account' : 'project scope'}) (run '${codingCmd}' to enable edits & shell)`;
|
|
54
|
+
const title = `${indicator} Starmux is ${status.phase} (instance: ${instance.name})`;
|
|
55
|
+
const lines = ['', title, row('Instance', instance.id)];
|
|
59
56
|
if (ready && status.port) {
|
|
60
57
|
lines.push(row('Listen', `http://127.0.0.1:${status.port}/mcp (this computer only)`));
|
|
61
58
|
}
|
|
62
|
-
lines.push(row('
|
|
59
|
+
lines.push(row('Project', config?.projectRoot ?? 'Unavailable'), row('Mode', mode), row('Auth', auth.mode ? labels[auth.mode] + (ready ? '' : saved) : 'Unavailable'));
|
|
63
60
|
if (ready && status.port && auth.mode === 'oauth' && auth.oauth) {
|
|
64
61
|
lines.push(row('Client URL', auth.oauth.resource), row('Issuer', auth.oauth.issuer));
|
|
65
62
|
}
|
|
@@ -119,4 +116,3 @@ export async function formatStarmuxStatus(instance, status, config, options = {}
|
|
|
119
116
|
lines.push(row('Stop', command('down')));
|
|
120
117
|
return lines.join('\n') + '\n';
|
|
121
118
|
}
|
|
122
|
-
//# sourceMappingURL=starmux-status.js.map
|