staff-mcp 1.3.0 → 2.0.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 (94) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +152 -55
  3. package/README_zh.md +155 -53
  4. package/dist/src/data/HttpDataSession.d.ts +22 -0
  5. package/dist/src/data/HttpDataSession.js +925 -0
  6. package/dist/src/data/HttpDataSession.js.map +1 -0
  7. package/dist/src/data/node.d.ts +4 -0
  8. package/dist/src/data/node.js +93 -0
  9. package/dist/src/data/node.js.map +1 -0
  10. package/dist/src/data/types.d.ts +54 -0
  11. package/dist/src/data/types.js +2 -0
  12. package/dist/src/data/types.js.map +1 -0
  13. package/dist/src/data-stdio-docker.d.ts +10 -0
  14. package/dist/src/data-stdio-docker.js +140 -0
  15. package/dist/src/data-stdio-docker.js.map +1 -0
  16. package/dist/src/docker-proxy.d.ts +6 -1
  17. package/dist/src/docker-proxy.js +324 -147
  18. package/dist/src/docker-proxy.js.map +1 -1
  19. package/dist/src/extensions/apps.d.ts +12 -0
  20. package/dist/src/extensions/apps.js +85 -0
  21. package/dist/src/extensions/apps.js.map +1 -0
  22. package/dist/src/extensions/config.d.ts +27 -0
  23. package/dist/src/extensions/config.js +91 -0
  24. package/dist/src/extensions/config.js.map +1 -0
  25. package/dist/src/extensions/protocol.d.ts +35 -0
  26. package/dist/src/extensions/protocol.js +47 -0
  27. package/dist/src/extensions/protocol.js.map +1 -0
  28. package/dist/src/extensions/runtime.d.ts +16 -0
  29. package/dist/src/extensions/runtime.js +266 -0
  30. package/dist/src/extensions/runtime.js.map +1 -0
  31. package/dist/src/file-manager/extension.d.ts +21 -0
  32. package/dist/src/file-manager/extension.js +514 -0
  33. package/dist/src/file-manager/extension.js.map +1 -0
  34. package/dist/src/file-manager/file-core.d.ts +128 -0
  35. package/dist/src/file-manager/file-core.js +750 -0
  36. package/dist/src/file-manager/file-core.js.map +1 -0
  37. package/dist/src/file-manager/transfers.d.ts +423 -0
  38. package/dist/src/file-manager/transfers.js +540 -0
  39. package/dist/src/file-manager/transfers.js.map +1 -0
  40. package/dist/src/file-manager/ui/index.html +81 -0
  41. package/dist/src/index.js +276 -131
  42. package/dist/src/index.js.map +1 -1
  43. package/dist/src/reverse/SseTransport.d.ts +34 -0
  44. package/dist/src/reverse/SseTransport.js +215 -0
  45. package/dist/src/reverse/SseTransport.js.map +1 -0
  46. package/dist/src/reverse/http.d.ts +7 -0
  47. package/dist/src/reverse/http.js +85 -0
  48. package/dist/src/reverse/http.js.map +1 -0
  49. package/dist/src/server.d.ts +4 -2
  50. package/dist/src/server.js +80 -18
  51. package/dist/src/server.js.map +1 -1
  52. package/dist/src/terminal/extension.d.ts +4 -0
  53. package/dist/src/terminal/extension.js +108 -0
  54. package/dist/src/terminal/extension.js.map +1 -0
  55. package/dist/src/terminal/pty.d.ts +14 -0
  56. package/dist/src/terminal/pty.js +121 -0
  57. package/dist/src/terminal/pty.js.map +1 -0
  58. package/dist/src/terminal/session.d.ts +136 -0
  59. package/dist/src/terminal/session.js +422 -0
  60. package/dist/src/terminal/session.js.map +1 -0
  61. package/dist/src/terminal/ui/index.html +183 -0
  62. package/dist/src/tools/file-tools.d.ts +2 -1
  63. package/dist/src/tools/file-tools.js +19 -7
  64. package/dist/src/tools/file-tools.js.map +1 -1
  65. package/dist/src/tools/mcp-client-tools.js +243 -103
  66. package/dist/src/tools/mcp-client-tools.js.map +1 -1
  67. package/dist/src/tools/shell-tools.js +1 -1
  68. package/dist/src/tools/shell-tools.js.map +1 -1
  69. package/dist/src/tools/skills.js +26 -1
  70. package/dist/src/tools/skills.js.map +1 -1
  71. package/dist/src/transports/http.d.ts +15 -2
  72. package/dist/src/transports/http.js +244 -65
  73. package/dist/src/transports/http.js.map +1 -1
  74. package/dist/src/transports/reverse.d.ts +23 -19
  75. package/dist/src/transports/reverse.js +130 -74
  76. package/dist/src/transports/reverse.js.map +1 -1
  77. package/dist/src/transports/stdio.d.ts +5 -2
  78. package/dist/src/transports/stdio.js +130 -20
  79. package/dist/src/transports/stdio.js.map +1 -1
  80. package/dist/src/utils/proxy.d.ts +2 -2
  81. package/dist/src/utils/proxy.js +84 -62
  82. package/dist/src/utils/proxy.js.map +1 -1
  83. package/dist/src/utils/server-activity.d.ts +6 -0
  84. package/dist/src/utils/server-activity.js +46 -0
  85. package/dist/src/utils/server-activity.js.map +1 -0
  86. package/dist/src/utils/server-lifecycle.d.ts +5 -0
  87. package/dist/src/utils/server-lifecycle.js +51 -0
  88. package/dist/src/utils/server-lifecycle.js.map +1 -0
  89. package/docs/file-manager.md +179 -0
  90. package/docs/terminal.md +66 -0
  91. package/package.json +28 -6
  92. package/dist/src/transports/fte-lifecycle.d.ts +0 -13
  93. package/dist/src/transports/fte-lifecycle.js +0 -76
  94. package/dist/src/transports/fte-lifecycle.js.map +0 -1
@@ -1,9 +1,10 @@
1
- import { spawn } from "node:child_process";
2
- import { randomBytes } from "node:crypto";
3
- import * as fs from "node:fs";
4
- import * as os from "node:os";
5
- import * as path from "node:path";
6
- const HANDLED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
1
+ import { spawn } from 'node:child_process';
2
+ import { randomBytes } from 'node:crypto';
3
+ import * as fs from 'node:fs';
4
+ import * as os from 'node:os';
5
+ import * as path from 'node:path';
6
+ import { assertLocalDockerDaemon, DOCKER_DATA_STARTUP_TIMEOUT_MS, prepareDockerDataArgs, publishDockerDataBaseUrl, failDockerDataBootstrap, } from './data-stdio-docker.js';
7
+ const HANDLED_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP'];
7
8
  const DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_MS = 20_000;
8
9
  const DEFAULT_CONTAINER_ID_WAIT_TIMEOUT_MS = 1_000;
9
10
  const DEFAULT_CONTAINER_CLEANUP_TIMEOUT_MS = 20_000;
@@ -11,27 +12,27 @@ const DEFAULT_FORCE_KILL_TIMEOUT_MS = 5_000;
11
12
  const CONTAINER_ID_POLL_INTERVAL_MS = 50;
12
13
  function exitCodeFromSignal(signal) {
13
14
  switch (signal) {
14
- case "SIGHUP":
15
+ case 'SIGHUP':
15
16
  return 129;
16
- case "SIGINT":
17
+ case 'SIGINT':
17
18
  return 130;
18
- case "SIGKILL":
19
+ case 'SIGKILL':
19
20
  return 137;
20
- case "SIGTERM":
21
+ case 'SIGTERM':
21
22
  return 143;
22
23
  default:
23
24
  return 1;
24
25
  }
25
26
  }
26
27
  function hasManagedIdentityArg(args) {
27
- return args.some((arg) => arg === "--cidfile" ||
28
- arg.startsWith("--cidfile=") ||
29
- arg === "--name" ||
30
- arg.startsWith("--name="));
28
+ return args.some((arg) => arg === '--cidfile' ||
29
+ arg.startsWith('--cidfile=') ||
30
+ arg === '--name' ||
31
+ arg.startsWith('--name='));
31
32
  }
32
- const CONTAINER_NAME_PREFIX = "staff-mcp";
33
+ const CONTAINER_NAME_PREFIX = 'staff-mcp';
33
34
  const CONTAINER_NAME_HINT_MAX_LENGTH = 40;
34
- const NAME_SEGMENT_FALLBACK = "session";
35
+ const NAME_SEGMENT_FALLBACK = 'session';
35
36
  /**
36
37
  * Sanitize an arbitrary string into a Docker-name-safe segment.
37
38
  * Docker names only accept [a-zA-Z0-9][a-zA-Z0-9_.-], so everything else is
@@ -40,11 +41,11 @@ const NAME_SEGMENT_FALLBACK = "session";
40
41
  export function sanitizeNameSegment(value, maxLength = 24) {
41
42
  return value
42
43
  .toLowerCase()
43
- .replace(/[^a-z0-9_.-]+/g, "-")
44
- .replace(/-{2,}/g, "-")
45
- .replace(/^[-_.]+|[-_.]+$/g, "")
44
+ .replace(/[^a-z0-9_.-]+/g, '-')
45
+ .replace(/-{2,}/g, '-')
46
+ .replace(/^[-_.]+|[-_.]+$/g, '')
46
47
  .slice(0, maxLength)
47
- .replace(/[-_.]+$/g, "");
48
+ .replace(/[-_.]+$/g, '');
48
49
  }
49
50
  /**
50
51
  * Build a human-meaningful hint for the container name from launch context,
@@ -55,10 +56,10 @@ export function sanitizeNameSegment(value, maxLength = 24) {
55
56
  */
56
57
  export function buildDockerNameHint(parts) {
57
58
  const workspace = path.basename(parts.workspaceDir);
58
- if (parts.transport === "http" && parts.port) {
59
+ if (parts.transport === 'http' && parts.port) {
59
60
  return `${workspace}-${parts.port}`;
60
61
  }
61
- if (parts.transport === "reverse" && parts.reverseName) {
62
+ if (parts.transport === 'reverse' && parts.reverseName) {
62
63
  const workspaceSlug = sanitizeNameSegment(workspace);
63
64
  const reverseSlug = sanitizeNameSegment(parts.reverseName);
64
65
  if (workspaceSlug && workspaceSlug === reverseSlug) {
@@ -75,34 +76,34 @@ export function buildDockerNameHint(parts) {
75
76
  * rejected safely by Docker at container creation time.
76
77
  */
77
78
  function buildContainerName(nameHint) {
78
- const randomSuffix = randomBytes(3).toString("hex");
79
- const hint = sanitizeNameSegment(nameHint ?? "", CONTAINER_NAME_HINT_MAX_LENGTH);
79
+ const randomSuffix = randomBytes(3).toString('hex');
80
+ const hint = sanitizeNameSegment(nameHint ?? '', CONTAINER_NAME_HINT_MAX_LENGTH);
80
81
  const discriminator = hint || NAME_SEGMENT_FALLBACK;
81
82
  return `${CONTAINER_NAME_PREFIX}-${discriminator}-${randomSuffix}`;
82
83
  }
83
84
  function prepareContainerIdentity(dockerArgs, nameHint) {
84
85
  const args = [...dockerArgs];
85
86
  if (hasManagedIdentityArg(args)) {
86
- throw new Error("--cidfile and --name are managed internally by staff-mcp.");
87
+ throw new Error('--cidfile and --name are managed internally by staff-mcp.');
87
88
  }
88
- const runIndex = args.indexOf("run");
89
+ const runIndex = args.indexOf('run');
89
90
  if (runIndex < 0) {
90
91
  throw new Error("Docker proxy arguments must contain the 'run' subcommand.");
91
92
  }
92
- const ownedDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "staff-mcp-container-"));
93
- const cidFilePath = path.join(ownedDirectory, "container.cid");
93
+ const ownedDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'staff-mcp-container-'));
94
+ const cidFilePath = path.join(ownedDirectory, 'container.cid');
94
95
  const containerName = buildContainerName(nameHint);
95
- args.splice(runIndex + 1, 0, "--cidfile", cidFilePath, "--name", containerName);
96
+ args.splice(runIndex + 1, 0, '--cidfile', cidFilePath, '--name', containerName);
96
97
  return { args, cidFilePath, containerName, ownedDirectory };
97
98
  }
98
99
  function readContainerId(cidFilePath) {
99
100
  try {
100
- const containerId = fs.readFileSync(cidFilePath, "utf8").trim();
101
+ const containerId = fs.readFileSync(cidFilePath, 'utf8').trim();
101
102
  return /^[a-f0-9]{64}$/i.test(containerId) ? containerId : undefined;
102
103
  }
103
104
  catch (error) {
104
105
  const code = error.code;
105
- if (code === "ENOENT") {
106
+ if (code === 'ENOENT') {
106
107
  return undefined;
107
108
  }
108
109
  console.error(`[staff-mcp] Failed to read Docker cidfile '${cidFilePath}':`, error);
@@ -121,32 +122,82 @@ function isMissingContainerError(stderr) {
121
122
  * user-supplied Docker options only, because the command executed inside the
122
123
  * container can legitimately contain similarly named flags.
123
124
  */
124
- export function validateAdditionalDockerArgs(args) {
125
- for (const arg of args) {
125
+ export function validateAdditionalDockerArgs(args, dataAuto = false) {
126
+ for (const [index, arg] of args.entries()) {
126
127
  const normalized = arg.toLowerCase();
127
- const isShortDetachOption = (/^-[^-][a-z]*$/i.test(arg) && arg.slice(1).includes("d")) || normalized.startsWith("-d=");
128
+ if (dataAuto &&
129
+ (/STAFF_MCP_DATA_(?:BOOTSTRAP|HOST|PORT)/i.test(arg) ||
130
+ arg.includes('/run/staff-mcp-data.json') ||
131
+ /(?:^|[^0-9])39173(?:[^0-9]|$)/.test(arg))) {
132
+ throw new Error('Docker data bootstrap environment, mount and port are managed internally.');
133
+ }
134
+ const isShortDetachOption = (/^-[^-][a-z]*$/i.test(arg) && arg.slice(1).includes('d')) ||
135
+ normalized.startsWith('-d=');
128
136
  if (isShortDetachOption ||
129
- normalized === "--detach" ||
130
- (normalized.startsWith("--detach=") && normalized !== "--detach=false")) {
131
- throw new Error("detached containers are not supported by the staff-mcp Docker proxy");
137
+ normalized === '--detach' ||
138
+ (normalized.startsWith('--detach=') &&
139
+ normalized !== '--detach=false')) {
140
+ throw new Error('detached containers are not supported by the staff-mcp Docker proxy');
132
141
  }
133
- if (normalized.startsWith("--rm=") && !["--rm=true", "--rm=t", "--rm=1"].includes(normalized)) {
134
- throw new Error("--rm cannot be disabled by the staff-mcp Docker proxy");
142
+ if (normalized.startsWith('--rm=') &&
143
+ !['--rm=true', '--rm=t', '--rm=1'].includes(normalized)) {
144
+ throw new Error('--rm cannot be disabled by the staff-mcp Docker proxy');
135
145
  }
136
- if (normalized.startsWith("--interactive=") &&
137
- !["--interactive=true", "--interactive=t", "--interactive=1"].includes(normalized)) {
138
- throw new Error("interactive stdin cannot be disabled by the staff-mcp Docker proxy");
146
+ if (normalized.startsWith('--interactive=') &&
147
+ ![
148
+ '--interactive=true',
149
+ '--interactive=t',
150
+ '--interactive=1',
151
+ ].includes(normalized)) {
152
+ throw new Error('interactive stdin cannot be disabled by the staff-mcp Docker proxy');
139
153
  }
140
154
  if (/^-[^-]*i[^=]*=/.test(arg)) {
141
- throw new Error("assigned -i values are not supported; use -i or --interactive=true");
155
+ throw new Error('assigned -i values are not supported; use -i or --interactive=true');
156
+ }
157
+ if (normalized === '--cidfile' ||
158
+ normalized.startsWith('--cidfile=') ||
159
+ normalized === '--name' ||
160
+ normalized.startsWith('--name=')) {
161
+ throw new Error('--cidfile and --name are managed internally by staff-mcp');
142
162
  }
143
- if (normalized === "--cidfile" ||
144
- normalized.startsWith("--cidfile=") ||
145
- normalized === "--name" ||
146
- normalized.startsWith("--name=")) {
147
- throw new Error("--cidfile and --name are managed internally by staff-mcp");
163
+ }
164
+ }
165
+ /** Topology is a capability limit, not permission to bypass managed options. */
166
+ function unsupportedDataNetwork(args) {
167
+ return args.some((arg, index) => /^(?:--network|--net)=(?:host$|container:|none$)/i.test(arg) ||
168
+ (/^(?:--network|--net)$/i.test(arg) &&
169
+ /^(?:host$|container:|none$)/i.test(args[index + 1] ?? '')));
170
+ }
171
+ /** Validate requested permissions before reducing optional capabilities. */
172
+ export async function preflightDockerExtensions(requested, transport, additionalArgs) {
173
+ const requestedDataAuto = transport === 'stdio' && requested.length > 0;
174
+ validateAdditionalDockerArgs(additionalArgs, requestedDataAuto);
175
+ if (!requested.length)
176
+ return [];
177
+ let reason;
178
+ if (os.platform() !== 'linux') {
179
+ reason = 'Docker extensions require a compatible Linux host package';
180
+ }
181
+ else if (requestedDataAuto) {
182
+ if (unsupportedDataNetwork(additionalArgs)) {
183
+ reason =
184
+ 'Docker stdio data auto does not support host/container/none networking';
148
185
  }
186
+ else {
187
+ try {
188
+ await assertLocalDockerDaemon('docker', []);
189
+ }
190
+ catch {
191
+ reason =
192
+ 'Docker stdio data auto requires a verified local daemon; remote/TCP/SSH or unavailable contexts are unsupported';
193
+ }
194
+ }
195
+ }
196
+ if (reason) {
197
+ console.error(`[staff-mcp] Disabling requested Docker extensions (${requested.join(',')}): ${reason}. Basic MCP tools/control remain enabled.`);
198
+ return [];
149
199
  }
200
+ return [...requested];
150
201
  }
151
202
  /**
152
203
  * Run the Docker-compatible CLI and keep its lifecycle tied to this process.
@@ -158,17 +209,38 @@ export function validateAdditionalDockerArgs(args) {
158
209
  * still making the container exit if the host proxy disappears unexpectedly.
159
210
  */
160
211
  export async function runDockerProxy(dockerArgs, transport, options = {}) {
161
- const command = options.command ?? "docker";
212
+ const command = options.command ?? 'docker';
162
213
  const commandPrefixArgs = [...(options.commandPrefixArgs ?? [])];
163
214
  const input = options.input ?? process.stdin;
164
215
  const signalSource = options.signalSource ?? process;
165
- const gracefulShutdownTimeoutMs = options.gracefulShutdownTimeoutMs ?? DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_MS;
166
- const containerIdWaitTimeoutMs = options.containerIdWaitTimeoutMs ?? DEFAULT_CONTAINER_ID_WAIT_TIMEOUT_MS;
167
- const containerCleanupTimeoutMs = options.containerCleanupTimeoutMs ?? DEFAULT_CONTAINER_CLEANUP_TIMEOUT_MS;
216
+ const gracefulShutdownTimeoutMs = options.gracefulShutdownTimeoutMs ??
217
+ DEFAULT_GRACEFUL_SHUTDOWN_TIMEOUT_MS;
218
+ const containerIdWaitTimeoutMs = options.containerIdWaitTimeoutMs ??
219
+ DEFAULT_CONTAINER_ID_WAIT_TIMEOUT_MS;
220
+ const containerCleanupTimeoutMs = options.containerCleanupTimeoutMs ??
221
+ DEFAULT_CONTAINER_CLEANUP_TIMEOUT_MS;
168
222
  const forceKillTimeoutMs = options.forceKillTimeoutMs ?? DEFAULT_FORCE_KILL_TIMEOUT_MS;
223
+ if (options.dataAuto &&
224
+ (transport !== 'stdio' || unsupportedDataNetwork(dockerArgs))) {
225
+ console.error('[staff-mcp] Docker data auto requires stdio with supported publication networking.');
226
+ return 1;
227
+ }
228
+ let metadataPath;
169
229
  let containerIdentity;
170
230
  try {
171
231
  containerIdentity = prepareContainerIdentity(dockerArgs, options.nameHint);
232
+ if (options.dataAuto) {
233
+ try {
234
+ metadataPath = prepareDockerDataArgs(containerIdentity.args, containerIdentity.ownedDirectory);
235
+ }
236
+ catch (error) {
237
+ fs.rmSync(containerIdentity.ownedDirectory, {
238
+ recursive: true,
239
+ force: true,
240
+ });
241
+ throw error;
242
+ }
243
+ }
172
244
  }
173
245
  catch (error) {
174
246
  const message = error instanceof Error ? error.message : String(error);
@@ -192,6 +264,10 @@ export async function runDockerProxy(dockerArgs, transport, options = {}) {
192
264
  let postCleanupTimer;
193
265
  let postCleanupFinalTimer;
194
266
  let cidPollTimer;
267
+ let startupTimer;
268
+ let parentWatch;
269
+ let bootstrapPending = metadataPath !== undefined;
270
+ const startupAbort = new AbortController();
195
271
  const signalHandlers = new Map();
196
272
  const captureContainerId = () => {
197
273
  if (!trackedContainerId) {
@@ -199,16 +275,23 @@ export async function runDockerProxy(dockerArgs, transport, options = {}) {
199
275
  }
200
276
  return trackedContainerId;
201
277
  };
202
- const isChildRunning = () => child !== undefined && child.exitCode === null && child.signalCode === null;
278
+ const isChildRunning = () => child !== undefined &&
279
+ child.exitCode === null &&
280
+ child.signalCode === null;
203
281
  const removeListeners = () => {
204
282
  for (const [signal, handler] of signalHandlers) {
205
283
  signalSource.removeListener(signal, handler);
206
284
  }
207
285
  signalHandlers.clear();
208
- if (transport === "stdio" && child?.stdin) {
209
- input.unpipe(child.stdin);
210
- input.removeListener("end", handleInputEnd);
211
- input.removeListener("error", handleInputError);
286
+ if (transport === 'stdio') {
287
+ if (child?.stdin)
288
+ input.unpipe(child.stdin);
289
+ input.removeListener('end', handleInputEnd);
290
+ input.removeListener('error', handleInputError);
291
+ }
292
+ if (metadataPath) {
293
+ process.stdout.removeListener('error', handleOutputError);
294
+ process.stderr.removeListener('error', handleOutputError);
212
295
  }
213
296
  };
214
297
  const finish = (exitCode) => {
@@ -216,6 +299,7 @@ export async function runDockerProxy(dockerArgs, transport, options = {}) {
216
299
  return;
217
300
  }
218
301
  settled = true;
302
+ startupAbort.abort();
219
303
  if (gracefulTimer) {
220
304
  clearTimeout(gracefulTimer);
221
305
  }
@@ -228,9 +312,16 @@ export async function runDockerProxy(dockerArgs, transport, options = {}) {
228
312
  if (cidPollTimer) {
229
313
  clearInterval(cidPollTimer);
230
314
  }
315
+ if (startupTimer)
316
+ clearTimeout(startupTimer);
317
+ if (parentWatch)
318
+ clearInterval(parentWatch);
231
319
  removeListeners();
232
320
  try {
233
- fs.rmSync(containerIdentity.ownedDirectory, { recursive: true, force: true });
321
+ fs.rmSync(containerIdentity.ownedDirectory, {
322
+ recursive: true,
323
+ force: true,
324
+ });
234
325
  }
235
326
  catch (error) {
236
327
  console.error(`[staff-mcp] Failed to remove temporary cidfile directory '${containerIdentity.ownedDirectory}':`, error);
@@ -241,13 +332,15 @@ export async function runDockerProxy(dockerArgs, transport, options = {}) {
241
332
  if (!runExit || cleanupInProgress) {
242
333
  return;
243
334
  }
244
- finish(cleanupFailed ? 1 : (runExit.code ?? exitCodeFromSignal(runExit.signal)));
335
+ finish(cleanupFailed
336
+ ? 1
337
+ : (runExit.code ?? exitCodeFromSignal(runExit.signal)));
245
338
  };
246
339
  const closeLifecyclePipe = () => {
247
340
  if (!child?.stdin) {
248
341
  return;
249
342
  }
250
- if (transport === "stdio") {
343
+ if (transport === 'stdio') {
251
344
  input.unpipe(child.stdin);
252
345
  }
253
346
  if (!child.stdin.destroyed && !child.stdin.writableEnded) {
@@ -269,7 +362,7 @@ export async function runDockerProxy(dockerArgs, transport, options = {}) {
269
362
  let cleanupTimer;
270
363
  let cleanupForceTimer;
271
364
  let cleanupFinalTimer;
272
- let stderr = "";
365
+ let stderr = '';
273
366
  const complete = (success) => {
274
367
  if (completed) {
275
368
  return;
@@ -288,8 +381,8 @@ export async function runDockerProxy(dockerArgs, transport, options = {}) {
288
381
  resolveCleanup(success);
289
382
  };
290
383
  try {
291
- cleanupChild = spawn(command, [...commandPrefixArgs, "rm", "-f", containerId], {
292
- stdio: ["ignore", "ignore", "pipe"],
384
+ cleanupChild = spawn(command, [...commandPrefixArgs, 'rm', '-f', containerId], {
385
+ stdio: ['ignore', 'ignore', 'pipe'],
293
386
  });
294
387
  }
295
388
  catch (error) {
@@ -297,21 +390,21 @@ export async function runDockerProxy(dockerArgs, transport, options = {}) {
297
390
  complete(false);
298
391
  return;
299
392
  }
300
- cleanupChild.once("spawn", () => {
393
+ cleanupChild.once('spawn', () => {
301
394
  cleanupSpawned = true;
302
395
  });
303
- cleanupChild.stderr?.on("data", (chunk) => {
396
+ cleanupChild.stderr?.on('data', (chunk) => {
304
397
  if (stderr.length < 8_192) {
305
398
  stderr += chunk.toString();
306
399
  }
307
400
  });
308
- cleanupChild.once("error", (error) => {
401
+ cleanupChild.once('error', (error) => {
309
402
  console.error(`[staff-mcp] Failed to run fallback container cleanup for ${containerId}: ${error.message}`);
310
403
  if (!cleanupSpawned && cleanupChild?.pid === undefined) {
311
404
  complete(false);
312
405
  }
313
406
  });
314
- cleanupChild.once("close", (code, signal) => {
407
+ cleanupChild.once('close', (code, signal) => {
315
408
  if (completed) {
316
409
  return;
317
410
  }
@@ -321,17 +414,17 @@ export async function runDockerProxy(dockerArgs, transport, options = {}) {
321
414
  complete(true);
322
415
  return;
323
416
  }
324
- console.error(`[staff-mcp] Fallback container cleanup failed for ${containerId}: code ${code ?? "null"}` +
325
- `${signal ? `, signal ${signal}` : ""}${detail ? `, ${detail}` : ""}`);
417
+ console.error(`[staff-mcp] Fallback container cleanup failed for ${containerId}: code ${code ?? 'null'}` +
418
+ `${signal ? `, signal ${signal}` : ''}${detail ? `, ${detail}` : ''}`);
326
419
  complete(false);
327
420
  });
328
421
  cleanupTimer = setTimeout(() => {
329
422
  console.error(`[staff-mcp] Fallback container cleanup for ${containerId} exceeded ${containerCleanupTimeoutMs}ms; sending SIGTERM.`);
330
- cleanupChild?.kill("SIGTERM");
423
+ cleanupChild?.kill('SIGTERM');
331
424
  cleanupForceTimer = setTimeout(() => {
332
425
  if (!completed) {
333
426
  console.error(`[staff-mcp] Fallback container cleanup for ${containerId} did not stop within an additional ${forceKillTimeoutMs}ms; sending SIGKILL.`);
334
- cleanupChild?.kill("SIGKILL");
427
+ cleanupChild?.kill('SIGKILL');
335
428
  cleanupFinalTimer = setTimeout(() => {
336
429
  if (!completed) {
337
430
  console.error(`[staff-mcp] Fallback container cleanup for ${containerId} did not report completion after SIGKILL.`);
@@ -346,16 +439,16 @@ export async function runDockerProxy(dockerArgs, transport, options = {}) {
346
439
  if (!isChildRunning() || postCleanupTimer) {
347
440
  return;
348
441
  }
349
- child.kill("SIGTERM");
442
+ child.kill('SIGTERM');
350
443
  postCleanupTimer = setTimeout(() => {
351
444
  if (!isChildRunning()) {
352
445
  return;
353
446
  }
354
447
  console.error(`[staff-mcp] Docker CLI did not exit within ${forceKillTimeoutMs}ms; sending SIGKILL.`);
355
- child.kill("SIGKILL");
448
+ child.kill('SIGKILL');
356
449
  postCleanupFinalTimer = setTimeout(() => {
357
450
  if (!runExit && isChildRunning()) {
358
- console.error("[staff-mcp] Docker CLI did not report completion after SIGKILL.");
451
+ console.error('[staff-mcp] Docker CLI did not report completion after SIGKILL.');
359
452
  cleanupFailed = true;
360
453
  runExit = { code: 1, signal: null };
361
454
  maybeFinish();
@@ -433,6 +526,7 @@ export async function runDockerProxy(dockerArgs, transport, options = {}) {
433
526
  }
434
527
  if (!shutdownStarted) {
435
528
  shutdownStarted = true;
529
+ startupAbort.abort();
436
530
  console.error(`[staff-mcp] Docker proxy shutdown requested: ${reason}`);
437
531
  activateShutdown();
438
532
  return;
@@ -443,101 +537,184 @@ export async function runDockerProxy(dockerArgs, transport, options = {}) {
443
537
  }
444
538
  };
445
539
  function handleInputEnd() {
446
- requestShutdown("stdio input closed");
540
+ requestShutdown('stdio input closed');
447
541
  }
448
542
  function handleInputError(error) {
449
543
  console.error(`[staff-mcp] Docker proxy input error: ${error.message}`);
450
- requestShutdown("stdio input error");
544
+ requestShutdown('stdio input error');
545
+ }
546
+ function handleOutputError() {
547
+ requestShutdown('parent output pipe closed');
548
+ }
549
+ if (transport === 'stdio') {
550
+ input.once('end', handleInputEnd);
551
+ input.once('error', handleInputError);
552
+ if ('readableEnded' in input && input.readableEnded)
553
+ handleInputEnd();
451
554
  }
452
555
  for (const signal of HANDLED_SIGNALS) {
453
556
  const handler = () => requestShutdown(`received ${signal}`, true);
454
557
  signalHandlers.set(signal, handler);
455
558
  signalSource.on(signal, handler);
456
559
  }
457
- try {
458
- child = spawn(command, [...commandPrefixArgs, ...containerIdentity.args], {
459
- stdio: ["pipe", "inherit", "inherit"],
460
- });
461
- }
462
- catch (error) {
463
- const message = error instanceof Error ? error.message : String(error);
464
- console.error(`[staff-mcp] Failed to start docker proxy: ${message}`);
465
- finish(1);
466
- return;
467
- }
468
- if (!child.stdin) {
469
- console.error("[staff-mcp] Failed to start docker proxy: child stdin is unavailable.");
470
- child.kill("SIGKILL");
471
- finish(1);
472
- return;
473
- }
474
- child.stdin.on("error", (error) => {
475
- if (!settled && error.code !== "EPIPE") {
476
- console.error(`[staff-mcp] Docker proxy stdin error: ${error.message}`);
560
+ const failBootstrap = () => {
561
+ if (!bootstrapPending || settled || shutdownStarted)
562
+ return;
563
+ bootstrapPending = false;
564
+ if (startupTimer)
565
+ clearTimeout(startupTimer);
566
+ startupAbort.abort();
567
+ try {
568
+ failDockerDataBootstrap(metadataPath);
477
569
  }
478
- });
479
- child.once("error", (error) => {
480
- if (settled) {
570
+ catch {
571
+ // The reader also fails closed on unreadable/missing metadata or timeout.
572
+ console.error('[staff-mcp] Could not record Docker data bootstrap failure (details suppressed).');
573
+ }
574
+ console.error('[staff-mcp] Docker data bootstrap unavailable; extension data configure will fail. Basic MCP tools/control remain enabled (details suppressed).');
575
+ };
576
+ const start = async () => {
577
+ if (metadataPath) {
578
+ try {
579
+ // Recheck at launch: never publish a remote daemon's loopback as ours.
580
+ await assertLocalDockerDaemon(command, commandPrefixArgs, startupAbort.signal);
581
+ }
582
+ catch {
583
+ failBootstrap();
584
+ }
585
+ }
586
+ if (settled || shutdownStarted) {
587
+ finish(0);
481
588
  return;
482
589
  }
483
- if (child?.pid === undefined) {
484
- console.error(`[staff-mcp] Failed to start docker proxy: ${error.message}`);
590
+ try {
591
+ child = spawn(command, [...commandPrefixArgs, ...containerIdentity.args], {
592
+ stdio: ['pipe', 'inherit', 'inherit'],
593
+ });
594
+ }
595
+ catch (error) {
596
+ const message = error instanceof Error ? error.message : String(error);
597
+ console.error(`[staff-mcp] Failed to start docker proxy: ${message}`);
485
598
  finish(1);
486
599
  return;
487
600
  }
488
- console.error(`[staff-mcp] Docker proxy process error: ${error.message}`);
489
- cleanupFailed = true;
490
- requestShutdown("Docker proxy process error", true);
491
- });
492
- child.once("close", (code, signal) => {
493
- if (settled) {
601
+ if (!child.stdin) {
602
+ console.error('[staff-mcp] Failed to start docker proxy: child stdin is unavailable.');
603
+ child.kill('SIGKILL');
604
+ finish(1);
494
605
  return;
495
606
  }
496
- captureContainerId();
497
- runExit = { code, signal };
498
- if (gracefulTimer) {
499
- clearTimeout(gracefulTimer);
500
- }
501
- if (postCleanupTimer) {
502
- clearTimeout(postCleanupTimer);
607
+ child.stdin.on('error', (error) => {
608
+ if (!settled && error.code !== 'EPIPE') {
609
+ console.error(`[staff-mcp] Docker proxy stdin error: ${error.message}`);
610
+ }
611
+ });
612
+ child.once('error', (error) => {
613
+ if (settled) {
614
+ return;
615
+ }
616
+ if (child?.pid === undefined) {
617
+ console.error(`[staff-mcp] Failed to start docker proxy: ${error.message}`);
618
+ finish(1);
619
+ return;
620
+ }
621
+ console.error(`[staff-mcp] Docker proxy process error: ${error.message}`);
622
+ cleanupFailed = true;
623
+ requestShutdown('Docker proxy process error', true);
624
+ });
625
+ child.once('close', (code, signal) => {
626
+ if (settled) {
627
+ return;
628
+ }
629
+ captureContainerId();
630
+ runExit = { code, signal };
631
+ if (gracefulTimer) {
632
+ clearTimeout(gracefulTimer);
633
+ }
634
+ if (postCleanupTimer) {
635
+ clearTimeout(postCleanupTimer);
636
+ }
637
+ if (postCleanupFinalTimer) {
638
+ clearTimeout(postCleanupFinalTimer);
639
+ }
640
+ console.error(`[staff-mcp] Docker proxy exited with code ${code ?? 'null'}${signal ? ` (signal ${signal})` : ''}.`);
641
+ // If a supervisor killed the Docker CLI directly, --rm may not have
642
+ // completed. Clean up by exact ID when available, otherwise use the
643
+ // unique generated name that was part of the container-create request.
644
+ if (!cleanupInProgress &&
645
+ !cleanupAttempted &&
646
+ !containerCleanupConfirmed &&
647
+ (signal !== null || code !== 0)) {
648
+ const cleanupReference = trackedContainerId ?? containerIdentity.containerName;
649
+ cleanupInProgress = true;
650
+ cleanupAttempted = true;
651
+ void forceRemoveContainer(cleanupReference)
652
+ .then((removed) => {
653
+ containerCleanupConfirmed = removed;
654
+ cleanupFailed = !removed;
655
+ })
656
+ .finally(() => {
657
+ cleanupInProgress = false;
658
+ maybeFinish();
659
+ });
660
+ return;
661
+ }
662
+ maybeFinish();
663
+ });
664
+ cidPollTimer = setInterval(captureContainerId, CONTAINER_ID_POLL_INTERVAL_MS);
665
+ cidPollTimer.unref();
666
+ if (transport === 'stdio') {
667
+ input.pipe(child.stdin);
503
668
  }
504
- if (postCleanupFinalTimer) {
505
- clearTimeout(postCleanupFinalTimer);
669
+ // A signal source can synchronously request shutdown while handlers are
670
+ // being installed (and a real OS signal can arrive during startup).
671
+ activateShutdown();
672
+ if (metadataPath) {
673
+ const cancelled = () => !bootstrapPending ||
674
+ settled ||
675
+ shutdownStarted ||
676
+ !isChildRunning();
677
+ while (!cancelled()) {
678
+ const containerId = captureContainerId();
679
+ if (containerId &&
680
+ (await publishDockerDataBaseUrl(command, commandPrefixArgs, containerId, metadataPath, cancelled, startupAbort.signal))) {
681
+ bootstrapPending = false;
682
+ clearTimeout(startupTimer);
683
+ return;
684
+ }
685
+ await delay(CONTAINER_ID_POLL_INTERVAL_MS);
686
+ }
506
687
  }
507
- console.error(`[staff-mcp] Docker proxy exited with code ${code ?? "null"}${signal ? ` (signal ${signal})` : ""}.`);
508
- // If a supervisor killed the Docker CLI directly, --rm may not have
509
- // completed. Clean up by exact ID when available, otherwise use the
510
- // unique generated name that was part of the container-create request.
511
- if (!cleanupInProgress &&
512
- !cleanupAttempted &&
513
- !containerCleanupConfirmed &&
514
- (signal !== null || code !== 0)) {
515
- const cleanupReference = trackedContainerId ?? containerIdentity.containerName;
516
- cleanupInProgress = true;
517
- cleanupAttempted = true;
518
- void forceRemoveContainer(cleanupReference)
519
- .then((removed) => {
520
- containerCleanupConfirmed = removed;
521
- cleanupFailed = !removed;
522
- })
523
- .finally(() => {
524
- cleanupInProgress = false;
525
- maybeFinish();
526
- });
688
+ };
689
+ if (metadataPath) {
690
+ process.stdout.on('error', handleOutputError);
691
+ process.stderr.on('error', handleOutputError);
692
+ startupTimer = setTimeout(failBootstrap, DOCKER_DATA_STARTUP_TIMEOUT_MS);
693
+ const parentPid = process.ppid;
694
+ parentWatch = setInterval(() => {
695
+ let parentAlive = process.ppid === parentPid;
696
+ try {
697
+ process.kill(parentPid, 0);
698
+ }
699
+ catch {
700
+ parentAlive = false;
701
+ }
702
+ if (!parentAlive)
703
+ requestShutdown('parent exited');
704
+ }, 200);
705
+ parentWatch.unref();
706
+ }
707
+ void start().catch(() => {
708
+ if (settled)
709
+ return;
710
+ if (shutdownStarted) {
711
+ if (!child)
712
+ finish(0);
527
713
  return;
528
714
  }
529
- maybeFinish();
715
+ // Helper failures may include credentials; expose only a fixed diagnostic.
716
+ failBootstrap();
530
717
  });
531
- cidPollTimer = setInterval(captureContainerId, CONTAINER_ID_POLL_INTERVAL_MS);
532
- cidPollTimer.unref();
533
- if (transport === "stdio") {
534
- input.once("end", handleInputEnd);
535
- input.once("error", handleInputError);
536
- input.pipe(child.stdin);
537
- }
538
- // A signal source can synchronously request shutdown while handlers are
539
- // being installed (and a real OS signal can arrive during startup).
540
- activateShutdown();
541
718
  });
542
719
  }
543
720
  //# sourceMappingURL=docker-proxy.js.map