@hunterzhu/pulse-adapters 0.1.11 → 0.2.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/dist/documents/tools.d.ts +67 -0
- package/dist/documents/tools.js +338 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +1 -0
- package/dist/mcp/stdio.d.ts +78 -0
- package/dist/mcp/stdio.js +395 -0
- package/dist/providers/anthropic.js +4 -2
- package/dist/providers/normalize.d.ts +1 -1
- package/dist/providers/normalize.js +7 -5
- package/dist/providers/openai-compat.js +15 -4
- package/dist/providers/runtime-executor.js +3 -1
- package/dist/tools/filesystem.d.ts +4 -0
- package/dist/tools/filesystem.js +20 -2
- package/dist/tools/shell.d.ts +4 -0
- package/dist/tools/shell.js +255 -44
- package/package.json +6 -3
package/dist/tools/shell.js
CHANGED
|
@@ -1,51 +1,201 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, parse, resolve, sep } from 'node:path';
|
|
5
|
+
import { realpath } from 'node:fs/promises';
|
|
1
6
|
import { spawn } from 'node:child_process';
|
|
7
|
+
import { SandboxManager, VENDORED_SRT_WIN_EXE } from '@anthropic-ai/sandbox-runtime';
|
|
2
8
|
function shellError(code, retryable = false, cause) {
|
|
3
9
|
return Object.assign(new Error(code), { code, retryable, ...(cause === undefined ? {} : { cause }) });
|
|
4
10
|
}
|
|
11
|
+
// SandboxManager owns process-global proxy and policy state. A per-invocation
|
|
12
|
+
// policy is installed for exactly one child at a time to avoid one concurrent
|
|
13
|
+
// call widening another call's filesystem grants.
|
|
14
|
+
let sandboxTail = Promise.resolve();
|
|
15
|
+
function withSandboxLease(work) {
|
|
16
|
+
const result = sandboxTail.then(work, work);
|
|
17
|
+
sandboxTail = result.then(() => undefined, () => undefined);
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
function quotePosix(value) {
|
|
21
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
22
|
+
}
|
|
23
|
+
export function quoteWindowsArgument(value) {
|
|
24
|
+
if (value.length > 0 && !/[\s"]/u.test(value))
|
|
25
|
+
return value;
|
|
26
|
+
let output = '"';
|
|
27
|
+
let slashes = 0;
|
|
28
|
+
for (const character of value) {
|
|
29
|
+
if (character === '\\') {
|
|
30
|
+
slashes++;
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (character === '"')
|
|
34
|
+
output += '\\'.repeat(slashes * 2 + 1) + '"';
|
|
35
|
+
else
|
|
36
|
+
output += '\\'.repeat(slashes) + character;
|
|
37
|
+
slashes = 0;
|
|
38
|
+
}
|
|
39
|
+
return output + '\\'.repeat(slashes * 2) + '"';
|
|
40
|
+
}
|
|
41
|
+
const powershellArgvRunner = String.raw `
|
|
42
|
+
$ErrorActionPreference = 'Stop'
|
|
43
|
+
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
|
|
44
|
+
$payload = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('__PULSE_ARGV_JSON__'))
|
|
45
|
+
$spec = ConvertFrom-Json -InputObject $payload
|
|
46
|
+
$psi = [System.Diagnostics.ProcessStartInfo]::new()
|
|
47
|
+
$psi.FileName = [string]$spec.command
|
|
48
|
+
$psi.Arguments = [string]$spec.arguments
|
|
49
|
+
$psi.UseShellExecute = $false
|
|
50
|
+
$psi.CreateNoWindow = $true
|
|
51
|
+
$psi.RedirectStandardOutput = $true
|
|
52
|
+
$psi.RedirectStandardError = $true
|
|
53
|
+
$psi.RedirectStandardInput = $true
|
|
54
|
+
$psi.StandardOutputEncoding = [System.Text.Encoding]::UTF8
|
|
55
|
+
$psi.StandardErrorEncoding = [System.Text.Encoding]::UTF8
|
|
56
|
+
$child = [System.Diagnostics.Process]::new()
|
|
57
|
+
$child.StartInfo = $psi
|
|
58
|
+
try { if (-not $child.Start()) { exit 127 } }
|
|
59
|
+
catch {
|
|
60
|
+
$cause = $_.Exception
|
|
61
|
+
while ($cause.InnerException) { $cause = $cause.InnerException }
|
|
62
|
+
[Console]::Error.WriteLine($cause.Message)
|
|
63
|
+
if ($cause -is [System.ComponentModel.Win32Exception] -and $cause.NativeErrorCode -in 2,3) { exit 127 }
|
|
64
|
+
exit 126
|
|
65
|
+
}
|
|
66
|
+
$outTask = $child.StandardOutput.ReadToEndAsync()
|
|
67
|
+
$errTask = $child.StandardError.ReadToEndAsync()
|
|
68
|
+
$inTask = [Console]::OpenStandardInput().CopyToAsync($child.StandardInput.BaseStream)
|
|
69
|
+
[void]$inTask.GetAwaiter().GetResult()
|
|
70
|
+
$child.StandardInput.Close()
|
|
71
|
+
$child.WaitForExit()
|
|
72
|
+
try { [Console]::Out.Write([string]$outTask.GetAwaiter().GetResult()) } catch {}
|
|
73
|
+
try { [Console]::Error.Write([string]$errTask.GetAwaiter().GetResult()) } catch {}
|
|
74
|
+
exit $child.ExitCode
|
|
75
|
+
`.trim();
|
|
76
|
+
/** Build the command text expected by srt without interpreting model argv as shell syntax. */
|
|
77
|
+
export function encodeSandboxCommand(command, args, platform = process.platform) {
|
|
78
|
+
if ([command, ...args].some((value) => value.includes('\0')))
|
|
79
|
+
throw shellError('INVALID_SHELL_ARGUMENT');
|
|
80
|
+
if (platform === 'win32') {
|
|
81
|
+
// The only bytes placed in cmd.exe's command string are fixed tokens and
|
|
82
|
+
// base64. PowerShell decodes JSON and ProcessStartInfo.Arguments uses a
|
|
83
|
+
// Windows CRT-compatible encoder, preserving argv without a shell parse.
|
|
84
|
+
const payload = Buffer.from(JSON.stringify({ command, arguments: args.map(quoteWindowsArgument).join(' ') }), 'utf8').toString('base64');
|
|
85
|
+
const encoded = Buffer.from(powershellArgvRunner.replace('__PULSE_ARGV_JSON__', payload), 'utf16le').toString('base64');
|
|
86
|
+
const commandText = `powershell.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand ${encoded}`;
|
|
87
|
+
if (commandText.length > 24_000)
|
|
88
|
+
throw shellError('INVALID_SHELL_ARGUMENTS_TOO_LARGE');
|
|
89
|
+
return commandText;
|
|
90
|
+
}
|
|
91
|
+
return `exec ${[command, ...args].map(quotePosix).join(' ')}`;
|
|
92
|
+
}
|
|
93
|
+
async function sandboxConfig(cwd) {
|
|
94
|
+
// Linux invokes this trusted helper *inside* the read-restricted namespace.
|
|
95
|
+
// A user-local npm install otherwise hides it together with the home directory.
|
|
96
|
+
const seccompPath = process.platform === 'linux'
|
|
97
|
+
? await realpath(resolve(dirname(createRequire(import.meta.url).resolve('@anthropic-ai/sandbox-runtime')), '..', 'vendor', 'seccomp', process.arch, 'apply-seccomp'))
|
|
98
|
+
: undefined;
|
|
99
|
+
const home = homedir();
|
|
100
|
+
const parent = dirname(cwd);
|
|
101
|
+
const root = parse(cwd).root;
|
|
102
|
+
if (cwd === root || cwd === home || home.startsWith(cwd + sep) || parent === root) {
|
|
103
|
+
throw shellError('SANDBOX_WORKSPACE_TOO_BROAD');
|
|
104
|
+
}
|
|
105
|
+
const denyRead = new Set([home]);
|
|
106
|
+
// Close the common sibling-workspace and temporary-directory escape: deny
|
|
107
|
+
// the workspace's immediate container, then re-open only this invocation's
|
|
108
|
+
// cwd. Root workspaces cannot be carved this way and are rejected upstream.
|
|
109
|
+
if (parent !== parse(cwd).root && parent !== cwd)
|
|
110
|
+
denyRead.add(parent);
|
|
111
|
+
// Reads default to the system toolchain plus this workspace. Writes are
|
|
112
|
+
// limited to the workspace; srt adds only its required stdio/temp paths.
|
|
113
|
+
return {
|
|
114
|
+
network: { allowedDomains: [], deniedDomains: [], strictAllowlist: true },
|
|
115
|
+
filesystem: {
|
|
116
|
+
denyRead: [...denyRead],
|
|
117
|
+
allowRead: [cwd, ...(seccompPath ? [seccompPath] : [])],
|
|
118
|
+
allowWrite: [cwd],
|
|
119
|
+
denyWrite: [],
|
|
120
|
+
},
|
|
121
|
+
...(seccompPath ? { seccomp: { applyPath: seccompPath } } : {}),
|
|
122
|
+
...(process.platform === 'win32' ? { windows: { srtWin: { path: VENDORED_SRT_WIN_EXE } } } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function cleanPath(value) {
|
|
126
|
+
return resolve(value);
|
|
127
|
+
}
|
|
128
|
+
export function decodeUtf8WithinByteLimit(value, maxBytes) {
|
|
129
|
+
if (!Number.isFinite(maxBytes) || maxBytes < 0)
|
|
130
|
+
throw shellError('INVALID_SHELL_OUTPUT_LIMIT');
|
|
131
|
+
const decoded = value.subarray(0, maxBytes).toString('utf8');
|
|
132
|
+
let used = 0;
|
|
133
|
+
const output = [];
|
|
134
|
+
for (const character of decoded) {
|
|
135
|
+
const bytes = Buffer.byteLength(character, 'utf8');
|
|
136
|
+
if (used + bytes > maxBytes)
|
|
137
|
+
break;
|
|
138
|
+
output.push(character);
|
|
139
|
+
used += bytes;
|
|
140
|
+
}
|
|
141
|
+
return output.join('');
|
|
142
|
+
}
|
|
143
|
+
function shellEnvironment(env) {
|
|
144
|
+
const source = env ?? process.env;
|
|
145
|
+
return Object.fromEntries(Object.entries(source).filter(([key]) => !/(API_KEY|TOKEN|SECRET|PASSWORD|PRIVATE_KEY|AUTHORIZATION|BEARER|CREDENTIAL|COOKIE)/i.test(key) && key !== 'NODE_OPTIONS'));
|
|
146
|
+
}
|
|
5
147
|
export function runShell(command, args = [], options = {}) {
|
|
6
148
|
const max = options.maxOutputBytes ?? 256 * 1024;
|
|
7
149
|
if (!Number.isFinite(max) || max < 0)
|
|
8
150
|
return Promise.reject(shellError('INVALID_SHELL_OUTPUT_LIMIT'));
|
|
9
151
|
if (options.timeoutMs !== undefined && (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 0))
|
|
10
152
|
return Promise.reject(shellError('INVALID_SHELL_TIMEOUT'));
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
153
|
+
if (!command || typeof command !== 'string' || !Array.isArray(args) || args.some((arg) => typeof arg !== 'string'))
|
|
154
|
+
return Promise.reject(shellError('INVALID_SHELL_ARGUMENT'));
|
|
155
|
+
try {
|
|
156
|
+
encodeSandboxCommand(command, args);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
return Promise.reject(error);
|
|
160
|
+
}
|
|
161
|
+
return withSandboxLease(async () => {
|
|
162
|
+
if (options.signal?.aborted)
|
|
163
|
+
return { code: null, stdout: '', stderr: '', truncated: false, timedOut: false, aborted: true };
|
|
164
|
+
let cwd;
|
|
165
|
+
try {
|
|
166
|
+
cwd = await realpath(cleanPath(options.cwd ?? process.cwd()));
|
|
23
167
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
168
|
+
catch (cause) {
|
|
169
|
+
throw shellError('INVALID_SHELL_CWD', false, cause);
|
|
170
|
+
}
|
|
171
|
+
const invocationId = randomUUID();
|
|
172
|
+
const policy = await sandboxConfig(cwd);
|
|
173
|
+
let child;
|
|
174
|
+
const outputChunks = { stdout: [], stderr: [] };
|
|
175
|
+
const outputBytes = { stdout: 0, stderr: 0 };
|
|
176
|
+
let truncated = false;
|
|
28
177
|
let closed = false;
|
|
178
|
+
let termination;
|
|
179
|
+
let timer;
|
|
180
|
+
let killTimer;
|
|
29
181
|
const signalProcessGroup = (signal) => {
|
|
30
|
-
if (
|
|
182
|
+
if (!child?.pid)
|
|
183
|
+
return;
|
|
184
|
+
if (process.platform !== 'win32') {
|
|
31
185
|
try {
|
|
32
186
|
process.kill(-child.pid, signal);
|
|
33
187
|
return;
|
|
34
188
|
}
|
|
35
189
|
catch { /* process group may already be gone */ }
|
|
36
190
|
}
|
|
37
|
-
if (process.platform === 'win32'
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const tree = spawn('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true });
|
|
41
|
-
tree.once('error', () => child.kill(signal));
|
|
191
|
+
if (process.platform === 'win32') {
|
|
192
|
+
const tree = spawn('taskkill.exe', ['/PID', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true, env: shellEnvironment(options.env) });
|
|
193
|
+
tree.once('error', () => child?.kill(signal));
|
|
42
194
|
tree.unref();
|
|
43
195
|
return;
|
|
44
196
|
}
|
|
45
197
|
child.kill(signal);
|
|
46
198
|
};
|
|
47
|
-
let termination;
|
|
48
|
-
let killTimer;
|
|
49
199
|
const terminate = (reason) => {
|
|
50
200
|
if (closed)
|
|
51
201
|
return;
|
|
@@ -55,27 +205,88 @@ export function runShell(command, args = [], options = {}) {
|
|
|
55
205
|
killTimer = setTimeout(() => { if (!closed)
|
|
56
206
|
signalProcessGroup('SIGKILL'); }, 250);
|
|
57
207
|
};
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
208
|
+
const append = (target, chunk) => {
|
|
209
|
+
const remaining = Math.max(0, max - outputBytes[target]);
|
|
210
|
+
const kept = Math.min(chunk.byteLength, remaining);
|
|
211
|
+
if (kept > 0)
|
|
212
|
+
outputChunks[target].push(chunk.subarray(0, kept));
|
|
213
|
+
outputBytes[target] += kept;
|
|
214
|
+
if (kept < chunk.byteLength)
|
|
215
|
+
truncated = true;
|
|
216
|
+
};
|
|
217
|
+
const outputText = (target) => {
|
|
218
|
+
return decodeUtf8WithinByteLimit(Buffer.concat(outputChunks[target]), max);
|
|
219
|
+
};
|
|
220
|
+
try {
|
|
221
|
+
// reset first in case an earlier initialize failed part-way through.
|
|
222
|
+
await SandboxManager.reset();
|
|
223
|
+
await SandboxManager.initialize(policy, undefined, false);
|
|
224
|
+
const commandText = encodeSandboxCommand(command, args);
|
|
225
|
+
const descriptor = await SandboxManager.wrapWithSandboxArgv(commandText, undefined, undefined, options.signal, cwd, { commandId: invocationId, commandText: 'shell.exec' });
|
|
226
|
+
if (options.signal?.aborted) {
|
|
227
|
+
SandboxManager.cleanupAfterCommand();
|
|
228
|
+
await SandboxManager.reset();
|
|
229
|
+
return { code: null, stdout: '', stderr: '', truncated: false, timedOut: false, aborted: true };
|
|
230
|
+
}
|
|
231
|
+
child = spawn(descriptor.argv[0], descriptor.argv.slice(1), {
|
|
232
|
+
cwd,
|
|
233
|
+
env: shellEnvironment(options.env ?? descriptor.env),
|
|
234
|
+
shell: false,
|
|
235
|
+
detached: process.platform !== 'win32',
|
|
236
|
+
windowsHide: true,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
catch (cause) {
|
|
240
|
+
try {
|
|
241
|
+
await SandboxManager.reset();
|
|
242
|
+
}
|
|
243
|
+
catch { /* preserve the setup failure */ }
|
|
244
|
+
throw shellError('SANDBOX_SETUP_FAILED', false, cause);
|
|
65
245
|
}
|
|
66
|
-
|
|
67
|
-
child.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
246
|
+
// This API has no stdin payload; deliver EOF to readers inside the sandbox.
|
|
247
|
+
child.stdin?.end();
|
|
248
|
+
return await new Promise((resolve, reject) => {
|
|
249
|
+
const cleanup = () => {
|
|
250
|
+
if (timer)
|
|
251
|
+
clearTimeout(timer);
|
|
252
|
+
if (killTimer)
|
|
253
|
+
clearTimeout(killTimer);
|
|
254
|
+
options.signal?.removeEventListener('abort', abort);
|
|
255
|
+
};
|
|
256
|
+
const abort = () => terminate('aborted');
|
|
257
|
+
timer = options.timeoutMs && options.timeoutMs > 0 ? setTimeout(() => terminate('timeout'), options.timeoutMs) : undefined;
|
|
258
|
+
if (options.signal)
|
|
259
|
+
options.signal.addEventListener('abort', abort, { once: true });
|
|
260
|
+
if (options.signal?.aborted)
|
|
261
|
+
abort();
|
|
262
|
+
child.stdout?.on('data', (chunk) => append('stdout', chunk));
|
|
263
|
+
child.stderr?.on('data', (chunk) => append('stderr', chunk));
|
|
264
|
+
child.once('error', (cause) => {
|
|
265
|
+
closed = true;
|
|
266
|
+
cleanup();
|
|
267
|
+
const errorCode = cause && typeof cause === 'object' ? cause.code : undefined;
|
|
268
|
+
const code = typeof errorCode === 'string' ? errorCode : 'SHELL_EXECUTION_ERROR';
|
|
269
|
+
reject(shellError(code === 'ENOENT' || code === 'EACCES' ? code : 'SHELL_EXECUTION_ERROR', false, cause));
|
|
270
|
+
});
|
|
271
|
+
child.once('close', (code) => {
|
|
272
|
+
closed = true;
|
|
273
|
+
cleanup();
|
|
274
|
+
const stdout = outputText('stdout');
|
|
275
|
+
const stderr = outputText('stderr');
|
|
276
|
+
const annotated = SandboxManager.annotateStderrWithSandboxFailures(invocationId, stderr);
|
|
277
|
+
const boundedStderr = decodeUtf8WithinByteLimit(Buffer.from(annotated, 'utf8'), max);
|
|
278
|
+
const outputWasTruncated = truncated || Buffer.byteLength(annotated, 'utf8') > max;
|
|
279
|
+
resolve({ code, stdout, stderr: boundedStderr, truncated: outputWasTruncated, timedOut: termination === 'timeout', aborted: termination === 'aborted' });
|
|
280
|
+
});
|
|
281
|
+
}).finally(async () => {
|
|
282
|
+
try {
|
|
283
|
+
SandboxManager.cleanupAfterCommand();
|
|
284
|
+
await SandboxManager.reset();
|
|
285
|
+
}
|
|
286
|
+
catch (cause) {
|
|
287
|
+
// A teardown failure must be visible and fail closed for the next call.
|
|
288
|
+
throw shellError('SANDBOX_CLEANUP_FAILED', false, cause);
|
|
289
|
+
}
|
|
76
290
|
});
|
|
77
|
-
child.on('close', (code) => { closed = true; if (timer)
|
|
78
|
-
clearTimeout(timer); if (killTimer)
|
|
79
|
-
clearTimeout(killTimer); resolve({ code, stdout, stderr, truncated, timedOut: termination === 'timeout', aborted: termination === 'aborted' }); });
|
|
80
291
|
});
|
|
81
292
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hunterzhu/pulse-adapters",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/zhuhengtan/Pulse"
|
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
"registry": "https://registry.npmjs.org"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@
|
|
20
|
-
"@hunterzhu/pulse-
|
|
19
|
+
"@anthropic-ai/sandbox-runtime": "0.0.77",
|
|
20
|
+
"@hunterzhu/pulse-runtime": "0.2.0",
|
|
21
|
+
"@hunterzhu/pulse-tool-sdk": "0.2.0",
|
|
22
|
+
"exceljs": "4.4.0",
|
|
23
|
+
"pdfjs-dist": "4.10.38"
|
|
21
24
|
}
|
|
22
25
|
}
|