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.
Files changed (144) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +20 -30
  3. package/dist/src/access-control.js +1 -2
  4. package/dist/src/access-dashboard.js +2 -3
  5. package/dist/src/account-access-policy.js +54 -0
  6. package/dist/src/authorized-workspace-roots.js +0 -1
  7. package/dist/src/batch-read-policy.js +30 -0
  8. package/dist/src/cli/instances.js +6 -175
  9. package/dist/src/cli/oauth.js +0 -1
  10. package/dist/src/cli/starmux-status.js +10 -14
  11. package/dist/src/cli/starmux-supervisor.js +0 -1
  12. package/dist/src/cli/starmux.js +13 -25
  13. package/dist/src/config-schemas.js +0 -9
  14. package/dist/src/configured-mcp-upstreams.js +0 -1
  15. package/dist/src/contracts.js +0 -1
  16. package/dist/src/current-permission-probe.js +0 -1
  17. package/dist/src/dc-workspace-http.js +57 -33
  18. package/dist/src/deny-dashboard.js +2 -3
  19. package/dist/src/desktop-commander-facade.js +4 -4
  20. package/dist/src/explicit-deny-rules.js +2 -3
  21. package/dist/src/global-core-control.js +2 -3
  22. package/dist/src/home-write-sandbox.js +0 -1
  23. package/dist/src/http-host-policy.js +20 -0
  24. package/dist/src/instance-runtime.js +3 -3
  25. package/dist/src/instance-store.js +28 -22
  26. package/dist/src/instance-worker.js +2 -3
  27. package/dist/src/invocation.js +1 -2
  28. package/dist/src/local-desktop-read-probe.js +5 -88
  29. package/dist/src/managed-workspace-policy.js +38 -4
  30. package/dist/src/mcp-results.js +0 -1
  31. package/dist/src/mcp-upstream-router.js +0 -1
  32. package/dist/src/oauth-keycloak.js +0 -1
  33. package/dist/src/oauth-resource.js +0 -1
  34. package/dist/src/ops-dashboard.js +2 -179
  35. package/dist/src/permissions-dashboard.js +3 -4
  36. package/dist/src/platform.js +3 -4
  37. package/dist/src/privacy-diagnostics.js +0 -1
  38. package/dist/src/private-mcp-activity.js +0 -1
  39. package/dist/src/private-store-lock.js +0 -1
  40. package/dist/src/project-dev-presets.js +0 -1
  41. package/dist/src/project-git-workflow.js +0 -1
  42. package/dist/src/project-workspace-policy.js +10 -6
  43. package/dist/src/runtime-identity.js +0 -14
  44. package/dist/src/runtime-paths.js +0 -3
  45. package/dist/src/tool-dispatch.js +45 -46
  46. package/dist/src/validation.js +0 -1
  47. package/dist/src/workspace-grants.js +1 -2
  48. package/package.json +13 -17
  49. package/scripts/prepare.mjs +1 -1
  50. package/CONTRIBUTING.md +0 -18
  51. package/SECURITY.md +0 -7
  52. package/app/README.md +0 -29
  53. package/dist/src/access-control.js.map +0 -1
  54. package/dist/src/access-dashboard.js.map +0 -1
  55. package/dist/src/authorized-workspace-roots.js.map +0 -1
  56. package/dist/src/cli/access.js +0 -53
  57. package/dist/src/cli/access.js.map +0 -1
  58. package/dist/src/cli/community-remote-revoke.js +0 -32
  59. package/dist/src/cli/community-remote-revoke.js.map +0 -1
  60. package/dist/src/cli/instances.js.map +0 -1
  61. package/dist/src/cli/live-home-settings.js +0 -35
  62. package/dist/src/cli/live-home-settings.js.map +0 -1
  63. package/dist/src/cli/mac-mcp-gateway.js +0 -257
  64. package/dist/src/cli/mac-mcp-gateway.js.map +0 -1
  65. package/dist/src/cli/oauth.js.map +0 -1
  66. package/dist/src/cli/permissions-owner.js +0 -27
  67. package/dist/src/cli/permissions-owner.js.map +0 -1
  68. package/dist/src/cli/service-setup.js +0 -26
  69. package/dist/src/cli/service-setup.js.map +0 -1
  70. package/dist/src/cli/setup-wizard.js +0 -149
  71. package/dist/src/cli/setup-wizard.js.map +0 -1
  72. package/dist/src/cli/starmux-status.js.map +0 -1
  73. package/dist/src/cli/starmux-supervisor.js.map +0 -1
  74. package/dist/src/cli/starmux.js.map +0 -1
  75. package/dist/src/cli/verify-setup.js +0 -53
  76. package/dist/src/cli/verify-setup.js.map +0 -1
  77. package/dist/src/community-remote-revocations.js +0 -135
  78. package/dist/src/community-remote-revocations.js.map +0 -1
  79. package/dist/src/config-schemas.js.map +0 -1
  80. package/dist/src/configured-mcp-upstreams.js.map +0 -1
  81. package/dist/src/contracts.js.map +0 -1
  82. package/dist/src/current-permission-probe.js.map +0 -1
  83. package/dist/src/dc-workspace-http.js.map +0 -1
  84. package/dist/src/deny-dashboard.js.map +0 -1
  85. package/dist/src/desktop-commander-facade.js.map +0 -1
  86. package/dist/src/explicit-deny-rules.js.map +0 -1
  87. package/dist/src/gateway-status-format.js +0 -66
  88. package/dist/src/gateway-status-format.js.map +0 -1
  89. package/dist/src/global-core-control.js.map +0 -1
  90. package/dist/src/home-write-sandbox.js.map +0 -1
  91. package/dist/src/instance-migration.js +0 -138
  92. package/dist/src/instance-migration.js.map +0 -1
  93. package/dist/src/instance-runtime.js.map +0 -1
  94. package/dist/src/instance-store.js.map +0 -1
  95. package/dist/src/instance-worker.js.map +0 -1
  96. package/dist/src/invocation.js.map +0 -1
  97. package/dist/src/live-profile-home-settings.js +0 -80
  98. package/dist/src/live-profile-home-settings.js.map +0 -1
  99. package/dist/src/local-desktop-read-probe.js.map +0 -1
  100. package/dist/src/managed-workspace-policy.js.map +0 -1
  101. package/dist/src/mcp-results.js.map +0 -1
  102. package/dist/src/mcp-upstream-router.js.map +0 -1
  103. package/dist/src/oauth-keycloak.js.map +0 -1
  104. package/dist/src/oauth-resource.js.map +0 -1
  105. package/dist/src/operator-cli.js +0 -559
  106. package/dist/src/operator-cli.js.map +0 -1
  107. package/dist/src/ops-dashboard.js.map +0 -1
  108. package/dist/src/permissions-dashboard.js.map +0 -1
  109. package/dist/src/platform.js.map +0 -1
  110. package/dist/src/privacy-diagnostics.js.map +0 -1
  111. package/dist/src/private-mcp-activity.js.map +0 -1
  112. package/dist/src/private-store-lock.js.map +0 -1
  113. package/dist/src/project-dev-presets.js.map +0 -1
  114. package/dist/src/project-git-workflow.js.map +0 -1
  115. package/dist/src/project-workspace-policy.js.map +0 -1
  116. package/dist/src/read-scope.js +0 -44
  117. package/dist/src/read-scope.js.map +0 -1
  118. package/dist/src/release1-onboarding.js +0 -120
  119. package/dist/src/release1-onboarding.js.map +0 -1
  120. package/dist/src/release1-service.js +0 -131
  121. package/dist/src/release1-service.js.map +0 -1
  122. package/dist/src/remote-auth.js +0 -47
  123. package/dist/src/remote-auth.js.map +0 -1
  124. package/dist/src/runtime-identity.js.map +0 -1
  125. package/dist/src/runtime-paths.js.map +0 -1
  126. package/dist/src/starmux-migration.js +0 -172
  127. package/dist/src/starmux-migration.js.map +0 -1
  128. package/dist/src/tool-dispatch.js.map +0 -1
  129. package/dist/src/validation.js.map +0 -1
  130. package/dist/src/workspace-grants.js.map +0 -1
  131. package/docs/ACCESS_CONTROL.md +0 -51
  132. package/docs/CLIENTS.md +0 -22
  133. package/docs/CONNECT_CHATGPT.md +0 -148
  134. package/docs/INSTALL.md +0 -62
  135. package/docs/INSTANCES.md +0 -83
  136. package/docs/OAUTH.md +0 -116
  137. package/docs/PLATFORMS.md +0 -29
  138. package/docs/README.md +0 -16
  139. package/docs/REMOTE_SETUP.md +0 -280
  140. package/docs/SECURITY.md +0 -15
  141. package/docs/assets/starmux-app-en.png +0 -0
  142. package/docs/assets/starmux-architecture.svg +0 -315
  143. package/src/dc-workspace-http.mjs +0 -3
  144. package/test/fixtures/p1-fixture.txt +0 -2
@@ -1,23 +1,55 @@
1
1
  import { authorizeInvocation, createInvocationExecutor, observeSafely } from './invocation.js';
2
2
  import { errorCode } from './validation.js';
3
+ import { isReadOnlyTool } from './account-access-policy.js';
4
+ function workspaceErrorText(error, hasGrants) {
5
+ const code = errorCode(error);
6
+ switch (code) {
7
+ case 'BATCH_READ_COUNT_LIMIT':
8
+ return 'BATCH_READ_COUNT_LIMIT: read_multiple_files accepts at most 20 files per request. Split the request into smaller batches.';
9
+ case 'BATCH_READ_SIZE_LIMIT':
10
+ return 'BATCH_READ_SIZE_LIMIT: Combined file sizes exceed 4 MiB (4194304 bytes). Split the request into smaller batches.';
11
+ case 'BATCH_READ_ARGUMENTS_DENIED':
12
+ return 'BATCH_READ_ARGUMENTS_DENIED: Provide only a non-empty paths array of file path strings.';
13
+ case 'FILE_TOO_LARGE':
14
+ return 'FILE_TOO_LARGE: A file exceeds the per-file read size limit (1 MiB).';
15
+ case 'INVALID_PATH':
16
+ return 'INVALID_PATH: Provide a non-empty file path.';
17
+ case 'NOT_FOUND':
18
+ case 'ENOENT':
19
+ return 'ENOENT: no such file or directory';
20
+ case 'NOT_DIRECTORY':
21
+ case 'ENOTDIR':
22
+ return 'ENOTDIR: a path component is not a directory';
23
+ case 'NOT_FILE':
24
+ return 'NOT_FILE: The read target must be a regular file.';
25
+ case 'EACCES':
26
+ case 'EPERM':
27
+ return `${code}: The operating system denied access to the path.`;
28
+ default:
29
+ return hasGrants ? 'WORKSPACE_DENIED: Review directory grants on your local Dashboard /permissions' : 'WORKSPACE_DENIED';
30
+ }
31
+ }
3
32
  // The dispatch order is part of the compatibility contract: Gateway tools,
4
- // extension routes, owner-enabled native mode, workspace policy, then fixtures.
33
+ // extension routes, owner-enabled native mode, workspace policy, then deny.
5
34
  export function createToolDispatcher(upstream, audit, options) {
6
35
  const execute = createInvocationExecutor(upstream);
7
36
  return async (params, context) => {
8
37
  const id = context.requestId;
9
38
  const name = params.name;
10
39
  const args = params.arguments || {};
40
+ if (options.globalReadOnly && !isReadOnlyTool(name)) {
41
+ audit({ event: 'DC_BRIDGE_DENIED', requestId: id, tool: name, reason: 'READ_ONLY_MODE' });
42
+ return { isError: true, content: [{ type: 'text', text: "READ_ONLY_MODE: This operation requires Coding mode. Run 'starmux up --code' to enable edits & shell." }] };
43
+ }
11
44
  const observe = (fn) => observeSafely(fn, audit, context);
12
45
  if (name === "health_check") {
13
46
  audit({ event: "DC_BRIDGE_HEALTH_CHECK", requestId: id });
14
47
  return { content: [{ type: "text", text: JSON.stringify({
15
- status: "ok", service: "mac-mcp-gateway-dc-bridge",
48
+ status: "ok", service: "starmux-dc-bridge",
16
49
  upstream: "desktop-commander", tools: upstream.upstreamCatalog.length,
17
50
  routes: options.upstreamRouter?.routes || [{ id: "desktop", toolCount: upstream.upstreamCatalog.length }],
18
- workspaceMode: options.workspacePolicy?.mode || "fixed-fixture",
19
- developerCommands: options.workspacePolicy?.developerCommands || "fixed-fixture",
20
- nativeAccess: options.nativePassthrough === true,
51
+ mode: options.globalReadOnly ? 'read-only' : options.nativePassthrough ? 'coding' :
52
+ options.workspacePolicy?.mode || 'unconfigured',
21
53
  traceId: id
22
54
  }) }] };
23
55
  }
@@ -26,8 +58,9 @@ export function createToolDispatcher(upstream, audit, options) {
26
58
  const policy = options.workspacePolicy;
27
59
  audit({ event: "GATEWAY_CONFIG_READ", requestId: id });
28
60
  return { content: [{ type: "text", text: JSON.stringify({
29
- source: "mac-mcp-gateway", configType: "effective-workspace-summary",
61
+ source: "starmux", configType: "effective-workspace-summary",
30
62
  projectRoot: policy.root, workspaceMode: policy.mode,
63
+ permissionMode: options.permissionMode ?? 'workspace',
31
64
  developerCommands: policy.developerCommands,
32
65
  commandSafety: "Desktop Commander upstream blocklist; advisory, not OS sandbox",
33
66
  remoteConfigMutation: "owner-only",
@@ -38,7 +71,7 @@ export function createToolDispatcher(upstream, audit, options) {
38
71
  }
39
72
  if (name === "set_config_value" && options.workspacePolicy && !options.nativePassthrough) {
40
73
  audit({ event: "GATEWAY_CONFIG_MUTATION_DENIED", requestId: id });
41
- return { isError: true, content: [{ type: "text", text: "OWNER_ONLY_CONFIG: Remote AI cannot modify safety settings; use the Mac owner dashboard." }] };
74
+ return { isError: true, content: [{ type: "text", text: "OWNER_ONLY_CONFIG: Remote AI cannot modify safety settings; use the local owner dashboard." }] };
42
75
  }
43
76
  if (name === "gateway_disk_usage" && options.workspacePolicy?.prepareDiskUsage) {
44
77
  try {
@@ -56,7 +89,7 @@ export function createToolDispatcher(upstream, audit, options) {
56
89
  audit({ event: "GATEWAY_DISK_USAGE_DENIED", requestId: id,
57
90
  reason: errorCode(error) || "DISK_USAGE_ERROR" });
58
91
  return { isError: true, content: [{ type: "text", text: "WORKSPACE_DENIED: " +
59
- "Check directory grants on the Mac Dashboard /permissions" }] };
92
+ "Check directory grants on the local Dashboard /permissions" }] };
60
93
  }
61
94
  }
62
95
  if (options.gitWorkflow?.names.includes(name)) {
@@ -99,7 +132,7 @@ export function createToolDispatcher(upstream, audit, options) {
99
132
  }
100
133
  }
101
134
  // Deliberately trusted, owner-enabled native mode: the upstream server
102
- // and the macOS process account decide execution. Do not maintain a
135
+ // and the OS process account decide execution. Do not maintain a
103
136
  // second incomplete tool/path/argument whitelist here. Never use this
104
137
  // path for unrecognized names, routed extensions or Gateway tools.
105
138
  if (options.nativePassthrough &&
@@ -126,9 +159,7 @@ export function createToolDispatcher(upstream, audit, options) {
126
159
  catch (error) {
127
160
  audit({ event: "DC_BRIDGE_DENIED", requestId: id, tool: name,
128
161
  reason: errorCode(error) || "POLICY_ERROR" });
129
- return { isError: true, content: [{ type: "text", text: options.workspacePolicy.grantsSnapshot ?
130
- "WORKSPACE_DENIED: Review directory grants on your Mac Dashboard /permissions" :
131
- "WORKSPACE_DENIED" }] };
162
+ return { isError: true, content: [{ type: "text", text: workspaceErrorText(error, !!options.workspacePolicy.grantsSnapshot) }] };
132
163
  }
133
164
  audit({ event: "DC_BRIDGE_FORWARDED", requestId: id, tool: name });
134
165
  try {
@@ -144,39 +175,7 @@ export function createToolDispatcher(upstream, audit, options) {
144
175
  return { isError: true, content: [{ type: "text", text: "UPSTREAM_UNAVAILABLE" }] };
145
176
  }
146
177
  }
147
- const keys = Object.keys(args);
148
- const allowed = (name === "read_file" &&
149
- ["p1-fixture.txt", "dc-bridge-canary.txt", "README.md"].includes(String(args.path)) &&
150
- keys.length === 1 && keys[0] === "path") ||
151
- (name === "write_file" && args.path === "dc-bridge-canary.txt" &&
152
- typeof args.content === "string" && Buffer.byteLength(args.content) <= 256 &&
153
- keys.every(k => ["path", "content"].includes(k))) ||
154
- (name === "start_process" && args.command === "/usr/bin/uname -s" &&
155
- args.timeout_ms === 3000 &&
156
- keys.every(k => ["command", "timeout_ms"].includes(k)));
157
- if (!allowed) {
158
- audit({ event: "DC_BRIDGE_DENIED", requestId: id, tool: name });
159
- return { isError: true, content: [{ type: "text", text: "NOT_APPROVED_IN_P1_FIXTURE" }] };
160
- }
161
- audit({ event: "DC_BRIDGE_FORWARDED", requestId: id, tool: name });
162
- try {
163
- let response;
164
- if (name === "read_file" && args.path !== "README.md")
165
- response = await upstream.callApprovedRead(String(args.path));
166
- else if (name === "read_file")
167
- response = await upstream.callApprovedProjectRead();
168
- else if (name === "write_file")
169
- response = await upstream.callApprovedCanaryWrite(String(args.content));
170
- else
171
- response = await upstream.callApprovedSystemName();
172
- audit({ event: "DC_BRIDGE_COMPLETED", requestId: id, tool: name,
173
- isError: response.isError === true });
174
- return response;
175
- }
176
- catch {
177
- audit({ event: "DC_BRIDGE_UPSTREAM_FAILED", requestId: id, tool: name });
178
- return { isError: true, content: [{ type: "text", text: "UPSTREAM_FAILED" }] };
179
- }
178
+ audit({ event: "DC_BRIDGE_DENIED", requestId: id, tool: name, reason: "POLICY_REQUIRED" });
179
+ return { isError: true, content: [{ type: "text", text: "POLICY_REQUIRED: Configure an instance before using tools." }] };
180
180
  };
181
181
  }
182
- //# sourceMappingURL=tool-dispatch.js.map
@@ -32,4 +32,3 @@ export function errorMessage(error) {
32
32
  export function errorStderr(error) {
33
33
  return (error instanceof Error || isRecord(error)) && 'stderr' in error && typeof error.stderr === 'string' ? error.stderr : '';
34
34
  }
35
- //# sourceMappingURL=validation.js.map
@@ -10,7 +10,7 @@ import os from "node:os";
10
10
  import path from "node:path";
11
11
  function deny(s) { const e = new GatewayError(s); throw e; }
12
12
  ;
13
- export const defaultGrantsPath = (home = os.homedir()) => path.join(home, ".config", "mac-mcp-gateway", "workspace-grants-v1.json");
13
+ export const defaultGrantsPath = (home = os.homedir()) => path.join(home, ".config", "starmux", "workspace-grants-v1.json");
14
14
  const expected = new Set(["version", "homeRead", "outsideRead", "custom"]);
15
15
  export const isInside = within;
16
16
  async function safeStat(file, { directory = false, missing = false } = {}) {
@@ -156,4 +156,3 @@ export async function createWorkspaceGrants({ file = defaultGrantsPath(), home =
156
156
  await snapshot({ validatePaths: false });
157
157
  return Object.freeze({ file, tokenFile, home: canonicalHome, snapshot, authorized, update });
158
158
  }
159
- //# sourceMappingURL=workspace-grants.js.map
package/package.json CHANGED
@@ -1,8 +1,16 @@
1
1
  {
2
2
  "name": "starmux",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Connect AI clients to files, terminals and development tools on your own computer.",
5
5
  "license": "MIT",
6
+ "homepage": "https://github.com/cookyecat/starmux#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/cookyecat/starmux.git"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/cookyecat/starmux/issues"
13
+ },
6
14
  "type": "module",
7
15
  "engines": {
8
16
  "node": ">=22.12.0"
@@ -21,15 +29,11 @@
21
29
  "start:setup": "node bin/starmux.mjs up",
22
30
  "verify:setup": "node bin/starmux.mjs verify",
23
31
  "status": "node bin/starmux.mjs status",
24
- "remote:revoke": "node bin/community-remote-revoke.mjs",
25
- "test": "npm run build && node dist/test/run.js",
26
32
  "build": "node scripts/build.mjs",
27
33
  "typecheck": "tsc -p tsconfig.json --noEmit",
28
- "build:test": "npm run build",
29
34
  "prepare": "node scripts/prepare.mjs",
30
35
  "prepack": "npm run build",
31
- "sync:upstream": "npm install --save-dev --save-exact @wonderwhy-er/desktop-commander@latest --ignore-scripts && npm run build",
32
- "test:oauth:keycloak": "npm run build && node --test dist/test/keycloak.integration.js"
36
+ "sync:upstream": "npm install --save-dev --save-exact @wonderwhy-er/desktop-commander@latest --ignore-scripts && npm run build"
33
37
  },
34
38
  "dependencies": {
35
39
  "@modelcontextprotocol/sdk": "1.32.1",
@@ -54,19 +58,11 @@
54
58
  "typescript": "5.9.3"
55
59
  },
56
60
  "files": [
57
- "dist/src",
61
+ "dist/src/**/*.js",
58
62
  "dist/vendor",
59
- "src/dc-workspace-http.mjs",
63
+ "bin/starmux.mjs",
60
64
  "scripts/prepare.mjs",
61
- "test/fixtures/p1-fixture.txt",
62
65
  "LICENSE",
63
- "README.md",
64
- "CONTRIBUTING.md",
65
- "SECURITY.md",
66
- "bin/starmux.mjs",
67
- "docs/*.md",
68
- "docs/assets/*.svg",
69
- "docs/assets/starmux-app-en.png",
70
- "app/README.md"
66
+ "README.md"
71
67
  ]
72
68
  }
@@ -1,5 +1,5 @@
1
1
  // Published tarballs already contain dist; source installs build locally.
2
2
  import {existsSync} from 'node:fs';
3
- if (existsSync(new URL('../src/cli/mac-mcp-gateway.ts', import.meta.url))) {
3
+ if (existsSync(new URL('../src/cli/starmux.ts', import.meta.url))) {
4
4
  await import('./build.mjs');
5
5
  }
package/CONTRIBUTING.md DELETED
@@ -1,18 +0,0 @@
1
- # Contributing
2
-
3
- Thank you for helping build an independent, self-hosted MCP Gateway.
4
-
5
- - File reproducible issues with version, host OS and *sanitized* steps. Never share credentials, local IPs, Tunnel IDs, original MCP messages containing personal files or private logs.
6
- - Please open a discussion before large architectural changes. Keep local Core tests and authorization rules passing.
7
- - Fixes must preserve upstream Desktop Commander tool schemas and accurately separate local health from remote caller identity.
8
- - Feature ideas for programmable hooks, broader multi-upstream routing or multi-computer operation are welcome, but should be labeled **proposals** until independently implemented and tested.
9
-
10
- See [security](SECURITY.md) and [platform coverage](docs/PLATFORMS.md).
11
-
12
- ## TypeScript development
13
-
14
- Use Node 22.12 or newer. See [platform coverage](docs/PLATFORMS.md) before testing on another host. Run `npm ci`, `npm run typecheck`, and `npm test`. Tests execute compiled JavaScript and use temporary configuration directories and isolated ports. `npm run build` emits Core, CLI and tests to `dist/`; do not edit generated files. `npm pack` rebuilds and includes only the explicit runtime allowlist. No business logic belongs in the compatibility `.mjs` adapters.
15
-
16
- Keep `strict`, `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, `verbatimModuleSyntax` and `noEmitOnError` enabled. Treat JSON, CLI options, socket messages and environment input as unknown until checked. Use MCP SDK types for protocol objects; retain upstream metadata and extensions. Internal middleware changes must test refusal before execution and observer failure after execution, including tools/list versus tools/call consistency.
17
-
18
- The App builder installs locked build dependencies in a temporary staging directory, compiles there, then installs production dependencies in the bundle. Swift sources and the bundle launcher/helper paths are independent of this TypeScript build. Public signing and independent first-user acceptance remain separate release gates; remote authentication results are tracked per client and deployment in [client compatibility](docs/CLIENTS.md).
package/SECURITY.md DELETED
@@ -1,7 +0,0 @@
1
- # Security policy
2
-
3
- **Status:** Community preview, version 0.11.0. Optional Bearer API key and OAuth authentication with revocation are supported.
4
-
5
- Please **do not post security vulnerabilities, credentials or user data in public GitHub issues**. Use **GitHub Private Vulnerability Reporting** to report security issues privately.
6
-
7
- Do not expose the raw Gateway HTTP port (`127.0.0.1`) to the public internet without an authenticated reverse proxy or tunnel. Read [security and trust boundaries](docs/SECURITY.md).
package/app/README.md DELETED
@@ -1,29 +0,0 @@
1
- # App source (Apple Silicon)
2
-
3
- The Starmux menu bar App is written in Swift and controls the same local Core
4
- as the CLI. Its main panel shows the selected workspace, service health, permissions,
5
- connection topology and recent tool activity. Advanced instance management opens
6
- from **Workspaces & settings**; launch does not open a management window. The individual Swift sources and build script are included here,
7
- without historical internal research reports.
8
-
9
- **Build a UI-only preview (not a self-contained public App)**:
10
-
11
- ```bash
12
- python3 app/scripts/build_ui.py --output-dir /private/tmp/mac-mcp-ui-preview
13
- ```
14
-
15
- **Build a self-contained, ad-hoc signed developer candidate**:
16
-
17
- 1. Obtain the **official pinned** Node.js v26.10.0 darwin-arm64 tar.xz archive
18
- from nodejs.org, verifying its SHA-256 (enforced by the build script).
19
- 2. Run `npm ci`, then
20
- `python3 build/build_arm64_app.py --node-archive /path/to/node-v26.10.0-darwin-arm64.tar.xz --output /private/tmp/mac-mcp-app-build`.
21
- The builder creates a separate temporary TypeScript build directory, runs a locked development install and compilation, and copies `dist/src` plus the stable `.mjs` launchers into the App. It then installs only locked production dependencies. The bundled launcher continues to use the pinned internal Node executable; Swift sources and helper paths are unchanged.
22
-
23
- 3. Check codesign and test in a non-production macOS user profile.
24
-
25
- Ad-hoc signing is for developers. **No Developer ID / Apple notarization is
26
- provided**. Do not advertise this candidate as a notarized public binary;
27
- the App ZIP for GitHub Releases needs separate signing and end-user validation.
28
-
29
- For visual acceptance, launch the built executable with `--show-monitor` to open the same menu view in a window. Normal launch stays in the menu bar; reopening the App also shows the monitor.
@@ -1 +0,0 @@
1
- {"version":3,"file":"access-control.js","sourceRoot":"","sources":["../../src/access-control.ts"],"names":[],"mappings":"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","sourcesContent":["import {oauthConfigSchema,oauthConnectionSchema,validateOAuthConfig,createOAuthVerifier,OAuthError,type OAuthConfig} from './oauth-resource.js';\nimport {randomBytes,createHash,timingSafeEqual} from 'node:crypto';\nimport {lstat,readFile,writeFile,rename,rm,mkdir} from 'node:fs/promises';\nimport {homedir} from 'node:os';\nimport path from 'node:path';\nimport {z} from 'zod';\nimport {createPrivateDirectory,ownedByUser} from './platform.js';\nimport type {RequestContext} from './contracts.js';\n\nconst credential=z.object({id:z.string().regex(/^[a-f0-9]{24}$/),name:z.string().trim().min(1).max(80).regex(/^[^\\x00-\\x1f\\x7f]+$/),\n digest:z.string().regex(/^[a-f0-9]{64}$/),createdAt:z.string().datetime(),expiresAt:z.string().datetime().nullable(),\n revokedAt:z.string().datetime().nullable(),lastUsedAt:z.string().datetime().nullable(),permission:z.literal('owner-default')}).strict();\nconst schema=z.object({version:z.literal(1),mode:z.enum(['ingress','api-key','oauth']),credentials:z.array(credential).max(256),\n oauth:oauthConfigSchema.optional(),oauthConnections:z.array(oauthConnectionSchema).max(512).default([])}).strict();\ntype State=z.infer<typeof schema>;\nconst digest=(value:string)=>createHash('sha256').update(value).digest('hex');\nconst equal=(a:string,b:string)=>a.length===b.length&&timingSafeEqual(Buffer.from(a),Buffer.from(b));\nexport const defaultAccessFile=()=>process.env.MAC_MCP_ACCESS_FILE||path.join(homedir(),'.config','starmux','access-v1.json');\nconst active=(c:Pick<z.infer<typeof credential>,'revokedAt'|'expiresAt'>)=>!c.revokedAt&&(!c.expiresAt||Date.parse(c.expiresAt)>Date.now());\nexport async function createAccessControl(file=defaultAccessFile(),options:{initialize?:boolean}={}){\n if(!path.isAbsolute(file))throw Error('ACCESS_FILE_ABSOLUTE_REQUIRED');\n const folder=path.dirname(file);if(options.initialize!==false)createPrivateDirectory(folder);\n async function readPrivate(target:string){\n const st=await lstat(target);\n if(!st.isFile()||st.nlink!==1||!ownedByUser(target,st,true)||st.size>262144)throw Error('ACCESS_FILE_UNSAFE');\n return readFile(target,'utf8');\n }\n async function save(data:State){\n schema.parse(data);\n const temp=file+'.'+randomBytes(12).toString('hex');\n try{await writeFile(temp,JSON.stringify(data,null,2)+'\\n',{flag:'wx',mode:0o600});await rename(temp,file);}\n finally{await rm(temp,{force:true});}\n }\n // A filesystem lock serializes mutations across CLI and Core processes.\n async function locked<T>(action:()=>Promise<T>):Promise<T>{\n const lock=file+'.lock';let acquired=false;\n for(let i=0;i<100;i++){\n try{await mkdir(lock,{mode:0o700});acquired=true;break;}\n catch(e){if(!(e instanceof Error&&'code' in e&&e.code==='EEXIST'))throw e;await new Promise(r=>setTimeout(r,20));}\n }\n if(!acquired)throw Error('ACCESS_STORE_BUSY');\n try{return await action();}finally{await rm(lock,{recursive:true,force:true});}\n }\n async function load(){\n if(!ownedByUser(folder,await lstat(folder),true))throw Error('ACCESS_DIRECTORY_UNSAFE');\n const data=schema.parse(JSON.parse(await readPrivate(file)) as unknown);\n if(data.oauth)validateOAuthConfig(data.oauth);\n if(data.mode==='oauth'&&!data.oauth)throw Error('OAUTH_CONFIGURATION_REQUIRED');\n return data;\n }\n // Status readers validate existing state without creating stores or lock files.\n if(options.initialize!==false)await locked(async()=>{\n try{await load();}catch(e){\n if(!(e instanceof Error&&'code' in e&&e.code==='ENOENT'))throw e;\n // A persistent marker prevents a deleted store from silently resetting\n // enabled authentication to ingress mode after a CLI call or restart.\n try{await lstat(file+'.initialized');throw Error('ACCESS_STORE_MISSING');}\n catch(markerError){\n if(!(markerError instanceof Error&&'code' in markerError&&markerError.code==='ENOENT'))throw markerError;\n }\n await writeFile(file+'.initialized','1\\n',{flag:'wx',mode:0o600});\n await save({version:1,mode:'ingress',credentials:[],oauthConnections:[]});\n }\n });\n const ownerFile=file+'.owner-token';\n async function ownerToken(){\n return locked(async()=>{\n try{return (await readPrivate(ownerFile)).trim();}catch(e){\n if(!(e instanceof Error&&'code' in e&&e.code==='ENOENT'))throw e;\n const token=randomBytes(32).toString('hex');\n await writeFile(ownerFile,token+'\\n',{flag:'wx',mode:0o600});return token;\n }\n });\n }\n async function authorized(token:unknown){\n if(typeof token!=='string'||! /^[a-f0-9]{64}$/.test(token))return false;\n try{return equal(token,(await readPrivate(ownerFile)).trim());}catch{return false;}\n }\n async function snapshot(){\n const data=await load();return {mode:data.mode,permission:'owner-default' as const,oauth:data.oauth??null,oauthConnections:data.oauthConnections,\n credentials:data.credentials.map(({digest:_,...c})=>({...c,status:c.revokedAt?'revoked':active(c)?'active':'expired'}))};\n }\n async function create(name:string,expiresAt:string|null=null,activate=false){\n const secret='smx_'+randomBytes(32).toString('base64url');\n const c=credential.parse({id:randomBytes(12).toString('hex'),name,digest:digest(secret),createdAt:new Date().toISOString(),\n expiresAt,revokedAt:null,lastUsedAt:null,permission:'owner-default'});\n if(!active(c))throw Error('ACCESS_EXPIRY_MUST_BE_FUTURE');\n await locked(async()=>{const data=await load();data.credentials.push(c);if(activate)data.mode='api-key';await save(data);});\n return {id:c.id,name:c.name,key:secret,permission:c.permission};\n }\n async function setMode(mode:'ingress'|'api-key'|'oauth'){\n await locked(async()=>{const data=await load();\n if(mode==='api-key'&&!data.credentials.some(active))throw Error('ACCESS_ACTIVE_KEY_REQUIRED');\n if(mode==='oauth'&&!data.oauth)throw Error('OAUTH_CONFIGURATION_REQUIRED');\n data.mode=mode;await save(data);});\n }\n async function revoke(id:string){\n await locked(async()=>{const data=await load();const c=data.credentials.find(c=>c.id===id);\n if(!c)throw Error('ACCESS_CREDENTIAL_NOT_FOUND');c.revokedAt??=new Date().toISOString();await save(data);});\n }\n async function configureOAuth(value:unknown){\n const config=validateOAuthConfig(value);\n await locked(async()=>{const data=await load();data.oauth=config;await save(data);});\n }\n async function revokeOAuth(id:string){\n await locked(async()=>{const data=await load();const c=data.oauthConnections.find(c=>c.id===id);\n if(!c)throw Error('OAUTH_CONNECTION_NOT_FOUND');c.revokedAt??=new Date().toISOString();await save(data);});\n }\n async function oauthSettings(){const data=await load();return data.mode==='oauth'?data.oauth!:null;}\n let verifier:{signature:string;verify:ReturnType<typeof createOAuthVerifier>}|undefined;\n async function authenticateOAuth(config:OAuthConfig,header:string|undefined):Promise<RequestContext['identity']>{\n const signature=JSON.stringify(config);\n if(verifier?.signature!==signature)verifier={signature,verify:createOAuthVerifier(config)};\n const verified=await verifier.verify(header);\n await locked(async()=>{\n const data=await load();\n if(data.mode!=='oauth'||JSON.stringify(data.oauth)!==signature)throw new OAuthError(401,'OAUTH_CONFIGURATION_CHANGED');\n const c=data.oauthConnections.find(c=>c.id===verified.id);\n if(c?.revokedAt)throw new OAuthError(401,'OAUTH_CONNECTION_REVOKED');\n const now=new Date().toISOString();\n if(c)c.lastUsedAt=now;\n else{\n if(data.oauthConnections.length>=512)throw new OAuthError(503,'OAUTH_CONNECTION_LIMIT');\n data.oauthConnections.push({id:verified.id,subjectHash:verified.subjectHash,clientId:verified.clientId,\n createdAt:now,lastUsedAt:now,revokedAt:null,permission:'owner-default'});\n }\n await save(data);\n });\n return {kind:'verified-oauth',connectionId:verified.id,subjectHash:verified.subjectHash,scopes:verified.scopes,permission:'owner-default'};\n }\n async function authenticate(header:string|undefined):Promise<RequestContext['identity']>{\n const data=await load();\n if(data.mode==='ingress')return {kind:'unverified-local'};\n if(data.mode==='oauth')return authenticateOAuth(data.oauth!,header);\n if(!header||!/^Bearer (?:mgw_|smx_)[A-Za-z0-9_-]{43}$/i.test(header))throw Error('ACCESS_UNAUTHORIZED');\n const hash=digest(header.slice(7));const c=data.credentials.find(c=>equal(c.digest,hash)&&active(c));\n if(!c)throw Error('ACCESS_UNAUTHORIZED');\n // Recheck under the mutation lock: revocation cannot be lost to a usage update.\n await locked(async()=>{const current=await load();const found=current.credentials.find(v=>v.id===c.id);\n if(current.mode!=='api-key'||!found||!active(found))throw Error('ACCESS_UNAUTHORIZED');\n found.lastUsedAt=new Date().toISOString();await save(current);});\n return {kind:'verified-api-key',credentialId:c.id,permission:'owner-default'};\n }\n return {file,snapshot,create,setMode,revoke,authenticate,ownerToken,authorized,configureOAuth,revokeOAuth,oauthSettings};\n}\nexport type AccessControl=Awaited<ReturnType<typeof createAccessControl>>;\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"access-dashboard.js","sourceRoot":"","sources":["../../src/access-dashboard.ts"],"names":[],"mappings":"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","sourcesContent":["import {discoverOAuth,metadataUrl} from './oauth-resource.js';\nimport type {IncomingMessage} from 'node:http';\nimport type {AccessControl} from './access-control.js';\nimport {permissionHeaders} from './permissions-dashboard.js';\nimport {createHash} from 'node:crypto';\nconst loginScript=`(()=>{const k='starmux-owner-token';const q=new URLSearchParams(location.hash.slice(1));const t=q.get('token');if(t){history.replaceState(null,'',location.pathname);sessionStorage.setItem(k,t);}const token=sessionStorage.getItem(k);if(token)for(const input of document.querySelectorAll('input[name=\"token\"]'))input.value=token;})();`;\nconst scriptHash=createHash('sha256').update(loginScript).digest('base64');\nexport const accessHeaders=Object.freeze({...permissionHeaders,'content-security-policy':permissionHeaders['content-security-policy']+`; script-src 'sha256-${scriptHash}'`});\nconst e=(s:unknown)=>String(s??'').replace(/[&<>\"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]!));\nexport function accessPage(state:Awaited<ReturnType<AccessControl['snapshot']>>,created?:{key:string}){\n const form=(op:string,fields:string,label:string)=>`<form method=\"post\" action=\"/access\"><input type=\"hidden\" name=\"op\" value=\"${op}\">${fields}<label>Owner management token <input type=\"password\" name=\"token\" required autocomplete=\"off\"></label><button>${label}</button></form>`;\n return `<!doctype html><html lang=\"en\"><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width\"><title>Starmux · Access Control</title><style>body{font:16px system-ui;max-width:960px;margin:40px auto;padding:0 24px;background:#f5f7fa;color:#243249}section{background:white;border:1px solid #dae0e8;border-radius:12px;padding:24px;margin:20px 0}input,select,button{font:inherit;padding:8px;margin:8px}label{display:block}code{overflow-wrap:anywhere}td,th{text-align:left;padding:10px;border-bottom:1px solid #ddd}table{width:100%}</style><h1>Starmux · Access Control</h1>\n <p><a href=\"/dashboard\">Dashboard</a> · <a href=\"/permissions\">Workspace permissions</a></p>\n <section><h2>${state.mode==='ingress'?'None (local or ingress-managed)':state.mode==='oauth'?'OAuth':'Bearer API Key'}</h2><p>Permission: Owner default. Credentials do not change workspace, tool or operating-system permissions.</p><p>${state.mode==='ingress'?'Starmux does not verify a token. Restrict remote access through your HTTPS proxy or tunnel.':'Every MCP request requires an active credential. Missing, invalid, expired or revoked credentials are rejected.'}</p>\n <table><tr><th>MCP Client Field</th><th>Value to Copy</th></tr>\n <tr><td>Transport</td><td><code>Streamable HTTP</code></td></tr>\n <tr><td>URL (Local)</td><td><code>http://127.0.0.1:&lt;PORT&gt;/mcp</code></td></tr>\n <tr><td>URL (Remote)</td><td><code>${state.mode==='oauth'&&state.oauth?e(state.oauth.resource):'https://&lt;your-domain-or-tunnel&gt;/mcp'}</code></td></tr>\n <tr><td>Authentication</td><td><code>${state.mode==='ingress'?'None (or optional Bearer API key)':state.mode==='api-key'?'Bearer API key — Header: Authorization: Bearer &lt;YOUR_API_KEY&gt;':`OAuth — Client ID: ${e(state.oauth?.clientId??'')} · Scope: ${e(state.oauth?.scope??'gateway:access')} · Issuer: ${e(state.oauth?.issuer??'')}`}</code></td></tr>\n </table></section>\n ${created?`<section><h2>Copy this key now</h2><code>${e(created.key)}</code><p>This key is shown only in this response. Keep it private; it grants the owner-approved MCP capabilities.</p></section>`:''}\n <section><h2>Client credentials</h2><table><tr><th>Name / ID</th><th>Status / Revoked</th><th>Created / Last used / Expiry</th></tr>${state.credentials.map(c=>`<tr><td>${e(c.name)}<br><code>${e(c.id)}</code></td><td>${e(c.status)}<br>${e(c.revokedAt||'—')}</td><td>${e(c.createdAt)}<br>${e(c.lastUsedAt||'Never')}<br>${e(c.expiresAt||'No expiry')}</td></tr>`).join('')||'<tr><td colspan=\"3\">No credentials</td></tr>'}</table></section>\n <section><h2>Create credential</h2>${form('create','<label>Name <input name=\"name\" maxlength=\"80\" required></label><label>Expiry (optional, UTC ISO 8601) <input name=\"expiresAt\" placeholder=\"2027-01-01T00:00:00.000Z\"></label>','Create')}</section>\n <section><h2>Authentication mode</h2>${form('mode','<select name=\"mode\"><option value=\"api-key\">Require Gateway API key</option><option value=\"oauth\">Require OAuth (configure first)</option><option value=\"ingress\">Managed by ingress (no Gateway key required)</option></select>','Apply mode')}<p>Switching to ingress mode permits requests without a Gateway key. Existing owner permissions still apply.</p></section>\n <section><h2>OAuth configuration</h2><p>Use an existing authorization server. Configuration does not enable OAuth until you change the mode above.</p>\n ${state.oauth?`<p>Issuer: <code>${e(state.oauth.issuer)}</code><br>Resource: <code>${e(state.oauth.resource)}</code><br>Discovery: <code>${e(metadataUrl(state.oauth))}</code><br>Client: ${e(state.oauth.clientId)} · ${e(state.oauth.clientRegistration)}<br>Scope: ${e(state.oauth.scope)}</p>`:'<p>Not configured</p>'}\n ${form('oauth-configure','<label>Issuer URL <input name=\"issuer\" type=\"url\" required></label><label>Public MCP resource URL <input name=\"resource\" type=\"url\" required></label><label>Registered client ID <input name=\"clientId\" required></label><label>Allowed owner subject IDs (comma separated) <input name=\"subjects\" required></label><label>Token endpoint authentication <select name=\"tokenAuth\"><option value=\"none\">none (public client + PKCE)</option><option value=\"client_secret_basic\">client_secret_basic</option><option value=\"client_secret_post\">client_secret_post</option></select></label>','Discover and save')}\n <p>This form uses a predefined client and HTTPS. Client secrets stay with the authorization server and ChatGPT; do not enter them here. Advanced registration and local development are available through the CLI.</p></section>\n <section><h2>OAuth connections</h2><p>Revocation blocks the session, including tokens refreshed from it. A new owner-approved login may establish a new session.</p>\n <table><tr><th>Connection ID / Client</th><th>Status</th><th>Last used</th></tr>${state.oauthConnections.map(c=>`<tr><td><code>${e(c.id)}</code><br>${e(c.clientId)}</td><td>${c.revokedAt?'Revoked':'Active'}</td><td>${e(c.lastUsedAt)}</td></tr>`).join('')||'<tr><td colspan=\"3\">No OAuth connections</td></tr>'}</table>\n ${form('oauth-revoke','<label>Connection ID <input name=\"id\" required></label>','Revoke OAuth connection')}</section>\n <section><h2>Revoke credential</h2>${form('revoke','<label>Credential ID <input name=\"id\" required></label>','Revoke')}<p>Revocation blocks subsequent requests; it does not cancel an already executing tool.</p></section>\n <p>Get the separate owner management token locally: <code>starmux dashboard --token</code>. Do not give this token to AI clients.</p><script>${loginScript}</script></html>`;\n}\nexport async function accessPost(req:IncomingMessage,access:AccessControl){\n if(req.headers.origin!=='http://'+req.headers.host||(req.headers['content-type']||'').split(';')[0]!=='application/x-www-form-urlencoded')return {status:403};\n const size=Number(req.headers['content-length']);if(!Number.isSafeInteger(size)||size<1||size>4096)return {status:413};\n let body='';for await(const chunk of req){body+=String(chunk);if(Buffer.byteLength(body)>4096)return {status:413};}\n const q=new URLSearchParams(body);\n for(const key of q.keys())if(!['op','token','name','expiresAt','mode','id','issuer','resource','clientId','subjects','tokenAuth'].includes(key)||q.getAll(key).length!==1)return {status:400};\n if(!await access.authorized(q.get('token')))return {status:403};\n try{\n let created:{key:string}|undefined;\n if(q.get('op')==='create')created=await access.create(q.get('name')||'',q.get('expiresAt')||null);\n else if(q.get('op')==='revoke')await access.revoke(q.get('id')||'');\n else if(q.get('op')==='mode'&&(q.get('mode')==='ingress'||q.get('mode')==='api-key'||q.get('mode')==='oauth'))await access.setMode(q.get('mode') as 'ingress'|'api-key'|'oauth');\n else if(q.get('op')==='oauth-revoke')await access.revokeOAuth(q.get('id')||'');\n else if(q.get('op')==='oauth-configure'){\n const method=q.get('tokenAuth')||'none';\n if(!['none','client_secret_basic','client_secret_post'].includes(method))return {status:400};\n await access.configureOAuth(await discoverOAuth({issuer:q.get('issuer')||'',resource:q.get('resource')||'',\n clientId:q.get('clientId')||'',allowedSubjects:(q.get('subjects')||'').split(',').map(s=>s.trim()).filter(Boolean),\n tokenEndpointAuthMethod:method as 'none'|'client_secret_basic'|'client_secret_post'}));\n }\n else return {status:400};\n return {status:200,html:accessPage(await access.snapshot(),created)};\n }catch{return {status:400};}\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"file":"authorized-workspace-roots.js","sourceRoot":"","sources":["../../src/authorized-workspace-roots.ts"],"names":[],"mappings":"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","sourcesContent":["import {ownedByUser,gitBinary,within} from \"./platform.js\";\nimport type { WorkspacePolicy } from \"./contracts.js\";\n// Generic Gateway workspace authorization selector.\n// Routing to an allowed directory is a Gateway policy decision, NOT a new\n// Desktop Commander tool. Actual file operations stay upstream MCP calls.\nimport { execFile } from \"node:child_process\";\nimport { lstat,realpath } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { promisify } from \"node:util\";\nimport { createProjectPolicy } from \"./project-workspace-policy.js\";\n\nconst exec=promisify(execFile);\nconst filePathKey=new Map([\n [\"read_file\",\"path\"],[\"write_file\",\"path\"],[\"edit_block\",\"file_path\"],\n [\"get_file_info\",\"path\"],[\"list_directory\",\"path\"]\n]);\nconst inside=(candidate: string,root: string)=>within(candidate,root);\n\nexport function createAuthorizedWorkspacePolicy({basePolicy,linkedGitWorktrees=\"off\"}: {basePolicy: WorkspacePolicy; linkedGitWorktrees?: string}): WorkspacePolicy{\n if(!basePolicy?.root||![\"off\",\"linked\"].includes(linkedGitWorktrees))\n throw new Error(\"INVALID_AUTHORIZED_WORKSPACES\");\n if(linkedGitWorktrees===\"off\")return basePolicy;\n const base=basePolicy.root;\n const policies=new Map([[base,basePolicy]]);\n async function linkedRoots(){\n const {stdout}=await exec(gitBinary,\n [\"-C\",base,\"worktree\",\"list\",\"--porcelain\"],{\n timeout:3000,maxBuffer:65536,\n env:process.env\n });\n const candidates=stdout.split(/\\n\\s*\\n/)\n .map(part=>part.split(\"\\n\").find(line=>line.startsWith(\"worktree \"))?.slice(9))\n .filter((value): value is string=>typeof value===\"string\" && value.length>0);\n if(candidates.length>64)throw Error(\"WORKSPACE_TOO_MANY_LINKED_ROOTS\");\n const roots=[];\n for(const given of candidates){\n if(!path.isAbsolute(given)||path.normalize(given)!==given)continue;\n const canonical=await realpath(given).catch(()=>null);\n if(canonical!==given)continue;\n const st=await lstat(canonical).catch(()=>null);\n if(!st?.isDirectory()||st.isSymbolicLink()||!ownedByUser(canonical,st))continue;\n roots.push(canonical);\n }\n if(!roots.includes(base))throw Error(\"WORKSPACE_ROOT_NOT_IN_GIT\");\n return roots;\n }\n return Object.freeze<WorkspacePolicy>({\n root:base,mode:basePolicy.mode,\n developerCommands:basePolicy.developerCommands,\n observe(name,result,args){basePolicy.observe?.(name,result,args);},\n validateGitPath(file){return basePolicy.validateGitPath(file);},\n async prepare(name,args={}){\n const key=filePathKey.get(name);\n if(!key||typeof args?.[key]!==\"string\"||\n !path.isAbsolute(args[key]))return basePolicy.prepare(name,args);\n const roots=await linkedRoots();\n const target=args[key];\n const root=roots.filter(entry=>inside(target,entry))\n .sort((a,b)=>b.length-a.length)[0];\n if(!root)return basePolicy.prepare(name,args);\n let policy=policies.get(root);\n if(!policy){\n policy=await createProjectPolicy({\n root,mode:basePolicy.mode,\n developerCommands:basePolicy.developerCommands\n });\n policies.set(root,policy);\n }\n return policy.prepare(name,args);\n }\n });\n}\n"]}
@@ -1,53 +0,0 @@
1
- import { createAccessControl } from '../access-control.js';
2
- export async function accessCommand(args) {
3
- const json = args.includes('--json');
4
- args = args.filter(a => a !== '--json');
5
- const [op, ...rest] = args;
6
- if (op === 'oauth') {
7
- await (await import('./oauth.js')).oauthCommand([...rest, ...(json ? ['--json'] : [])]);
8
- return;
9
- }
10
- const access = await createAccessControl();
11
- let result;
12
- if ((op === 'status' || op === 'list' || !op) && !rest.length)
13
- result = await access.snapshot();
14
- else if (op === 'create' && rest[0] && (rest.length === 1 || (rest.length === 3 && rest[1] === '--expires')))
15
- result = await access.create(rest[0], rest[2] || null);
16
- else if (op === 'mode' && rest.length === 1 && (rest[0] === 'ingress' || rest[0] === 'api-key' || rest[0] === 'oauth')) {
17
- await access.setMode(rest[0]);
18
- result = await access.snapshot();
19
- }
20
- else if (op === 'revoke' && rest.length === 1) {
21
- await access.revoke(rest[0]);
22
- result = await access.snapshot();
23
- }
24
- else if (op === 'revoke-oauth' && rest.length === 1) {
25
- await access.revokeOAuth(rest[0]);
26
- result = await access.snapshot();
27
- }
28
- else if (op === 'owner-token' && !rest.length) {
29
- process.stdout.write(await access.ownerToken() + '\n');
30
- return;
31
- }
32
- else
33
- throw Error('Usage: access status|list|create "Name" [--expires ISO8601]|mode ingress|mode api-key|mode oauth|revoke ID|revoke-oauth ID|oauth configure|oauth doctor|owner-token [--json]');
34
- if (json) {
35
- process.stdout.write(JSON.stringify(result) + '\n');
36
- return;
37
- }
38
- if (result && typeof result === 'object' && 'key' in result) {
39
- const c = result;
40
- process.stdout.write(`\n Credential created: ${c.name}\n ID: ${c.id}\n Permission: Owner default\n\n ${c.key}\n\n Copy this key now; it cannot be retrieved later.\n Creating a key does not enable authentication.\n Enable: mac-mcp-gateway access mode api-key\n\n`);
41
- return;
42
- }
43
- const state = await access.snapshot();
44
- process.stdout.write(`\n Gateway access control\n ────────────────────────────────────────────────\n Mode ${state.mode === 'ingress' ? 'Managed by ingress' : state.mode === 'oauth' ? 'OAuth required' : 'Gateway API key required'}\n Permission Owner default\n Config ${access.file}\n\n`);
45
- for (const c of state.credentials)
46
- process.stdout.write(` ${c.name} · ${c.status}\n ID: ${c.id}\n Created: ${c.createdAt}\n Last used: ${c.lastUsedAt || 'Never'} · Expires: ${c.expiresAt || 'Never'}\n`);
47
- for (const c of state.oauthConnections)
48
- process.stdout.write(` OAuth · ${c.revokedAt ? 'Revoked' : 'Active'} · ${c.clientId}\n ID: ${c.id}\n Last used: ${c.lastUsedAt}\n`);
49
- if (!state.credentials.length)
50
- process.stdout.write(' No client credentials.\n');
51
- process.stdout.write(state.mode === 'ingress' ? '\n Caller identity is not verified by Gateway. Restrict access at ingress.\n\n' : '\n Missing, invalid, expired and revoked keys are rejected.\n\n');
52
- }
53
- //# sourceMappingURL=access.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"access.js","sourceRoot":"","sources":["../../../src/cli/access.ts"],"names":[],"mappings":"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","sourcesContent":["import {createAccessControl} from '../access-control.js';\nexport async function accessCommand(args:string[]){\n const json=args.includes('--json');args=args.filter(a=>a!=='--json');\n const [op,...rest]=args;\n if(op==='oauth'){await (await import('./oauth.js')).oauthCommand([...rest,...(json?['--json']:[])]);return;}\n const access=await createAccessControl();\n let result:unknown;\n if((op==='status'||op==='list'||!op)&&!rest.length)result=await access.snapshot();\n else if(op==='create'&&rest[0]&&(rest.length===1||(rest.length===3&&rest[1]==='--expires')))result=await access.create(rest[0],rest[2]||null);\n else if(op==='mode'&&rest.length===1&&(rest[0]==='ingress'||rest[0]==='api-key'||rest[0]==='oauth')){await access.setMode(rest[0]);result=await access.snapshot();}\n else if(op==='revoke'&&rest.length===1){await access.revoke(rest[0]!);result=await access.snapshot();}\n else if(op==='revoke-oauth'&&rest.length===1){await access.revokeOAuth(rest[0]!);result=await access.snapshot();}\n else if(op==='owner-token'&&!rest.length){process.stdout.write(await access.ownerToken()+'\\n');return;}\n else throw Error('Usage: access status|list|create \"Name\" [--expires ISO8601]|mode ingress|mode api-key|mode oauth|revoke ID|revoke-oauth ID|oauth configure|oauth doctor|owner-token [--json]');\n if(json){process.stdout.write(JSON.stringify(result)+'\\n');return;}\n if(result&&typeof result==='object'&&'key' in result){\n const c=result as {id:string;name:string;key:string};\n process.stdout.write(`\\n Credential created: ${c.name}\\n ID: ${c.id}\\n Permission: Owner default\\n\\n ${c.key}\\n\\n Copy this key now; it cannot be retrieved later.\\n Creating a key does not enable authentication.\\n Enable: mac-mcp-gateway access mode api-key\\n\\n`);return;\n }\n const state=await access.snapshot();\n process.stdout.write(`\\n Gateway access control\\n ────────────────────────────────────────────────\\n Mode ${state.mode==='ingress'?'Managed by ingress':state.mode==='oauth'?'OAuth required':'Gateway API key required'}\\n Permission Owner default\\n Config ${access.file}\\n\\n`);\n for(const c of state.credentials)process.stdout.write(` ${c.name} · ${c.status}\\n ID: ${c.id}\\n Created: ${c.createdAt}\\n Last used: ${c.lastUsedAt||'Never'} · Expires: ${c.expiresAt||'Never'}\\n`);\n for(const c of state.oauthConnections)process.stdout.write(` OAuth · ${c.revokedAt?'Revoked':'Active'} · ${c.clientId}\\n ID: ${c.id}\\n Last used: ${c.lastUsedAt}\\n`);\n if(!state.credentials.length)process.stdout.write(' No client credentials.\\n');\n process.stdout.write(state.mode==='ingress'?'\\n Caller identity is not verified by Gateway. Restrict access at ingress.\\n\\n':'\\n Missing, invalid, expired and revoked keys are rejected.\\n\\n');\n}\n"]}
@@ -1,32 +0,0 @@
1
- import { errorCode, errorMessage } from "../validation.js";
2
- // Owner-local revocation operations. Never contacts an IdP or network, and
3
- // never changes production Core, Tunnel, Bridge, Caddy, or login items.
4
- import { initializeRemoteRevocations, readRemoteRevocations, revokeRemoteIdentity } from "../community-remote-revocations.js";
5
- const [command, ...args] = process.argv.slice(2);
6
- const file = process.env.MAC_MCP_REMOTE_REVOCATIONS_FILE;
7
- if (!file)
8
- throw Error("REMOTE_REVOCATION_FILE_REQUIRED");
9
- try {
10
- let result;
11
- if (command === "init" && args.length === 0)
12
- result = await initializeRemoteRevocations(file);
13
- else if (command === "status" && args.length === 0) {
14
- const data = await readRemoteRevocations(file);
15
- result = { ready: true, revokedSubjects: data.revokedSubjects.length,
16
- revokedJtis: data.revokedJtis.length };
17
- }
18
- else if (command === "revoke-subject" && args.length === 1)
19
- result = await revokeRemoteIdentity(file, { subject: args[0] });
20
- else if (command === "revoke-jti" && args.length === 1)
21
- result = await revokeRemoteIdentity(file, { jti: args[0] });
22
- else
23
- throw Error("USAGE_INIT_STATUS_REVOKE_SUBJECT_OR_JTI");
24
- process.stdout.write(JSON.stringify({ event: "REMOTE_REVOCATION_OWNER_OPERATION",
25
- command, ...result }) + "\n");
26
- }
27
- catch (error) {
28
- process.stderr.write(JSON.stringify({ event: "REMOTE_REVOCATION_OWNER_FAILED",
29
- reason: errorCode(error) || errorMessage(error) || "UNKNOWN" }) + "\n");
30
- process.exitCode = 1;
31
- }
32
- //# sourceMappingURL=community-remote-revoke.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"community-remote-revoke.js","sourceRoot":"","sources":["../../../src/cli/community-remote-revoke.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAC,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAC1D,2EAA2E;AAC3E,wEAAwE;AACxE,OAAO,EACP,2BAA2B,EAAC,qBAAqB,EACjD,oBAAoB,EACnB,MAAM,oCAAoC,CAAC;AAC5C,MAAM,CAAC,OAAO,EAAC,GAAG,IAAI,CAAC,GAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AAC9C,MAAM,IAAI,GAAC,OAAO,CAAC,GAAG,CAAC,+BAA+B,CAAC;AACvD,IAAG,CAAC,IAAI;IAAC,MAAM,KAAK,CAAC,iCAAiC,CAAC,CAAC;AACxD,IAAI,CAAC;IACH,IAAI,MAAM,CAAC;IACX,IAAG,OAAO,KAAG,MAAM,IAAE,IAAI,CAAC,MAAM,KAAG,CAAC;QAAC,MAAM,GAAC,MAAM,2BAA2B,CAAC,IAAI,CAAC,CAAC;SAC/E,IAAG,OAAO,KAAG,QAAQ,IAAE,IAAI,CAAC,MAAM,KAAG,CAAC,EAAC,CAAC;QAC3C,MAAM,IAAI,GAAC,MAAM,qBAAqB,CAAC,IAAI,CAAC,CAAC;QAC7C,MAAM,GAAC,EAAC,KAAK,EAAC,IAAI,EAAC,eAAe,EAAC,IAAI,CAAC,eAAe,CAAC,MAAM;YAC5D,WAAW,EAAC,IAAI,CAAC,WAAW,CAAC,MAAM,EAAC,CAAC;IACzC,CAAC;SAAK,IAAG,OAAO,KAAG,gBAAgB,IAAE,IAAI,CAAC,MAAM,KAAG,CAAC;QAClD,MAAM,GAAC,MAAM,oBAAoB,CAAC,IAAI,EAAC,EAAC,OAAO,EAAC,IAAI,CAAC,CAAC,CAAC,EAAC,CAAC,CAAC;SACvD,IAAG,OAAO,KAAG,YAAY,IAAE,IAAI,CAAC,MAAM,KAAG,CAAC;QAC7C,MAAM,GAAC,MAAM,oBAAoB,CAAC,IAAI,EAAC,EAAC,GAAG,EAAC,IAAI,CAAC,CAAC,CAAC,EAAC,CAAC,CAAC;;QACnD,MAAM,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC5D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAC,KAAK,EAAC,mCAAmC;QAC5E,OAAO,EAAC,GAAG,MAAM,EAAC,CAAC,GAAC,IAAI,CAAC,CAAC;AAC9B,CAAC;AAAA,OAAM,KAAK,EAAC,CAAC;IACZ,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAC,KAAK,EAAC,gCAAgC;QACzE,MAAM,EAAC,SAAS,CAAC,KAAK,CAAC,IAAE,YAAY,CAAC,KAAK,CAAC,IAAE,SAAS,EAAC,CAAC,GAAC,IAAI,CAAC,CAAC;IAClE,OAAO,CAAC,QAAQ,GAAC,CAAC,CAAC;AACrB,CAAC","sourcesContent":["import { errorCode,errorMessage } from \"../validation.js\";\n// Owner-local revocation operations. Never contacts an IdP or network, and\n// never changes production Core, Tunnel, Bridge, Caddy, or login items.\nimport {\ninitializeRemoteRevocations,readRemoteRevocations,\nrevokeRemoteIdentity\n} from \"../community-remote-revocations.js\";\nconst [command,...args]=process.argv.slice(2);\nconst file=process.env.MAC_MCP_REMOTE_REVOCATIONS_FILE;\nif(!file)throw Error(\"REMOTE_REVOCATION_FILE_REQUIRED\");\ntry {\n let result;\n if(command===\"init\"&&args.length===0)result=await initializeRemoteRevocations(file);\n else if(command===\"status\"&&args.length===0){\n const data=await readRemoteRevocations(file);\n result={ready:true,revokedSubjects:data.revokedSubjects.length,\n revokedJtis:data.revokedJtis.length};\n }else if(command===\"revoke-subject\"&&args.length===1)\n result=await revokeRemoteIdentity(file,{subject:args[0]});\n else if(command===\"revoke-jti\"&&args.length===1)\n result=await revokeRemoteIdentity(file,{jti:args[0]});\n else throw Error(\"USAGE_INIT_STATUS_REVOKE_SUBJECT_OR_JTI\");\n process.stdout.write(JSON.stringify({event:\"REMOTE_REVOCATION_OWNER_OPERATION\",\n command,...result})+\"\\n\");\n}catch(error){\n process.stderr.write(JSON.stringify({event:\"REMOTE_REVOCATION_OWNER_FAILED\",\n reason:errorCode(error)||errorMessage(error)||\"UNKNOWN\"})+\"\\n\");\n process.exitCode=1;\n}\n"]}