starmux 0.13.1 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/LICENSE +1 -1
- package/README.md +20 -28
- package/dist/src/access-control.js +1 -2
- package/dist/src/access-dashboard.js +2 -3
- package/dist/src/account-access-policy.js +54 -0
- package/dist/src/authorized-workspace-roots.js +0 -1
- package/dist/src/batch-read-policy.js +30 -0
- package/dist/src/cli/instances.js +27 -187
- package/dist/src/cli/oauth.js +0 -1
- package/dist/src/cli/starmux-status.js +25 -18
- package/dist/src/cli/starmux-supervisor.js +0 -1
- package/dist/src/cli/starmux.js +47 -37
- package/dist/src/config-schemas.js +1 -10
- package/dist/src/configured-mcp-upstreams.js +0 -1
- package/dist/src/contracts.js +0 -1
- package/dist/src/current-permission-probe.js +0 -1
- package/dist/src/dc-workspace-http.js +57 -33
- package/dist/src/deny-dashboard.js +2 -3
- package/dist/src/desktop-commander-facade.js +4 -4
- package/dist/src/explicit-deny-rules.js +2 -3
- package/dist/src/global-core-control.js +108 -56
- package/dist/src/home-write-sandbox.js +0 -1
- package/dist/src/http-host-policy.js +20 -0
- package/dist/src/instance-control-protocol.js +27 -0
- package/dist/src/instance-runtime.js +215 -76
- package/dist/src/instance-store.js +28 -22
- package/dist/src/instance-worker.js +2 -3
- package/dist/src/invocation.js +1 -2
- package/dist/src/local-desktop-read-probe.js +5 -88
- package/dist/src/managed-workspace-policy.js +38 -4
- package/dist/src/mcp-results.js +0 -1
- package/dist/src/mcp-upstream-router.js +0 -1
- package/dist/src/oauth-keycloak.js +0 -1
- package/dist/src/oauth-resource.js +0 -1
- package/dist/src/ops-dashboard.js +2 -179
- package/dist/src/permissions-dashboard.js +3 -4
- package/dist/src/platform.js +6 -4
- package/dist/src/privacy-diagnostics.js +0 -1
- package/dist/src/private-mcp-activity.js +0 -1
- package/dist/src/private-store-lock.js +0 -1
- package/dist/src/project-dev-presets.js +0 -1
- package/dist/src/project-git-workflow.js +0 -1
- package/dist/src/project-workspace-policy.js +10 -6
- package/dist/src/runtime-identity.js +0 -14
- package/dist/src/runtime-paths.js +0 -3
- package/dist/src/tool-dispatch.js +45 -46
- package/dist/src/validation.js +0 -1
- package/dist/src/workspace-grants.js +1 -2
- package/package.json +7 -15
- package/scripts/prepare.mjs +1 -1
- package/CONTRIBUTING.md +0 -18
- package/SECURITY.md +0 -7
- package/app/README.md +0 -29
- package/dist/src/access-control.js.map +0 -1
- package/dist/src/access-dashboard.js.map +0 -1
- package/dist/src/authorized-workspace-roots.js.map +0 -1
- package/dist/src/cli/access.js +0 -53
- package/dist/src/cli/access.js.map +0 -1
- package/dist/src/cli/community-remote-revoke.js +0 -32
- package/dist/src/cli/community-remote-revoke.js.map +0 -1
- package/dist/src/cli/instances.js.map +0 -1
- package/dist/src/cli/live-home-settings.js +0 -35
- package/dist/src/cli/live-home-settings.js.map +0 -1
- package/dist/src/cli/mac-mcp-gateway.js +0 -257
- package/dist/src/cli/mac-mcp-gateway.js.map +0 -1
- package/dist/src/cli/oauth.js.map +0 -1
- package/dist/src/cli/permissions-owner.js +0 -27
- package/dist/src/cli/permissions-owner.js.map +0 -1
- package/dist/src/cli/service-setup.js +0 -26
- package/dist/src/cli/service-setup.js.map +0 -1
- package/dist/src/cli/setup-wizard.js +0 -149
- package/dist/src/cli/setup-wizard.js.map +0 -1
- package/dist/src/cli/starmux-status.js.map +0 -1
- package/dist/src/cli/starmux-supervisor.js.map +0 -1
- package/dist/src/cli/starmux.js.map +0 -1
- package/dist/src/cli/verify-setup.js +0 -53
- package/dist/src/cli/verify-setup.js.map +0 -1
- package/dist/src/community-remote-revocations.js +0 -135
- package/dist/src/community-remote-revocations.js.map +0 -1
- package/dist/src/config-schemas.js.map +0 -1
- package/dist/src/configured-mcp-upstreams.js.map +0 -1
- package/dist/src/contracts.js.map +0 -1
- package/dist/src/current-permission-probe.js.map +0 -1
- package/dist/src/dc-workspace-http.js.map +0 -1
- package/dist/src/deny-dashboard.js.map +0 -1
- package/dist/src/desktop-commander-facade.js.map +0 -1
- package/dist/src/explicit-deny-rules.js.map +0 -1
- package/dist/src/gateway-status-format.js +0 -66
- package/dist/src/gateway-status-format.js.map +0 -1
- package/dist/src/global-core-control.js.map +0 -1
- package/dist/src/home-write-sandbox.js.map +0 -1
- package/dist/src/instance-migration.js +0 -138
- package/dist/src/instance-migration.js.map +0 -1
- package/dist/src/instance-runtime.js.map +0 -1
- package/dist/src/instance-store.js.map +0 -1
- package/dist/src/instance-worker.js.map +0 -1
- package/dist/src/invocation.js.map +0 -1
- package/dist/src/live-profile-home-settings.js +0 -80
- package/dist/src/live-profile-home-settings.js.map +0 -1
- package/dist/src/local-desktop-read-probe.js.map +0 -1
- package/dist/src/managed-workspace-policy.js.map +0 -1
- package/dist/src/mcp-results.js.map +0 -1
- package/dist/src/mcp-upstream-router.js.map +0 -1
- package/dist/src/oauth-keycloak.js.map +0 -1
- package/dist/src/oauth-resource.js.map +0 -1
- package/dist/src/operator-cli.js +0 -559
- package/dist/src/operator-cli.js.map +0 -1
- package/dist/src/ops-dashboard.js.map +0 -1
- package/dist/src/permissions-dashboard.js.map +0 -1
- package/dist/src/platform.js.map +0 -1
- package/dist/src/privacy-diagnostics.js.map +0 -1
- package/dist/src/private-mcp-activity.js.map +0 -1
- package/dist/src/private-store-lock.js.map +0 -1
- package/dist/src/project-dev-presets.js.map +0 -1
- package/dist/src/project-git-workflow.js.map +0 -1
- package/dist/src/project-workspace-policy.js.map +0 -1
- package/dist/src/read-scope.js +0 -44
- package/dist/src/read-scope.js.map +0 -1
- package/dist/src/release1-onboarding.js +0 -120
- package/dist/src/release1-onboarding.js.map +0 -1
- package/dist/src/release1-service.js +0 -131
- package/dist/src/release1-service.js.map +0 -1
- package/dist/src/remote-auth.js +0 -47
- package/dist/src/remote-auth.js.map +0 -1
- package/dist/src/runtime-identity.js.map +0 -1
- package/dist/src/runtime-paths.js.map +0 -1
- package/dist/src/starmux-migration.js +0 -172
- package/dist/src/starmux-migration.js.map +0 -1
- package/dist/src/tool-dispatch.js.map +0 -1
- package/dist/src/validation.js.map +0 -1
- package/dist/src/workspace-grants.js.map +0 -1
- package/docs/ACCESS_CONTROL.md +0 -51
- package/docs/CLIENTS.md +0 -22
- package/docs/CONNECT_CHATGPT.md +0 -148
- package/docs/INSTALL.md +0 -62
- package/docs/INSTANCES.md +0 -83
- package/docs/OAUTH.md +0 -116
- package/docs/PLATFORMS.md +0 -29
- package/docs/README.md +0 -16
- package/docs/REMOTE_SETUP.md +0 -280
- package/docs/SECURITY.md +0 -15
- package/src/dc-workspace-http.mjs +0 -3
- package/test/fixtures/p1-fixture.txt +0 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ownedByUser, createPrivateDirectory, controlRoot, windows } from "./platform.js";
|
|
2
2
|
import { ownerSchema } from "./config-schemas.js";
|
|
3
3
|
import { GatewayError, errorCode, isSafeInteger, parseJSON, record } from "./validation.js";
|
|
4
|
+
const CONTROL_VERSION = 1;
|
|
4
5
|
// One Core / two equal frontends. Owner-only local supervisor control.
|
|
5
6
|
// The socket is never an MCP/public endpoint, and never signals a discovered PID.
|
|
6
7
|
import crypto from "node:crypto";
|
|
@@ -17,7 +18,7 @@ export function controlPaths(port, root = controlRoot()) {
|
|
|
17
18
|
deny("CORE_INVALID_PORT");
|
|
18
19
|
const lock = path.join(root, `core-${port}.lease`);
|
|
19
20
|
return { root, lock, recovery: lock + ".recovery",
|
|
20
|
-
meta: path.join(lock, "owner.json"), socket: windows ? `\\\\.\\pipe\\
|
|
21
|
+
meta: path.join(lock, "owner.json"), socket: windows ? `\\\\.\\pipe\\starmux-${crypto.createHash("sha256").update(path.resolve(lock).toLowerCase()).digest("hex").slice(0, 32)}` : path.join(lock, "control.sock") };
|
|
21
22
|
}
|
|
22
23
|
function secureDir(p, create = false) {
|
|
23
24
|
if (create)
|
|
@@ -39,7 +40,10 @@ function readOwner(paths) {
|
|
|
39
40
|
const d = fs.readFileSync(paths.meta);
|
|
40
41
|
if (d.length > 1024)
|
|
41
42
|
deny("CORE_CONTROL_METADATA_INVALID");
|
|
42
|
-
const
|
|
43
|
+
const metadata = record(parseJSON(d.toString("utf8")), "CORE_CONTROL_METADATA_INVALID");
|
|
44
|
+
if (metadata.version !== CONTROL_VERSION)
|
|
45
|
+
deny("CORE_CONTROL_PROTOCOL_UNSUPPORTED");
|
|
46
|
+
const checked = ownerSchema.safeParse(metadata);
|
|
43
47
|
if (!checked.success)
|
|
44
48
|
deny("CORE_CONTROL_METADATA_INVALID");
|
|
45
49
|
const owner = checked.data;
|
|
@@ -71,7 +75,7 @@ async function exchange(paths, owner, method, timeoutMs = 11500) {
|
|
|
71
75
|
resolve(result);
|
|
72
76
|
};
|
|
73
77
|
client.once("connect", () => client.write(JSON.stringify({
|
|
74
|
-
version:
|
|
78
|
+
version: CONTROL_VERSION, ...(windows ? { nonce, proof: proof(owner.token, nonce + ":" + owner.runId + ":" + method) } : { token: owner.token }), runId: owner.runId, method
|
|
75
79
|
}) + "\n"));
|
|
76
80
|
client.on("data", d => {
|
|
77
81
|
body += d.toString("utf8");
|
|
@@ -83,8 +87,10 @@ async function exchange(paths, owner, method, timeoutMs = 11500) {
|
|
|
83
87
|
const reply = record(parseJSON(body.split("\n")[0]));
|
|
84
88
|
if (windows && reply.proof !== proof(owner.token, nonce + ":" + String(reply.result)))
|
|
85
89
|
throw Error("CORE_CONTROL_REPLY_UNAUTHENTICATED");
|
|
86
|
-
if (reply.
|
|
87
|
-
"
|
|
90
|
+
if (reply.version !== CONTROL_VERSION || reply.result === "PROTOCOL_UNSUPPORTED")
|
|
91
|
+
throw Error("CORE_CONTROL_PROTOCOL_UNSUPPORTED");
|
|
92
|
+
if (reply.runId !== owner.runId || !["RUNNING", "STARTING", "STOPPING", "STOPPED",
|
|
93
|
+
"STOP_ACCEPTED", "STOP_FAILED"].includes(String(reply.result)))
|
|
88
94
|
throw Error("CORE_CONTROL_REPLY_INVALID");
|
|
89
95
|
finish(null, { result: String(reply.result), runId: owner.runId, launcher: owner.launcher });
|
|
90
96
|
}
|
|
@@ -153,35 +159,70 @@ function cleanStale(paths) {
|
|
|
153
159
|
catch { }
|
|
154
160
|
}
|
|
155
161
|
}
|
|
156
|
-
export
|
|
162
|
+
export function inspectControlOwner(port, { root } = {}) {
|
|
157
163
|
const paths = controlPaths(port, root);
|
|
158
|
-
let owner;
|
|
159
164
|
try {
|
|
160
|
-
|
|
165
|
+
return readOwner(paths);
|
|
161
166
|
}
|
|
162
167
|
catch (e) {
|
|
163
|
-
if (
|
|
168
|
+
if (errorCode(e) === "ENOENT" && !fs.existsSync(paths.lock))
|
|
164
169
|
return null;
|
|
165
170
|
throw e;
|
|
166
171
|
}
|
|
172
|
+
}
|
|
173
|
+
export async function connectOwner(port, { root } = {}) {
|
|
174
|
+
const paths = controlPaths(port, root);
|
|
175
|
+
const owner = inspectControlOwner(port, { root });
|
|
176
|
+
if (!owner || !alive(owner.pid))
|
|
177
|
+
return null;
|
|
167
178
|
const result = await exchange(paths, owner, "status", 1500);
|
|
168
179
|
return { owner, result };
|
|
169
180
|
}
|
|
170
|
-
export async function globalStop(port, { root } = {}) {
|
|
181
|
+
export async function globalStop(port, { root, timeoutMs = 65000 } = {}) {
|
|
182
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0)
|
|
183
|
+
deny("CORE_STOP_TIMEOUT_INVALID");
|
|
171
184
|
const paths = controlPaths(port, root);
|
|
172
185
|
let owner;
|
|
173
186
|
try {
|
|
174
187
|
owner = readOwner(paths);
|
|
175
188
|
}
|
|
176
189
|
catch (e) {
|
|
177
|
-
if (errorCode(e) === "ENOENT")
|
|
190
|
+
if (errorCode(e) === "ENOENT" && !fs.existsSync(paths.lock))
|
|
178
191
|
return { result: "NO_CONTROL_OWNER" };
|
|
179
192
|
throw e;
|
|
180
193
|
}
|
|
181
|
-
const
|
|
182
|
-
|
|
194
|
+
const deadline = Date.now() + timeoutMs;
|
|
195
|
+
const result = await exchange(paths, owner, "stop", Math.min(11500, timeoutMs));
|
|
196
|
+
if (result.result !== "STOP_ACCEPTED")
|
|
183
197
|
deny("CORE_STOP_UNVERIFIED");
|
|
184
|
-
|
|
198
|
+
// The acknowledgment only accepts the request. Completion belongs to this
|
|
199
|
+
// lease, not to whatever may bind the same port after it has been released.
|
|
200
|
+
while (Date.now() < deadline) {
|
|
201
|
+
try {
|
|
202
|
+
const current = readOwner(paths);
|
|
203
|
+
if (current.runId !== owner.runId)
|
|
204
|
+
return { result: "STOPPED", launcher: owner.launcher, ownerPid: owner.pid, restarted: true };
|
|
205
|
+
try {
|
|
206
|
+
const state = await exchange(paths, owner, "status", Math.min(250, Math.max(1, deadline - Date.now())));
|
|
207
|
+
if (state.result === "STOP_FAILED")
|
|
208
|
+
deny("CORE_STOP_FAILED");
|
|
209
|
+
}
|
|
210
|
+
catch (e) {
|
|
211
|
+
// Closing the control listener is part of releasing the lease.
|
|
212
|
+
if (!["ENOENT", "ECONNREFUSED", "ECONNRESET", "EPIPE"].includes(errorCode(e) ?? "") &&
|
|
213
|
+
!["CORE_CONTROL_EOF", "CORE_CONTROL_TIMEOUT"].includes(e instanceof Error ? e.message : ""))
|
|
214
|
+
throw e;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (e) {
|
|
218
|
+
if (errorCode(e) !== "ENOENT")
|
|
219
|
+
throw e;
|
|
220
|
+
if (!fs.existsSync(paths.lock))
|
|
221
|
+
return { result: "STOPPED", launcher: owner.launcher, ownerPid: owner.pid, restarted: false };
|
|
222
|
+
}
|
|
223
|
+
await delay(Math.min(50, Math.max(1, deadline - Date.now())));
|
|
224
|
+
}
|
|
225
|
+
deny("CORE_STOP_TIMEOUT");
|
|
185
226
|
}
|
|
186
227
|
export async function acquireControl(port, { root, launcher = "cli" } = {}) {
|
|
187
228
|
if (!["cli", "app"].includes(launcher))
|
|
@@ -226,18 +267,21 @@ export async function acquireControl(port, { root, launcher = "cli" } = {}) {
|
|
|
226
267
|
deny("CORE_CONTROL_CLAIM_TIMEOUT");
|
|
227
268
|
}
|
|
228
269
|
secureDir(paths.lock);
|
|
229
|
-
const owner = { version:
|
|
270
|
+
const owner = { version: CONTROL_VERSION, port, pid: process.pid, launcher,
|
|
230
271
|
token: crypto.randomBytes(24).toString("hex"),
|
|
231
272
|
runId: crypto.randomBytes(16).toString("hex") };
|
|
232
273
|
fs.writeFileSync(paths.meta, JSON.stringify(owner), { mode: 0o600, flag: "wx" });
|
|
233
|
-
let onStop = null, phase = "STARTING";
|
|
274
|
+
let onStop = null, phase = "STARTING", stopAccepted = false;
|
|
234
275
|
const clients = new Set();
|
|
235
276
|
const server = net.createServer(socket => {
|
|
236
277
|
clients.add(socket);
|
|
237
278
|
socket.setTimeout(12000, () => socket.destroy());
|
|
279
|
+
socket.on("error", () => { });
|
|
238
280
|
socket.once("close", () => clients.delete(socket));
|
|
239
|
-
let raw = "";
|
|
240
|
-
socket.on("data",
|
|
281
|
+
let raw = "", handled = false;
|
|
282
|
+
socket.on("data", chunk => {
|
|
283
|
+
if (handled)
|
|
284
|
+
return;
|
|
241
285
|
raw += chunk.toString("utf8");
|
|
242
286
|
if (raw.length > 2048) {
|
|
243
287
|
socket.destroy();
|
|
@@ -246,7 +290,7 @@ export async function acquireControl(port, { root, launcher = "cli" } = {}) {
|
|
|
246
290
|
if (!raw.includes("\n"))
|
|
247
291
|
return;
|
|
248
292
|
const line = raw.split("\n")[0];
|
|
249
|
-
|
|
293
|
+
handled = true;
|
|
250
294
|
let request;
|
|
251
295
|
try {
|
|
252
296
|
request = record(parseJSON(line));
|
|
@@ -255,24 +299,30 @@ export async function acquireControl(port, { root, launcher = "cli" } = {}) {
|
|
|
255
299
|
socket.destroy();
|
|
256
300
|
return;
|
|
257
301
|
}
|
|
258
|
-
if (request.
|
|
259
|
-
(windows ? typeof request.nonce !== "string" || request.nonce.length !== 48 || request.proof !== proof(owner.token, request.nonce + ":" + owner.runId + ":" + String(request.method)) : request.token !== owner.token) ||
|
|
302
|
+
if ((windows ? typeof request.nonce !== "string" || request.nonce.length !== 48 || request.proof !== proof(owner.token, request.nonce + ":" + owner.runId + ":" + String(request.method)) : request.token !== owner.token) ||
|
|
260
303
|
request.runId !== owner.runId) {
|
|
261
304
|
socket.destroy();
|
|
262
305
|
return;
|
|
263
306
|
}
|
|
264
|
-
let result = "STOP_FAILED";
|
|
265
|
-
if (request.
|
|
307
|
+
let result = "STOP_FAILED", startStop = false;
|
|
308
|
+
if (request.version !== CONTROL_VERSION)
|
|
309
|
+
result = "PROTOCOL_UNSUPPORTED";
|
|
310
|
+
else if (request.method === "status")
|
|
266
311
|
result = phase;
|
|
267
312
|
else if (request.method === "stop" && onStop) {
|
|
268
|
-
|
|
269
|
-
result =
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
313
|
+
if (phase !== "STOP_FAILED") {
|
|
314
|
+
result = "STOP_ACCEPTED";
|
|
315
|
+
startStop = !stopAccepted;
|
|
316
|
+
stopAccepted = true;
|
|
317
|
+
phase = "STOPPING";
|
|
273
318
|
}
|
|
274
319
|
}
|
|
275
|
-
socket.end(JSON.stringify({ result, runId: owner.runId, launcher: owner.launcher, ...(windows ? { proof: proof(owner.token, String(request.nonce) + ":" + result) } : {}) }) + "\n")
|
|
320
|
+
socket.end(JSON.stringify({ version: CONTROL_VERSION, result, runId: owner.runId, launcher: owner.launcher, ...(windows ? { proof: proof(owner.token, String(request.nonce) + ":" + result) } : {}) }) + "\n", () => {
|
|
321
|
+
// Never await shutdown on its own control connection: release() waits
|
|
322
|
+
// for these sockets to close. Acceptance and completion are distinct.
|
|
323
|
+
if (startStop)
|
|
324
|
+
void Promise.resolve().then(() => onStop()).catch(() => { phase = "STOP_FAILED"; });
|
|
325
|
+
});
|
|
276
326
|
});
|
|
277
327
|
});
|
|
278
328
|
try {
|
|
@@ -301,41 +351,44 @@ export async function acquireControl(port, { root, launcher = "cli" } = {}) {
|
|
|
301
351
|
catch { }
|
|
302
352
|
throw e;
|
|
303
353
|
}
|
|
304
|
-
let
|
|
354
|
+
let releasePromise = null;
|
|
305
355
|
return {
|
|
306
356
|
owner,
|
|
307
357
|
markRunning() { phase = "RUNNING"; },
|
|
308
358
|
markStopped() { phase = "STOPPED"; },
|
|
309
359
|
onStop(fn) { onStop = fn; },
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
360
|
+
release() {
|
|
361
|
+
return releasePromise ??= (async () => {
|
|
362
|
+
// Response sockets close before the lease disappears.
|
|
363
|
+
await new Promise((resolve, reject) => {
|
|
364
|
+
const timer = setTimeout(() => { for (const s of clients)
|
|
365
|
+
s.destroy(); }, 1200);
|
|
366
|
+
timer.unref();
|
|
367
|
+
server.close(error => { clearTimeout(timer); if (error)
|
|
368
|
+
reject(error);
|
|
369
|
+
else
|
|
370
|
+
resolve(); });
|
|
371
|
+
});
|
|
372
|
+
if (readOwner(paths).runId !== owner.runId)
|
|
373
|
+
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
|
+
try {
|
|
377
|
+
if (!windows)
|
|
378
|
+
fs.unlinkSync(paths.socket);
|
|
379
|
+
}
|
|
380
|
+
catch (e) {
|
|
381
|
+
if (errorCode(e) !== "ENOENT")
|
|
382
|
+
throw e;
|
|
383
|
+
}
|
|
384
|
+
fs.unlinkSync(paths.meta);
|
|
385
|
+
fs.rmdirSync(paths.lock);
|
|
386
|
+
})();
|
|
334
387
|
}
|
|
335
388
|
};
|
|
336
389
|
}
|
|
337
390
|
// Private worker handoff for the setup profile. This is a same-UID
|
|
338
|
-
// supervisor identity check, not an OS boundary against the
|
|
391
|
+
// supervisor identity check, not an OS boundary against the local owner.
|
|
339
392
|
export function verifyInternalSetupWorker({ port, ownerPid, runId, token }) {
|
|
340
393
|
const paths = controlPaths(port);
|
|
341
394
|
const owner = readOwner(paths);
|
|
@@ -345,4 +398,3 @@ export function verifyInternalSetupWorker({ port, ownerPid, runId, token }) {
|
|
|
345
398
|
deny("CORE_SETUP_WORKER_NOT_SUPERVISED");
|
|
346
399
|
return true;
|
|
347
400
|
}
|
|
348
|
-
//# sourceMappingURL=global-core-control.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Host is an HTTP authority, not a URL; do not normalize alternate IP spellings or userinfo. */
|
|
2
|
+
export function isAllowedGatewayHost(req, listenerPort, route) {
|
|
3
|
+
// Node keeps only one Host in headers; inspect rawHeaders to reject duplicates too.
|
|
4
|
+
let hostCount = 0;
|
|
5
|
+
for (let i = 0; i < req.rawHeaders.length; i += 2)
|
|
6
|
+
if (req.rawHeaders[i]?.toLowerCase() === 'host')
|
|
7
|
+
hostCount++;
|
|
8
|
+
const host = req.headers.host;
|
|
9
|
+
if (hostCount !== 1 || !host || listenerPort === null)
|
|
10
|
+
return false;
|
|
11
|
+
// Owner pages and discovery retain their existing listener-specific boundary.
|
|
12
|
+
if (route !== '/mcp')
|
|
13
|
+
return host === `127.0.0.1:${listenerPort}` || host === `localhost:${listenerPort}`;
|
|
14
|
+
// SSH forwards TCP bytes unchanged, including the client's local Host port.
|
|
15
|
+
// Keep the hostname allowlist and validate the entire authority, including its port.
|
|
16
|
+
const match = /^(?:127\.0\.0\.1|localhost)(?::([0-9]{1,5}))?$/i.exec(host);
|
|
17
|
+
if (!match || match[0] !== host)
|
|
18
|
+
return false;
|
|
19
|
+
return match[1] === undefined || (Number(match[1]) >= 1 && Number(match[1]) <= 65535);
|
|
20
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { instanceConfigSchema } from './instance-store.js';
|
|
3
|
+
// Version the local control contract independently of npm releases. Unknown
|
|
4
|
+
// versions fail explicitly; additional descriptive fields are safe to ignore.
|
|
5
|
+
export const INSTANCE_CONTROL_VERSION = 1;
|
|
6
|
+
const runId = z.string().regex(/^[a-f0-9]{32}$/);
|
|
7
|
+
export const instanceOwnerSchema = z.object({
|
|
8
|
+
protocolVersion: z.literal(INSTANCE_CONTROL_VERSION), instanceId: z.string().uuid(), runId,
|
|
9
|
+
pid: z.number().int().min(2), port: z.number().int().min(1024).max(65535),
|
|
10
|
+
token: z.string().regex(/^[a-f0-9]{64}$/), launcher: z.enum(['cli', 'app']), appParentPid: z.number().int().nullable()
|
|
11
|
+
});
|
|
12
|
+
export const activeConfigSchema = z.object({ config: instanceConfigSchema, revision: z.string(), effectiveRevision: z.string(), sourcePath: z.string() });
|
|
13
|
+
export const runtimeStateSchema = z.object({
|
|
14
|
+
protocolVersion: z.literal(INSTANCE_CONTROL_VERSION), instanceId: z.string().uuid(), runId, pid: z.number().int().min(2),
|
|
15
|
+
launcher: z.enum(['cli', 'app']), appParentPid: z.number().int().nullable(), executionMode: z.enum(['foreground', 'background']).optional(),
|
|
16
|
+
permissionMode: z.enum(['global', 'workspace']).optional(), phase: z.enum(['starting', 'running', 'stopping', 'stopped', 'error']),
|
|
17
|
+
port: z.number().int().min(1024).max(65535).nullable(), active: activeConfigSchema.nullable(), problem: z.string().nullable(),
|
|
18
|
+
coreProgram: z.string(), buildVersion: z.string()
|
|
19
|
+
});
|
|
20
|
+
export const controlRequestSchema = z.object({ protocolVersion: z.literal(INSTANCE_CONTROL_VERSION), method: z.enum(['status', 'stop', 'apply']),
|
|
21
|
+
revision: z.string().nullable(), runId, nonce: z.string().regex(/^[a-f0-9]{48}$/) });
|
|
22
|
+
export const controlReplySchema = z.object({ protocolVersion: z.literal(INSTANCE_CONTROL_VERSION), state: runtimeStateSchema,
|
|
23
|
+
result: z.enum(['STATE', 'STOP_ACCEPTED']), error: z.string().optional() });
|
|
24
|
+
export function checkInstanceProtocol(value) {
|
|
25
|
+
if (!value || typeof value !== 'object' || !('protocolVersion' in value) || value.protocolVersion !== INSTANCE_CONTROL_VERSION)
|
|
26
|
+
throw Error('INSTANCE_CONTROL_PROTOCOL_UNSUPPORTED');
|
|
27
|
+
}
|