@hunterzhu/pulse-adapters 0.5.1 → 0.7.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/docx.d.ts +2 -0
- package/dist/documents/docx.js +45 -0
- package/dist/documents/tools.d.ts +12 -0
- package/dist/documents/tools.js +23 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/stdio.js +12 -1
- package/dist/native/LICENSE +201 -0
- package/dist/native/srt-win.exe +0 -0
- package/dist/native/srt-win.json +11 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +7 -50
- package/dist/providers/canonical/lowering.js +12 -0
- package/dist/providers/canonical/mappers/anthropic.js +4 -3
- package/dist/providers/canonical/mappers/openai.js +1 -1
- package/dist/providers/normalize.d.ts +6 -1
- package/dist/providers/normalize.js +29 -3
- package/dist/providers/openai-compat.d.ts +2 -0
- package/dist/providers/openai-compat.js +10 -1
- package/dist/providers/runtime-executor.d.ts +8 -1
- package/dist/providers/runtime-executor.js +55 -10
- package/dist/providers/types.d.ts +2 -0
- package/dist/tools/installation-dependencies.d.ts +4 -0
- package/dist/tools/installation-dependencies.js +59 -0
- package/dist/tools/registry.js +5 -2
- package/dist/tools/sandbox-initialization.d.ts +1 -1
- package/dist/tools/sandbox-initialization.js +63 -7
- package/dist/tools/shell-output.d.ts +23 -0
- package/dist/tools/shell-output.js +82 -0
- package/dist/tools/shell.d.ts +6 -0
- package/dist/tools/shell.js +62 -24
- package/dist/tools/windows-read-policy.d.ts +4 -0
- package/dist/tools/windows-read-policy.js +11 -1
- package/dist/tools/windows-srt-path.d.ts +2 -0
- package/dist/tools/windows-srt-path.js +18 -0
- package/package.json +4 -3
package/dist/tools/shell.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ShellOutput } from './shell-output.js';
|
|
1
2
|
import { createRequire } from 'node:module';
|
|
2
3
|
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import { homedir, tmpdir } from 'node:os';
|
|
@@ -5,10 +6,12 @@ import { basename, delimiter, dirname, join, parse, resolve, sep } from 'node:pa
|
|
|
5
6
|
import { mkdtemp, realpath, rm } from 'node:fs/promises';
|
|
6
7
|
import { execFile, spawn } from 'node:child_process';
|
|
7
8
|
import { promisify } from 'node:util';
|
|
8
|
-
import { SandboxManager,
|
|
9
|
+
import { SandboxManager, checkWindowsDependenciesAsync, resolveSrtWin } from '@anthropic-ai/sandbox-runtime';
|
|
9
10
|
import { resolveWindowsPackageManager } from './windows-package-manager.js';
|
|
10
|
-
import { carveWindowsReadDenies } from './windows-read-policy.js';
|
|
11
|
+
import { carveWindowsReadDenies, windowsReadDenyRoots, windowsToolchainWriteDenies } from './windows-read-policy.js';
|
|
11
12
|
import { initializeShellSandbox } from './sandbox-initialization.js';
|
|
13
|
+
import { installationDependencyRoots, workspaceUsesInstallationDependencies } from './installation-dependencies.js';
|
|
14
|
+
import { verifiedWindowsSrt } from './windows-srt-path.js';
|
|
12
15
|
function shellError(code, retryable = false, cause) {
|
|
13
16
|
return Object.assign(new Error(code), { code, retryable, ...(cause === undefined ? {} : { cause }) });
|
|
14
17
|
}
|
|
@@ -26,7 +29,7 @@ export async function checkShellSandbox() {
|
|
|
26
29
|
// doctor runs before initialize(), so the manager has no Windows config yet.
|
|
27
30
|
// Probe the same packaged helper used for execution without provisioning it.
|
|
28
31
|
return withSandboxLease(() => process.platform === 'win32'
|
|
29
|
-
? checkWindowsDependenciesAsync({ srtWin: resolveSrtWin({ path:
|
|
32
|
+
? checkWindowsDependenciesAsync({ srtWin: resolveSrtWin({ path: verifiedWindowsSrt() }) })
|
|
30
33
|
: SandboxManager.checkDependenciesAsync());
|
|
31
34
|
}
|
|
32
35
|
function quotePosix(value) {
|
|
@@ -102,7 +105,7 @@ export function encodeSandboxCommand(command, args, platform = process.platform)
|
|
|
102
105
|
}
|
|
103
106
|
return `exec ${[command, ...args].map(quotePosix).join(' ')}`;
|
|
104
107
|
}
|
|
105
|
-
async function sandboxConfig(cwd, allowedDomains = [], toolchainRoots = [], scratch) {
|
|
108
|
+
async function sandboxConfig(cwd, allowedDomains = [], toolchainRoots = [], scratch, dependencyRoots = []) {
|
|
106
109
|
// Linux invokes this trusted helper *inside* the read-restricted namespace.
|
|
107
110
|
// A user-local npm install otherwise hides it together with the home directory.
|
|
108
111
|
const seccompPath = process.platform === 'linux'
|
|
@@ -117,26 +120,31 @@ async function sandboxConfig(cwd, allowedDomains = [], toolchainRoots = [], scra
|
|
|
117
120
|
// Windows runs as a separate srt-sandbox account, which has no access to
|
|
118
121
|
// private host-profile files without explicit grants. Stamping the entire
|
|
119
122
|
// profile recursively is both redundant and very expensive (registry/cache
|
|
120
|
-
// trees);
|
|
123
|
+
// trees); carve workspace and installation dependency boundaries instead.
|
|
121
124
|
const denyRead = new Set(process.platform === 'win32' ? [] : [home]);
|
|
122
125
|
// Close the common sibling-workspace and temporary-directory escape: deny
|
|
123
126
|
// the workspace's immediate container, then re-open only this invocation's
|
|
124
127
|
// cwd. Root workspaces cannot be carved this way and are rejected upstream.
|
|
125
128
|
if (parent !== parse(cwd).root && parent !== cwd)
|
|
126
129
|
denyRead.add(parent);
|
|
130
|
+
if (process.platform === 'win32')
|
|
131
|
+
for (const boundary of windowsReadDenyRoots(cwd, dependencyRoots))
|
|
132
|
+
denyRead.add(boundary);
|
|
127
133
|
const allowRead = [cwd, ...toolchainRoots, ...(scratch ? [scratch] : []), ...(seccompPath ? [seccompPath] : [])];
|
|
128
134
|
// Reads default to the system toolchain plus this workspace. Writes are
|
|
129
135
|
// limited to the workspace; srt adds only its required stdio/temp paths.
|
|
130
136
|
return {
|
|
131
137
|
network: { allowedDomains, deniedDomains: [], strictAllowlist: true },
|
|
132
138
|
filesystem: {
|
|
133
|
-
|
|
139
|
+
// Known dependency trees remain outside source denies. Only workspaces
|
|
140
|
+
// that actually reference them need recursive Windows read grants.
|
|
141
|
+
denyRead: process.platform === 'win32' ? await carveWindowsReadDenies([...denyRead], [...allowRead, ...dependencyRoots]) : [...denyRead],
|
|
134
142
|
allowRead,
|
|
135
143
|
allowWrite: [cwd, ...(scratch ? [scratch] : [])],
|
|
136
|
-
denyWrite: [],
|
|
144
|
+
denyWrite: process.platform === 'win32' ? await windowsToolchainWriteDenies([...toolchainRoots, ...dependencyRoots], [cwd, ...(scratch ? [scratch] : [])]) : [],
|
|
137
145
|
},
|
|
138
146
|
...(seccompPath ? { seccomp: { applyPath: seccompPath } } : {}),
|
|
139
|
-
...(process.platform === 'win32' ? { windows: { srtWin: { path:
|
|
147
|
+
...(process.platform === 'win32' ? { windows: { srtWin: { path: verifiedWindowsSrt() } } } : {}),
|
|
140
148
|
};
|
|
141
149
|
}
|
|
142
150
|
function cleanPath(value) {
|
|
@@ -167,16 +175,26 @@ function shellLaunchesCommand(command, args) {
|
|
|
167
175
|
return false;
|
|
168
176
|
return args.some((arg) => /^(?:-lc|-c|-command|\/c)$/i.test(arg));
|
|
169
177
|
}
|
|
178
|
+
/** Deterministic argv validation, shared by pre-approval and actual execution. */
|
|
179
|
+
export function validateShellInvocation(command, args = []) {
|
|
180
|
+
if (!command || typeof command !== 'string' || !Array.isArray(args) || args.some((arg) => typeof arg !== 'string'))
|
|
181
|
+
throw shellError('INVALID_SHELL_ARGUMENT');
|
|
182
|
+
if (/&&|\|\||[|&;<>\n`]|\$\(/.test(command) || shellLaunchesCommand(command, args))
|
|
183
|
+
throw Object.assign(shellError('SHELL_SYNTAX_UNSUPPORTED'), { message: 'shell.exec does not start a shell. Pass the executable in command and each argument in args. Do not put &&, pipes, redirects, command substitution, or sh -c in command.' });
|
|
184
|
+
encodeSandboxCommand(command, args);
|
|
185
|
+
}
|
|
170
186
|
export function runShell(command, args = [], options = {}) {
|
|
171
187
|
const max = options.maxOutputBytes ?? 256 * 1024;
|
|
172
188
|
if (!Number.isFinite(max) || max < 0)
|
|
173
189
|
return Promise.reject(shellError('INVALID_SHELL_OUTPUT_LIMIT'));
|
|
174
190
|
if (options.timeoutMs !== undefined && (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 0))
|
|
175
191
|
return Promise.reject(shellError('INVALID_SHELL_TIMEOUT'));
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
192
|
+
try {
|
|
193
|
+
validateShellInvocation(command, args);
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
return Promise.reject(error);
|
|
197
|
+
}
|
|
180
198
|
try {
|
|
181
199
|
encodeSandboxCommand(command, args);
|
|
182
200
|
}
|
|
@@ -184,6 +202,9 @@ export function runShell(command, args = [], options = {}) {
|
|
|
184
202
|
return Promise.reject(error);
|
|
185
203
|
}
|
|
186
204
|
return withSandboxLease(async () => {
|
|
205
|
+
const started = Date.now();
|
|
206
|
+
const checkpoint = (phase) => { if (process.env.PULSE_SANDBOX_TIMING === '1')
|
|
207
|
+
console.info(`[sandbox timing] ${Date.now() - started}ms ${phase}`); };
|
|
187
208
|
if (options.signal?.aborted)
|
|
188
209
|
return { code: null, stdout: '', stderr: '', truncated: false, timedOut: false, aborted: true };
|
|
189
210
|
let cwd;
|
|
@@ -198,6 +219,13 @@ export function runShell(command, args = [], options = {}) {
|
|
|
198
219
|
const nodeBin = dirname(nodeExecutable);
|
|
199
220
|
const installation = basename(nodeBin) === 'bin' ? dirname(nodeBin) : nodeBin;
|
|
200
221
|
const toolchainRoots = installation !== homedir() && installation !== parse(installation).root ? [installation] : [nodeExecutable];
|
|
222
|
+
const dependencyRoots = await installationDependencyRoots();
|
|
223
|
+
if (process.platform !== 'win32' || await workspaceUsesInstallationDependencies(cwd, dependencyRoots))
|
|
224
|
+
toolchainRoots.push(...dependencyRoots);
|
|
225
|
+
// The Windows broker relaunches itself as the sandbox user before starting
|
|
226
|
+
// the target command. Permit only its fixed, integrity-checked image folder.
|
|
227
|
+
if (process.platform === 'win32')
|
|
228
|
+
toolchainRoots.push(dirname(verifiedWindowsSrt()));
|
|
201
229
|
// PNPM_HOME is a host installation setting, never supplied by model argv.
|
|
202
230
|
let pnpmHome;
|
|
203
231
|
if (process.env.PNPM_HOME) {
|
|
@@ -240,9 +268,9 @@ export function runShell(command, args = [], options = {}) {
|
|
|
240
268
|
}
|
|
241
269
|
}
|
|
242
270
|
const invocationId = randomUUID();
|
|
243
|
-
const policy = await sandboxConfig(cwd, options.allowedDomains ?? [], toolchainRoots, scratch);
|
|
271
|
+
const policy = await sandboxConfig(cwd, options.allowedDomains ?? [], toolchainRoots, scratch, dependencyRoots);
|
|
244
272
|
let child;
|
|
245
|
-
const
|
|
273
|
+
const archive = new ShellOutput(max, options.outputDirectory, options.maxArchiveBytes, options.outputDirectory !== undefined);
|
|
246
274
|
const outputBytes = { stdout: 0, stderr: 0 };
|
|
247
275
|
let truncated = false;
|
|
248
276
|
let closed = false;
|
|
@@ -277,26 +305,27 @@ export function runShell(command, args = [], options = {}) {
|
|
|
277
305
|
signalProcessGroup('SIGKILL'); }, 250);
|
|
278
306
|
};
|
|
279
307
|
const append = (target, chunk) => {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (
|
|
283
|
-
outputChunks[target].push(chunk.subarray(0, kept));
|
|
284
|
-
outputBytes[target] += kept;
|
|
285
|
-
if (kept < chunk.byteLength)
|
|
308
|
+
archive.append(target, chunk);
|
|
309
|
+
outputBytes[target] += chunk.byteLength;
|
|
310
|
+
if (outputBytes[target] > max)
|
|
286
311
|
truncated = true;
|
|
287
312
|
};
|
|
288
313
|
const outputText = (target) => {
|
|
289
|
-
return decodeUtf8WithinByteLimit(Buffer.
|
|
314
|
+
return decodeUtf8WithinByteLimit(Buffer.from(archive.text(target)), max);
|
|
290
315
|
};
|
|
316
|
+
checkpoint('policy ready');
|
|
291
317
|
let setupStage = 'reset';
|
|
292
318
|
try {
|
|
293
319
|
// reset first in case an earlier initialize failed part-way through.
|
|
294
320
|
await SandboxManager.reset();
|
|
321
|
+
checkpoint('reset complete');
|
|
295
322
|
setupStage = 'initialize';
|
|
296
|
-
await initializeShellSandbox(policy);
|
|
323
|
+
await initializeShellSandbox(policy, process.platform, options.signal);
|
|
324
|
+
checkpoint('initialized');
|
|
297
325
|
const commandText = encodeSandboxCommand(executable, executableArgs);
|
|
298
326
|
setupStage = 'wrap';
|
|
299
327
|
const descriptor = await SandboxManager.wrapWithSandboxArgv(commandText, undefined, undefined, options.signal, cwd, { commandId: invocationId, commandText: 'shell.exec' });
|
|
328
|
+
checkpoint('wrapped');
|
|
300
329
|
if (options.signal?.aborted) {
|
|
301
330
|
SandboxManager.cleanupAfterCommand();
|
|
302
331
|
await SandboxManager.reset();
|
|
@@ -311,10 +340,15 @@ export function runShell(command, args = [], options = {}) {
|
|
|
311
340
|
});
|
|
312
341
|
}
|
|
313
342
|
catch (cause) {
|
|
343
|
+
let resetFailed = false;
|
|
314
344
|
try {
|
|
315
345
|
await SandboxManager.reset();
|
|
316
346
|
}
|
|
317
|
-
catch {
|
|
347
|
+
catch {
|
|
348
|
+
resetFailed = true; /* preserve the setup failure */
|
|
349
|
+
}
|
|
350
|
+
if (options.signal?.aborted && !resetFailed)
|
|
351
|
+
return { code: null, stdout: '', stderr: '', truncated: false, timedOut: false, aborted: true };
|
|
318
352
|
// Persist only classified diagnostics, never raw subprocess output or credentials.
|
|
319
353
|
const message = cause instanceof Error ? cause.message : '';
|
|
320
354
|
const nativeCode = cause && typeof cause === 'object' && 'code' in cause ? String(cause.code) : '';
|
|
@@ -348,9 +382,11 @@ export function runShell(command, args = [], options = {}) {
|
|
|
348
382
|
cleanup();
|
|
349
383
|
const errorCode = cause && typeof cause === 'object' ? cause.code : undefined;
|
|
350
384
|
const code = typeof errorCode === 'string' ? errorCode : 'SHELL_EXECUTION_ERROR';
|
|
385
|
+
archive.finish();
|
|
351
386
|
reject(shellError(code === 'ENOENT' || code === 'EACCES' ? code : 'SHELL_EXECUTION_ERROR', false, cause));
|
|
352
387
|
});
|
|
353
388
|
child.once('close', (code) => {
|
|
389
|
+
checkpoint('child closed');
|
|
354
390
|
closed = true;
|
|
355
391
|
cleanup();
|
|
356
392
|
const stdout = outputText('stdout');
|
|
@@ -358,12 +394,14 @@ export function runShell(command, args = [], options = {}) {
|
|
|
358
394
|
const annotated = SandboxManager.annotateStderrWithSandboxFailures(invocationId, stderr);
|
|
359
395
|
const boundedStderr = decodeUtf8WithinByteLimit(Buffer.from(annotated, 'utf8'), max);
|
|
360
396
|
const outputWasTruncated = truncated || Buffer.byteLength(annotated, 'utf8') > max;
|
|
361
|
-
resolve({ code, stdout, stderr: boundedStderr, truncated: outputWasTruncated, timedOut: termination === 'timeout', aborted: termination === 'aborted' });
|
|
397
|
+
resolve({ code, stdout, stderr: boundedStderr, truncated: outputWasTruncated, timedOut: termination === 'timeout', aborted: termination === 'aborted', outputArchive: archive.finish() });
|
|
362
398
|
});
|
|
363
399
|
}).finally(async () => {
|
|
400
|
+
checkpoint('cleanup start');
|
|
364
401
|
try {
|
|
365
402
|
SandboxManager.cleanupAfterCommand();
|
|
366
403
|
await SandboxManager.reset();
|
|
404
|
+
checkpoint('cleanup complete');
|
|
367
405
|
}
|
|
368
406
|
catch (cause) {
|
|
369
407
|
// A teardown failure must be visible and fail closed for the next call.
|
|
@@ -1,2 +1,6 @@
|
|
|
1
|
+
/** Dependency reads do not authorize adjacent installation source or secrets. */
|
|
2
|
+
export declare function windowsReadDenyRoots(workspace: string, dependencyRoots: string[]): string[];
|
|
1
3
|
/** Keep denied subtrees off the ancestors Git must stat to reach an allowed workspace. */
|
|
2
4
|
export declare function carveWindowsReadDenies(denied: string[], allowed: string[]): Promise<string[]>;
|
|
5
|
+
/** Read grants are additive; inherited Users/Everyone write rights need a deny. */
|
|
6
|
+
export declare function windowsToolchainWriteDenies(readRoots: string[], writeRoots: string[]): Promise<string[]>;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { readdir } from 'node:fs/promises';
|
|
2
|
-
import { join, relative, isAbsolute, sep } from 'node:path';
|
|
2
|
+
import { dirname, join, relative, isAbsolute, sep } from 'node:path';
|
|
3
|
+
/** Dependency reads do not authorize adjacent installation source or secrets. */
|
|
4
|
+
export function windowsReadDenyRoots(workspace, dependencyRoots) {
|
|
5
|
+
return [...new Set([dirname(workspace), ...dependencyRoots.map(root => dirname(root))])];
|
|
6
|
+
}
|
|
3
7
|
function within(path, root) {
|
|
4
8
|
const rel = relative(root, path);
|
|
5
9
|
return rel === '' || (rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel));
|
|
@@ -26,3 +30,9 @@ export async function carveWindowsReadDenies(denied, allowed) {
|
|
|
26
30
|
await visit(path);
|
|
27
31
|
return [...result];
|
|
28
32
|
}
|
|
33
|
+
/** Read grants are additive; inherited Users/Everyone write rights need a deny. */
|
|
34
|
+
export async function windowsToolchainWriteDenies(readRoots, writeRoots) {
|
|
35
|
+
const unique = readRoots.filter((path, index) => !readRoots.slice(0, index).some(other => relative(path, other) === ''));
|
|
36
|
+
const roots = unique.filter(path => !unique.some(other => other !== path && within(path, other)));
|
|
37
|
+
return carveWindowsReadDenies(roots, writeRoots);
|
|
38
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
/** Only the packaged host installation can select the sandbox broker. */
|
|
6
|
+
export function verifiedWindowsSrt(directory = fileURLToPath(new URL('../../dist/native/', import.meta.url))) {
|
|
7
|
+
const path = join(directory, 'srt-win.exe');
|
|
8
|
+
try {
|
|
9
|
+
const manifest = JSON.parse(readFileSync(join(directory, 'srt-win.json'), 'utf8'));
|
|
10
|
+
const sha256 = createHash('sha256').update(readFileSync(path)).digest('hex');
|
|
11
|
+
if (manifest.sha256 !== sha256)
|
|
12
|
+
throw new Error('WINDOWS_SANDBOX_HELPER_HASH_MISMATCH');
|
|
13
|
+
return path;
|
|
14
|
+
}
|
|
15
|
+
catch (cause) {
|
|
16
|
+
throw Object.assign(new Error('WINDOWS_SANDBOX_HELPER_UNAVAILABLE: run pnpm desktop:build or reinstall the release package'), { code: 'WINDOWS_SANDBOX_HELPER_UNAVAILABLE', cause });
|
|
17
|
+
}
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hunterzhu/pulse-adapters",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@anthropic-ai/sandbox-runtime": "0.0.77",
|
|
21
|
-
"@hunterzhu/pulse-runtime": "0.
|
|
22
|
-
"@hunterzhu/pulse-tool-sdk": "0.
|
|
21
|
+
"@hunterzhu/pulse-runtime": "0.7.0",
|
|
22
|
+
"@hunterzhu/pulse-tool-sdk": "0.7.0",
|
|
23
23
|
"exceljs": "4.4.0",
|
|
24
|
+
"mammoth": "1.11.0",
|
|
24
25
|
"pdfjs-dist": "4.10.38"
|
|
25
26
|
}
|
|
26
27
|
}
|