staff-mcp 1.2.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.
- package/CHANGELOG.md +29 -0
- package/README.md +178 -49
- package/README_zh.md +176 -47
- package/dist/src/data/HttpDataSession.d.ts +22 -0
- package/dist/src/data/HttpDataSession.js +925 -0
- package/dist/src/data/HttpDataSession.js.map +1 -0
- package/dist/src/data/node.d.ts +4 -0
- package/dist/src/data/node.js +93 -0
- package/dist/src/data/node.js.map +1 -0
- package/dist/src/data/types.d.ts +54 -0
- package/dist/src/data/types.js +2 -0
- package/dist/src/data/types.js.map +1 -0
- package/dist/src/data-stdio-docker.d.ts +10 -0
- package/dist/src/data-stdio-docker.js +140 -0
- package/dist/src/data-stdio-docker.js.map +1 -0
- package/dist/src/docker-proxy.d.ts +6 -1
- package/dist/src/docker-proxy.js +324 -147
- package/dist/src/docker-proxy.js.map +1 -1
- package/dist/src/extensions/apps.d.ts +12 -0
- package/dist/src/extensions/apps.js +85 -0
- package/dist/src/extensions/apps.js.map +1 -0
- package/dist/src/extensions/config.d.ts +27 -0
- package/dist/src/extensions/config.js +91 -0
- package/dist/src/extensions/config.js.map +1 -0
- package/dist/src/extensions/protocol.d.ts +35 -0
- package/dist/src/extensions/protocol.js +47 -0
- package/dist/src/extensions/protocol.js.map +1 -0
- package/dist/src/extensions/runtime.d.ts +16 -0
- package/dist/src/extensions/runtime.js +266 -0
- package/dist/src/extensions/runtime.js.map +1 -0
- package/dist/src/file-manager/extension.d.ts +21 -0
- package/dist/src/file-manager/extension.js +514 -0
- package/dist/src/file-manager/extension.js.map +1 -0
- package/dist/src/file-manager/file-core.d.ts +128 -0
- package/dist/src/file-manager/file-core.js +750 -0
- package/dist/src/file-manager/file-core.js.map +1 -0
- package/dist/src/file-manager/transfers.d.ts +423 -0
- package/dist/src/file-manager/transfers.js +540 -0
- package/dist/src/file-manager/transfers.js.map +1 -0
- package/dist/src/file-manager/ui/index.html +81 -0
- package/dist/src/index.js +289 -128
- package/dist/src/index.js.map +1 -1
- package/dist/src/lsp/manager.d.ts +2 -0
- package/dist/src/lsp/manager.js +22 -10
- package/dist/src/lsp/manager.js.map +1 -1
- package/dist/src/reverse/SseTransport.d.ts +34 -0
- package/dist/src/reverse/SseTransport.js +215 -0
- package/dist/src/reverse/SseTransport.js.map +1 -0
- package/dist/src/reverse/http.d.ts +7 -0
- package/dist/src/reverse/http.js +85 -0
- package/dist/src/reverse/http.js.map +1 -0
- package/dist/src/server.d.ts +4 -2
- package/dist/src/server.js +80 -18
- package/dist/src/server.js.map +1 -1
- package/dist/src/terminal/extension.d.ts +4 -0
- package/dist/src/terminal/extension.js +108 -0
- package/dist/src/terminal/extension.js.map +1 -0
- package/dist/src/terminal/pty.d.ts +14 -0
- package/dist/src/terminal/pty.js +121 -0
- package/dist/src/terminal/pty.js.map +1 -0
- package/dist/src/terminal/session.d.ts +136 -0
- package/dist/src/terminal/session.js +422 -0
- package/dist/src/terminal/session.js.map +1 -0
- package/dist/src/terminal/ui/index.html +183 -0
- package/dist/src/tools/file-tools.d.ts +2 -1
- package/dist/src/tools/file-tools.js +19 -7
- package/dist/src/tools/file-tools.js.map +1 -1
- package/dist/src/tools/mcp-client-tools.js +244 -104
- package/dist/src/tools/mcp-client-tools.js.map +1 -1
- package/dist/src/tools/shell-tools.js +1 -1
- package/dist/src/tools/shell-tools.js.map +1 -1
- package/dist/src/tools/skills.js +26 -1
- package/dist/src/tools/skills.js.map +1 -1
- package/dist/src/tools/system-tools.d.ts +9 -3
- package/dist/src/tools/system-tools.js +31 -48
- package/dist/src/tools/system-tools.js.map +1 -1
- package/dist/src/transports/http.d.ts +16 -2
- package/dist/src/transports/http.js +261 -59
- package/dist/src/transports/http.js.map +1 -1
- package/dist/src/transports/reverse.d.ts +23 -19
- package/dist/src/transports/reverse.js +130 -74
- package/dist/src/transports/reverse.js.map +1 -1
- package/dist/src/transports/stdio.d.ts +6 -2
- package/dist/src/transports/stdio.js +131 -19
- package/dist/src/transports/stdio.js.map +1 -1
- package/dist/src/utils/proxy.d.ts +3 -3
- package/dist/src/utils/proxy.js +84 -62
- package/dist/src/utils/proxy.js.map +1 -1
- package/dist/src/utils/server-activity.d.ts +6 -0
- package/dist/src/utils/server-activity.js +46 -0
- package/dist/src/utils/server-activity.js.map +1 -0
- package/dist/src/utils/server-lifecycle.d.ts +5 -0
- package/dist/src/utils/server-lifecycle.js +51 -0
- package/dist/src/utils/server-lifecycle.js.map +1 -0
- package/docs/file-manager.md +179 -0
- package/docs/terminal.md +66 -0
- package/package.json +40 -19
- package/dist/src/transports/fte-lifecycle.d.ts +0 -13
- package/dist/src/transports/fte-lifecycle.js +0 -76
- package/dist/src/transports/fte-lifecycle.js.map +0 -1
- package/dist/tests/docker-proxy.test.d.ts +0 -1
- package/dist/tests/docker-proxy.test.js +0 -395
- package/dist/tests/docker-proxy.test.js.map +0 -1
- package/dist/tests/file-write-queue.test.d.ts +0 -1
- package/dist/tests/file-write-queue.test.js +0 -105
- package/dist/tests/file-write-queue.test.js.map +0 -1
- package/dist/tests/fte-lifecycle.test.d.ts +0 -1
- package/dist/tests/fte-lifecycle.test.js +0 -118
- package/dist/tests/fte-lifecycle.test.js.map +0 -1
- package/dist/tests/lsp-client.test.d.ts +0 -1
- package/dist/tests/lsp-client.test.js +0 -52
- package/dist/tests/lsp-client.test.js.map +0 -1
- package/dist/tests/package-info.test.d.ts +0 -1
- package/dist/tests/package-info.test.js +0 -54
- package/dist/tests/package-info.test.js.map +0 -1
- package/dist/tests/proxy-no-proxy.test.d.ts +0 -1
- package/dist/tests/proxy-no-proxy.test.js +0 -156
- package/dist/tests/proxy-no-proxy.test.js.map +0 -1
- package/dist/tests/proxy.test.d.ts +0 -1
- package/dist/tests/proxy.test.js +0 -89
- package/dist/tests/proxy.test.js.map +0 -1
- package/dist/tests/reverse-reconnect.test.d.ts +0 -1
- package/dist/tests/reverse-reconnect.test.js +0 -89
- package/dist/tests/reverse-reconnect.test.js.map +0 -1
- package/dist/tests/security.test.d.ts +0 -1
- package/dist/tests/security.test.js +0 -21
- package/dist/tests/security.test.js.map +0 -1
- package/dist/tests/shell-tools.test.d.ts +0 -1
- package/dist/tests/shell-tools.test.js +0 -113
- package/dist/tests/shell-tools.test.js.map +0 -1
- package/dist/tests/test_files/test.d.ts +0 -1
- package/dist/tests/test_files/test.js +0 -4
- package/dist/tests/test_files/test.js.map +0 -1
package/dist/src/docker-proxy.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { spawn } from
|
|
2
|
-
import { randomBytes } from
|
|
3
|
-
import * as fs from
|
|
4
|
-
import * as os from
|
|
5
|
-
import * as path from
|
|
6
|
-
|
|
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
|
|
15
|
+
case 'SIGHUP':
|
|
15
16
|
return 129;
|
|
16
|
-
case
|
|
17
|
+
case 'SIGINT':
|
|
17
18
|
return 130;
|
|
18
|
-
case
|
|
19
|
+
case 'SIGKILL':
|
|
19
20
|
return 137;
|
|
20
|
-
case
|
|
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 ===
|
|
28
|
-
arg.startsWith(
|
|
29
|
-
arg ===
|
|
30
|
-
arg.startsWith(
|
|
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 =
|
|
33
|
+
const CONTAINER_NAME_PREFIX = 'staff-mcp';
|
|
33
34
|
const CONTAINER_NAME_HINT_MAX_LENGTH = 40;
|
|
34
|
-
const NAME_SEGMENT_FALLBACK =
|
|
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 ===
|
|
59
|
+
if (parts.transport === 'http' && parts.port) {
|
|
59
60
|
return `${workspace}-${parts.port}`;
|
|
60
61
|
}
|
|
61
|
-
if (parts.transport ===
|
|
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(
|
|
79
|
-
const hint = sanitizeNameSegment(nameHint ??
|
|
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(
|
|
87
|
+
throw new Error('--cidfile and --name are managed internally by staff-mcp.');
|
|
87
88
|
}
|
|
88
|
-
const runIndex = args.indexOf(
|
|
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(),
|
|
93
|
-
const cidFilePath = path.join(ownedDirectory,
|
|
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,
|
|
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,
|
|
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 ===
|
|
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
|
-
|
|
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 ===
|
|
130
|
-
(normalized.startsWith(
|
|
131
|
-
|
|
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(
|
|
134
|
-
|
|
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(
|
|
137
|
-
![
|
|
138
|
-
|
|
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(
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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 ??
|
|
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 ??
|
|
166
|
-
|
|
167
|
-
const
|
|
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 &&
|
|
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 ===
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
input.removeListener(
|
|
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, {
|
|
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
|
|
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 ===
|
|
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,
|
|
292
|
-
stdio: [
|
|
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(
|
|
393
|
+
cleanupChild.once('spawn', () => {
|
|
301
394
|
cleanupSpawned = true;
|
|
302
395
|
});
|
|
303
|
-
cleanupChild.stderr?.on(
|
|
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(
|
|
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(
|
|
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 ??
|
|
325
|
-
`${signal ? `, signal ${signal}` :
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
448
|
+
child.kill('SIGKILL');
|
|
356
449
|
postCleanupFinalTimer = setTimeout(() => {
|
|
357
450
|
if (!runExit && isChildRunning()) {
|
|
358
|
-
console.error(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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
|
-
|
|
480
|
-
|
|
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
|
-
|
|
484
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
505
|
-
|
|
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
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
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
|