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
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.15.0
|
|
4
|
+
|
|
5
|
+
### Instance lifecycle
|
|
6
|
+
|
|
7
|
+
- Version and validate the local instance control protocol independently of package releases.
|
|
8
|
+
- Report `unknown` when control records or channels cannot establish the instance's state; do not mistake a missing control channel for a stopped service.
|
|
9
|
+
- Confirm cleanup before `down` reports success. Repeated stops report `Already stopped`; failed or timed-out cleanup returns a nonzero exit code.
|
|
10
|
+
- Use the same stop confirmation for instance archival and protect successor runs during concurrent operations.
|
|
11
|
+
- Separate confirmed active configuration, saved settings and historical snapshots in CLI and JSON output.
|
|
12
|
+
- Prevent `up` from changing permissions in an unknown state, prevent `doctor` from passing unresolved runtime errors, and show unknown states in the macOS App.
|
|
13
|
+
- Add isolated lifecycle and real CLI regression tests, validated on macOS and NVIDIA NX Linux ARM64.
|
|
14
|
+
|
|
15
|
+
### Upgrade
|
|
16
|
+
|
|
17
|
+
Stop each running instance with the currently installed CLI before upgrading, then start it again with the new CLI. This release introduces a versioned instance control contract and does not migrate old runtime directories or support the previous unversioned contract. Replacing installed files does not update an already-running supervisor.
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
starmux ls
|
|
21
|
+
starmux down # Default instance
|
|
22
|
+
starmux down -i NAME # Repeat for other running instances
|
|
23
|
+
npm install -g starmux@0.15.0
|
|
24
|
+
starmux up
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Proceed with installation only after stopping succeeds. If the current CLI cannot contact its supervisor, resolve that runtime problem first; a port conflict is not proof of successful shutdown.
|
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
|
---
|
|
@@ -39,12 +39,14 @@ Requires **Node.js 22.12+**.
|
|
|
39
39
|
npm install -g starmux
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
**Upgrading an existing installation?** Stop each running instance with the current CLI (`starmux down`, or `starmux down -i NAME`) before installing the new version, then run `starmux up` again. See the [upgrade instructions](https://github.com/cookyecat/starmux/blob/main/docs/INSTALL.md#upgrade) and [release notes](https://github.com/cookyecat/starmux/blob/main/CHANGELOG.md).
|
|
43
|
+
|
|
42
44
|
### 2. Start in your project directory
|
|
43
45
|
|
|
44
46
|
```bash
|
|
45
47
|
cd /path/to/your/project
|
|
46
48
|
|
|
47
|
-
# Option A:
|
|
49
|
+
# Option A: Global read-only mode (default — inspect files & code)
|
|
48
50
|
starmux up
|
|
49
51
|
|
|
50
52
|
# Option B: Full coding mode (edit files, run Shell & Git)
|
|
@@ -53,28 +55,18 @@ starmux up --code
|
|
|
53
55
|
|
|
54
56
|
Starmux initializes itself and runs in the background on port `18893` (coding mode asks for confirmation the first time):
|
|
55
57
|
|
|
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
|
|
58
|
+
Switch back from Coding mode with
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
starmux up --read
|
|
76
62
|
```
|
|
77
63
|
|
|
64
|
+
Add `-i NAME` for a named instance. Both `up` and `status` show the mode-switch command.
|
|
65
|
+
|
|
66
|
+
`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.
|
|
67
|
+
|
|
68
|
+
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.
|
|
69
|
+
|
|
78
70
|
### 3. Add Starmux to your AI client
|
|
79
71
|
|
|
80
72
|
Need a Bearer API key for remote access? Generate one first (optional for local use):
|
|
@@ -176,7 +168,7 @@ See [Client Compatibility](docs/CLIENTS.md) and [Remote Setup](docs/REMOTE_SETUP
|
|
|
176
168
|
|
|
177
169
|
---
|
|
178
170
|
|
|
179
|
-
## Everyday CLI &
|
|
171
|
+
## Everyday CLI & Multiple Instances
|
|
180
172
|
|
|
181
173
|
Manage your gateway with simple, human-friendly commands:
|
|
182
174
|
|
|
@@ -190,11 +182,11 @@ starmux logs -f # Stream live gateway logs
|
|
|
190
182
|
starmux down # Stop the gateway (alias: stop)
|
|
191
183
|
```
|
|
192
184
|
|
|
193
|
-
After initialization, `starmux up` reuses your saved
|
|
185
|
+
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
186
|
|
|
195
|
-
### Advanced: Running multiple
|
|
187
|
+
### Advanced: Running multiple instances (`-i`)
|
|
196
188
|
|
|
197
|
-
*(Optional)* By default, Starmux manages a single `default`
|
|
189
|
+
*(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
190
|
|
|
199
191
|
```bash
|
|
200
192
|
starmux up ~/work/repo-a -i work --code
|
|
@@ -238,8 +230,8 @@ Explore the `docs/` guides by topic:
|
|
|
238
230
|
- [Access Control & API Keys](docs/ACCESS_CONTROL.md) — Creating/revoking Bearer API keys, expiration policies, and using the local Web Access Console.
|
|
239
231
|
- [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
232
|
- **⚙️ Advanced Configuration & Security**
|
|
241
|
-
- [Multi-Instance & JSONC Config](docs/INSTANCES.md) — Managing
|
|
242
|
-
- [Security &
|
|
233
|
+
- [Multi-Instance & JSONC Config](docs/INSTANCES.md) — Managing instances, hot-reloading `config.jsonc`, mounting extra `stdio` MCP upstreams, and secrets.
|
|
234
|
+
- [Security & Permissions](docs/SECURITY.md) — Understanding global Read-Only vs. Coding Mode, upstream permissions, and macOS privacy boundaries.
|
|
243
235
|
- [Client Compatibility](docs/CLIENTS.md) & [Platform Details](docs/PLATFORMS.md) — Tested AI client matrix and macOS / Linux / Windows runtime specifics.
|
|
244
236
|
- **🛠️ Reference & Project**
|
|
245
237
|
- [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, stopInstance } 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);
|
|
@@ -131,16 +65,17 @@ export async function handle(command, args) {
|
|
|
131
65
|
: 'No instances. Run: starmux up\n');
|
|
132
66
|
return;
|
|
133
67
|
}
|
|
134
|
-
const rows =
|
|
135
|
-
const
|
|
136
|
-
const
|
|
68
|
+
const rows = instances.map(s => {
|
|
69
|
+
const cfg = s.phase === 'stopped' ? s.saved?.config : s.active?.config;
|
|
70
|
+
const saved = s.saved?.config;
|
|
137
71
|
const mark = s.instanceId === registry.defaultInstanceId ? '*' : ' ';
|
|
138
72
|
const state = s.archived ? 'archived' : s.configState === 'pending' ? `${s.phase} (pending)` : s.phase;
|
|
139
|
-
const
|
|
73
|
+
const describe = (value) => !value ? 'unavailable' : value.accessRole === 'trusted' ? 'coding' : value.mode;
|
|
74
|
+
const mode = s.phase === 'stopped' ? `${describe(saved)} (saved)` : s.phase === 'unknown' ? `unavailable / ${describe(saved)} (saved)` : s.configState === 'pending' ? `${describe(cfg)} / ${describe(saved)} (saved)` : describe(cfg);
|
|
140
75
|
const port = String(s.port ?? '—');
|
|
141
|
-
const workspace = cfg?.projectRoot ?? s.source.path;
|
|
76
|
+
const workspace = cfg?.projectRoot ?? (saved ? `${saved.projectRoot} (saved)` : s.source.path);
|
|
142
77
|
return { mark, name: s.name, state, mode, port, workspace };
|
|
143
|
-
})
|
|
78
|
+
});
|
|
144
79
|
const wName = Math.max(4, ...rows.map(r => r.name.length));
|
|
145
80
|
const wState = Math.max(6, ...rows.map(r => r.state.length));
|
|
146
81
|
const wMode = Math.max(4, ...rows.map(r => r.mode.length));
|
|
@@ -152,12 +87,6 @@ export async function handle(command, args) {
|
|
|
152
87
|
}
|
|
153
88
|
return;
|
|
154
89
|
}
|
|
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
90
|
if (op === 'import') {
|
|
162
91
|
const file = positional[1];
|
|
163
92
|
if (!file)
|
|
@@ -168,11 +97,6 @@ export async function handle(command, args) {
|
|
|
168
97
|
message: flags.has('--link') ? 'Bound to original file. Changes require apply.' : 'Imported a private copy. The original file is no longer used.' });
|
|
169
98
|
return;
|
|
170
99
|
}
|
|
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
100
|
const instance = await resolveInstance(positional[1] ?? flags.get('--instance'), homedir(), op === 'restore');
|
|
177
101
|
if (op === 'copy') {
|
|
178
102
|
const config = { ...(await loadInstanceConfig(instance)).config, port: await suggestPort() };
|
|
@@ -198,9 +122,9 @@ export async function handle(command, args) {
|
|
|
198
122
|
else if (op === 'default')
|
|
199
123
|
registry.defaultInstanceId = item.id;
|
|
200
124
|
else if (op === 'archive' || op === 'remove') {
|
|
201
|
-
const stopped = await
|
|
202
|
-
if (stopped
|
|
203
|
-
throw Error('
|
|
125
|
+
const stopped = await stopInstance(instance);
|
|
126
|
+
if (stopped.restarted)
|
|
127
|
+
throw Error('INSTANCE_RESTARTED: cannot archive a new running instance');
|
|
204
128
|
item.archived = true;
|
|
205
129
|
if (registry.defaultInstanceId === item.id)
|
|
206
130
|
registry.defaultInstanceId = null;
|
|
@@ -222,47 +146,9 @@ export async function handle(command, args) {
|
|
|
222
146
|
process.stdout.write(`Restored instance '${instance.name}'.\n`);
|
|
223
147
|
return;
|
|
224
148
|
}
|
|
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
|
-
}
|
|
149
|
+
const selector = flags.get('--instance');
|
|
239
150
|
const instance = await resolveInstance(selector);
|
|
240
151
|
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
152
|
if (command === 'config') {
|
|
267
153
|
const op = positional[0] ?? 'show';
|
|
268
154
|
if (op === 'path') {
|
|
@@ -278,11 +164,12 @@ export async function handle(command, args) {
|
|
|
278
164
|
}
|
|
279
165
|
if (op === 'show') {
|
|
280
166
|
if (flags.has('--effective')) {
|
|
281
|
-
const
|
|
167
|
+
const status = await instanceStatus(instance);
|
|
168
|
+
const active = status.active;
|
|
282
169
|
if (json)
|
|
283
170
|
output(active);
|
|
284
171
|
else if (!active)
|
|
285
|
-
process.stdout.write(`Instance ${instance.name} has no active
|
|
172
|
+
process.stdout.write(`Instance ${instance.name} has no confirmed active configuration (state: ${status.phase}).\n`);
|
|
286
173
|
else
|
|
287
174
|
process.stdout.write(`Instance ${instance.name} (effective running config)\n` +
|
|
288
175
|
`Workspace ${active.config.projectRoot}\n` +
|
|
@@ -320,7 +207,7 @@ export async function handle(command, args) {
|
|
|
320
207
|
return;
|
|
321
208
|
}
|
|
322
209
|
if (op === 'recover') {
|
|
323
|
-
const snapshot = (await instanceStatus(instance)).
|
|
210
|
+
const snapshot = (await instanceStatus(instance)).lastActive;
|
|
324
211
|
if (!snapshot)
|
|
325
212
|
throw Error('NO_SUCCESSFUL_CONFIG_SNAPSHOT');
|
|
326
213
|
await validateConfig(snapshot.config);
|
|
@@ -406,7 +293,14 @@ export async function handle(command, args) {
|
|
|
406
293
|
return;
|
|
407
294
|
}
|
|
408
295
|
if (op === 'apply') {
|
|
296
|
+
const before = await instanceStatus(instance);
|
|
297
|
+
if (before.phase !== 'running' && before.phase !== 'stopped')
|
|
298
|
+
throw Error(`INSTANCE_NOT_READY: ${before.runtimeProblem ?? before.phase}`);
|
|
409
299
|
const state = await instanceRequest(instance, 'apply', flags.get('--revision') ?? loaded.revision);
|
|
300
|
+
if (state && state.phase !== 'running')
|
|
301
|
+
throw Error('INSTANCE_APPLY_UNVERIFIED');
|
|
302
|
+
if (!state && before.phase === 'running')
|
|
303
|
+
throw Error('INSTANCE_CHANGED_DURING_APPLY');
|
|
410
304
|
if (json)
|
|
411
305
|
output({ result: state ? 'APPLIED' : 'VALID_ON_NEXT_START', instanceId: instance.id, runtime: state });
|
|
412
306
|
else
|
|
@@ -444,29 +338,6 @@ export async function handle(command, args) {
|
|
|
444
338
|
}
|
|
445
339
|
throw Error('CONFIG_ACTION_INVALID');
|
|
446
340
|
}
|
|
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
341
|
if (command === 'permissions') {
|
|
471
342
|
const grants = await createWorkspaceGrants({ file: p.grants });
|
|
472
343
|
const deny = await createDenyRules({ file: p.deny });
|
|
@@ -482,15 +353,6 @@ export async function handle(command, args) {
|
|
|
482
353
|
}
|
|
483
354
|
return;
|
|
484
355
|
}
|
|
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
356
|
if (command === 'privacy') {
|
|
495
357
|
const status = await instanceStatus(instance);
|
|
496
358
|
if (!status.runtime?.port)
|
|
@@ -498,27 +360,5 @@ export async function handle(command, args) {
|
|
|
498
360
|
output(await (await import('../privacy-diagnostics.js')).privacyReport({ port: status.runtime.port, home: homedir(), all: flags.has('--all'), target: flags.get('--target') ?? null }));
|
|
499
361
|
return;
|
|
500
362
|
}
|
|
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
|
-
}
|
|
363
|
+
throw Error('INSTANCE_COMMAND_UNSUPPORTED');
|
|
523
364
|
}
|
|
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
|