starmux 0.13.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +1 -1
  3. package/README.md +20 -28
  4. package/dist/src/access-control.js +1 -2
  5. package/dist/src/access-dashboard.js +2 -3
  6. package/dist/src/account-access-policy.js +54 -0
  7. package/dist/src/authorized-workspace-roots.js +0 -1
  8. package/dist/src/batch-read-policy.js +30 -0
  9. package/dist/src/cli/instances.js +27 -187
  10. package/dist/src/cli/oauth.js +0 -1
  11. package/dist/src/cli/starmux-status.js +25 -18
  12. package/dist/src/cli/starmux-supervisor.js +0 -1
  13. package/dist/src/cli/starmux.js +47 -37
  14. package/dist/src/config-schemas.js +1 -10
  15. package/dist/src/configured-mcp-upstreams.js +0 -1
  16. package/dist/src/contracts.js +0 -1
  17. package/dist/src/current-permission-probe.js +0 -1
  18. package/dist/src/dc-workspace-http.js +57 -33
  19. package/dist/src/deny-dashboard.js +2 -3
  20. package/dist/src/desktop-commander-facade.js +4 -4
  21. package/dist/src/explicit-deny-rules.js +2 -3
  22. package/dist/src/global-core-control.js +108 -56
  23. package/dist/src/home-write-sandbox.js +0 -1
  24. package/dist/src/http-host-policy.js +20 -0
  25. package/dist/src/instance-control-protocol.js +27 -0
  26. package/dist/src/instance-runtime.js +215 -76
  27. package/dist/src/instance-store.js +28 -22
  28. package/dist/src/instance-worker.js +2 -3
  29. package/dist/src/invocation.js +1 -2
  30. package/dist/src/local-desktop-read-probe.js +5 -88
  31. package/dist/src/managed-workspace-policy.js +38 -4
  32. package/dist/src/mcp-results.js +0 -1
  33. package/dist/src/mcp-upstream-router.js +0 -1
  34. package/dist/src/oauth-keycloak.js +0 -1
  35. package/dist/src/oauth-resource.js +0 -1
  36. package/dist/src/ops-dashboard.js +2 -179
  37. package/dist/src/permissions-dashboard.js +3 -4
  38. package/dist/src/platform.js +6 -4
  39. package/dist/src/privacy-diagnostics.js +0 -1
  40. package/dist/src/private-mcp-activity.js +0 -1
  41. package/dist/src/private-store-lock.js +0 -1
  42. package/dist/src/project-dev-presets.js +0 -1
  43. package/dist/src/project-git-workflow.js +0 -1
  44. package/dist/src/project-workspace-policy.js +10 -6
  45. package/dist/src/runtime-identity.js +0 -14
  46. package/dist/src/runtime-paths.js +0 -3
  47. package/dist/src/tool-dispatch.js +45 -46
  48. package/dist/src/validation.js +0 -1
  49. package/dist/src/workspace-grants.js +1 -2
  50. package/package.json +7 -15
  51. package/scripts/prepare.mjs +1 -1
  52. package/CONTRIBUTING.md +0 -18
  53. package/SECURITY.md +0 -7
  54. package/app/README.md +0 -29
  55. package/dist/src/access-control.js.map +0 -1
  56. package/dist/src/access-dashboard.js.map +0 -1
  57. package/dist/src/authorized-workspace-roots.js.map +0 -1
  58. package/dist/src/cli/access.js +0 -53
  59. package/dist/src/cli/access.js.map +0 -1
  60. package/dist/src/cli/community-remote-revoke.js +0 -32
  61. package/dist/src/cli/community-remote-revoke.js.map +0 -1
  62. package/dist/src/cli/instances.js.map +0 -1
  63. package/dist/src/cli/live-home-settings.js +0 -35
  64. package/dist/src/cli/live-home-settings.js.map +0 -1
  65. package/dist/src/cli/mac-mcp-gateway.js +0 -257
  66. package/dist/src/cli/mac-mcp-gateway.js.map +0 -1
  67. package/dist/src/cli/oauth.js.map +0 -1
  68. package/dist/src/cli/permissions-owner.js +0 -27
  69. package/dist/src/cli/permissions-owner.js.map +0 -1
  70. package/dist/src/cli/service-setup.js +0 -26
  71. package/dist/src/cli/service-setup.js.map +0 -1
  72. package/dist/src/cli/setup-wizard.js +0 -149
  73. package/dist/src/cli/setup-wizard.js.map +0 -1
  74. package/dist/src/cli/starmux-status.js.map +0 -1
  75. package/dist/src/cli/starmux-supervisor.js.map +0 -1
  76. package/dist/src/cli/starmux.js.map +0 -1
  77. package/dist/src/cli/verify-setup.js +0 -53
  78. package/dist/src/cli/verify-setup.js.map +0 -1
  79. package/dist/src/community-remote-revocations.js +0 -135
  80. package/dist/src/community-remote-revocations.js.map +0 -1
  81. package/dist/src/config-schemas.js.map +0 -1
  82. package/dist/src/configured-mcp-upstreams.js.map +0 -1
  83. package/dist/src/contracts.js.map +0 -1
  84. package/dist/src/current-permission-probe.js.map +0 -1
  85. package/dist/src/dc-workspace-http.js.map +0 -1
  86. package/dist/src/deny-dashboard.js.map +0 -1
  87. package/dist/src/desktop-commander-facade.js.map +0 -1
  88. package/dist/src/explicit-deny-rules.js.map +0 -1
  89. package/dist/src/gateway-status-format.js +0 -66
  90. package/dist/src/gateway-status-format.js.map +0 -1
  91. package/dist/src/global-core-control.js.map +0 -1
  92. package/dist/src/home-write-sandbox.js.map +0 -1
  93. package/dist/src/instance-migration.js +0 -138
  94. package/dist/src/instance-migration.js.map +0 -1
  95. package/dist/src/instance-runtime.js.map +0 -1
  96. package/dist/src/instance-store.js.map +0 -1
  97. package/dist/src/instance-worker.js.map +0 -1
  98. package/dist/src/invocation.js.map +0 -1
  99. package/dist/src/live-profile-home-settings.js +0 -80
  100. package/dist/src/live-profile-home-settings.js.map +0 -1
  101. package/dist/src/local-desktop-read-probe.js.map +0 -1
  102. package/dist/src/managed-workspace-policy.js.map +0 -1
  103. package/dist/src/mcp-results.js.map +0 -1
  104. package/dist/src/mcp-upstream-router.js.map +0 -1
  105. package/dist/src/oauth-keycloak.js.map +0 -1
  106. package/dist/src/oauth-resource.js.map +0 -1
  107. package/dist/src/operator-cli.js +0 -559
  108. package/dist/src/operator-cli.js.map +0 -1
  109. package/dist/src/ops-dashboard.js.map +0 -1
  110. package/dist/src/permissions-dashboard.js.map +0 -1
  111. package/dist/src/platform.js.map +0 -1
  112. package/dist/src/privacy-diagnostics.js.map +0 -1
  113. package/dist/src/private-mcp-activity.js.map +0 -1
  114. package/dist/src/private-store-lock.js.map +0 -1
  115. package/dist/src/project-dev-presets.js.map +0 -1
  116. package/dist/src/project-git-workflow.js.map +0 -1
  117. package/dist/src/project-workspace-policy.js.map +0 -1
  118. package/dist/src/read-scope.js +0 -44
  119. package/dist/src/read-scope.js.map +0 -1
  120. package/dist/src/release1-onboarding.js +0 -120
  121. package/dist/src/release1-onboarding.js.map +0 -1
  122. package/dist/src/release1-service.js +0 -131
  123. package/dist/src/release1-service.js.map +0 -1
  124. package/dist/src/remote-auth.js +0 -47
  125. package/dist/src/remote-auth.js.map +0 -1
  126. package/dist/src/runtime-identity.js.map +0 -1
  127. package/dist/src/runtime-paths.js.map +0 -1
  128. package/dist/src/starmux-migration.js +0 -172
  129. package/dist/src/starmux-migration.js.map +0 -1
  130. package/dist/src/tool-dispatch.js.map +0 -1
  131. package/dist/src/validation.js.map +0 -1
  132. package/dist/src/workspace-grants.js.map +0 -1
  133. package/docs/ACCESS_CONTROL.md +0 -51
  134. package/docs/CLIENTS.md +0 -22
  135. package/docs/CONNECT_CHATGPT.md +0 -148
  136. package/docs/INSTALL.md +0 -62
  137. package/docs/INSTANCES.md +0 -83
  138. package/docs/OAUTH.md +0 -116
  139. package/docs/PLATFORMS.md +0 -29
  140. package/docs/README.md +0 -16
  141. package/docs/REMOTE_SETUP.md +0 -280
  142. package/docs/SECURITY.md +0 -15
  143. package/src/dc-workspace-http.mjs +0 -3
  144. package/test/fixtures/p1-fixture.txt +0 -2
@@ -1,17 +1,17 @@
1
1
  import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
2
2
  import { createServer, createConnection } from 'node:net';
3
3
  import { fork } from 'node:child_process';
4
- import { mkdir, rm, readFile, open, lstat } from 'node:fs/promises';
4
+ import { mkdir, rm, open } from 'node:fs/promises';
5
+ import { setTimeout as pause } from 'node:timers/promises';
5
6
  import { fileURLToPath } from 'node:url';
6
7
  import path from 'node:path';
7
8
  import { homedir } from 'node:os';
8
- import { z } from 'zod';
9
+ import { INSTANCE_CONTROL_VERSION, instanceOwnerSchema, activeConfigSchema, runtimeStateSchema, controlRequestSchema, controlReplySchema, checkInstanceProtocol } from './instance-control-protocol.js';
9
10
  import { controlRoot, createPrivateDirectory, childEnvKeys, windows } from './platform.js';
10
- import { acquireControl } from './global-core-control.js';
11
+ import { acquireControl, inspectControlOwner } from './global-core-control.js';
11
12
  import { runtimeIdentity } from './runtime-identity.js';
12
13
  import { atomicPrivate, changeRegistry, availablePort, ensureAccessReady, exists, instanceEnv, instancePaths, loadInstanceConfig, prepareUpstreams, readTrusted, resolveInstance } from './instance-store.js';
13
- const metadataSchema = z.object({ instanceId: z.string().uuid(), runId: z.string().regex(/^[a-f0-9]{32}$/), pid: z.number().int().min(2), token: z.string().regex(/^[a-f0-9]{64}$/), launcher: z.enum(['cli', 'app']), appParentPid: z.number().int().nullable() }).strict();
14
- function runtimePaths(id) {
14
+ export function runtimePaths(id) {
15
15
  const root = path.join(controlRoot(), 'i-' + id);
16
16
  const socket = windows ? '\\\\.\\pipe\\gateway-instance-' + createHash('sha256').update(root).digest('hex').slice(0, 28) : path.join(root, 'control.sock');
17
17
  return { root, socket, meta: path.join(root, 'owner.json') };
@@ -29,21 +29,21 @@ async function ownerFor(instance) {
29
29
  const p = runtimePaths(instance.id);
30
30
  if (!await exists(p.root))
31
31
  return null;
32
- const owner = metadataSchema.parse(JSON.parse(await readTrusted(p.meta)));
32
+ const raw = JSON.parse(await readTrusted(p.meta));
33
+ checkInstanceProtocol(raw);
34
+ const owner = instanceOwnerSchema.parse(raw);
33
35
  if (owner.instanceId !== instance.id)
34
36
  throw Error('INSTANCE_CONTROL_IDENTITY_INVALID');
35
37
  return owner;
36
38
  }
37
- export async function instanceRequest(instance, method, revision) {
38
- const owner = await ownerFor(instance);
39
- if (!owner || !alive(owner.pid))
40
- return null;
39
+ async function exchange(instance, owner, method, revision, timeoutMs = method === 'apply' ? 65000 : 2000) {
41
40
  const nonce = randomBytes(24).toString('hex');
42
- const body = JSON.stringify({ method, revision: revision ?? null, runId: owner.runId, nonce });
41
+ const body = JSON.stringify({ protocolVersion: INSTANCE_CONTROL_VERSION, method, revision: revision ?? null, runId: owner.runId, nonce });
43
42
  return new Promise((resolve, reject) => {
44
43
  const socket = createConnection(runtimePaths(instance.id).socket);
45
44
  let raw = '';
46
- socket.setTimeout(method === 'status' ? 2000 : 65000, () => socket.destroy(Error('INSTANCE_CONTROL_TIMEOUT')));
45
+ const timer = setTimeout(() => socket.destroy(Error('INSTANCE_CONTROL_TIMEOUT')), timeoutMs);
46
+ socket.once('close', () => clearTimeout(timer));
47
47
  socket.on('error', reject);
48
48
  socket.on('connect', () => socket.end(JSON.stringify({ body, proof: signature(owner.token, body) }) + '\n'));
49
49
  socket.on('data', data => { raw += String(data); if (raw.length > 524288)
@@ -51,14 +51,18 @@ export async function instanceRequest(instance, method, revision) {
51
51
  socket.on('end', () => {
52
52
  try {
53
53
  const reply = JSON.parse(raw);
54
- if (!equal(reply.proof, signature(owner.token, nonce + reply.body)))
54
+ if (typeof reply.body !== 'string' || !equal(reply.proof, signature(owner.token, nonce + reply.body)))
55
55
  throw Error('INSTANCE_CONTROL_REPLY_UNTRUSTED');
56
- const data = JSON.parse(reply.body);
56
+ const parsed = JSON.parse(reply.body);
57
+ checkInstanceProtocol(parsed);
58
+ const data = controlReplySchema.parse(parsed);
57
59
  if (data.error)
58
60
  throw Error(data.error);
59
61
  if (data.state.instanceId !== instance.id || data.state.runId !== owner.runId)
60
62
  throw Error('INSTANCE_CONTROL_IDENTITY_CHANGED');
61
- resolve(data.state);
63
+ if (data.result !== (method === 'stop' ? 'STOP_ACCEPTED' : 'STATE'))
64
+ throw Error('INSTANCE_CONTROL_REPLY_INVALID');
65
+ resolve(data);
62
66
  }
63
67
  catch (e) {
64
68
  reject(e);
@@ -66,64 +70,139 @@ export async function instanceRequest(instance, method, revision) {
66
70
  });
67
71
  });
68
72
  }
73
+ export async function instanceRequest(instance, method, revision) {
74
+ const owner = await ownerFor(instance);
75
+ if (!owner || !alive(owner.pid))
76
+ return null;
77
+ return (await exchange(instance, owner, method, revision)).state;
78
+ }
79
+ async function snapshot(file, schema) {
80
+ try {
81
+ return schema.parse(JSON.parse(await readTrusted(file)));
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
69
87
  export async function instanceStatus(instance, home = homedir()) {
70
88
  let runtime = null;
71
89
  let runtimeProblem = null;
90
+ let owner = null;
72
91
  try {
73
- runtime = await instanceRequest(instance, 'status');
92
+ owner = await ownerFor(instance);
93
+ if (owner && alive(owner.pid))
94
+ runtime = (await exchange(instance, owner, 'status')).state;
74
95
  }
75
96
  catch (e) {
76
97
  runtimeProblem = e.message;
77
98
  }
78
- let saved = null;
99
+ let loaded = null;
79
100
  let configProblem = null;
80
101
  try {
81
- saved = await loadInstanceConfig(instance);
102
+ loaded = await loadInstanceConfig(instance);
82
103
  }
83
104
  catch (e) {
84
105
  configProblem = e.message;
85
106
  }
86
- let lastRuntime = null;
87
- if (!runtime)
88
- try {
89
- lastRuntime = JSON.parse(await readTrusted(path.join(instancePaths(instance, home).snapshots, 'last-runtime.json')));
90
- }
91
- catch { }
92
- if (!runtime && lastRuntime?.problem)
93
- runtimeProblem ??= lastRuntime.problem;
107
+ const saved = loaded ? { config: loaded.config, revision: loaded.revision, effectiveRevision: loaded.effectiveRevision, sourcePath: loaded.source.path } : null;
108
+ const snapshots = instancePaths(instance, home).snapshots;
109
+ const lastRun = await snapshot(path.join(snapshots, 'last-runtime.json'), runtimeStateSchema);
110
+ const lastActive = await snapshot(path.join(snapshots, 'active.json'), activeConfigSchema);
94
111
  let health = 'not-running';
95
112
  let accessMode = null;
96
113
  let lastToolCallAt = null;
114
+ if (!runtime && !runtimeProblem) {
115
+ // Historical ports are hints for detecting a lost control channel, never
116
+ // authority to stop a process or claim a configuration is currently active.
117
+ const ports = new Set([owner?.port, saved?.config.port, lastActive?.config.port].filter((p) => p !== undefined));
118
+ for (const port of ports) {
119
+ if (!await availablePort(port)) {
120
+ runtimeProblem = 'INSTANCE_CONTROL_UNAVAILABLE: port ' + port + ' is in use; ownership is unconfirmed';
121
+ break;
122
+ }
123
+ try {
124
+ const lease = inspectControlOwner(port);
125
+ if (lease && alive(lease.pid)) {
126
+ runtimeProblem = 'INSTANCE_CONTROL_UNAVAILABLE: live port lease ' + port;
127
+ break;
128
+ }
129
+ }
130
+ catch (e) {
131
+ runtimeProblem = e.message;
132
+ break;
133
+ }
134
+ }
135
+ }
97
136
  if (runtime?.phase === 'running' && runtime.port) {
98
137
  try {
99
138
  const response = await fetch(`http://127.0.0.1:${runtime.port}/healthz`, { signal: AbortSignal.timeout(1500) });
100
139
  const data = await response.json();
101
140
  health = response.ok && data.status === 'ok' && data.instanceId === instance.id ? 'ok' : 'unrecognized';
102
141
  accessMode = data.accessMode ?? null;
103
- const activity = await fetch(`http://127.0.0.1:${runtime.port}/activity.json`, { signal: AbortSignal.timeout(1000) }).then(r => r.json());
104
- lastToolCallAt = typeof activity.lastToolCallAt === 'string' ? activity.lastToolCallAt : null;
142
+ if (health === 'ok')
143
+ try {
144
+ const activity = await fetch(`http://127.0.0.1:${runtime.port}/activity.json`, { signal: AbortSignal.timeout(1000) }).then(r => r.json());
145
+ lastToolCallAt = typeof activity.lastToolCallAt === 'string' ? activity.lastToolCallAt : null;
146
+ }
147
+ catch { /* Activity is optional; it must not turn a healthy service into an outage. */ }
105
148
  }
106
149
  catch {
107
150
  health = 'unreachable';
108
151
  }
109
152
  }
110
- let active = runtime?.active ?? null;
111
- if (!active)
112
- try {
113
- active = JSON.parse(await readTrusted(path.join(instancePaths(instance, home).snapshots, 'active.json')));
114
- }
115
- catch { }
153
+ const active = runtime?.active ?? null;
154
+ const phase = runtime?.phase ?? (runtimeProblem ? 'unknown' : 'stopped');
155
+ if (phase === 'unknown')
156
+ health = 'unknown';
116
157
  const configState = configProblem ? (configProblem.includes('ENOENT') ? 'missing' : 'invalid') : runtime?.phase === 'running' ? (saved?.effectiveRevision === active?.effectiveRevision ? 'applied' : 'pending') : 'on-next-start';
117
158
  return { schemaVersion: 1, event: 'GATEWAY_STATUS', instanceId: instance.id, name: instance.name, archived: instance.archived, source: instance.source,
118
- phase: runtime?.phase ?? (runtimeProblem ? 'error' : 'stopped'), configState, configProblem, runtimeProblem, runtime,
159
+ phase, configState, configProblem, runtimeProblem, runtime, saved, lastActive, lastRun,
119
160
  port: runtime?.port ?? saved?.config.port ?? null, configuredPort: saved?.config.port ?? null, active, revision: saved?.revision ?? null,
120
161
  health, accessMode, lastToolCallAt, profile: instance.legacy ?? 'setup', installed: false, registered: false,
121
- label: instance.name, workspaceMode: runtime?.active?.config.mode ?? saved?.config.mode ?? null,
122
- writeScope: runtime?.active?.config.writeScope ?? saved?.config.writeScope ?? null, coreControl: runtime?.phase === 'running' ? 'RUNNING' : 'unavailable',
123
- coreLauncher: runtime?.launcher ?? 'unknown', coreOwnerPid: runtime?.pid ?? null, listenerPid: runtime?.pid ?? null,
124
- executionMode: runtime?.phase === 'running' ? (runtime.executionMode === 'background' ? 'background' : 'foreground-owned') : 'stopped', loginAutostart: 'disabled',
162
+ label: instance.name, workspaceMode: active?.config.mode ?? (phase === 'stopped' ? saved?.config.mode : null) ?? null,
163
+ writeScope: active?.config.writeScope ?? (phase === 'stopped' ? saved?.config.writeScope : null) ?? null, coreControl: runtime?.phase === 'running' ? 'RUNNING' : 'unavailable',
164
+ coreLauncher: runtime?.launcher ?? 'unknown', coreOwnerPid: runtime?.pid ?? null,
165
+ executionMode: runtime ? (runtime.executionMode === 'background' ? 'background' : 'foreground-owned') : phase === 'stopped' ? 'stopped' : 'unknown', loginAutostart: 'disabled',
125
166
  connectionState: health === 'ok' ? (lastToolCallAt ? 'local-ready-activity-recorded' : 'local-ready-remote-unverified') : health, requiredAccess: instance.requiredAccess };
126
167
  }
168
+ export async function stopInstance(instance, home = homedir(), options = {}) {
169
+ const timeoutMs = options.timeoutMs ?? 65000;
170
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0)
171
+ throw Error('INSTANCE_STOP_TIMEOUT_INVALID');
172
+ const deadline = Date.now() + timeoutMs;
173
+ const owner = await ownerFor(instance);
174
+ if (!owner || !alive(owner.pid)) {
175
+ const status = await instanceStatus(instance, home);
176
+ if (status.phase !== 'stopped')
177
+ throw Error(status.runtimeProblem ?? 'INSTANCE_STOP_UNVERIFIED');
178
+ return { result: 'ALREADY_STOPPED', instanceId: instance.id, runId: null, restarted: false };
179
+ }
180
+ await exchange(instance, owner, 'stop', undefined, Math.max(1, Math.min(2000, deadline - Date.now())));
181
+ const lastFile = path.join(instancePaths(instance, home).snapshots, 'last-runtime.json');
182
+ // The terminal snapshot is committed only after worker/lease/socket cleanup.
183
+ // Do not use a free port or a dead PID alone as proof of successful shutdown.
184
+ while (Date.now() < deadline) {
185
+ let current;
186
+ try {
187
+ current = await ownerFor(instance);
188
+ }
189
+ catch (e) {
190
+ if (e.code === 'ENOENT') {
191
+ await pause(25);
192
+ continue;
193
+ }
194
+ throw e;
195
+ }
196
+ const lastRun = await snapshot(lastFile, runtimeStateSchema);
197
+ if (lastRun?.runId === owner.runId && lastRun.phase === 'stopped' && current?.runId !== owner.runId) {
198
+ return { result: 'STOPPED', instanceId: instance.id, runId: owner.runId, restarted: current !== null };
199
+ }
200
+ if (lastRun?.runId === owner.runId && lastRun.phase === 'error')
201
+ throw Error('INSTANCE_STOP_FAILED: ' + lastRun.problem);
202
+ await pause(Math.min(50, Math.max(1, deadline - Date.now())));
203
+ }
204
+ throw Error('INSTANCE_STOP_TIMEOUT: cleanup was not confirmed; run starmux doctor');
205
+ }
127
206
  export async function runInstance(instance, options = {}) {
128
207
  const home = options.home ?? homedir();
129
208
  const parent = options.appParentPid;
@@ -133,9 +212,10 @@ export async function runInstance(instance, options = {}) {
133
212
  throw Error('INSTANCE_ARCHIVED');
134
213
  const initial = await loadInstanceConfig(instance);
135
214
  await ensureAccessReady(await resolveInstance(instance.id, home), home);
215
+ const identity = await runtimeIdentity();
136
216
  const p = runtimePaths(instance.id);
137
217
  createPrivateDirectory(controlRoot());
138
- const owner = { instanceId: instance.id, runId: randomBytes(16).toString('hex'), pid: process.pid, token: randomBytes(32).toString('hex'), launcher: parent ? 'app' : 'cli', appParentPid: parent ?? null };
218
+ const owner = { protocolVersion: INSTANCE_CONTROL_VERSION, port: initial.config.port, instanceId: instance.id, runId: randomBytes(16).toString('hex'), pid: process.pid, token: randomBytes(32).toString('hex'), launcher: parent ? 'app' : 'cli', appParentPid: parent ?? null };
139
219
  await changeRegistry(async (registry) => {
140
220
  const registered = registry.instances.find(i => i.id === instance.id);
141
221
  if (!registered || registered.archived)
@@ -165,14 +245,15 @@ export async function runInstance(instance, options = {}) {
165
245
  }
166
246
  await atomicPrivate(p.meta, JSON.stringify(owner));
167
247
  }, home);
168
- const identity = await runtimeIdentity();
169
- const state = { instanceId: instance.id, runId: owner.runId, pid: process.pid, launcher: owner.launcher, appParentPid: parent ?? null, executionMode: options.background ? 'background' : 'foreground', phase: 'starting', port: null, active: null, problem: null, ...identity };
248
+ const state = { protocolVersion: INSTANCE_CONTROL_VERSION, instanceId: instance.id, runId: owner.runId, pid: process.pid, launcher: owner.launcher, appParentPid: parent ?? null, executionMode: options.background ? 'background' : 'foreground', phase: 'starting', port: null, active: null, problem: null, ...identity };
170
249
  let worker = null;
171
250
  let portLease = null;
172
251
  let server = null;
173
252
  let ending = false;
174
253
  let expectedWorkerExit = false;
175
254
  let sequence = Promise.resolve();
255
+ let stopRequest = null;
256
+ const clients = new Set();
176
257
  let finish;
177
258
  const finished = new Promise(r => { finish = r; });
178
259
  const serialize = (fn) => { const next = sequence.then(fn); sequence = next.catch(() => { }); return next; };
@@ -189,22 +270,40 @@ export async function runInstance(instance, options = {}) {
189
270
  if (!child)
190
271
  return;
191
272
  expectedWorkerExit = true;
192
- await new Promise(resolve => {
273
+ await new Promise((resolve, reject) => {
193
274
  if (child.exitCode !== null || child.signalCode !== null) {
194
275
  resolve();
195
276
  return;
196
277
  }
197
278
  let forced = false;
198
- const timer = setTimeout(() => { forced = true; child.kill('SIGKILL'); }, 8000);
199
- child.once('exit', () => { clearTimeout(timer); if (forced)
200
- state.problem = 'WORKER_STOP_FORCED'; resolve(); });
279
+ const finish = (error) => { clearTimeout(grace); clearTimeout(deadline); child.off('exit', exited); child.off('error', failed); error ? reject(error) : resolve(); };
280
+ const exited = () => { if (forced)
281
+ state.problem = 'WORKER_STOP_FORCED'; finish(); };
282
+ const failed = (error) => finish(error);
283
+ const grace = setTimeout(() => { forced = true; try {
284
+ child.kill('SIGKILL');
285
+ }
286
+ catch (e) {
287
+ finish(e);
288
+ } }, 8000);
289
+ const deadline = setTimeout(() => finish(Error('INSTANCE_WORKER_STOP_TIMEOUT')), 12000);
290
+ child.once('exit', exited);
291
+ child.once('error', failed);
201
292
  if (child.connected)
202
- child.send({ type: 'gateway-stop' });
293
+ child.send({ type: 'gateway-stop' }, error => { if (error && child.exitCode === null && child.signalCode === null)
294
+ try {
295
+ child.kill('SIGTERM');
296
+ }
297
+ catch (e) {
298
+ finish(e);
299
+ } });
203
300
  else
204
301
  child.kill('SIGTERM');
205
302
  });
206
303
  worker = null;
207
304
  expectedWorkerExit = false;
305
+ state.active = null;
306
+ state.port = null;
208
307
  }
209
308
  async function shutdown(reason) {
210
309
  if (ending)
@@ -215,11 +314,24 @@ export async function runInstance(instance, options = {}) {
215
314
  await stopWorker();
216
315
  await portLease?.release();
217
316
  portLease = null;
218
- state.phase = 'stopped';
219
317
  state.port = null;
220
- emit('INSTANCE_STOPPED');
221
318
  finish();
222
319
  }
320
+ function requestStop(reason) {
321
+ if (!stopRequest) {
322
+ stopRequest = serialize(() => shutdown(reason));
323
+ void stopRequest.catch(async (error) => {
324
+ state.phase = 'error';
325
+ state.problem = 'INSTANCE_STOP_FAILED: ' + error.message;
326
+ emit('INSTANCE_ERROR');
327
+ try {
328
+ await atomicPrivate(path.join(instancePaths(instance, home).snapshots, 'last-runtime.json'), JSON.stringify(state));
329
+ }
330
+ catch { }
331
+ });
332
+ }
333
+ return stopRequest;
334
+ }
223
335
  async function startWorker(config, lease) {
224
336
  await ensureAccessReady(await resolveInstance(instance.id, home), home);
225
337
  if (!await availablePort(config.config.port))
@@ -228,10 +340,10 @@ export async function runInstance(instance, options = {}) {
228
340
  for (const key of childEnvKeys)
229
341
  if (process.env[key])
230
342
  env[key] = process.env[key];
231
- Object.assign(env, instanceEnv(instance, config.config, home), { MAC_MCP_INTERNAL_SETUP_OWNER_PID: String(process.pid), MAC_MCP_INTERNAL_SETUP_RUN_ID: lease.owner.runId, MAC_MCP_INTERNAL_SETUP_TOKEN: lease.owner.token });
343
+ Object.assign(env, instanceEnv(instance, config.config, home), { STARMUX_INTERNAL_SETUP_OWNER_PID: String(process.pid), STARMUX_INTERNAL_SETUP_RUN_ID: lease.owner.runId, STARMUX_INTERNAL_SETUP_TOKEN: lease.owner.token });
232
344
  const upstreamFile = await prepareUpstreams(instance, config.config, home);
233
345
  if (upstreamFile)
234
- env.MAC_MCP_UPSTREAMS_FILE = upstreamFile;
346
+ env.STARMUX_UPSTREAMS_FILE = upstreamFile;
235
347
  const log = path.join(instancePaths(instance, home).logs, 'core.log');
236
348
  if (await exists(log))
237
349
  await readTrusted(log, true, 32 * 1024 * 1024);
@@ -254,6 +366,7 @@ export async function runInstance(instance, options = {}) {
254
366
  child.on('message', ready);
255
367
  });
256
368
  state.active = config;
369
+ state.permissionMode = env.STARMUX_PERMISSION_MODE === 'global' ? 'global' : 'workspace';
257
370
  state.port = config.config.port;
258
371
  state.phase = 'running';
259
372
  lease.markRunning();
@@ -262,13 +375,13 @@ export async function runInstance(instance, options = {}) {
262
375
  state.phase = 'error';
263
376
  state.problem = 'INSTANCE_WORKER_EXITED';
264
377
  emit('INSTANCE_ERROR');
265
- void serialize(() => shutdown('INSTANCE_WORKER_EXITED'));
378
+ void requestStop('INSTANCE_WORKER_EXITED');
266
379
  } });
267
380
  emit('INSTANCE_RUNNING');
268
381
  }
269
382
  const active = (loaded) => ({ config: loaded.config, revision: loaded.revision, effectiveRevision: loaded.effectiveRevision, sourcePath: loaded.source.path });
270
383
  async function apply(revision) {
271
- if (ending)
384
+ if (ending || stopRequest)
272
385
  throw Error('INSTANCE_STOPPING');
273
386
  const current = await resolveInstance(instance.id, home);
274
387
  const next = await loadInstanceConfig(current);
@@ -285,7 +398,7 @@ export async function runInstance(instance, options = {}) {
285
398
  if (!await availablePort(next.config.port))
286
399
  throw Error('PORT_IN_USE: ' + next.config.port);
287
400
  candidateLease = await acquireControl(next.config.port, { launcher: owner.launcher });
288
- candidateLease.onStop(async () => { void serialize(shutdown); return 'STOPPED'; });
401
+ candidateLease.onStop(() => requestStop());
289
402
  }
290
403
  state.phase = 'starting';
291
404
  await stopWorker();
@@ -314,7 +427,7 @@ export async function runInstance(instance, options = {}) {
314
427
  throw Error(state.problem);
315
428
  }
316
429
  }
317
- const signal = () => { void serialize(shutdown); };
430
+ const signal = () => { void requestStop(); };
318
431
  const parentTimer = parent ? setInterval(() => { if (process.ppid !== parent)
319
432
  signal(); }, 400) : null;
320
433
  parentTimer?.unref();
@@ -322,6 +435,8 @@ export async function runInstance(instance, options = {}) {
322
435
  process.on('SIGINT', signal);
323
436
  try {
324
437
  server = createServer({ allowHalfOpen: true }, socket => {
438
+ clients.add(socket);
439
+ socket.once('close', () => clients.delete(socket));
325
440
  socket.setTimeout(65000, () => socket.destroy());
326
441
  let raw = '';
327
442
  let handled = false;
@@ -343,21 +458,25 @@ export async function runInstance(instance, options = {}) {
343
458
  const envelope = JSON.parse(raw);
344
459
  if (typeof envelope.body !== 'string' || !equal(envelope.proof, signature(owner.token, envelope.body)))
345
460
  throw Error('CONTROL_DENIED');
346
- const req = JSON.parse(envelope.body);
347
- nonce = req.nonce;
348
- if (req.runId !== owner.runId || !/^[a-f0-9]{48}$/.test(nonce))
461
+ const parsed = JSON.parse(envelope.body);
462
+ // Recover the nonce before validating the version so an authenticated
463
+ // caller receives an explicit protocol error rather than a bad proof.
464
+ if (parsed && typeof parsed === 'object' && 'nonce' in parsed && typeof parsed.nonce === 'string')
465
+ nonce = parsed.nonce;
466
+ checkInstanceProtocol(parsed);
467
+ const req = controlRequestSchema.parse(parsed);
468
+ if (req.runId !== owner.runId)
349
469
  throw Error('CONTROL_DENIED');
350
- if (req.method === 'stop')
351
- await serialize(shutdown);
352
- else if (req.method === 'apply')
470
+ if (req.method === 'apply')
353
471
  await serialize(() => apply(req.revision));
354
- else if (req.method !== 'status')
355
- throw Error('CONTROL_INVALID_METHOD');
356
- const body = JSON.stringify({ state });
357
- socket.end(JSON.stringify({ body, proof: signature(owner.token, nonce + body) }) + '\n');
472
+ const body = JSON.stringify({ protocolVersion: INSTANCE_CONTROL_VERSION, state, result: req.method === 'stop' ? 'STOP_ACCEPTED' : 'STATE' });
473
+ socket.end(JSON.stringify({ body, proof: signature(owner.token, nonce + body) }) + '\n', () => {
474
+ if (req.method === 'stop')
475
+ void requestStop();
476
+ });
358
477
  }
359
478
  catch (error) {
360
- const body = JSON.stringify({ state, error: error.message });
479
+ const body = JSON.stringify({ protocolVersion: INSTANCE_CONTROL_VERSION, state, result: 'STATE', error: error.message });
361
480
  socket.end(JSON.stringify({ body, proof: signature(owner.token, nonce + body) }) + '\n');
362
481
  }
363
482
  })();
@@ -365,7 +484,7 @@ export async function runInstance(instance, options = {}) {
365
484
  });
366
485
  await new Promise((resolve, reject) => { server.once('error', reject); server.listen(p.socket, () => resolve()); });
367
486
  portLease = await acquireControl(initial.config.port, { launcher: owner.launcher });
368
- portLease.onStop(async () => { void serialize(shutdown); return 'STOPPED'; });
487
+ portLease.onStop(() => requestStop());
369
488
  await serialize(() => startWorker(active(initial), portLease));
370
489
  if (options.onReady)
371
490
  await options.onReady();
@@ -383,14 +502,34 @@ export async function runInstance(instance, options = {}) {
383
502
  clearInterval(parentTimer);
384
503
  process.off('SIGTERM', signal);
385
504
  process.off('SIGINT', signal);
386
- await stopWorker();
387
- await portLease?.release();
388
- if (server)
389
- await new Promise(resolve => server.close(() => resolve()));
390
- await atomicPrivate(path.join(instancePaths(instance, home).snapshots, 'last-runtime.json'), JSON.stringify(state));
391
- const current = await ownerFor(instance);
392
- if (current?.runId === owner.runId)
393
- await rm(p.root, { recursive: true, force: true });
505
+ try {
506
+ await stopWorker();
507
+ await portLease?.release();
508
+ if (server)
509
+ await new Promise((resolve, reject) => {
510
+ const timer = setTimeout(() => { for (const client of clients)
511
+ client.destroy(); }, 1500);
512
+ server.close(error => { clearTimeout(timer); error ? reject(error) : resolve(); });
513
+ });
514
+ if (ending)
515
+ state.phase = 'stopped';
516
+ // Commit completion last. A stop acknowledgement only means accepted;
517
+ // readers require this snapshot AND release of this run's owner record.
518
+ await atomicPrivate(path.join(instancePaths(instance, home).snapshots, 'last-runtime.json'), JSON.stringify(state));
519
+ const current = await ownerFor(instance);
520
+ if (current?.runId === owner.runId)
521
+ await rm(p.root, { recursive: true, force: true });
522
+ if (state.phase === 'stopped')
523
+ emit('INSTANCE_STOPPED');
524
+ }
525
+ catch (error) {
526
+ state.phase = 'error';
527
+ state.problem = 'INSTANCE_CLEANUP_FAILED: ' + error.message;
528
+ try {
529
+ await atomicPrivate(path.join(instancePaths(instance, home).snapshots, 'last-runtime.json'), JSON.stringify(state));
530
+ }
531
+ catch { }
532
+ throw error;
533
+ }
394
534
  }
395
535
  }
396
- //# sourceMappingURL=instance-runtime.js.map
@@ -10,8 +10,10 @@ import { z } from 'zod';
10
10
  import { zodToJsonSchema } from 'zod-to-json-schema';
11
11
  import { validateConfiguredUpstreams } from './configured-mcp-upstreams.js';
12
12
  import { createPrivateDirectory, ownedByUser } from './platform.js';
13
- import { checkSetupValues, setupRuntimeEnv } from './release1-onboarding.js';
13
+ import { supportedHost } from './platform.js';
14
+ import { createProjectPolicy } from './project-workspace-policy.js';
14
15
  import { createAccessControl } from './access-control.js';
16
+ import { createAccountAccessPolicy } from './account-access-policy.js';
15
17
  const nameSchema = z.string().trim().min(1).max(80).regex(/^[^\x00-\x1f\x7f/\\]+$/);
16
18
  const sourceSchema = z.object({ kind: z.enum(['managed', 'linked']), path: z.string() }).strict();
17
19
  export const instanceSchema = z.object({ id: z.string().uuid(), name: nameSchema, source: sourceSchema, archived: z.boolean().default(false),
@@ -118,17 +120,6 @@ export function parseConfig(text) {
118
120
  const prefix = text.slice(0, e.offset);
119
121
  throw Error(`CONFIG_JSON_INVALID line ${prefix.split('\n').length}, column ${e.offset - (prefix.lastIndexOf('\n') + 1) + 1}: ${printParseErrorCode(e.error)}`);
120
122
  }
121
- // Import the three historical onboarding formats without changing their authority.
122
- if (value && typeof value === 'object' && 'version' in value) {
123
- const legacy = value;
124
- if (![1, 2, 3].includes(Number(legacy.version)))
125
- throw Error('CONFIG_VERSION_UNSUPPORTED');
126
- const allowed = new Set(['version', 'projectRoot', 'mode', 'connection', 'port', 'accessRole', 'writeScope']);
127
- if (Object.keys(legacy).some(k => !allowed.has(k)))
128
- throw Error('CONFIG_UNKNOWN_LEGACY_FIELD');
129
- value = { schemaVersion: 1, projectRoot: legacy.projectRoot, mode: legacy.mode, connection: legacy.connection, port: legacy.port,
130
- accessRole: legacy.version === 1 ? 'restricted' : legacy.accessRole, writeScope: legacy.version === 3 ? 'home-only' : 'unrestricted' };
131
- }
132
123
  const result = instanceConfigSchema.safeParse(value);
133
124
  if (!result.success)
134
125
  throw Error('CONFIG_INVALID: ' + result.error.issues.map(i => `${i.path.join('.') || 'config'}: ${i.message}`).join('; '));
@@ -138,8 +129,19 @@ export async function validateConfig(config) {
138
129
  const { accessRole, writeScope } = config;
139
130
  if (writeScope === 'home-only' && (accessRole !== 'trusted' || config.upstreams.length || config.gitMode !== 'off'))
140
131
  throw Error('CONFIG_INVALID writeScope: home-only requires trusted mode without extra upstreams or Gateway Git');
141
- const legacy = { projectRoot: config.projectRoot, mode: config.mode, port: config.port, connection: config.connection === 'local' ? 'https' : config.connection };
142
- await checkSetupValues(accessRole === 'restricted' && config.mode === 'read-write' ? { ...legacy, version: 1 } : writeScope === 'home-only' ? { ...legacy, version: 3, accessRole, writeScope } : { ...legacy, version: 2, accessRole });
132
+ supportedHost();
133
+ instanceConfigSchema.parse(config);
134
+ if (!path.isAbsolute(config.projectRoot))
135
+ throw Error('CONFIG_INVALID projectRoot: absolute path required');
136
+ if (accessRole === 'trusted' && config.mode !== 'read-write')
137
+ throw Error('CONFIG_ACCESS_ROLE_MISMATCH');
138
+ if (writeScope === 'home-only' && process.platform !== 'darwin')
139
+ throw Error('HOME_WRITE_SANDBOX_PLATFORM_UNSUPPORTED');
140
+ if (usesGlobalPermissions(config))
141
+ await createAccountAccessPolicy(config.projectRoot, config.mode);
142
+ else
143
+ await createProjectPolicy({ root: config.projectRoot, mode: config.mode,
144
+ developerCommands: accessRole === 'trusted' ? 'trusted-project' : 'presets', allowTrustedHomeRoot: accessRole === 'trusted' });
143
145
  validateConfiguredUpstreams({ version: 1, upstreams: config.upstreams.map(({ envRefs, ...u }) => ({ ...u, env: Object.fromEntries(Object.keys(envRefs).map(k => [k, 'reference'])) })) });
144
146
  return config;
145
147
  }
@@ -185,7 +187,7 @@ export async function createInstance(name, options = {}, home = homedir()) {
185
187
  const id = randomUUID();
186
188
  const root = path.join(instancesRoot(home), 'instances', id);
187
189
  const instance = instanceSchema.parse({ id, name, archived: false, requiredAccess: options.requiredAccess ?? 'ingress',
188
- source: { kind: options.link ? 'linked' : 'managed', path: options.link ? input : path.join(root, 'config.jsonc') }, ...(options.legacy ? { legacy: options.legacy } : {}) });
190
+ source: { kind: options.link ? 'linked' : 'managed', path: options.link ? input : path.join(root, 'config.jsonc') } });
189
191
  createPrivateDirectory(path.join(instancesRoot(home), 'instances'));
190
192
  createPrivateDirectory(root);
191
193
  for (const dir of ['state', 'logs', 'snapshots'])
@@ -193,7 +195,7 @@ export async function createInstance(name, options = {}, home = homedir()) {
193
195
  try {
194
196
  await atomicPrivate(path.join(instancesRoot(home), 'gateway.schema.json'), JSON.stringify(configJSONSchema(), null, 2) + '\n');
195
197
  if (!options.link) {
196
- const original = text && !('version' in parse(text)) ? text : configTemplate(config);
198
+ const original = text ?? configTemplate(config);
197
199
  await atomicPrivate(instance.source.path, applyEdits(original, modify(original, ['$schema'], '../../gateway.schema.json', { formattingOptions: { insertSpaces: true, tabSize: 2 } })));
198
200
  }
199
201
  registry.instances.push(instance);
@@ -234,13 +236,18 @@ export async function patchInstanceConfig(instance, patch, revision, home = home
234
236
  text = applyEdits(text, modify(text, [key], value, { formattingOptions: { insertSpaces: true, tabSize: 2 } }));
235
237
  return saveInstanceConfig(instance, text, revision, home);
236
238
  }
239
+ export function usesGlobalPermissions(config) {
240
+ return config.writeScope === 'unrestricted' &&
241
+ (config.accessRole === 'restricted' && config.mode === 'read-only' || config.accessRole === 'trusted' && config.mode === 'read-write');
242
+ }
237
243
  export function instanceEnv(instance, config, home = homedir()) {
238
- const legacy = { projectRoot: config.projectRoot, mode: config.mode, port: config.port, connection: config.connection === 'local' ? 'https' : config.connection,
239
- ...(config.accessRole === 'restricted' && config.mode === 'read-write' ? { version: 1 } : config.writeScope === 'home-only' ? { version: 3, accessRole: config.accessRole, writeScope: 'home-only' } : { version: 2, accessRole: config.accessRole }) };
240
244
  const p = instancePaths(instance, home);
241
- return { ...setupRuntimeEnv(legacy), MAC_MCP_ACCESS_FILE: p.access, MAC_MCP_GRANTS_FILE: p.grants, MAC_MCP_DENY_FILE: p.deny,
242
- MAC_MCP_GIT_MODE: config.gitMode, MAC_MCP_WORKTREE_MODE: config.worktreeMode, MAC_MCP_INSTANCE_ID: instance.id, MAC_MCP_INSTANCE_NAME: instance.name,
243
- MAC_MCP_PRIVATE_ACTIVITY_OBSERVER: '1' };
245
+ return { STARMUX_PROJECT_ROOT: config.projectRoot, STARMUX_PROJECT_PORT: String(config.port),
246
+ STARMUX_WORKSPACE_MODE: config.mode, STARMUX_DEV_COMMANDS: config.accessRole === 'trusted' ? 'trusted-project' : 'presets',
247
+ STARMUX_DC_ACCESS_MODE: config.accessRole === 'trusted' ? 'native' : 'workspace', STARMUX_DC_WRITE_SCOPE: config.writeScope,
248
+ STARMUX_ACCESS_FILE: p.access, STARMUX_GRANTS_FILE: p.grants, STARMUX_DENY_FILE: p.deny,
249
+ STARMUX_GIT_MODE: config.gitMode, STARMUX_WORKTREE_MODE: config.worktreeMode, STARMUX_INSTANCE_ID: instance.id, STARMUX_INSTANCE_NAME: instance.name,
250
+ STARMUX_PRIVATE_ACTIVITY_OBSERVER: '1', STARMUX_PERMISSION_MODE: usesGlobalPermissions(config) ? 'global' : 'workspace' };
244
251
  }
245
252
  export async function ensureAccessReady(instance, home = homedir()) {
246
253
  const access = await createAccessControl(instancePaths(instance, home).access);
@@ -266,4 +273,3 @@ export async function prepareUpstreams(instance, config, home = homedir()) {
266
273
  await atomicPrivate(file, JSON.stringify({ version: 1, upstreams: definitions }));
267
274
  return file;
268
275
  }
269
- //# sourceMappingURL=instance-store.js.map
@@ -2,8 +2,7 @@ import { verifyInternalSetupWorker } from './global-core-control.js';
2
2
  import { runGateway } from './dc-workspace-http.js';
3
3
  if (!process.send)
4
4
  throw Error('INSTANCE_WORKER_REQUIRES_SUPERVISOR');
5
- verifyInternalSetupWorker({ port: Number(process.env.MAC_MCP_PROJECT_PORT), ownerPid: Number(process.env.MAC_MCP_INTERNAL_SETUP_OWNER_PID),
6
- runId: process.env.MAC_MCP_INTERNAL_SETUP_RUN_ID, token: process.env.MAC_MCP_INTERNAL_SETUP_TOKEN });
5
+ verifyInternalSetupWorker({ port: Number(process.env.STARMUX_PROJECT_PORT), ownerPid: Number(process.env.STARMUX_INTERNAL_SETUP_OWNER_PID),
6
+ runId: process.env.STARMUX_INTERNAL_SETUP_RUN_ID, token: process.env.STARMUX_INTERNAL_SETUP_TOKEN });
7
7
  await runGateway();
8
8
  process.send({ type: 'gateway-ready' });
9
- //# sourceMappingURL=instance-worker.js.map
@@ -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 Mac owner Dashboard /filters' }] };
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