@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.
Files changed (36) hide show
  1. package/dist/documents/docx.d.ts +2 -0
  2. package/dist/documents/docx.js +45 -0
  3. package/dist/documents/tools.d.ts +12 -0
  4. package/dist/documents/tools.js +23 -8
  5. package/dist/index.d.ts +1 -0
  6. package/dist/index.js +1 -0
  7. package/dist/mcp/stdio.js +12 -1
  8. package/dist/native/LICENSE +201 -0
  9. package/dist/native/srt-win.exe +0 -0
  10. package/dist/native/srt-win.json +11 -0
  11. package/dist/providers/anthropic.d.ts +1 -0
  12. package/dist/providers/anthropic.js +7 -50
  13. package/dist/providers/canonical/lowering.js +12 -0
  14. package/dist/providers/canonical/mappers/anthropic.js +4 -3
  15. package/dist/providers/canonical/mappers/openai.js +1 -1
  16. package/dist/providers/normalize.d.ts +6 -1
  17. package/dist/providers/normalize.js +29 -3
  18. package/dist/providers/openai-compat.d.ts +2 -0
  19. package/dist/providers/openai-compat.js +10 -1
  20. package/dist/providers/runtime-executor.d.ts +8 -1
  21. package/dist/providers/runtime-executor.js +55 -10
  22. package/dist/providers/types.d.ts +2 -0
  23. package/dist/tools/installation-dependencies.d.ts +4 -0
  24. package/dist/tools/installation-dependencies.js +59 -0
  25. package/dist/tools/registry.js +5 -2
  26. package/dist/tools/sandbox-initialization.d.ts +1 -1
  27. package/dist/tools/sandbox-initialization.js +63 -7
  28. package/dist/tools/shell-output.d.ts +23 -0
  29. package/dist/tools/shell-output.js +82 -0
  30. package/dist/tools/shell.d.ts +6 -0
  31. package/dist/tools/shell.js +62 -24
  32. package/dist/tools/windows-read-policy.d.ts +4 -0
  33. package/dist/tools/windows-read-policy.js +11 -1
  34. package/dist/tools/windows-srt-path.d.ts +2 -0
  35. package/dist/tools/windows-srt-path.js +18 -0
  36. package/package.json +4 -3
@@ -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, VENDORED_SRT_WIN_EXE, checkWindowsDependenciesAsync, resolveSrtWin } from '@anthropic-ai/sandbox-runtime';
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: VENDORED_SRT_WIN_EXE }) })
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); only carve the workspace's sibling boundary on that platform.
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
- denyRead: process.platform === 'win32' ? await carveWindowsReadDenies([...denyRead], allowRead) : [...denyRead],
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: VENDORED_SRT_WIN_EXE } } } : {}),
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
- if (!command || typeof command !== 'string' || !Array.isArray(args) || args.some((arg) => typeof arg !== 'string'))
177
- return Promise.reject(shellError('INVALID_SHELL_ARGUMENT'));
178
- if (/&&|\|\||[|&;<>\n`]|\$\(/.test(command) || shellLaunchesCommand(command, args))
179
- return Promise.reject(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.' }));
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 outputChunks = { stdout: [], stderr: [] };
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
- const remaining = Math.max(0, max - outputBytes[target]);
281
- const kept = Math.min(chunk.byteLength, remaining);
282
- if (kept > 0)
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.concat(outputChunks[target]), max);
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 { /* preserve the setup failure */ }
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,2 @@
1
+ /** Only the packaged host installation can select the sandbox broker. */
2
+ export declare function verifiedWindowsSrt(directory?: string): string;
@@ -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.5.1",
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.5.1",
22
- "@hunterzhu/pulse-tool-sdk": "0.5.1",
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
  }