Skip to content

feat: Windows WHP support, deterministic VM dispose, npm audit fixes#15

Closed
simongdavies wants to merge 1 commit intohyperlight-dev:mainfrom
simongdavies:main
Closed

feat: Windows WHP support, deterministic VM dispose, npm audit fixes#15
simongdavies wants to merge 1 commit intohyperlight-dev:mainfrom
simongdavies:main

Conversation

@simongdavies
Copy link
Copy Markdown
Contributor

Windows platform support:

  • Justfile: [windows] recipes for build-hyperlight, resolve-hyperlight-dir, start-debug
  • Justfile: runtime-cflags forward-slash fix for clang cross-compilation
  • build-binary.js: .cmd launcher, platform-aware post-build output
  • plugins: O_NOFOLLOW fallback (Windows lacks O_NOFOLLOW, relies on lstat pre-check)
  • agent/index.ts: pathToFileURL for ESM plugin imports on Windows
  • build.rs: forward-slash CFLAGS for clang on Windows
  • code-validator/guest: win32-x64-msvc NAPI target
  • .gitattributes: enforce LF line endings across platforms

VM resource management:

  • sandbox/tool.js: invalidateSandbox() now calls dispose() on LoadedJSSandbox and JSSandbox for deterministic VM cleanup instead of relying on V8 GC
  • Updated hyperlight-js dep to include dispose() API

Error handling:

  • agent/event-handler.ts: suppress duplicate 'Tool execution failed' messages
  • sandbox/tool.js: MMIO error detection in compilation and runtime paths
  • agent/index.ts: surrogate pool env vars (HYPERLIGHT_INITIAL/MAX_SURROGATES)

Test fixes (Windows compatibility):

  • tests: symlink EPERM skip for Windows (path-jail, fs-read, fs-write)
  • tests/dts-sync: rmSync instead of shell rm -rf
  • tests/pattern-loader: unique tmpdir per test to avoid Windows EBUSY locks

CI:

  • pr-validate.yml: Windows WHP matrix
  • publish.yml: Windows build support

Security:

  • npm audit fix across all workspaces (picomatch, brace-expansion)
  • plugin-system/manager.ts: simplified ternary

Windows platform support:
- Justfile: [windows] recipes for build-hyperlight, resolve-hyperlight-dir, start-debug
- Justfile: runtime-cflags forward-slash fix for clang cross-compilation
- build-binary.js: .cmd launcher, platform-aware post-build output
- plugins: O_NOFOLLOW fallback (Windows lacks O_NOFOLLOW, relies on lstat pre-check)
- agent/index.ts: pathToFileURL for ESM plugin imports on Windows
- build.rs: forward-slash CFLAGS for clang on Windows
- code-validator/guest: win32-x64-msvc NAPI target
- .gitattributes: enforce LF line endings across platforms

VM resource management:
- sandbox/tool.js: invalidateSandbox() now calls dispose() on LoadedJSSandbox
  and JSSandbox for deterministic VM cleanup instead of relying on V8 GC
- Updated hyperlight-js dep to include dispose() API

Error handling:
- agent/event-handler.ts: suppress duplicate 'Tool execution failed' messages
- sandbox/tool.js: MMIO error detection in compilation and runtime paths
- agent/index.ts: surrogate pool env vars (HYPERLIGHT_INITIAL/MAX_SURROGATES)

Test fixes (Windows compatibility):
- tests: symlink EPERM skip for Windows (path-jail, fs-read, fs-write)
- tests/dts-sync: rmSync instead of shell rm -rf
- tests/pattern-loader: unique tmpdir per test to avoid Windows EBUSY locks

CI:
- pr-validate.yml: Windows WHP matrix
- publish.yml: Windows build support

Security:
- npm audit fix across all workspaces (picomatch, brace-expansion)
- plugin-system/manager.ts: simplified ternary

Signed-off-by: Simon Davies <simongdavies@users.noreply.github.com>
Copilot AI review requested due to automatic review settings March 27, 2026 17:23
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds Windows (WHP) support and improves cross-platform reliability across build tooling, sandbox lifecycle management, plugins, tests, and CI, plus applies dependency security updates.

Changes:

  • Add Windows WHP build/test support across Just recipes, CI matrices, binary packaging, and ESM plugin loading.
  • Make sandbox VM cleanup deterministic via dispose() and improve MMIO/out-of-memory error detection/messaging.
  • Improve Windows compatibility in tests and housekeeping scripts; apply npm audit upgrades (e.g., picomatch, brace-expansion).

Reviewed changes

Copilot reviewed 31 out of 33 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/pattern-loader.test.ts Use per-test temp directories to avoid Windows file locking issues.
tests/path-jail.test.ts Skip symlink creation-dependent tests when Windows lacks symlink privileges.
tests/fs-write.test.ts Skip symlink tests on Windows EPERM; cleanup for baseDir symlink case.
tests/fs-read.test.ts Skip symlink tests on Windows EPERM; cleanup for baseDir symlink case.
tests/dts-sync.test.ts Replace rm -rf shell usage with rmSync for Windows compatibility.
src/sandbox/tool.js Deterministic VM disposal + MMIO/unmapped-address error detection improvements.
src/plugin-system/manager.ts Simplify plugin entrypoint selection logic.
src/code-validator/guest/package.json Add Windows N-API build target.
src/code-validator/guest/package-lock.json Update vulnerable dependency versions (e.g., picomatch).
src/code-validator/guest/index.js Formatting + Windows triple mapping support.
src/code-validator/guest/host/build.rs Normalize include path slashes for clang on Windows.
src/agent/index.ts Windows surrogate pool env defaults + pathToFileURL for ESM plugin imports.
src/agent/event-handler.ts Suppress duplicate tool failure errors when handler already displayed a user-facing error.
scripts/update-module-hashes.ts Formatting-only adjustments.
scripts/generate-native-dts.ts Formatting-only adjustments.
scripts/generate-host-modules-dts.ts Formatting-only adjustments.
scripts/generate-ha-modules-dts.ts Formatting-only adjustments.
scripts/check-native-runtime.js Formatting-only adjustments.
scripts/build-modules.js Formatting-only adjustments.
scripts/build-binary.js Platform-aware binary packaging + add .cmd launcher and improved output messaging.
plugins/fs-write/index.ts Add Windows fallback for missing O_NOFOLLOW.
plugins/fs-read/index.ts Add Windows fallback for missing O_NOFOLLOW.
package.json Make prepare/postinstall scripts cross-platform (no POSIX shell conditionals).
package-lock.json Update vulnerable dependency versions (e.g., brace-expansion, picomatch).
builtin-modules/pptx.json Update module source hash metadata.
builtin-modules/pptx-tables.json Update module source hash metadata and formatting.
builtin-modules/pptx-charts.json Update module source hash metadata.
builtin-modules/ooxml-core.json Update module source hash metadata.
README.md Update prerequisites to include Windows WHP.
Justfile Add Windows recipes + fix runtime CFLAGS path separators for clang.
.github/workflows/publish.yml Rename matrix label used in job name.
.github/workflows/pr-validate.yml Add Windows WHP build/test matrix entries.
.gitattributes Enforce LF line endings and mark binary file patterns.
Files not reviewed (1)
  • src/code-validator/guest/package-lock.json: Language not supported
Comments suppressed due to low confidence (2)

plugins/fs-read/index.ts:267

  • This introduces O_NOFOLLOW = FS_CONSTANTS.O_NOFOLLOW ?? 0, but the read paths still use FS_CONSTANTS.O_NOFOLLOW in the openSync flags (so the new constant is unused) and there is no post-open lstat/fstat comparison despite the comment claiming one. On Windows this leaves a real TOCTOU window where a path can be swapped to a symlink between validatePath() and openSync(). Either implement the post-open verification (before reading) and use the O_NOFOLLOW constant in open flags, or adjust the comment/behavior accordingly.
  // O_NOFOLLOW atomically rejects symlinks at open() on POSIX.
  // On Windows it doesn't exist — we rely on the lstatSync pre-check
  // in validatePath() plus a post-open fstatSync/lstatSync comparison.
  // The residual TOCTOU window is narrow and requires symlink creation
  // privileges (SeCreateSymbolicLinkPrivilege or Developer Mode).
  const O_NOFOLLOW = FS_CONSTANTS.O_NOFOLLOW ?? 0;

  // ── Host function implementations ────────────────────────────

  /**
   * readFile(path, encoding?) — Read a file's contents.
   * Uses O_NOFOLLOW to atomically reject symlinks at open time.
   */
  function readFile(filePath: string, encoding?: string): ReadFileResult {
    const enc =
      typeof encoding === "string" && ALLOWED_ENCODINGS.has(encoding)
        ? encoding
        : "utf8";

    const check = validatePath(filePath, resolvedBase);
    if (!check.valid) {
      return { error: check.error };
    }

    let fd: number | undefined;
    try {
      fd = openSync(
        check.realPath!,
        FS_CONSTANTS.O_RDONLY | FS_CONSTANTS.O_NOFOLLOW,
      );

plugins/fs-write/index.ts:277

  • O_NOFOLLOW is introduced with a Windows fallback (FS_CONSTANTS.O_NOFOLLOW ?? 0), but the write paths still OR in FS_CONSTANTS.O_NOFOLLOW directly (leaving the new constant unused) and there is no post-open lstat/fstat verification even though the comment says there is. For Windows, that means validatePath() can be bypassed via TOCTOU (swap to a symlink between validation and openSync()), potentially allowing writes outside the jail. Use the O_NOFOLLOW constant in open flags and add a post-open check (before writing) to ensure the opened fd still corresponds to the non-symlink path.
  // O_NOFOLLOW atomically rejects symlinks at open() on POSIX.
  // On Windows it doesn't exist — we rely on the lstatSync pre-check
  // in validatePath() plus a post-open fstatSync/lstatSync comparison.
  // The residual TOCTOU window is narrow and requires symlink creation
  // privileges (SeCreateSymbolicLinkPrivilege or Developer Mode).
  const O_NOFOLLOW = FS_CONSTANTS.O_NOFOLLOW ?? 0;

  const maxEntries = Math.floor(
    safeNumericConfig(cfg.maxEntries, 500, MAX_ENTRIES_LIMIT),
  );
  let entriesCreated = 0;

  // ── Host function implementations ────────────────────────────

  function writeFile(
    filePath: string,
    content: string,
    encoding?: string,
  ): WriteResult {
    const enc =
      typeof encoding === "string" && ALLOWED_ENCODINGS.has(encoding)
        ? encoding
        : "utf8";

    const check = validatePath(filePath, resolvedBase);
    if (!check.valid) {
      return { error: check.error };
    }

    if (typeof content !== "string") {
      return {
        error:
          "writeFile expects a string. For binary data (Uint8Array from " +
          "createZip, etc.), use writeFileBinary(path, data) instead.",
      };
    }

    const contentBytes =
      enc === "base64"
        ? Math.ceil((content.length * 3) / 4)
        : Buffer.byteLength(content, "utf8");

    if (contentBytes > maxWriteChunkBytes) {
      return {
        error: `Content too large for single write: ${contentBytes} bytes exceeds per-call limit of ${MAX_WRITE_CHUNK_KB}KB. Split into multiple appendFile calls.`,
      };
    }
    if (contentBytes > maxWriteBytes) {
      return {
        error: `Content too large: exceeds cumulative file write limit of ${maxWriteBytes / 1024}KB`,
      };
    }

    let fd: number | undefined;
    let isNew = false;
    try {
      const parentDir = dirname(check.realPath!);
      if (!existsSync(parentDir)) {
        return { error: "Parent directory does not exist" };
      }
      isNew = !existsSync(check.realPath!);
      if (isNew) {
        if (entriesCreated >= maxEntries) {
          return {
            error: `Entry limit reached: cannot create more than ${maxEntries} files/directories`,
          };
        }
        entriesCreated++;
      }
      fd = openSync(
        check.realPath!,
        FS_CONSTANTS.O_WRONLY |
          FS_CONSTANTS.O_CREAT |
          FS_CONSTANTS.O_TRUNC |
          FS_CONSTANTS.O_NOFOLLOW,
        FILE_MODE,
      );

Comment on lines +429 to +432
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const cjs = join(__dirname, '..', 'lib', 'hyperagent-launcher.cjs');
await import('file://' + cjs.replace(/\\\\/g, '/'));
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated Node.js launcher builds a file URL via string concatenation ('file://' + cjs.replace(...)), which produces invalid URLs on both Windows (file://C:/... instead of file:///C:/...) and POSIX (file:////... for absolute paths). This will break import() of the CJS launcher in the built binary. Use pathToFileURL(cjs).href (or equivalent URL construction) instead of manual concatenation.

Suggested change
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const cjs = join(__dirname, '..', 'lib', 'hyperagent-launcher.cjs');
await import('file://' + cjs.replace(/\\\\/g, '/'));
import { fileURLToPath, pathToFileURL } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const cjs = join(__dirname, '..', 'lib', 'hyperagent-launcher.cjs');
await import(pathToFileURL(cjs).href);

Copilot uses AI. Check for mistakes.
Comment on lines 493 to +502
@@ -417,7 +499,17 @@ To run (option 2 - add to PATH):

To run (option 3 - symlink):
sudo ln -sf ${launcherPath} /usr/local/bin/hyperagent
hyperagent
hyperagent`;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The printed Unix run instructions escape $PATH (export PATH="${BIN_DIR}:\$PATH"), which will set PATH to a literal $PATH instead of expanding the existing value. Remove the backslash so the instructions actually prepend BIN_DIR.

Copilot uses AI. Check for mistakes.
Comment on lines 23 to +28
afterEach(() => {
rmSync(TMP_DIR, { recursive: true, force: true });
try {
rmSync(TMP_DIR, { recursive: true, force: true });
} catch {
// Windows: Defender/indexer may hold a lock — not worth failing the test
}
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The afterEach cleanup swallows all rmSync failures unconditionally. That can hide real test isolation issues on non-Windows platforms (e.g., permission problems) and leave temp dirs behind. Consider only suppressing expected Windows lock errors (e.g., when process.platform === "win32" and error code is EBUSY/EPERM), and rethrow otherwise.

Copilot uses AI. Check for mistakes.
@simongdavies
Copy link
Copy Markdown
Contributor Author

Reopening from windows-support branch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants