starmux 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +20 -30
- package/dist/src/access-control.js +1 -2
- package/dist/src/access-dashboard.js +2 -3
- package/dist/src/account-access-policy.js +54 -0
- package/dist/src/authorized-workspace-roots.js +0 -1
- package/dist/src/batch-read-policy.js +30 -0
- package/dist/src/cli/instances.js +6 -175
- package/dist/src/cli/oauth.js +0 -1
- package/dist/src/cli/starmux-status.js +10 -14
- package/dist/src/cli/starmux-supervisor.js +0 -1
- package/dist/src/cli/starmux.js +13 -25
- package/dist/src/config-schemas.js +0 -9
- package/dist/src/configured-mcp-upstreams.js +0 -1
- package/dist/src/contracts.js +0 -1
- package/dist/src/current-permission-probe.js +0 -1
- package/dist/src/dc-workspace-http.js +57 -33
- package/dist/src/deny-dashboard.js +2 -3
- package/dist/src/desktop-commander-facade.js +4 -4
- package/dist/src/explicit-deny-rules.js +2 -3
- package/dist/src/global-core-control.js +2 -3
- package/dist/src/home-write-sandbox.js +0 -1
- package/dist/src/http-host-policy.js +20 -0
- package/dist/src/instance-runtime.js +3 -3
- package/dist/src/instance-store.js +28 -22
- package/dist/src/instance-worker.js +2 -3
- package/dist/src/invocation.js +1 -2
- package/dist/src/local-desktop-read-probe.js +5 -88
- package/dist/src/managed-workspace-policy.js +38 -4
- package/dist/src/mcp-results.js +0 -1
- package/dist/src/mcp-upstream-router.js +0 -1
- package/dist/src/oauth-keycloak.js +0 -1
- package/dist/src/oauth-resource.js +0 -1
- package/dist/src/ops-dashboard.js +2 -179
- package/dist/src/permissions-dashboard.js +3 -4
- package/dist/src/platform.js +3 -4
- package/dist/src/privacy-diagnostics.js +0 -1
- package/dist/src/private-mcp-activity.js +0 -1
- package/dist/src/private-store-lock.js +0 -1
- package/dist/src/project-dev-presets.js +0 -1
- package/dist/src/project-git-workflow.js +0 -1
- package/dist/src/project-workspace-policy.js +10 -6
- package/dist/src/runtime-identity.js +0 -14
- package/dist/src/runtime-paths.js +0 -3
- package/dist/src/tool-dispatch.js +45 -46
- package/dist/src/validation.js +0 -1
- package/dist/src/workspace-grants.js +1 -2
- package/package.json +13 -17
- package/scripts/prepare.mjs +1 -1
- package/CONTRIBUTING.md +0 -18
- package/SECURITY.md +0 -7
- package/app/README.md +0 -29
- package/dist/src/access-control.js.map +0 -1
- package/dist/src/access-dashboard.js.map +0 -1
- package/dist/src/authorized-workspace-roots.js.map +0 -1
- package/dist/src/cli/access.js +0 -53
- package/dist/src/cli/access.js.map +0 -1
- package/dist/src/cli/community-remote-revoke.js +0 -32
- package/dist/src/cli/community-remote-revoke.js.map +0 -1
- package/dist/src/cli/instances.js.map +0 -1
- package/dist/src/cli/live-home-settings.js +0 -35
- package/dist/src/cli/live-home-settings.js.map +0 -1
- package/dist/src/cli/mac-mcp-gateway.js +0 -257
- package/dist/src/cli/mac-mcp-gateway.js.map +0 -1
- package/dist/src/cli/oauth.js.map +0 -1
- package/dist/src/cli/permissions-owner.js +0 -27
- package/dist/src/cli/permissions-owner.js.map +0 -1
- package/dist/src/cli/service-setup.js +0 -26
- package/dist/src/cli/service-setup.js.map +0 -1
- package/dist/src/cli/setup-wizard.js +0 -149
- package/dist/src/cli/setup-wizard.js.map +0 -1
- package/dist/src/cli/starmux-status.js.map +0 -1
- package/dist/src/cli/starmux-supervisor.js.map +0 -1
- package/dist/src/cli/starmux.js.map +0 -1
- package/dist/src/cli/verify-setup.js +0 -53
- package/dist/src/cli/verify-setup.js.map +0 -1
- package/dist/src/community-remote-revocations.js +0 -135
- package/dist/src/community-remote-revocations.js.map +0 -1
- package/dist/src/config-schemas.js.map +0 -1
- package/dist/src/configured-mcp-upstreams.js.map +0 -1
- package/dist/src/contracts.js.map +0 -1
- package/dist/src/current-permission-probe.js.map +0 -1
- package/dist/src/dc-workspace-http.js.map +0 -1
- package/dist/src/deny-dashboard.js.map +0 -1
- package/dist/src/desktop-commander-facade.js.map +0 -1
- package/dist/src/explicit-deny-rules.js.map +0 -1
- package/dist/src/gateway-status-format.js +0 -66
- package/dist/src/gateway-status-format.js.map +0 -1
- package/dist/src/global-core-control.js.map +0 -1
- package/dist/src/home-write-sandbox.js.map +0 -1
- package/dist/src/instance-migration.js +0 -138
- package/dist/src/instance-migration.js.map +0 -1
- package/dist/src/instance-runtime.js.map +0 -1
- package/dist/src/instance-store.js.map +0 -1
- package/dist/src/instance-worker.js.map +0 -1
- package/dist/src/invocation.js.map +0 -1
- package/dist/src/live-profile-home-settings.js +0 -80
- package/dist/src/live-profile-home-settings.js.map +0 -1
- package/dist/src/local-desktop-read-probe.js.map +0 -1
- package/dist/src/managed-workspace-policy.js.map +0 -1
- package/dist/src/mcp-results.js.map +0 -1
- package/dist/src/mcp-upstream-router.js.map +0 -1
- package/dist/src/oauth-keycloak.js.map +0 -1
- package/dist/src/oauth-resource.js.map +0 -1
- package/dist/src/operator-cli.js +0 -559
- package/dist/src/operator-cli.js.map +0 -1
- package/dist/src/ops-dashboard.js.map +0 -1
- package/dist/src/permissions-dashboard.js.map +0 -1
- package/dist/src/platform.js.map +0 -1
- package/dist/src/privacy-diagnostics.js.map +0 -1
- package/dist/src/private-mcp-activity.js.map +0 -1
- package/dist/src/private-store-lock.js.map +0 -1
- package/dist/src/project-dev-presets.js.map +0 -1
- package/dist/src/project-git-workflow.js.map +0 -1
- package/dist/src/project-workspace-policy.js.map +0 -1
- package/dist/src/read-scope.js +0 -44
- package/dist/src/read-scope.js.map +0 -1
- package/dist/src/release1-onboarding.js +0 -120
- package/dist/src/release1-onboarding.js.map +0 -1
- package/dist/src/release1-service.js +0 -131
- package/dist/src/release1-service.js.map +0 -1
- package/dist/src/remote-auth.js +0 -47
- package/dist/src/remote-auth.js.map +0 -1
- package/dist/src/runtime-identity.js.map +0 -1
- package/dist/src/runtime-paths.js.map +0 -1
- package/dist/src/starmux-migration.js +0 -172
- package/dist/src/starmux-migration.js.map +0 -1
- package/dist/src/tool-dispatch.js.map +0 -1
- package/dist/src/validation.js.map +0 -1
- package/dist/src/workspace-grants.js.map +0 -1
- package/docs/ACCESS_CONTROL.md +0 -51
- package/docs/CLIENTS.md +0 -22
- package/docs/CONNECT_CHATGPT.md +0 -148
- package/docs/INSTALL.md +0 -62
- package/docs/INSTANCES.md +0 -83
- package/docs/OAUTH.md +0 -116
- package/docs/PLATFORMS.md +0 -29
- package/docs/README.md +0 -16
- package/docs/REMOTE_SETUP.md +0 -280
- package/docs/SECURITY.md +0 -15
- package/docs/assets/starmux-app-en.png +0 -0
- package/docs/assets/starmux-architecture.svg +0 -315
- package/src/dc-workspace-http.mjs +0 -3
- package/test/fixtures/p1-fixture.txt +0 -2
package/dist/src/invocation.js
CHANGED
|
@@ -61,7 +61,7 @@ export function createInvocationPipeline(options) {
|
|
|
61
61
|
catch (error) {
|
|
62
62
|
const reason = errorCode(error) || 'DENY_RULE_CHECK_FAILED';
|
|
63
63
|
audit({ event: 'TOOL_EXPLICITLY_DENIED', requestId: request.requestId, tool: invocation.name, reason });
|
|
64
|
-
return { isError: true, content: [{ type: 'text', text: reason + ': Review filters on the
|
|
64
|
+
return { isError: true, content: [{ type: 'text', text: reason + ': Review filters on the local owner Dashboard /filters' }] };
|
|
65
65
|
}
|
|
66
66
|
const result = await options.dispatch(invocation, request);
|
|
67
67
|
for (const hook of hooks) {
|
|
@@ -77,4 +77,3 @@ export function createInvocationPipeline(options) {
|
|
|
77
77
|
return result; // original MCP result, including unknown extension fields
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
|
-
//# sourceMappingURL=invocation.js.map
|
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
|
-
import { windows,
|
|
3
|
+
import { windows, childEnvKeys } from "./platform.js";
|
|
4
4
|
import { toolResult } from "./mcp-results.js";
|
|
5
|
-
import { desktopCommanderEntry
|
|
6
|
-
|
|
7
|
-
// Desktop Commander stdio MCP client for fixture tests and the private P1 bridge.
|
|
8
|
-
// NOT imported into public P0 src/server.mjs; only explicitly approved operations.
|
|
9
|
-
// This adapter exposes no arbitrary callTool capability.
|
|
5
|
+
import { desktopCommanderEntry } from "./runtime-paths.js";
|
|
6
|
+
// Embedded Desktop Commander transport. The dispatcher authorizes every invocation.
|
|
10
7
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
11
8
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
12
|
-
import { access, constants
|
|
13
|
-
import path from "node:path";
|
|
9
|
+
import { access, constants } from "node:fs/promises";
|
|
14
10
|
import { sandboxedDesktopSpawn } from "./home-write-sandbox.js";
|
|
15
|
-
import { resolveScopedRead } from "./read-scope.js";
|
|
16
|
-
const PROJECT = packageRoot;
|
|
17
|
-
const FIXTURES = fixtureRoot;
|
|
18
11
|
const DC_BIN = desktopCommanderEntry;
|
|
19
12
|
// The embedded upstream must not launch browser onboarding on every Gateway
|
|
20
13
|
// connection or after test/dev child restarts. Supported upstream CLI option.
|
|
@@ -22,8 +15,6 @@ export function desktopCommanderSpawnArgs(binary = DC_BIN) {
|
|
|
22
15
|
return [binary, "--no-onboarding"];
|
|
23
16
|
}
|
|
24
17
|
const EXPOSED_LOCAL_TOOLS = Object.freeze(["read_file"]);
|
|
25
|
-
const MAX_READ = 4096;
|
|
26
|
-
const MAX_PROJECT_README_BYTES = 16384; // Project README may grow; synthetic fixture limit remains 4096.
|
|
27
18
|
function sanitizedEnv() {
|
|
28
19
|
const allow = childEnvKeys;
|
|
29
20
|
const env = {};
|
|
@@ -67,7 +58,7 @@ export class LocalDesktopReadProbe {
|
|
|
67
58
|
const transport = new StdioClientTransport({
|
|
68
59
|
...executable, env, stderr: "pipe"
|
|
69
60
|
});
|
|
70
|
-
const client = new Client({ name: "
|
|
61
|
+
const client = new Client({ name: "starmux-local-read-probe", version: "0.1.0" }, { capabilities: {} });
|
|
71
62
|
try {
|
|
72
63
|
await client.connect(transport);
|
|
73
64
|
const tools = await client.listTools();
|
|
@@ -113,77 +104,6 @@ export class LocalDesktopReadProbe {
|
|
|
113
104
|
throw new Error("Unknown upstream tool");
|
|
114
105
|
return toolResult(await this.#client.callTool(invocation));
|
|
115
106
|
}
|
|
116
|
-
async callApprovedRead(pathToFile) {
|
|
117
|
-
if (!this.#client)
|
|
118
|
-
throw new Error("Not connected");
|
|
119
|
-
const { absolutePath } = await resolveScopedRead({
|
|
120
|
-
root: FIXTURES, relativePath: pathToFile,
|
|
121
|
-
allowedFiles: ["p1-fixture.txt", "dc-bridge-canary.txt"], maxBytes: MAX_READ
|
|
122
|
-
});
|
|
123
|
-
return toolResult(await this.#client.callTool({ name: "read_file", arguments: { path: absolutePath } }));
|
|
124
|
-
}
|
|
125
|
-
async callApprovedProjectRead() {
|
|
126
|
-
if (!this.#client)
|
|
127
|
-
throw new Error("Not connected");
|
|
128
|
-
const candidate = path.join(PROJECT, "README.md");
|
|
129
|
-
const st = await lstat(candidate);
|
|
130
|
-
if (!st.isFile() || st.isSymbolicLink() || st.nlink !== 1 ||
|
|
131
|
-
!ownedByUser(candidate, st) || st.size > MAX_PROJECT_README_BYTES)
|
|
132
|
-
throw new Error("Project file failed read approval");
|
|
133
|
-
return toolResult(await this.#client.callTool({ name: "read_file", arguments: { path: candidate } }));
|
|
134
|
-
}
|
|
135
|
-
async callApprovedCanaryWrite(content) {
|
|
136
|
-
if (!this.#client)
|
|
137
|
-
throw new Error("Not connected");
|
|
138
|
-
if (typeof content !== "string" || Buffer.byteLength(content) > 256)
|
|
139
|
-
throw new Error("Canary content exceeds approved bound");
|
|
140
|
-
const root = await lstat(FIXTURES);
|
|
141
|
-
if (!root.isDirectory() || root.isSymbolicLink() ||
|
|
142
|
-
!ownedByUser(FIXTURES, root))
|
|
143
|
-
throw new Error("Fixture directory denied");
|
|
144
|
-
const candidate = path.join(FIXTURES, "dc-bridge-canary.txt");
|
|
145
|
-
const existing = await lstat(candidate).catch(e => {
|
|
146
|
-
if (errorCode(e) === "ENOENT")
|
|
147
|
-
return null;
|
|
148
|
-
throw e;
|
|
149
|
-
});
|
|
150
|
-
if (existing && (!existing.isFile() || existing.isSymbolicLink() ||
|
|
151
|
-
existing.nlink !== 1 || !ownedByUser(candidate, existing)))
|
|
152
|
-
throw new Error("Canary target denied");
|
|
153
|
-
return toolResult(await this.#client.callTool({ name: "write_file", arguments: {
|
|
154
|
-
path: candidate, content, mode: "rewrite"
|
|
155
|
-
} }));
|
|
156
|
-
}
|
|
157
|
-
async callApprovedSystemName() {
|
|
158
|
-
if (!this.#client)
|
|
159
|
-
throw new Error("Not connected");
|
|
160
|
-
return toolResult(await this.#client.callTool({ name: "start_process", arguments: {
|
|
161
|
-
...commandOptions(systemNameCommand), timeout_ms: 3000
|
|
162
|
-
} }));
|
|
163
|
-
}
|
|
164
|
-
async readFixture(basename = "p1-fixture.txt") {
|
|
165
|
-
if (!this.#client)
|
|
166
|
-
throw new Error("Not connected");
|
|
167
|
-
if (basename !== "p1-fixture.txt")
|
|
168
|
-
throw new Error("Only the synthetic fixture is approved");
|
|
169
|
-
const { absolutePath: candidate } = await resolveScopedRead({
|
|
170
|
-
root: FIXTURES, relativePath: basename,
|
|
171
|
-
allowedFiles: ["p1-fixture.txt"], maxBytes: MAX_READ
|
|
172
|
-
});
|
|
173
|
-
const expected = await readFile(candidate, "utf8");
|
|
174
|
-
const result = toolResult(await this.#client.callTool({
|
|
175
|
-
name: "read_file",
|
|
176
|
-
arguments: { path: candidate }
|
|
177
|
-
}));
|
|
178
|
-
if (result.isError)
|
|
179
|
-
throw new Error("Upstream read_file returned error");
|
|
180
|
-
const combined = (result.content || []).filter(x => x.type === "text").map(x => x.text).join("\n");
|
|
181
|
-
if (!combined.includes(expected.trim())) {
|
|
182
|
-
throw new Error("Unexpected tool result; refusing to display private content");
|
|
183
|
-
}
|
|
184
|
-
return { ok: true, fixture: basename, bytes: Buffer.byteLength(expected),
|
|
185
|
-
allowedToolNames: this.allowedToolNames };
|
|
186
|
-
}
|
|
187
107
|
async close() {
|
|
188
108
|
const client = this.#client;
|
|
189
109
|
const pid = this.#transport?.pid;
|
|
@@ -197,6 +117,3 @@ export class LocalDesktopReadProbe {
|
|
|
197
117
|
await client.close().catch(() => { });
|
|
198
118
|
}
|
|
199
119
|
}
|
|
200
|
-
// No general-purpose remote forwarding. All interactions live inside this
|
|
201
|
-
// explicitly opted-in local test, not the public HTTP MCP request handlers.
|
|
202
|
-
//# sourceMappingURL=local-desktop-read-probe.js.map
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { windows, commandOptions, diskUsageCommand } from "./platform.js";
|
|
2
|
-
import { GatewayError, isInteger, isSafeInteger } from "./validation.js";
|
|
2
|
+
import { GatewayError, errorCode, isInteger, isSafeInteger } from "./validation.js";
|
|
3
3
|
// Release 1 broad read-only inventory, narrow independent write grants.
|
|
4
4
|
// No OS sandbox claim: trusted-project Shell remains same-UID privileged.
|
|
5
5
|
import { lstat, realpath } from "node:fs/promises";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { createProjectPolicy } from "./project-workspace-policy.js";
|
|
8
8
|
import { isInside } from "./workspace-grants.js";
|
|
9
|
+
import { prepareBatchRead } from "./batch-read-policy.js";
|
|
9
10
|
function fail(reason) { const e = new GatewayError(reason); throw e; }
|
|
10
11
|
;
|
|
11
12
|
const fileTools = new Map([
|
|
@@ -29,9 +30,41 @@ async function broadPath(raw, kind) {
|
|
|
29
30
|
path.normalize(raw) !== raw)
|
|
30
31
|
fail("INVENTORY_ABSOLUTE_PATH_REQUIRED");
|
|
31
32
|
safeSegments(raw);
|
|
32
|
-
|
|
33
|
+
// A read target may not exist yet. Resolve its nearest existing ancestor so
|
|
34
|
+
// the caller can still enforce grants on the canonical path before forwarding.
|
|
35
|
+
let current = raw, canonical;
|
|
36
|
+
const suffix = [];
|
|
37
|
+
for (;;) {
|
|
38
|
+
try {
|
|
39
|
+
canonical = path.join(await realpath(current), ...suffix.slice().reverse());
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (kind !== "file" || errorCode(error) !== "ENOENT")
|
|
44
|
+
throw error;
|
|
45
|
+
const entry = await lstat(current).catch(e => {
|
|
46
|
+
if (errorCode(e) === "ENOENT")
|
|
47
|
+
return null;
|
|
48
|
+
throw e;
|
|
49
|
+
});
|
|
50
|
+
// Do not treat a dangling symlink as an ordinary missing path.
|
|
51
|
+
if (entry?.isSymbolicLink())
|
|
52
|
+
fail("INVENTORY_SYMLINK_DENIED");
|
|
53
|
+
const parent = path.dirname(current);
|
|
54
|
+
if (parent === current)
|
|
55
|
+
throw error;
|
|
56
|
+
suffix.push(path.basename(current));
|
|
57
|
+
current = parent;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
33
60
|
safeSegments(canonical);
|
|
34
|
-
const st = await lstat(canonical)
|
|
61
|
+
const st = await lstat(canonical).catch(error => {
|
|
62
|
+
if (kind === "file" && errorCode(error) === "ENOENT")
|
|
63
|
+
return null;
|
|
64
|
+
throw error;
|
|
65
|
+
});
|
|
66
|
+
if (!st)
|
|
67
|
+
return canonical;
|
|
35
68
|
if (st.isSymbolicLink())
|
|
36
69
|
fail("INVENTORY_SYMLINK_DENIED");
|
|
37
70
|
if (kind === "file" && (!st.isFile() || st.nlink !== 1 || st.size > 1024 * 1024))
|
|
@@ -96,6 +129,8 @@ export function createManagedWorkspacePolicy({ basePolicy, grants }) {
|
|
|
96
129
|
return { kind: "denied" };
|
|
97
130
|
}
|
|
98
131
|
async function prepare(name, args = {}) {
|
|
132
|
+
if (name === "read_multiple_files")
|
|
133
|
+
return prepareBatchRead(args, prepare);
|
|
99
134
|
const target = await locate(name, args);
|
|
100
135
|
if (!target)
|
|
101
136
|
return basePolicy.prepare(name, args);
|
|
@@ -162,4 +197,3 @@ export function createManagedWorkspacePolicy({ basePolicy, grants }) {
|
|
|
162
197
|
grantsSnapshot: () => grants.snapshot()
|
|
163
198
|
});
|
|
164
199
|
}
|
|
165
|
-
//# sourceMappingURL=managed-workspace-policy.js.map
|
package/dist/src/mcp-results.js
CHANGED
|
@@ -1,129 +1,7 @@
|
|
|
1
|
-
import { gitBinary } from "./platform.js";
|
|
2
|
-
import { packageRoot } from "./runtime-paths.js";
|
|
3
|
-
import { errorCode } from "./validation.js";
|
|
4
|
-
// Read-only, loopback-only Mac MCP Gateway operations dashboard.
|
|
5
|
-
// No service control, arbitrary command, file content, credentials or extra port.
|
|
6
|
-
import { execFile } from "node:child_process";
|
|
7
|
-
import { readFile } from "node:fs/promises";
|
|
8
|
-
import path from "node:path";
|
|
9
|
-
import { promisify } from "node:util";
|
|
10
|
-
const execute = promisify(execFile);
|
|
11
|
-
const root = packageRoot;
|
|
12
|
-
const home = process.env.HOME;
|
|
13
|
-
export const MODULES = Object.freeze([
|
|
14
|
-
{ id: "workspace", label: "Project Workspace Gateway", service: "ai.home.mac-mcp-project-workspace", ports: [8793], phase: "active", role: "当前 ChatGPT 文件读写编辑服务", transport: "loopback" },
|
|
15
|
-
{ id: "tunnel", label: "OpenAI Secure MCP Tunnel", service: "ai.home.mac-mcp-tunnel", ports: [18891], phase: "active", role: "云端出站接入;18891 为本地管理端口", transport: "outbound" },
|
|
16
|
-
{ id: "dc-standby", label: "Desktop Commander P1 Bridge", service: "ai.home.mac-mcp-dc-bridge", ports: [8792], phase: "standby", role: "已验收旧版本、可回滚", transport: "loopback" },
|
|
17
|
-
{ id: "p0", label: "Gateway P0 / health_check", service: "ai.home.mac-mcp-gateway-p0", ports: [8787], phase: "legacy", role: "历史 P0,仅健康检查", transport: "loopback" },
|
|
18
|
-
{ id: "canary", label: "Synthetic Canary", service: "ai.home.mac-mcp-private-pilot", ports: [8790], phase: "paused", role: "历史合成读写测试;冷备份", transport: "loopback" },
|
|
19
|
-
{ id: "caddy", label: "Caddy / Direct URL HTTPS", service: "ai.home.mac-mcp-caddy", ports: [80, 443], phase: "paused", role: "未来公网 URL 直连;保留证书与配置", transport: "inbound" },
|
|
20
|
-
{ id: "uid450", label: "UID450 Isolated Worker v2", service: "ai.home.mac-mcp-worker-v2", ports: [], phase: "research", role: "系统级 Unix Socket Worker;不在当前 DC 主链", transport: "unix" }
|
|
21
|
-
]);
|
|
22
|
-
const env = process.env;
|
|
23
|
-
async function cmd(exe, args) {
|
|
24
|
-
try {
|
|
25
|
-
const v = await execute(exe, args, { timeout: 3000, maxBuffer: 130000, env });
|
|
26
|
-
return { ok: true, stdout: v.stdout };
|
|
27
|
-
}
|
|
28
|
-
catch (e) {
|
|
29
|
-
return { ok: false, error: errorCode(e) === "ETIMEDOUT" ? "timeout" : "unavailable" };
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
function launched(stdout) {
|
|
33
|
-
return new Map(stdout.split("\n").map(line => line.trim().split(/\s+/))
|
|
34
|
-
.filter(p => p.length >= 3 && p[2]?.startsWith("ai.home.mac-mcp"))
|
|
35
|
-
.map(p => [p[2], { pid: /^\d+$/.test(p[0] ?? "") ? Number(p[0]) : null, lastExit: p[1] }]));
|
|
36
|
-
}
|
|
37
|
-
function disabledLabels(stdout) {
|
|
38
|
-
const names = new Set();
|
|
39
|
-
for (const m of stdout.matchAll(/"(ai\.home\.mac-mcp-[^"]+)"\s*=>\s*disabled/g))
|
|
40
|
-
names.add(m[1]);
|
|
41
|
-
return names;
|
|
42
|
-
}
|
|
43
|
-
function listening(stdout) {
|
|
44
|
-
const known = new Set(MODULES.flatMap(x => x.ports));
|
|
45
|
-
const ports = new Map();
|
|
46
|
-
for (const line of stdout.split("\n")) {
|
|
47
|
-
const cols = line.trim().split(/\s+/);
|
|
48
|
-
const match = line.match(/TCP\s+(\S+):(\d+)\s+\(LISTEN\)/);
|
|
49
|
-
if (!match)
|
|
50
|
-
continue;
|
|
51
|
-
const port = Number(match[2]);
|
|
52
|
-
if (known.has(port))
|
|
53
|
-
ports.set(port, { port, address: match[1], process: cols[0],
|
|
54
|
-
pid: /^\d+$/.test(cols[1] ?? "") ? Number(cols[1]) : null });
|
|
55
|
-
}
|
|
56
|
-
return ports;
|
|
57
|
-
}
|
|
58
|
-
function tunnelTarget(xml) {
|
|
59
|
-
// Only classify known filenames; never return full plist, identity or tokens.
|
|
60
|
-
for (const [file, target] of [
|
|
61
|
-
["run_tunnel_workspace.py", "8793 · Project Workspace"],
|
|
62
|
-
["run_tunnel_dc_bridge.py", "8792 · DC Standby"],
|
|
63
|
-
["run_tunnel_pilot.py", "8790 · Synthetic Canary"],
|
|
64
|
-
["run_tunnel.py", "8787 · P0"]
|
|
65
|
-
])
|
|
66
|
-
if (xml.includes(file + "</string>"))
|
|
67
|
-
return target;
|
|
68
|
-
return "unknown (inspect LaunchAgent)";
|
|
69
|
-
}
|
|
70
|
-
async function projectGit(at) {
|
|
71
|
-
const r = await cmd(gitBinary, ["-C", at, "status", "--porcelain=v1"]);
|
|
72
|
-
const br = await cmd(gitBinary, ["-C", at, "branch", "--show-current"]);
|
|
73
|
-
const h = await cmd(gitBinary, ["-C", at, "rev-parse", "--short", "HEAD"]);
|
|
74
|
-
return { branch: br.ok ? br.stdout.trim() : "unavailable",
|
|
75
|
-
commit: h.ok ? h.stdout.trim() : "unavailable",
|
|
76
|
-
dirtyCount: r.ok ? r.stdout.split("\n").filter(Boolean).length : null };
|
|
77
|
-
}
|
|
78
1
|
export async function collectOperationsSnapshot() {
|
|
79
|
-
|
|
80
|
-
return { capturedAt: new Date().toISOString(), host: process.platform + " local (loopback only)", mode: "read-only", tunnelBackend: "not verified", modules: [], git: await projectGit(root), workspaceGit: await projectGit(process.env.MAC_MCP_PROJECT_ROOT || root), errors: [], milestones: [] };
|
|
81
|
-
const uid = process.getuid?.();
|
|
82
|
-
const [jobs, disables, listeners, worker, git, plist] = await Promise.all([
|
|
83
|
-
cmd("/bin/launchctl", ["list"]),
|
|
84
|
-
cmd("/bin/launchctl", ["print-disabled", "gui/" + uid]),
|
|
85
|
-
cmd("/usr/sbin/lsof", ["-nP", "-iTCP", "-sTCP:LISTEN"]),
|
|
86
|
-
cmd("/bin/launchctl", ["print", "system/ai.home.mac-mcp-worker-v2"]),
|
|
87
|
-
projectGit(root),
|
|
88
|
-
readFile(path.join(home || "/", "Library/LaunchAgents/ai.home.mac-mcp-tunnel.plist"), "utf8")
|
|
89
|
-
.then(x => ({ ok: true, text: x })).catch(() => ({ ok: false, text: "" }))
|
|
90
|
-
]);
|
|
91
|
-
const active = jobs.ok ? launched(jobs.stdout) : new Map();
|
|
92
|
-
const disabled = disables.ok ? disabledLabels(disables.stdout) : new Set();
|
|
93
|
-
const ports = listeners.ok ? listening(listeners.stdout) : new Map();
|
|
94
|
-
const modules = MODULES.map(m => {
|
|
95
|
-
const status = m.id === "uid450" ?
|
|
96
|
-
(worker.ok ? /state = running/.test(worker.stdout) ? "running" : "stopped" : "unknown") :
|
|
97
|
-
(!jobs.ok ? "unknown" : active.has(m.service) ? "running" :
|
|
98
|
-
disabled.has(m.service) ? "disabled" : "stopped");
|
|
99
|
-
const occupied = m.ports.map(p => ports.get(p)).filter((x) => x !== undefined);
|
|
100
|
-
const ready = status === "running" && (m.ports.length === 0 ||
|
|
101
|
-
occupied.length > 0);
|
|
102
|
-
return { ...m, status, ready, pid: m.id === "uid450" ?
|
|
103
|
-
Number(worker.stdout?.match(/pid = (\d+)/)?.[1]) || null :
|
|
104
|
-
active.get(m.service)?.pid || null, listeners: occupied };
|
|
105
|
-
});
|
|
106
|
-
const workspaceGit = await projectGit(process.env.MAC_MCP_PROJECT_ROOT || root);
|
|
107
|
-
const errors = [["launchctl", jobs], ["disabled", disables],
|
|
108
|
-
["lsof", listeners], ["system-worker", worker], ["Tunnel plist", plist]]
|
|
109
|
-
.filter(([, value]) => !value.ok).map(([name]) => name + " unavailable");
|
|
110
|
-
return { capturedAt: new Date().toISOString(), host: "Mac local (loopback only)",
|
|
111
|
-
mode: "read-only", tunnelBackend: plist.ok ? tunnelTarget(plist.text) : "unknown",
|
|
112
|
-
modules, git, workspaceGit, errors,
|
|
113
|
-
milestones: [
|
|
114
|
-
{ name: "P2 ChatGPT Workspace", status: "accepted", detail: "7/7 real DC calls; read/write/edit and .git denial" },
|
|
115
|
-
{ name: "P3 Operations Dashboard", status: "active", detail: "Current service/port monitor on existing 8793" },
|
|
116
|
-
{ name: "P3 Development Workflow", status: "limited-live", detail: "Git status / diff / npm smoke via real DC; Git commit approval pending" },
|
|
117
|
-
{ name: "Direct URL deployment", status: "paused", detail: "Caddy disabled; HTTP ingress not public" }
|
|
118
|
-
] };
|
|
2
|
+
return { capturedAt: new Date().toISOString(), host: process.platform, architecture: process.arch, node: process.version, pid: process.pid };
|
|
119
3
|
}
|
|
120
4
|
const esc = (x) => String(x ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
|
121
|
-
function stateLabel(x) {
|
|
122
|
-
return { running: "运行中", disabled: "已禁用", stopped: "已停止", unknown: "未能确定" }[x] || x;
|
|
123
|
-
}
|
|
124
|
-
function phaseLabel(x) {
|
|
125
|
-
return { active: "主线", standby: "回退", legacy: "历史", paused: "暂停", research: "研究" }[x] || x;
|
|
126
|
-
}
|
|
127
5
|
export function renderOperationsHtml(data) {
|
|
128
6
|
if (data.instance) {
|
|
129
7
|
const i = data.instance;
|
|
@@ -134,61 +12,7 @@ export function renderOperationsHtml(data) {
|
|
|
134
12
|
<section><h2>Manage this instance</h2><nav><a href="/access">Credentials and OAuth sessions</a>${data.permissionManagementEnabled ? '<a href="/permissions">Workspace permissions</a>' : ''}${data.denyRuleManagementEnabled ? '<a href="/filters">Tool and directory filters</a>' : ''}</nav><p>Use the App or CLI to edit configuration, apply changes, inspect logs or stop this instance.</p></section>
|
|
135
13
|
<section><h2>Additional upstreams</h2>${(data.configuredUpstreams ?? []).map(u => `<p>${esc(u.id)} · ${esc(u.status)} · ${u.toolCount} tools</p>`).join('') || '<p>No additional upstreams configured.</p>'}</section><small>Owner-local management · <a href="/dashboard.json">JSON status</a></small></main></body></html>`;
|
|
136
14
|
}
|
|
137
|
-
|
|
138
|
-
const livePorts = data.modules.flatMap(x => x.listeners).map(x => x.port);
|
|
139
|
-
const expected = data.modules.filter(x => x.phase === "active" && x.status !== "running");
|
|
140
|
-
const tr = data.modules.map(m => `<tr><td><strong>${esc(m.label)}</strong><small>${esc(m.role)}</small></td><td><span class="pill ${esc(m.status)}">${esc(stateLabel(m.status))}</span></td><td><span class="phase">${esc(phaseLabel(m.phase))}</span></td><td class="mono">${esc(m.ports.length ? m.ports.join(" / ") : "Unix Socket")}</td><td class="mono">${esc(m.listeners.length ? m.listeners.map(x => x.port + " · PID " + x.pid).join(", ") : m.status === "running" ? "-" : "未监听")}</td></tr>`).join("");
|
|
141
|
-
const additional = data.configuredUpstreams || [];
|
|
142
|
-
const upstreamRows = additional.map(u => "<tr><td><strong>" + esc(u.id) + "</strong></td><td>" +
|
|
143
|
-
esc(u.status) + "</td><td>" + esc(u.toolCount) + "</td><td>" +
|
|
144
|
-
esc(u.lastError || "—") + "</td></tr>").join("");
|
|
145
|
-
const upstreamPanel = additional.length ?
|
|
146
|
-
'<section class="panel"><h2>Configured MCP Upstreams</h2><table><thead><tr><th>上游</th><th>当前状态</th><th>已发现工具</th><th>错误代码</th></tr></thead><tbody>' + upstreamRows + '</tbody></table></section>' : '';
|
|
147
|
-
const milestones = data.milestones.map(x => `<li><span class="step">${esc(x.status)}</span><div><b>${esc(x.name)}</b><p>${esc(x.detail)}</p></div></li>`).join("");
|
|
148
|
-
const messages = data.errors.length ? [...data.errors] : [];
|
|
149
|
-
if (expected.length)
|
|
150
|
-
messages.push(expected.length + " 个主线服务不在运行");
|
|
151
|
-
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="refresh" content="25"><title>Starmux · Operations</title><style>
|
|
152
|
-
:root{color-scheme:light;font-family:system-ui,-apple-system,"PingFang SC","Microsoft Yahei",sans-serif}
|
|
153
|
-
*{box-sizing:border-box}body{margin:0;background:#f4f7fb;color:#142138}
|
|
154
|
-
main{max-width:1220px;margin:0 auto;padding:34px 25px 64px}
|
|
155
|
-
header{display:flex;align-items:flex-end;justify-content:space-between;gap:22px;margin-bottom:27px;flex-wrap:wrap}
|
|
156
|
-
h1{font-size:26px;margin:0 0 8px}h2{font-size:16px;margin:0 0 17px}p{line-height:1.55}
|
|
157
|
-
.subtitle{color:#67748b;margin:0}.time{color:#697890;font-size:12px}
|
|
158
|
-
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:14px;margin:0 0 24px}
|
|
159
|
-
.stat,.panel{background:white;border:1px solid #e0e7f0;border-radius:13px;padding:18px}
|
|
160
|
-
.stat small{color:#6b7b93;display:block;margin-bottom:9px}.stat strong{font-size:23px}
|
|
161
|
-
.stat strong.short{font-size:16px}.status-good{color:#137a53}
|
|
162
|
-
.grid{display:grid;grid-template-columns:2fr 1fr;gap:18px}.panel{margin-bottom:18px;overflow:auto}
|
|
163
|
-
table{border-collapse:collapse;width:100%;font-size:13px}th{text-align:left;background:#f7f9fc;color:#687991;font-weight:600}
|
|
164
|
-
td,th{padding:13px 11px;border-bottom:1px solid #e9eef6;vertical-align:middle}tr:last-child td{border:0}
|
|
165
|
-
td small{display:block;color:#718096;margin-top:5px}.pill{padding:5px 9px;border-radius:8px;background:#f2f4f7;font-size:12px;white-space:nowrap}
|
|
166
|
-
.pill.running{color:#0c7451;background:#ddf6e9}.pill.disabled{color:#9a5409;background:#fff1d9}
|
|
167
|
-
.pill.unknown{color:#aa251b;background:#ffefee}.phase{font-size:12px;color:#62728a}
|
|
168
|
-
.mono{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;white-space:nowrap}
|
|
169
|
-
ul{list-style:none;margin:0;padding:0}li{display:flex;gap:12px;padding:0 0 20px}li:last-child{padding-bottom:0}
|
|
170
|
-
li p{margin:5px 0 0;color:#718096;font-size:12px}
|
|
171
|
-
.step{background:#e9f0ff;color:#2755ad;border-radius:6px;padding:6px;align-self:flex-start;text-transform:uppercase;font-size:10px}
|
|
172
|
-
.notice{border-left:3px solid #7598d6;background:#f0f5ff;padding:11px;color:#425b88;font-size:12px;margin-top:20px}
|
|
173
|
-
footer{color:#8291a3;font-size:12px;padding:10px}
|
|
174
|
-
@media(max-width:950px){.stats{grid-template-columns:repeat(2,1fr)}.grid{grid-template-columns:1fr}}
|
|
175
|
-
@media(max-width:560px){main{padding:19px 10px}.stats{grid-template-columns:1fr 1fr}th,td{padding:10px 7px}}
|
|
176
|
-
</style></head><body><main><header><div><h1>Starmux <span style="color:#4777c6">/ Operations</span></h1><p class="subtitle">运行资产 · 端口占用 · 开发里程碑 · 双入口状态</p></div><div class="time">采样 ${esc(data.capturedAt)}<br>本机只读 · 每 25 秒刷新</div></header>
|
|
177
|
-
<section class="stats"><div class="stat"><small>当前在线服务</small><strong class="status-good">${active} / ${data.modules.length}</strong></div>
|
|
178
|
-
<div class="stat"><small>正在监听的项目端口</small><strong>${new Set(livePorts).size}</strong></div>
|
|
179
|
-
<div class="stat"><small>Private Tunnel 目标</small><strong class="short">${esc(data.tunnelBackend)}</strong></div>
|
|
180
|
-
<div class="stat"><small>正在运行的代码</small><strong class="short">${esc(data.git.branch)} · ${esc(data.git.commit)}</strong><small>${data.git.dirtyCount === null ? "状态未知" : data.git.dirtyCount + " 个未提交变更"}</small></div>
|
|
181
|
-
<div class="stat"><small>实际授权项目 Git</small><strong class="short">${esc(data.workspaceGit.branch)} · ${esc(data.workspaceGit.commit)}</strong><small>${data.workspaceGit.dirtyCount === null ? "状态未知" : data.workspaceGit.dirtyCount + " 个未提交变更"}</small></div></section>
|
|
182
|
-
<div class="grid"><section class="panel"><h2>服务运行清单</h2><table><thead><tr><th>模块</th><th>系统状态</th><th>研发定位</th><th>登记端口</th><th>实际监听</th></tr></thead><tbody>${tr}</tbody></table></section>
|
|
183
|
-
<section class="panel"><h2>里程碑与开发进度</h2><ul>${milestones}</ul><div class="notice">该面板只观察,不执行任何启动、停用、Git、Shell 命令。全量 DC 工具可发现 ≠ 全部已授权。</div></section></div>
|
|
184
|
-
${upstreamPanel}
|
|
185
|
-
${data.denyRuleManagementEnabled ? '<section class="panel"><h2>Tool Filters · Explicit Deny</h2><p>所有上游 MCP 工具默认允许。所有者可以禁用指定工具或添加非沙箱型目录过滤。<a href="/filters">管理黑名单</a></p></section>' : ''}
|
|
186
|
-
${data.permissionManagementEnabled ? '<section class="panel"><h2>Workspace Permissions</h2><p>' +
|
|
187
|
-
(data.nativeAccessEnabled ? '当前为可信开发原生透传:Home/自定义目录授权开关不限制原生文件及 Shell 工具;由本地 Desktop Commander 与 macOS 用户权限控制。' : '当前支持 Home 与 Home 外只读扫描、自定义目录读写授权。') +
|
|
188
|
-
' <a href="/permissions">查看目录授权与说明</a></p></section>' : ''}
|
|
189
|
-
${messages.length ? `<section class="panel"><h2>采集警告</h2><p>${esc(messages.join("; "))}</p></section>` : ""}
|
|
190
|
-
<section><h2>Access control</h2><p><a href="/access">Manage authentication and client credentials</a></p><p>Credentials use the owner permission configuration. Network access and tool permissions are managed separately.</p></section>
|
|
191
|
-
<footer>JSON 数据接口:/dashboard.json · 不另占用端口 · 127.0.0.1 本机访问</footer></main></body></html>`;
|
|
15
|
+
return '<!doctype html><html lang="en"><meta charset="utf-8"><title>Starmux</title><body><h1>Starmux</h1><p>No instance context is available.</p></body></html>';
|
|
192
16
|
}
|
|
193
17
|
export const dashboardHeaders = Object.freeze({
|
|
194
18
|
"cache-control": "no-store", "x-content-type-options": "nosniff",
|
|
@@ -197,4 +21,3 @@ export const dashboardHeaders = Object.freeze({
|
|
|
197
21
|
export function instancePage(html, name, id) {
|
|
198
22
|
return id ? html.replace(/<body([^>]*)>/, `<body$1><aside style="padding:12px 24px;border-bottom:1px solid #ddd">Gateway instance: <strong>${esc(name ?? id)}</strong> · <small>${esc(id)}</small> · <a href="/dashboard">Overview</a></aside>`) : html;
|
|
199
23
|
}
|
|
200
|
-
//# sourceMappingURL=ops-dashboard.js.map
|
|
@@ -29,13 +29,13 @@ export function renderPermissionDashboard({ project, home, config, warning = "",
|
|
|
29
29
|
'input,select,button{font:inherit;padding:6px;margin:4px}input[name=target]{width:90%;max-width:550px}' +
|
|
30
30
|
'small{display:block;color:#657083}code{overflow-wrap:anywhere}button{cursor:pointer}</style></head><body>' +
|
|
31
31
|
'<h1>Starmux · 目录授权</h1>' +
|
|
32
|
-
'<p
|
|
32
|
+
'<p>本页在本机管理 Gateway 权限。远程 AI 不可自行增加权限。' +
|
|
33
33
|
'增加或撤销授权后,下一次工具调用立即使用新权限。</p>' +
|
|
34
34
|
(warning ? '<section role="alert">' + e(warning) + '</section>' : "") +
|
|
35
35
|
(nativeAccessEnabled ? '<section role="alert"><h2>可信开发模式 · 完整本地执行权限</h2>' +
|
|
36
36
|
'<p>此设备所有者启用了原生 Desktop Commander 工具透传。' +
|
|
37
37
|
'以下 Home/外部/自定义目录开关<strong>不限制原生 DC 文件和 Shell 工具</strong>;' +
|
|
38
|
-
'可访问范围由 Desktop Commander
|
|
38
|
+
'可访问范围由 Desktop Commander 和操作系统用户账户决定。' +
|
|
39
39
|
'开关仅在切回受限工作区模式后生效。请勿将此服务暴露给未经认证的客户端。</p>' +
|
|
40
40
|
'</section>' : "") +
|
|
41
41
|
'<section><h2>工作目录 · 读写范围</h2><code>' + e(project.path) + '</code>' +
|
|
@@ -58,7 +58,7 @@ export function renderPermissionDashboard({ project, home, config, warning = "",
|
|
|
58
58
|
'<button type="submit">确认授权</button></form></section>' +
|
|
59
59
|
'<p><a href="/dashboard">返回运行 Dashboard</a> · ' +
|
|
60
60
|
'<a href="/filters">工具黑名单</a></p>' +
|
|
61
|
-
'<small
|
|
61
|
+
'<small>授权管理密钥仅在本地终端显示;不可交给远程 Agent。' +
|
|
62
62
|
'本页不改变 trusted-project 的同 UID Shell 风险。</small></body></html>';
|
|
63
63
|
}
|
|
64
64
|
export async function applyPermissionPost(req, grants, host) {
|
|
@@ -94,4 +94,3 @@ export async function applyPermissionPost(req, grants, host) {
|
|
|
94
94
|
return { status: 400, message: "Grant rejected: verify directory and permissions" };
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
//# sourceMappingURL=permissions-dashboard.js.map
|
package/dist/src/platform.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Host integration for the shared CLI
|
|
1
|
+
// Host integration for the shared CLI and native App.
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
3
|
import { lstatSync, mkdirSync } from 'node:fs';
|
|
4
4
|
import { homedir, tmpdir } from 'node:os';
|
|
@@ -60,9 +60,9 @@ export function createPrivateDirectory(folder) {
|
|
|
60
60
|
}
|
|
61
61
|
export function controlRoot() {
|
|
62
62
|
if (process.platform === 'darwin')
|
|
63
|
-
return `/private/tmp/
|
|
63
|
+
return `/private/tmp/starmux-${process.getuid()}`;
|
|
64
64
|
const id = windows ? createHash('sha256').update(homedir().toLowerCase()).digest('hex').slice(0, 16) : String(process.getuid());
|
|
65
|
-
return path.join(tmpdir(), `
|
|
65
|
+
return path.join(tmpdir(), `starmux-${id}`);
|
|
66
66
|
}
|
|
67
67
|
export function within(candidate, root) {
|
|
68
68
|
const relative = path.relative(root, candidate);
|
|
@@ -99,4 +99,3 @@ export function diskUsageCommand(target, depth) {
|
|
|
99
99
|
};` : ''}
|
|
100
100
|
'{0} {1}' -f [math]::Ceiling((Measure-Tree $root)/1024),$root.FullName`;
|
|
101
101
|
}
|
|
102
|
-
//# sourceMappingURL=platform.js.map
|
|
@@ -152,4 +152,3 @@ export async function privacyReport({ port, home = os.homedir(), all = false, ta
|
|
|
152
152
|
note: "This tests actual directory access, not the TCC database or the responsible app identity. launchd and Termius may differ. No permission is changed."
|
|
153
153
|
};
|
|
154
154
|
}
|
|
155
|
-
//# sourceMappingURL=privacy-diagnostics.js.map
|
|
@@ -6,6 +6,7 @@ import { lstat, realpath } from "node:fs/promises";
|
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { createDevPresets } from "./project-dev-presets.js";
|
|
9
|
+
import { prepareBatchRead } from "./batch-read-policy.js";
|
|
9
10
|
export class WorkspaceDenied extends Error {
|
|
10
11
|
code;
|
|
11
12
|
constructor(code) {
|
|
@@ -24,14 +25,14 @@ function keysExact(args, required, optional = []) {
|
|
|
24
25
|
Object.keys(args).some(k => ![...required, ...optional].includes(k)))
|
|
25
26
|
fail("ARGUMENTS_NOT_ALLOWED");
|
|
26
27
|
}
|
|
27
|
-
async function checked(target, kind, bytes) {
|
|
28
|
+
async function checked(target, kind, bytes, allowMissing = false) {
|
|
28
29
|
const st = await lstat(target).catch(e => {
|
|
29
30
|
if (errorCode(e) === "ENOENT")
|
|
30
31
|
return null;
|
|
31
32
|
throw e;
|
|
32
33
|
});
|
|
33
34
|
if (!st) {
|
|
34
|
-
if (kind !== "new")
|
|
35
|
+
if (kind !== "new" && !allowMissing)
|
|
35
36
|
fail("NOT_FOUND");
|
|
36
37
|
return;
|
|
37
38
|
}
|
|
@@ -68,7 +69,7 @@ export async function createProjectPolicy({ root, mode = "read-only", developerC
|
|
|
68
69
|
fail("ROOT_MUST_BE_CANONICAL");
|
|
69
70
|
await checked(base, "directory");
|
|
70
71
|
const devPresets = createDevPresets(base, mode);
|
|
71
|
-
async function scoped(raw, kind, bytes) {
|
|
72
|
+
async function scoped(raw, kind, bytes, allowMissing = false) {
|
|
72
73
|
if (typeof raw !== "string" || !raw || raw.length > 1024 ||
|
|
73
74
|
raw.includes("\0") || (!windows && raw.includes("\\")) ||
|
|
74
75
|
raw.startsWith("~") || /^[a-z]+:\/\//i.test(raw))
|
|
@@ -84,7 +85,7 @@ export async function createProjectPolicy({ root, mode = "read-only", developerC
|
|
|
84
85
|
let current = base;
|
|
85
86
|
for (let i = 0; i < parts.length; i++) {
|
|
86
87
|
current = path.join(current, parts[i]);
|
|
87
|
-
await checked(current, i === parts.length - 1 ? kind : "directory", i === parts.length - 1 ? bytes : undefined);
|
|
88
|
+
await checked(current, i === parts.length - 1 ? kind : "directory", i === parts.length - 1 ? bytes : undefined, allowMissing);
|
|
88
89
|
}
|
|
89
90
|
if (parts.length === 0 && kind !== "directory")
|
|
90
91
|
fail("ROOT_IS_DIRECTORY");
|
|
@@ -98,6 +99,8 @@ export async function createProjectPolicy({ root, mode = "read-only", developerC
|
|
|
98
99
|
},
|
|
99
100
|
observe(name, result, args) { devPresets.observe(name, result, args); },
|
|
100
101
|
async prepare(name, args = {}) {
|
|
102
|
+
if (name === "read_multiple_files")
|
|
103
|
+
return prepareBatchRead(args, (tool, values) => this.prepare(tool, values));
|
|
101
104
|
if (name === "start_process" && developerCommands === "trusted-project") {
|
|
102
105
|
keysExact(args, ["command", "timeout_ms"]);
|
|
103
106
|
if (typeof args.command !== "string" || !args.command.trim() ||
|
|
@@ -129,7 +132,9 @@ export async function createProjectPolicy({ root, mode = "read-only", developerC
|
|
|
129
132
|
fail("OFFSET_LIMIT");
|
|
130
133
|
if (args.length !== undefined && (!isInteger(args.length) || args.length < 1 || args.length > 1000))
|
|
131
134
|
fail("LENGTH_LIMIT");
|
|
132
|
-
|
|
135
|
+
// Missing files are an upstream read result, not a permission denial.
|
|
136
|
+
// Existing ancestors still undergo the same owner/link/scope checks.
|
|
137
|
+
return { name, arguments: { path: await scoped(args.path, "file", maxReadBytes, true),
|
|
133
138
|
offset: args.offset ?? 0, length: args.length ?? 1000 } };
|
|
134
139
|
}
|
|
135
140
|
if (mode !== "read-write")
|
|
@@ -158,4 +163,3 @@ export async function createProjectPolicy({ root, mode = "read-only", developerC
|
|
|
158
163
|
}
|
|
159
164
|
});
|
|
160
165
|
}
|
|
161
|
-
//# sourceMappingURL=project-workspace-policy.js.map
|
|
@@ -25,17 +25,3 @@ export async function runtimeIdentity() {
|
|
|
25
25
|
return { coreProgram: path.join(root, 'dist', 'src', 'dc-workspace-http.js'),
|
|
26
26
|
buildVersion: `${pkg.version} (sha256:${hash.digest('hex').slice(0, 16)})` };
|
|
27
27
|
}
|
|
28
|
-
export function formatStartup(details) {
|
|
29
|
-
const row = (name, value) => ` ${name.padEnd(17)}${value}`;
|
|
30
|
-
return [
|
|
31
|
-
'', ' Mac MCP Gateway · Starting', ' ' + '─'.repeat(64),
|
|
32
|
-
row('Config source', details.configSource),
|
|
33
|
-
row('Core program', details.coreProgram),
|
|
34
|
-
row('Build version', details.buildVersion),
|
|
35
|
-
row('Listening port', String(details.port) + ' (127.0.0.1)'),
|
|
36
|
-
row('Endpoint', `http://127.0.0.1:${details.port}/mcp`),
|
|
37
|
-
' ' + '─'.repeat(64),
|
|
38
|
-
' Starting the Core bundled with this CLI. Press Ctrl+C to stop.', ''
|
|
39
|
-
].join('\n') + '\n';
|
|
40
|
-
}
|
|
41
|
-
//# sourceMappingURL=runtime-identity.js.map
|
|
@@ -6,6 +6,3 @@ export const cliEntry = (name = 'starmux') => path.join(packageRoot, 'bin', name
|
|
|
6
6
|
// Vendored zero-telemetry build of Desktop Commander bundled inside dist/vendor.
|
|
7
7
|
export const desktopCommanderEntry = path.join(packageRoot, 'dist', 'vendor', 'desktop-commander', 'dist', 'index.js');
|
|
8
8
|
export const desktopCommanderPackageJson = path.join(packageRoot, 'dist', 'vendor', 'desktop-commander', 'package.json');
|
|
9
|
-
export const fixtureRoot = path.join(packageRoot, 'test', 'fixtures');
|
|
10
|
-
export const legacyCliEntry = () => path.join(packageRoot, 'dist/src/cli/mac-mcp-gateway.js');
|
|
11
|
-
//# sourceMappingURL=runtime-paths.js.map
|