@hachej/boring-execution 0.4.8 → 0.4.10

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.
@@ -378,14 +378,34 @@ export function createCodeInterpreterEnv(options) {
378
378
  readTextLines: (path, lines, context) => guarded([[path, true]], file => local.readTextLines(file, lines, context)),
379
379
  openTextLineReader: (path, context) => guarded([[path, true]], file => local.openTextLineReader(file, context)),
380
380
  openBinaryReader: (path, readerOptions, context) => guarded([[path, !readerOptions?.noFollow]], file => local.openBinaryReader(file, readerOptions, context)),
381
- openDirReader: (path, context) => guarded([[path, true]], directory => local.openDirReader(directory, context)),
382
- // The mount's own watcher, with its paths reported in the workspace's namespace.
381
+ // The reader's entries carry host paths; map them into the workspace namespace like `listDir`.
382
+ openDirReader: async (path, context) => {
383
+ const opened = await guarded([[path, true]], directory => local.openDirReader(directory, context));
384
+ if (!opened.ok)
385
+ return opened;
386
+ const reader = opened.value;
387
+ return ok({
388
+ next: async (maxEntries, readContext) => {
389
+ const page = await reader.next(maxEntries, readContext);
390
+ return page.ok ? ok({ ...page.value, entries: page.value.entries.map(mapInfo) }) : err(mapError(page.error));
391
+ },
392
+ close: readContext => reader.close(readContext),
393
+ });
394
+ },
395
+ // The mount's own watcher, confined like every file tool, with its paths reported in the workspace's namespace.
383
396
  watch: async (targets, onChange, context) => {
384
397
  const mapped = [];
385
398
  for (const target of targets) {
386
399
  const path = toLocal(target.path);
387
400
  if (path === undefined)
388
401
  return err(outside(target.path));
402
+ try {
403
+ if (!await confined(path, true))
404
+ return err(outside(target.path));
405
+ }
406
+ catch (error) {
407
+ return err(new FileError('unknown', errorText(error), namespace(target.path)));
408
+ }
389
409
  mapped.push({ ...target, path });
390
410
  }
391
411
  const watcher = await local.watch(mapped, change => onChange('paths' in change ? { paths: change.paths.map(path => fromLocal(path)) } : 'error' in change ? { error: mapError(change.error) } : change), context);
@@ -20,6 +20,12 @@ export interface RemoteShellHandlerOptions {
20
20
  readonly maxRequestBytes?: number;
21
21
  readonly maxFrameBytes?: number;
22
22
  readonly maxQueuedBytes?: number;
23
+ /**
24
+ * A client-chosen `env` may not set loader or shell-startup variables (`LD_*`, `DYLD_*`, `BASH_ENV`, `ENV`, `NODE_OPTIONS`,
25
+ * `PYTHONSTARTUP`, `PERL5OPT` and a few `GIT_*` command hooks): they run code before the command, which a host that authorizes
26
+ * only the command string would not see. The request is refused (403) before `authorize`. Set true to let clients pass them.
27
+ */
28
+ readonly allowLoaderEnv?: boolean;
23
29
  }
24
30
  export declare function createRemoteShellHandler(options: RemoteShellHandlerOptions): (request: Request) => Promise<Response>;
25
31
  //# sourceMappingURL=remote-shell-handler.d.ts.map
@@ -3,6 +3,7 @@ import { ExecutionError, err } from '@earendil-works/pi-durable/env';
3
3
  import { guardStatus, hasJsonContentType, readJsonBody } from '@hachej/boring-files/request-guard';
4
4
  import { windowedOutput } from './remote-shell-window.js';
5
5
  import { contentType, identity, nativeVersion, positiveLimit, requestInput, sameIdentity, schema, version, wireResult } from './remote-shell-protocol.js';
6
+ const LOADER_ENV = /^(LD_|DYLD_)|^(BASH_ENV|ENV|NODE_OPTIONS|PYTHONSTARTUP|PERL5OPT|GIT_SSH_COMMAND|GIT_SSH|GIT_ASKPASS|GIT_EXTERNAL_DIFF|GIT_PROXY_COMMAND)$/;
6
7
  function failure(status) {
7
8
  return new Response(null, { status, headers: { 'cache-control': 'no-store' } });
8
9
  }
@@ -40,6 +41,8 @@ export function createRemoteShellHandler(options) {
40
41
  return failure(409);
41
42
  if (input.options.timeout !== undefined && supports.timeout !== true || input.options.spill !== undefined && supports.spill !== true)
42
43
  return failure(422);
44
+ if (!options.allowLoaderEnv && Object.keys(input.options.env ?? {}).some(key => LOADER_ENV.test(key)))
45
+ return failure(403);
43
46
  try {
44
47
  if (signal?.aborted || await awaitWithContext(Promise.resolve(authorize(input.command, structuredClone(input.options))), context) !== true || signal?.aborted)
45
48
  return failure(403);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-execution",
3
- "version": "0.4.8",
3
+ "version": "0.4.10",
4
4
  "license": "MIT",
5
5
  "private": false,
6
6
  "type": "module",
@@ -56,7 +56,7 @@
56
56
  "@earendil-works/chord": "~1.2.0",
57
57
  "just-bash": "^3.6.0",
58
58
  "isomorphic-git": "^1.42.6",
59
- "@hachej/boring-files": "^0.4.8",
59
+ "@hachej/boring-files": "^0.4.10",
60
60
  "zod": "^4.6.5",
61
61
  "@aws-sdk/client-bedrock-agentcore": "^3.1146.0"
62
62
  },