starmux 0.15.0 → 0.16.1
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 +17 -0
- package/README.md +130 -105
- package/dist/src/global-core-control.js +26 -8
- package/dist/src/instance-runtime.js +34 -7
- package/dist/src/platform.js +20 -5
- package/package.json +22 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.16.1
|
|
4
|
+
|
|
5
|
+
### Discovery and documentation
|
|
6
|
+
|
|
7
|
+
- Refine the npm package description to highlight the self-hosted remote MCP gateway, supported AI clients, Desktop Commander tools, and Linux / NVIDIA Jetson access.
|
|
8
|
+
- Update npm search keywords for MCP gateways, remote computer access, and compatible AI coding clients.
|
|
9
|
+
- Add task-focused use-case guides, including the tested WorkBuddy-to-Linux / NVIDIA Jetson connection and its verified `streamable-http` configuration.
|
|
10
|
+
- No runtime behavior or dependency changes in this release.
|
|
11
|
+
|
|
12
|
+
## 0.16.0
|
|
13
|
+
|
|
14
|
+
- Keep instance and port-lease identities in the private, durable `~/.config/starmux/control` directory instead of temporary storage.
|
|
15
|
+
- Use short hashed Unix socket paths and stop the owned worker if a control record or socket disappears, preserving a diagnostic reason.
|
|
16
|
+
- Allow a supervisor to finish cleanup after its own metadata is deleted, while refusing to remove a replaced control directory or another run's records.
|
|
17
|
+
- Add regression tests for deleting each instance/port owner record and Unix socket during a live run.
|
|
18
|
+
- Stop instances before changing to this control layout.
|
|
19
|
+
|
|
3
20
|
## 0.15.0
|
|
4
21
|
|
|
5
22
|
### Instance lifecycle
|
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
# Starmux MCP Gateway
|
|
3
|
+
# Starmux Self-hosted MCP Gateway
|
|
4
4
|
|
|
5
5
|
### Give cloud AI clients secure access to your local machine.
|
|
6
6
|
|
|
7
7
|
**Self-hosted MCP Gateway · Built-in File, Shell & Git Tools · macOS Menu Bar App · MIT**
|
|
8
8
|
|
|
9
|
-
[Why Starmux?](#why-starmux) · [Quick Start](#quick-start) · [
|
|
9
|
+
[Why Starmux?](#why-starmux) · [Quick Start](#quick-start) · [Use Cases](#common-use-cases) · [Securing & Client Guides](#securing-remote-access--client-guides) · [macOS App](#native-macos-app) · [Documentation](docs/README.md)
|
|
10
10
|
|
|
11
11
|
</div>
|
|
12
12
|
|
|
@@ -14,18 +14,18 @@
|
|
|
14
14
|
|
|
15
15
|
## Why Starmux?
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
AI assistants and coding agents (like **ChatGPT Web**, **Claude App / Code**, **Antigravity**, **Codex**, **Cursor**, **WorkBuddy**, **DSH-Harness**, **Kimi Code**, **Minimax Code**, **ZCode**, or **Doubao**) are powerful, but cloud clients are isolated in the browser and many desktop agents lack native Linux / ARM64 apps.
|
|
18
18
|
|
|
19
|
-
**Starmux** (`Star` topology + `Multiplexer`) is a self-hosted MCP gateway that connects
|
|
19
|
+
**Starmux** (`Star` topology + `Multiplexer`) is a self-hosted MCP gateway that connects any AI client to the real tools on your own computers:
|
|
20
20
|
|
|
21
21
|
<div align="center">
|
|
22
22
|
<img src="https://raw.githubusercontent.com/cookyecat/starmux/main/docs/assets/starmux-architecture.svg" width="100%" alt="Starmux architecture: Many AI clients connecting to self-hosted computers via Direct HTTPS or Secure MCP Tunnel" />
|
|
23
23
|
</div>
|
|
24
24
|
|
|
25
|
-
- **
|
|
26
|
-
- **Code on Any Host (macOS, Linux,
|
|
27
|
-
- **
|
|
28
|
-
- **Your Network, Your Keys:** No vendor-hosted relay required.
|
|
25
|
+
- **Give ChatGPT & Cloud AI Local "Computer Use":** Turn browser and chat AI into a local coding agent. Out of the box, your AI can read/edit local files, search code, run Shell commands, and manage Git via the built-in [Desktop Commander](https://github.com/wonderwhy-er/DesktopCommanderMCP) engine.
|
|
26
|
+
- **Code on Any Host (macOS, Linux, NVIDIA Jetson & Windows):** Desktop AI clients like **WorkBuddy** or **ChatGPT** have no native Linux or ARM64 app. Run Starmux on your target Linux server, **NVIDIA Jetson**, or workstation, and drive it remotely from your laptop or browser.
|
|
27
|
+
- **Programmable MCP Multiplexer & Middleware:** Aggregate built-in workspace tools and your own local `stdio` MCP servers behind a single Streamable HTTP `/mcp` endpoint—complete with unified authentication, tool allowlists, and live activity logs.
|
|
28
|
+
- **100% Self-Hosted — Your Network, Your Keys:** Totally self-hosted. No vendor-hosted relay required. Connect over Localhost, SSH, Direct HTTPS, or your own Tunnel, and lock down access with Bearer API Keys or OAuth 2.0 whenever you need.
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
@@ -39,132 +39,164 @@ Requires **Node.js 22.12+**.
|
|
|
39
39
|
npm install -g starmux
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
*(Upgrading? Run `starmux down` before installing the new version—see the [Upgrade Guide](docs/INSTALL.md#upgrade).)*
|
|
43
43
|
|
|
44
44
|
### 2. Start in your project directory
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
47
|
cd /path/to/your/project
|
|
48
48
|
|
|
49
|
-
# Option
|
|
49
|
+
# Option 1: Read-only mode (default — safely inspect files & code)
|
|
50
50
|
starmux up
|
|
51
51
|
|
|
52
|
-
# Option
|
|
52
|
+
# or, Option 2: Coding mode (edit files, run Shell & Git)
|
|
53
53
|
starmux up --code
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
Starmux
|
|
56
|
+
Starmux starts in the background on `http://127.0.0.1:18893/mcp` with **Authentication: `None`** by default so you can connect immediately:
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
```text
|
|
59
|
+
● Starmux is running (instance: default)
|
|
60
|
+
Instance <instance UUID>
|
|
61
|
+
Listen http://127.0.0.1:18893/mcp (this computer only)
|
|
62
|
+
Project /path/to/your/project
|
|
63
|
+
Mode Read-only (global · OS account) (run 'starmux up --code' to enable edits & shell)
|
|
64
|
+
Auth None (no token required)
|
|
62
65
|
```
|
|
63
66
|
|
|
64
|
-
|
|
67
|
+
*(Switch modes anytime with `starmux up --code` and `starmux up --read`.)*
|
|
68
|
+
|
|
69
|
+
### 3. Expose for Remote / Cloud AI *(Skip if your AI client is on this same computer)*
|
|
65
70
|
|
|
66
|
-
|
|
71
|
+
Because Starmux only listens on `127.0.0.1:18893`, forward traffic to it if your AI client is in the cloud (e.g., **ChatGPT Web**) or on another computer:
|
|
67
72
|
|
|
68
|
-
|
|
73
|
+
- **Option 1: Reachable IP (Public or LAN) — Direct Proxy with [Caddy](https://caddyserver.com):**
|
|
74
|
+
Run in your terminal (replace `mcp.example.com` with your public domain, or `http://YOUR_LAN_IP` on a local network):
|
|
75
|
+
```bash
|
|
76
|
+
caddy run --adapter caddyfile --config - << 'EOF'
|
|
77
|
+
mcp.example.com {
|
|
78
|
+
handle /mcp {
|
|
79
|
+
reverse_proxy 127.0.0.1:18893 {
|
|
80
|
+
header_up Host 127.0.0.1:18893
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
EOF
|
|
85
|
+
```
|
|
69
86
|
|
|
70
|
-
|
|
87
|
+
- **Option 2: Behind NAT / Firewall — Use a Tunnel or SSH:**
|
|
88
|
+
- **[OpenAI Secure MCP Tunnel](https://developers.openai.com/api/docs/guides/secure-mcp-tunnels) (for ChatGPT Web / Codex):**
|
|
89
|
+
```bash
|
|
90
|
+
tunnel-client run \
|
|
91
|
+
--control-plane.tunnel-id "tunnel_YOUR_TUNNEL_ID" \
|
|
92
|
+
--control-plane.api-key "env:MAC_MCP_TUNNEL_KEY" \
|
|
93
|
+
--mcp.server-url "http://127.0.0.1:18893/mcp" \
|
|
94
|
+
--health.listen-addr "127.0.0.1:18891" \
|
|
95
|
+
--log.level info
|
|
96
|
+
```
|
|
97
|
+
*(Behind a proxy? Append `--control-plane.http-proxy "http://127.0.0.1:PROXY_PORT"`.)*
|
|
98
|
+
- **or, Cloudflare Quick Tunnel:** `cloudflared tunnel --url http://127.0.0.1:18893 --http-host-header 127.0.0.1:18893`
|
|
99
|
+
- **or, SSH Forwarding (Laptop $\rightarrow$ Linux / Jetson):** `ssh -N -L 127.0.0.1:18893:127.0.0.1:18893 user@remote-host` *(add `-p PORT` if SSH uses a non-default port; change the first `18893` to `18894` if port `18893` is already used on your laptop)*
|
|
71
100
|
|
|
72
|
-
|
|
101
|
+
*(See [Installation & Networking](docs/INSTALL.md#4-remote-network-forwarding--ingress-engineering) for Tailscale, ngrok, and custom TLS ports.)*
|
|
73
102
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
103
|
+
### 4. Connect your AI client
|
|
104
|
+
|
|
105
|
+
With default settings (`Auth: None`), connect your AI client using any of the options below:
|
|
77
106
|
|
|
78
|
-
|
|
107
|
+
#### Option 1 — Ask your AI coding agent to add it for you *(Zero manual config)*
|
|
79
108
|
|
|
80
|
-
|
|
109
|
+
For agentic coding clients (**Claude Code**, **Antigravity**, **Codex**, **Cursor**, **WorkBuddy**, **DSH-Harness**, **Kimi Code**, **Minimax Code**, **ZCode**), simply send this prompt in the AI chat (replacing the URL with your LAN IP, domain, or `http://127.0.0.1:18893/mcp`):
|
|
81
110
|
|
|
82
|
-
|
|
111
|
+
```text
|
|
112
|
+
Create this MCP Server connector:
|
|
113
|
+
"starmux": {
|
|
114
|
+
"type": "streamable-http",
|
|
115
|
+
"url": "http://192.168.50.114/mcp"
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Your AI agent will automatically update its own MCP configuration and connect.
|
|
120
|
+
|
|
121
|
+
#### or, Option 2 — AI clients with a Form UI (e.g., ChatGPT Web, Claude, Doubao)
|
|
83
122
|
|
|
84
123
|
| MCP Client Field | Value to Fill In |
|
|
85
124
|
| :--- | :--- |
|
|
86
|
-
| **Transport** | `Streamable HTTP` |
|
|
87
|
-
| **URL (
|
|
88
|
-
| **
|
|
89
|
-
| **Authentication** | *Optional:* `None` *(default)*, `Bearer API key` (`Authorization: Bearer <YOUR_API_KEY>`), or `OAuth` |
|
|
90
|
-
|
|
91
|
-
#### Option B — AI clients with JSON config (e.g., WorkBuddy, Cursor, Windsurf)
|
|
125
|
+
| **Transport** | `Streamable HTTP` *(or select your **Tunnel** in ChatGPT Web)* |
|
|
126
|
+
| **URL** | `https://mcp.example.com/mcp` *(Remote HTTPS / Tunnel)* — or `http://127.0.0.1:18893/mcp` *(same computer)* |
|
|
127
|
+
| **Authentication** | `None` *(default)* |
|
|
92
128
|
|
|
93
|
-
|
|
129
|
+
#### or, Option 3 — Manually paste into `mcp.json`
|
|
94
130
|
|
|
95
|
-
|
|
131
|
+
If you prefer editing your client's `mcp.json` / `mcpServers` settings manually:
|
|
96
132
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
"
|
|
101
|
-
"
|
|
102
|
-
|
|
133
|
+
- **Remote / LAN connection (via Direct Proxy or Tunnel — do NOT use `127.0.0.1` across separate machines):**
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"mcpServers": {
|
|
137
|
+
"starmux": {
|
|
138
|
+
"type": "streamable-http",
|
|
139
|
+
"url": "https://mcp.example.com/mcp"
|
|
140
|
+
}
|
|
103
141
|
}
|
|
104
142
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"type": "streamable-http",
|
|
115
|
-
"url": "https://<your-domain-or-tunnel>/mcp",
|
|
116
|
-
"headers": {
|
|
117
|
-
"Authorization": "Bearer smx_YOUR_GENERATED_API_KEY"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
- **or, Same-computer connection (or when using local `ssh -L` port forwarding):**
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"mcpServers": {
|
|
149
|
+
"starmux": {
|
|
150
|
+
"type": "streamable-http",
|
|
151
|
+
"url": "http://127.0.0.1:18893/mcp"
|
|
118
152
|
}
|
|
119
153
|
}
|
|
120
154
|
}
|
|
121
|
-
|
|
122
|
-
```
|
|
155
|
+
```
|
|
123
156
|
|
|
124
157
|
---
|
|
125
158
|
|
|
126
|
-
##
|
|
127
|
-
|
|
128
|
-
By design, Starmux only listens on `127.0.0.1:18893/mcp` (loopback) and never exposes raw ports directly to external networks:
|
|
159
|
+
## Common Use Cases
|
|
129
160
|
|
|
130
|
-
-
|
|
131
|
-
-
|
|
161
|
+
- [How can ChatGPT access my local computer?](https://github.com/cookyecat/starmux/blob/main/docs/use-cases/chatgpt-access-local-computer.md)
|
|
162
|
+
- [How do I self-host a remote MCP gateway?](https://github.com/cookyecat/starmux/blob/main/docs/use-cases/self-hosted-remote-mcp-gateway.md)
|
|
163
|
+
- [How can I use Desktop Commander remotely?](https://github.com/cookyecat/starmux/blob/main/docs/use-cases/remote-desktop-commander.md)
|
|
164
|
+
- [Can ChatGPT access a Linux server or NVIDIA Jetson?](https://github.com/cookyecat/starmux/blob/main/docs/use-cases/chatgpt-linux-jetson.md)
|
|
165
|
+
- [How can WorkBuddy operate Linux or NVIDIA Jetson remotely?](https://github.com/cookyecat/starmux/blob/main/docs/use-cases/workbuddy-linux-jetson.md)
|
|
166
|
+
- [How do I connect an MCP server behind NAT — HTTPS or Tunnel?](https://github.com/cookyecat/starmux/blob/main/docs/use-cases/mcp-behind-nat-https-vs-tunnel.md)
|
|
167
|
+
- [How can I secure a remote MCP endpoint with OAuth?](https://github.com/cookyecat/starmux/blob/main/docs/use-cases/self-hosted-mcp-oauth.md)
|
|
132
168
|
|
|
133
|
-
|
|
134
|
-
Remote AI Client
|
|
135
|
-
──(HTTPS)──▶ HTTPS Reverse Proxy (Caddy) or Tunnel Client
|
|
136
|
-
──(Loopback)──▶ Starmux (127.0.0.1:18893/mcp)
|
|
137
|
-
──▶ Local Files, Shell & Git
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
### 1. Choose a Network Forwarder (for Remote / Cloud Access)
|
|
169
|
+
---
|
|
141
170
|
|
|
142
|
-
|
|
143
|
-
| :--- | :--- | :--- | :--- |
|
|
144
|
-
| **Direct HTTPS (Reverse Proxy)** | Hosts with a public IPv4 / IPv6 address and a domain name | [Caddy](https://caddyserver.com/docs/quick-starts/reverse-proxy) *(auto HTTPS)* · [Nginx](https://nginx.org/en/docs/http/ngx_http_proxy_module.html) | [Caddy HTTPS Setup](docs/REMOTE_SETUP.md#2-https--bearer-api-key) |
|
|
145
|
-
| **Secure Tunnel** | Hosts behind NAT, home Wi-Fi, or office firewalls *(no inbound port needed)* | [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) · [Tailscale Funnel / Serve](https://tailscale.com/kb/1223/funnel) · [OpenAI Secure MCP Tunnel](https://developers.openai.com/api/docs/guides/secure-mcp-tunnels) · [ngrok](https://ngrok.com/docs) | [Tunnel Setup](docs/CONNECT_CHATGPT.md#2-secure-mcp-tunnel--no-inbound-port) |
|
|
171
|
+
## Securing Remote Access & Client Guides
|
|
146
172
|
|
|
147
|
-
|
|
173
|
+
Once your connection works, **lock down public HTTPS / Tunnel endpoints** so only you can access your gateway:
|
|
148
174
|
|
|
149
|
-
|
|
175
|
+
### Add Authentication *(Recommended for Internet-Facing Endpoints)*
|
|
150
176
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
177
|
+
1. **Bearer API Key (10-second setup for Antigravity, Cursor, WorkBuddy, Kimi Code, Codex, etc.):**
|
|
178
|
+
```bash
|
|
179
|
+
starmux auth key "My AI Client"
|
|
180
|
+
```
|
|
181
|
+
Then add `"headers": { "Authorization": "Bearer smx_YOUR_GENERATED_API_KEY" }` to your client's JSON config (or `Authorization: Bearer <key>` in Form UI).
|
|
182
|
+
2. **OAuth 2.0 Browser Login (for ChatGPT Web):**
|
|
183
|
+
Supports full OAuth 2.0 + PKCE and instant session revocation. See [Security & Authentication — OAuth 2.0](docs/SECURITY.md#4-oauth-20--pkce-oauth-with-keycloak).
|
|
156
184
|
|
|
157
|
-
###
|
|
185
|
+
### Tested AI Client Matrix
|
|
158
186
|
|
|
159
|
-
| AI Client | Recommended
|
|
187
|
+
| AI Client | Recommended Setup | Status | Detailed Guide |
|
|
160
188
|
| :--- | :--- | :--- | :--- |
|
|
161
|
-
| **ChatGPT Web** |
|
|
162
|
-
| **Claude App** | Remote MCP Custom Connector |
|
|
163
|
-
| **
|
|
164
|
-
| **
|
|
165
|
-
| **
|
|
166
|
-
|
|
167
|
-
|
|
189
|
+
| **ChatGPT Web** | Direct HTTPS or OpenAI Tunnel (+ OAuth) | ✅ Tested | [ChatGPT Web Setup](docs/CLIENTS.md#2-chatgpt-web-tunnel-direct-https--oauth) |
|
|
190
|
+
| **Claude App / Code** | Chat Prompt (`Claude Code`) or Remote MCP Custom Connector (`Claude App`) | ✅ Tested | [AI Coding Agents](docs/CLIENTS.md#3-ai-coding-agents-claude-code-antigravity-codex-cursor-workbuddy-dsh-harness-kimi-code-minimax-code-zcode) · [Claude App](docs/CLIENTS.md#5-claude-app--doubao-豆包) |
|
|
191
|
+
| **Antigravity** | Chat Prompt / Local / LAN / SSH / HTTPS | ✅ Tested | [AI Coding Agents Setup](docs/CLIENTS.md#3-ai-coding-agents-claude-code-antigravity-codex-cursor-workbuddy-dsh-harness-kimi-code-minimax-code-zcode) |
|
|
192
|
+
| **Codex** | Chat Prompt / Tunnel or HTTPS + Bearer API Key | ✅ Tested | [Codex CLI Setup](docs/CLIENTS.md#4-codex-cli-configtoml) |
|
|
193
|
+
| **Cursor** | Chat Prompt / Local / LAN / SSH / HTTPS | ✅ Tested | [AI Coding Agents Setup](docs/CLIENTS.md#3-ai-coding-agents-claude-code-antigravity-codex-cursor-workbuddy-dsh-harness-kimi-code-minimax-code-zcode) |
|
|
194
|
+
| **WorkBuddy** | Chat Prompt / Local / LAN / SSH / HTTPS | ✅ Tested | [AI Coding Agents Setup](docs/CLIENTS.md#3-ai-coding-agents-claude-code-antigravity-codex-cursor-workbuddy-dsh-harness-kimi-code-minimax-code-zcode) |
|
|
195
|
+
| **DSH-Harness** | Chat Prompt / Local / LAN / SSH / HTTPS | ✅ Tested | [AI Coding Agents Setup](docs/CLIENTS.md#3-ai-coding-agents-claude-code-antigravity-codex-cursor-workbuddy-dsh-harness-kimi-code-minimax-code-zcode) |
|
|
196
|
+
| **Kimi Code** | Chat Prompt / Local / LAN / SSH / HTTPS | ✅ Tested | [AI Coding Agents Setup](docs/CLIENTS.md#3-ai-coding-agents-claude-code-antigravity-codex-cursor-workbuddy-dsh-harness-kimi-code-minimax-code-zcode) |
|
|
197
|
+
| **Minimax Code** | Chat Prompt / Local / LAN / SSH / HTTPS | ✅ Tested | [AI Coding Agents Setup](docs/CLIENTS.md#3-ai-coding-agents-claude-code-antigravity-codex-cursor-workbuddy-dsh-harness-kimi-code-minimax-code-zcode) |
|
|
198
|
+
| **ZCode** | Chat Prompt / Local / LAN / SSH / HTTPS | ✅ Tested | [AI Coding Agents Setup](docs/CLIENTS.md#3-ai-coding-agents-claude-code-antigravity-codex-cursor-workbuddy-dsh-harness-kimi-code-minimax-code-zcode) |
|
|
199
|
+
| **Doubao (豆包)** | Direct HTTPS or Tunnel | ✅ Tested | [Claude & Doubao Setup](docs/CLIENTS.md#5-claude-app--doubao-豆包) |
|
|
168
200
|
|
|
169
201
|
---
|
|
170
202
|
|
|
@@ -174,6 +206,7 @@ Manage your gateway with simple, human-friendly commands:
|
|
|
174
206
|
|
|
175
207
|
```bash
|
|
176
208
|
starmux status # Check health, endpoints, and active permission mode
|
|
209
|
+
starmux up . # Switch active project context to current directory
|
|
177
210
|
starmux auth # View auth mode, active API keys, and OAuth sessions
|
|
178
211
|
starmux verify # Test local MCP handshake, tool discovery, and workspace read
|
|
179
212
|
starmux doctor # Run environment and configuration diagnostics
|
|
@@ -182,11 +215,9 @@ starmux logs -f # Stream live gateway logs
|
|
|
182
215
|
starmux down # Stop the gateway (alias: stop)
|
|
183
216
|
```
|
|
184
217
|
|
|
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.
|
|
186
|
-
|
|
187
218
|
### Advanced: Running multiple instances (`-i`)
|
|
188
219
|
|
|
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
|
|
220
|
+
*(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:
|
|
190
221
|
|
|
191
222
|
```bash
|
|
192
223
|
starmux up ~/work/repo-a -i work --code
|
|
@@ -196,7 +227,7 @@ starmux down -i work # Stop a specific instance
|
|
|
196
227
|
starmux rm blog # Stop and archive an instance
|
|
197
228
|
```
|
|
198
229
|
|
|
199
|
-
See [Instances and Configuration](docs/INSTANCES.md) for advanced JSONC configuration (`starmux config edit` / `starmux config apply`) and hot-reload workflows.
|
|
230
|
+
See [Instances and Configuration](docs/INSTANCES.md) for advanced JSONC configuration (`starmux config edit` / `starmux config apply`), mounting extra `stdio` MCP servers, and hot-reload workflows.
|
|
200
231
|
|
|
201
232
|
---
|
|
202
233
|
|
|
@@ -223,18 +254,12 @@ The App and CLI manage the same underlying instances. Quitting the App stops onl
|
|
|
223
254
|
|
|
224
255
|
Starmux began with the [community request for a self-hosted Desktop Commander workbench](https://github.com/wonderwhy-er/DesktopCommanderMCP/issues/714) and is evolving into a universal MCP router and programmable middleware for multi-server environments.
|
|
225
256
|
|
|
226
|
-
Explore the
|
|
227
|
-
|
|
228
|
-
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
- **⚙️ Advanced Configuration & 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.
|
|
235
|
-
- [Client Compatibility](docs/CLIENTS.md) & [Platform Details](docs/PLATFORMS.md) — Tested AI client matrix and macOS / Linux / Windows runtime specifics.
|
|
236
|
-
- **🛠️ Reference & Project**
|
|
237
|
-
- [Documentation Index](docs/README.md) · [Installation](docs/INSTALL.md) · [Contributing](CONTRIBUTING.md)
|
|
257
|
+
Explore the [Documentation Index](docs/README.md) by category:
|
|
258
|
+
|
|
259
|
+
- **📦 [Installation, Platforms & Network Forwarding](docs/INSTALL.md)** — Supported hosts (`macOS`, `Linux`, `NVIDIA Jetson`, `Windows`), everyday CLI commands, upgrades, source builds, and advanced Caddy / Tunnel / SSH forwarding.
|
|
260
|
+
- **🔒 [Security, Access Control & Authentication](docs/SECURITY.md)** — Global Read-Only vs. Coding permissions, Bearer API keys (`starmux auth key`), Web Access Console, and self-hosted Keycloak OAuth 2.0 + PKCE.
|
|
261
|
+
- **🤖 [AI Client Configuration Guides](docs/CLIENTS.md)** — End-to-end setup (including 1-step Prompt Auto-Setup) for **ChatGPT Web**, **Claude App / Code**, **Antigravity**, **Codex**, **Cursor**, **WorkBuddy**, **DSH-Harness**, **Kimi Code**, **Minimax Code**, **ZCode**, and **Doubao (豆包)**.
|
|
262
|
+
- **⚙️ [Multi-Instance & JSONC Configuration](docs/INSTANCES.md)** — Running multiple instances (`-i NAME`), hot-reloading `config.jsonc`, mounting extra local `stdio` MCP servers (`upstreams`), and managing secrets.
|
|
238
263
|
|
|
239
264
|
## License
|
|
240
265
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ownedByUser, createPrivateDirectory, controlRoot, windows } from "./platform.js";
|
|
1
|
+
import { ownedByUser, createPrivateDirectory, controlRoot, controlSocket, controlSocketRoot, sameFile, windows } from "./platform.js";
|
|
2
2
|
import { ownerSchema } from "./config-schemas.js";
|
|
3
3
|
import { GatewayError, errorCode, isSafeInteger, parseJSON, record } from "./validation.js";
|
|
4
4
|
const CONTROL_VERSION = 1;
|
|
@@ -18,7 +18,7 @@ export function controlPaths(port, root = controlRoot()) {
|
|
|
18
18
|
deny("CORE_INVALID_PORT");
|
|
19
19
|
const lock = path.join(root, `core-${port}.lease`);
|
|
20
20
|
return { root, lock, recovery: lock + ".recovery",
|
|
21
|
-
meta: path.join(lock, "owner.json"), socket:
|
|
21
|
+
meta: path.join(lock, "owner.json"), socket: controlSocket(lock) };
|
|
22
22
|
}
|
|
23
23
|
function secureDir(p, create = false) {
|
|
24
24
|
if (create)
|
|
@@ -150,6 +150,10 @@ function cleanStale(paths) {
|
|
|
150
150
|
deny("CORE_CONTROL_STALE_UNTRUSTED");
|
|
151
151
|
fs.unlinkSync(item);
|
|
152
152
|
}
|
|
153
|
+
if (!windows && fs.existsSync(paths.socket)) {
|
|
154
|
+
secureFile(paths.socket, "socket");
|
|
155
|
+
fs.unlinkSync(paths.socket);
|
|
156
|
+
}
|
|
153
157
|
fs.rmdirSync(paths.lock);
|
|
154
158
|
}
|
|
155
159
|
finally {
|
|
@@ -229,6 +233,8 @@ export async function acquireControl(port, { root, launcher = "cli" } = {}) {
|
|
|
229
233
|
deny("CORE_LAUNCHER_INVALID");
|
|
230
234
|
const paths = controlPaths(port, root);
|
|
231
235
|
secureDir(paths.root, true);
|
|
236
|
+
if (!windows)
|
|
237
|
+
createPrivateDirectory(controlSocketRoot());
|
|
232
238
|
for (let attempt = 0; attempt < 12; attempt++) {
|
|
233
239
|
try {
|
|
234
240
|
fs.mkdirSync(paths.lock, { mode: 0o700 });
|
|
@@ -351,9 +357,13 @@ export async function acquireControl(port, { root, launcher = "cli" } = {}) {
|
|
|
351
357
|
catch { }
|
|
352
358
|
throw e;
|
|
353
359
|
}
|
|
360
|
+
const ownsDirectory = sameFile(paths.lock);
|
|
361
|
+
const ownsMetadata = sameFile(paths.meta);
|
|
362
|
+
const ownsSocket = windows ? () => true : sameFile(paths.socket);
|
|
354
363
|
let releasePromise = null;
|
|
355
364
|
return {
|
|
356
365
|
owner,
|
|
366
|
+
intact: () => ownsDirectory() && ownsMetadata() && ownsSocket(),
|
|
357
367
|
markRunning() { phase = "RUNNING"; },
|
|
358
368
|
markStopped() { phase = "STOPPED"; },
|
|
359
369
|
onStop(fn) { onStop = fn; },
|
|
@@ -369,19 +379,27 @@ export async function acquireControl(port, { root, launcher = "cli" } = {}) {
|
|
|
369
379
|
else
|
|
370
380
|
resolve(); });
|
|
371
381
|
});
|
|
372
|
-
|
|
382
|
+
// A missing record cannot be reconstructed by a CLI. Its live owner
|
|
383
|
+
// can still release its own unchanged directory without inventing keys.
|
|
384
|
+
if (!fs.existsSync(paths.lock))
|
|
385
|
+
return;
|
|
386
|
+
if (!ownsDirectory())
|
|
373
387
|
deny("CORE_CONTROL_LEASE_CHANGED");
|
|
374
|
-
// node:net automatically unlinks a Unix socket on server.close().
|
|
375
|
-
// A second unlink is not required and must not abort lease cleanup.
|
|
376
388
|
try {
|
|
377
|
-
if (
|
|
378
|
-
|
|
389
|
+
if (readOwner(paths).runId !== owner.runId)
|
|
390
|
+
deny("CORE_CONTROL_LEASE_CHANGED");
|
|
391
|
+
}
|
|
392
|
+
catch (e) {
|
|
393
|
+
if (errorCode(e) !== "ENOENT")
|
|
394
|
+
throw e;
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
fs.unlinkSync(paths.meta);
|
|
379
398
|
}
|
|
380
399
|
catch (e) {
|
|
381
400
|
if (errorCode(e) !== "ENOENT")
|
|
382
401
|
throw e;
|
|
383
402
|
}
|
|
384
|
-
fs.unlinkSync(paths.meta);
|
|
385
403
|
fs.rmdirSync(paths.lock);
|
|
386
404
|
})();
|
|
387
405
|
}
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { createServer, createConnection } from 'node:net';
|
|
3
3
|
import { fork } from 'node:child_process';
|
|
4
|
-
import { mkdir, rm, open } from 'node:fs/promises';
|
|
4
|
+
import { mkdir, rm, open, rmdir } from 'node:fs/promises';
|
|
5
5
|
import { setTimeout as pause } from 'node:timers/promises';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import path from 'node:path';
|
|
8
8
|
import { homedir } from 'node:os';
|
|
9
9
|
import { INSTANCE_CONTROL_VERSION, instanceOwnerSchema, activeConfigSchema, runtimeStateSchema, controlRequestSchema, controlReplySchema, checkInstanceProtocol } from './instance-control-protocol.js';
|
|
10
|
-
import { controlRoot, createPrivateDirectory, childEnvKeys, windows } from './platform.js';
|
|
10
|
+
import { controlRoot, controlSocket, controlSocketRoot, sameFile, createPrivateDirectory, childEnvKeys, windows } from './platform.js';
|
|
11
11
|
import { acquireControl, inspectControlOwner } from './global-core-control.js';
|
|
12
12
|
import { runtimeIdentity } from './runtime-identity.js';
|
|
13
13
|
import { atomicPrivate, changeRegistry, availablePort, ensureAccessReady, exists, instanceEnv, instancePaths, loadInstanceConfig, prepareUpstreams, readTrusted, resolveInstance } from './instance-store.js';
|
|
14
14
|
export function runtimePaths(id) {
|
|
15
15
|
const root = path.join(controlRoot(), 'i-' + id);
|
|
16
|
-
const socket =
|
|
16
|
+
const socket = controlSocket(root);
|
|
17
17
|
return { root, socket, meta: path.join(root, 'owner.json') };
|
|
18
18
|
}
|
|
19
19
|
const signature = (key, message) => createHmac('sha256', key).update(message).digest('hex');
|
|
@@ -236,6 +236,8 @@ export async function runInstance(instance, options = {}) {
|
|
|
236
236
|
const check = await ownerFor(instance);
|
|
237
237
|
if (!check || check.runId !== old.runId || alive(check.pid))
|
|
238
238
|
throw Error('INSTANCE_CONTROL_CHANGED');
|
|
239
|
+
if (!windows)
|
|
240
|
+
await rm(p.socket, { force: true });
|
|
239
241
|
await rm(p.root, { recursive: true });
|
|
240
242
|
await mkdir(p.root, { mode: 0o700 });
|
|
241
243
|
}
|
|
@@ -245,6 +247,9 @@ export async function runInstance(instance, options = {}) {
|
|
|
245
247
|
}
|
|
246
248
|
await atomicPrivate(p.meta, JSON.stringify(owner));
|
|
247
249
|
}, home);
|
|
250
|
+
const ownsDirectory = sameFile(p.root);
|
|
251
|
+
const ownsMetadata = sameFile(p.meta);
|
|
252
|
+
let controlTimer;
|
|
248
253
|
const state = { protocolVersion: INSTANCE_CONTROL_VERSION, instanceId: instance.id, runId: owner.runId, pid: process.pid, launcher: owner.launcher, appParentPid: parent ?? null, executionMode: options.background ? 'background' : 'foreground', phase: 'starting', port: null, active: null, problem: null, ...identity };
|
|
249
254
|
let worker = null;
|
|
250
255
|
let portLease = null;
|
|
@@ -482,10 +487,18 @@ export async function runInstance(instance, options = {}) {
|
|
|
482
487
|
})();
|
|
483
488
|
});
|
|
484
489
|
});
|
|
490
|
+
if (!windows)
|
|
491
|
+
createPrivateDirectory(controlSocketRoot());
|
|
485
492
|
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(p.socket, () => resolve()); });
|
|
493
|
+
const ownsSocket = windows ? () => true : sameFile(p.socket);
|
|
486
494
|
portLease = await acquireControl(initial.config.port, { launcher: owner.launcher });
|
|
487
495
|
portLease.onStop(() => requestStop());
|
|
488
496
|
await serialize(() => startWorker(active(initial), portLease));
|
|
497
|
+
controlTimer = setInterval(() => {
|
|
498
|
+
if (!ending && (!ownsDirectory() || !ownsMetadata() || !ownsSocket() || portLease && !portLease.intact()))
|
|
499
|
+
void requestStop('INSTANCE_CONTROL_FILES_LOST');
|
|
500
|
+
}, 1000);
|
|
501
|
+
controlTimer.unref();
|
|
489
502
|
if (options.onReady)
|
|
490
503
|
await options.onReady();
|
|
491
504
|
else if (!options.json)
|
|
@@ -498,6 +511,8 @@ export async function runInstance(instance, options = {}) {
|
|
|
498
511
|
throw error;
|
|
499
512
|
}
|
|
500
513
|
finally {
|
|
514
|
+
if (controlTimer)
|
|
515
|
+
clearInterval(controlTimer);
|
|
501
516
|
if (parentTimer)
|
|
502
517
|
clearInterval(parentTimer);
|
|
503
518
|
process.off('SIGTERM', signal);
|
|
@@ -516,9 +531,21 @@ export async function runInstance(instance, options = {}) {
|
|
|
516
531
|
// Commit completion last. A stop acknowledgement only means accepted;
|
|
517
532
|
// readers require this snapshot AND release of this run's owner record.
|
|
518
533
|
await atomicPrivate(path.join(instancePaths(instance, home).snapshots, 'last-runtime.json'), JSON.stringify(state));
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
534
|
+
if (await exists(p.root)) {
|
|
535
|
+
if (!ownsDirectory())
|
|
536
|
+
throw Error('INSTANCE_CONTROL_CHANGED');
|
|
537
|
+
try {
|
|
538
|
+
const current = await ownerFor(instance);
|
|
539
|
+
if (current && current.runId !== owner.runId)
|
|
540
|
+
throw Error('INSTANCE_CONTROL_CHANGED');
|
|
541
|
+
}
|
|
542
|
+
catch (e) {
|
|
543
|
+
if (e.code !== 'ENOENT')
|
|
544
|
+
throw e;
|
|
545
|
+
}
|
|
546
|
+
await rm(p.meta, { force: true });
|
|
547
|
+
await rmdir(p.root);
|
|
548
|
+
}
|
|
522
549
|
if (state.phase === 'stopped')
|
|
523
550
|
emit('INSTANCE_STOPPED');
|
|
524
551
|
}
|
package/dist/src/platform.js
CHANGED
|
@@ -58,14 +58,29 @@ export function createPrivateDirectory(folder) {
|
|
|
58
58
|
if (!ownedByUser(folder, lstatSync(folder), true))
|
|
59
59
|
throw Error('PRIVATE_DIRECTORY_UNSAFE');
|
|
60
60
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
// silently move the rendezvous point of an already-running supervisor.
|
|
61
|
+
// Identity records are durable state, not disposable temporary files.
|
|
62
|
+
export function controlRoot() { return path.join(homedir(), '.config', 'starmux', 'control'); }
|
|
63
|
+
export function controlSocketRoot() {
|
|
65
64
|
if (process.platform === 'darwin')
|
|
66
65
|
return `/private/tmp/starmux-${process.getuid()}`;
|
|
67
66
|
const id = windows ? createHash('sha256').update(homedir().toLowerCase()).digest('hex').slice(0, 16) : String(process.getuid());
|
|
68
|
-
return path.join(tmpdir(), `starmux-${id}`);
|
|
67
|
+
return path.join(windows ? tmpdir() : '/tmp', `starmux-${id}`);
|
|
68
|
+
}
|
|
69
|
+
// Hash the durable address: short Unix socket paths even with a long HOME,
|
|
70
|
+
// and separate endpoints for distinct accounts, homes and test control roots.
|
|
71
|
+
export function controlSocket(address) {
|
|
72
|
+
const id = createHash('sha256').update(path.resolve(address)).digest('hex').slice(0, 32);
|
|
73
|
+
return windows ? '\\\\.\\pipe\\starmux-' + id : path.join(controlSocketRoot(), id + '.sock');
|
|
74
|
+
}
|
|
75
|
+
export function sameFile(file) {
|
|
76
|
+
const original = lstatSync(file);
|
|
77
|
+
return () => { try {
|
|
78
|
+
const now = lstatSync(file);
|
|
79
|
+
return !now.isSymbolicLink() && now.dev === original.dev && now.ino === original.ino;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
} };
|
|
69
84
|
}
|
|
70
85
|
export function within(candidate, root) {
|
|
71
86
|
const relative = path.relative(root, candidate);
|
package/package.json
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starmux",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.16.1",
|
|
4
|
+
"description": "Self-hosted remote MCP gateway for ChatGPT, WorkBuddy, Codex and AI agents to access files, terminals and Desktop Commander tools on Linux, NVIDIA Jetson, macOS and Windows.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"mcp",
|
|
7
|
+
"model-context-protocol",
|
|
8
|
+
"mcp-gateway",
|
|
9
|
+
"mcp-server",
|
|
10
|
+
"remote-mcp",
|
|
11
|
+
"self-hosted",
|
|
12
|
+
"chatgpt",
|
|
13
|
+
"workbuddy",
|
|
14
|
+
"desktop-commander",
|
|
15
|
+
"claude-code",
|
|
16
|
+
"codex",
|
|
17
|
+
"cursor",
|
|
18
|
+
"ai-agents",
|
|
19
|
+
"computer-use",
|
|
20
|
+
"linux",
|
|
21
|
+
"nvidia-jetson",
|
|
22
|
+
"streamable-http",
|
|
23
|
+
"mcp-multiplexer"
|
|
24
|
+
],
|
|
5
25
|
"license": "MIT",
|
|
6
26
|
"homepage": "https://github.com/cookyecat/starmux#readme",
|
|
7
27
|
"repository": {
|