From d686a7927e2d6d36c1ba4009e5c93a72a542e138 Mon Sep 17 00:00:00 2001 From: RyuseiTaniguchi Date: Mon, 3 Aug 2026 16:30:29 -0700 Subject: [PATCH 1/4] ci: add native Windows compatibility harness --- .github/workflows/ci.yml | 88 +++++++++++++++++++++++++++++++++ CHANGELOG.md | 12 +++++ README.md | 4 +- docs/windows-compatibility.md | 69 ++++++++++++++++++++++++++ package.json | 2 +- packages/cli/package.json | 1 + scripts/windows-compat.test.mjs | 49 ++++++++++++++++++ 7 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 docs/windows-compatibility.md create mode 100644 scripts/windows-compat.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c6d2d8a..8c742845 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: [main] pull_request: + workflow_dispatch: # A new push to the same branch supersedes the run in flight. concurrency: @@ -99,3 +100,90 @@ jobs: exit 1 fi grep -F "No agentcall config found" "$RUNNER_TEMP/status-output" + + # Compatibility evidence only: #251 owns functional Windows support. Keep + # the published OS restriction in place and force-install only the tarball + # whose startup surface this job probes. + windows-compat: + needs: verify + runs-on: windows-2025 + strategy: + fail-fast: false + matrix: + node: [20, 22, 24] + defaults: + run: + shell: pwsh + steps: + # Node 24 carries the platform-neutral source gate. Node 20 and 22 only + # exercise the packed artifact, avoiding three copies of the slow suite. + - if: matrix.node == 24 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + - if: matrix.node == 24 + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 + with: + node-version: ${{ matrix.node }} + - if: matrix.node == 24 + run: pnpm install --frozen-lockfile + - if: matrix.node == 24 + run: pnpm -r build + - if: matrix.node == 24 + run: pnpm -r typecheck + - name: Platform-neutral tests + if: matrix.node == 24 + run: | + pnpm --filter @benree/agentcall-shared test + pnpm --filter @benree/agentcall test:windows + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: packed-cli + path: ${{ runner.temp }}/agentcall-packages + - name: "Packaged CLI startup and #251 blocker inventory" + env: + AGENTCALL_HOME: ${{ runner.temp }}/agentcall home with spaces + NPM_CONFIG_ENGINE_STRICT: "true" + run: | + $packDir = Join-Path $env:RUNNER_TEMP "agentcall-packages" + $shared = (Get-ChildItem $packDir -Filter "benree-agentcall-shared-*.tgz" | Select-Object -First 1).FullName + $cli = (Get-ChildItem $packDir -Filter "benree-agentcall-*.tgz" | + Where-Object Name -NotLike "*shared*" | Select-Object -First 1).FullName + if (-not $shared -or -not $cli) { throw "packed CLI artifacts are missing" } + + # The ordinary install must remain blocked until functional support + # lands in #251; a surprise success would overstate package support. + $normalPrefix = Join-Path $env:RUNNER_TEMP "agentcall normal install" + $normalOutput = (& npm.cmd install --global --prefix $normalPrefix $shared $cli 2>&1 | Out-String) + $normalCode = $LASTEXITCODE + if ($normalCode -eq 0) { throw "Windows install unexpectedly succeeded before #251" } + if ($normalOutput -notmatch "EBADPLATFORM|Unsupported platform") { + throw "Windows install failed outside the documented #251 platform gate: $normalOutput" + } + + # Force is a harness-only escape hatch. The clean prefix and HOME both + # contain spaces so npm's .cmd shim and PowerShell quoting are real. + $forcedPrefix = Join-Path $env:RUNNER_TEMP "agentcall forced install" + npm.cmd install --global --force --prefix $forcedPrefix $shared $cli + if ($LASTEXITCODE -ne 0) { throw "forced packed install failed" } + $agentcall = Join-Path $forcedPrefix "agentcall.cmd" + if (-not (Test-Path $agentcall)) { throw "npm did not create agentcall.cmd" } + & $agentcall --version + if ($LASTEXITCODE -ne 0) { throw "agentcall --version failed" } + & $agentcall --help + if ($LASTEXITCODE -ne 0) { throw "agentcall --help failed" } + + function Assert-BlockedBy251([string] $name, [string[]] $commandArgs) { + $output = (& $agentcall @commandArgs 2>&1 | Out-String) + $code = $LASTEXITCODE + if ($code -eq 0) { throw "$name unexpectedly succeeded before #251" } + if ($output -notmatch "Managed policy is not supported on win32") { + throw "$name failed outside the documented #251 blocker: $output" + } + } + Assert-BlockedBy251 "setup" @( + "setup", "--invite", "unused", "--handle", "windows-probe", + "--agent", "claude", "--relay", "https://relay.example", + "--skip-service", "--no-snippet", "--no-verify" + ) + Assert-BlockedBy251 "doctor" @("doctor") + Assert-BlockedBy251 "listen" @("listen") diff --git a/CHANGELOG.md b/CHANGELOG.md index af628527..63a6ae59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,18 @@ which are released together. ## Unreleased +### Native Windows compatibility evidence + +- Add a native `windows-2025` CI matrix for Node 20, 22, and 24 that exercises + the packed CLI from a clean path containing spaces while preserving the + public unsupported-platform install boundary. +- Run build, typecheck, shared tests, and an explicitly platform-neutral CLI + suite on Windows; classify every remaining setup, supervision, ACL, process, + executable-resolution, and path blocker under #251. +- Document the Daytona/manual setup, foreground listener, inbound call, + cancellation, timeout, status, and uninstall probe required before Windows + can be claimed as a supported callee platform. + ### Generation-bound handle recovery - Add an org-scoped, generation-versioned recovery proof that is issued only diff --git a/README.md b/README.md index 65597cec..5f73f250 100644 --- a/README.md +++ b/README.md @@ -328,7 +328,9 @@ observability spec records the rationale that preceded this implementation. ## Limitations - The hosted service and customer-owned relay path are pre-production. -- Native listeners support macOS and Linux, not Windows. +- Native listeners support macOS and Linux, not Windows. The + [native-Windows compatibility harness](./docs/windows-compatibility.md) + records current CI evidence and the remaining implementation blockers. - Calls are synchronous; there is no store-and-forward mailbox. - Each listener handles one call at a time. A concurrent call returns `busy`. - Handles cannot currently be released or reclaimed. diff --git a/docs/windows-compatibility.md b/docs/windows-compatibility.md new file mode 100644 index 00000000..4649f6bd --- /dev/null +++ b/docs/windows-compatibility.md @@ -0,0 +1,69 @@ +# Native Windows compatibility harness + +Windows is not a supported AgentCall callee platform yet. GitHub Actions runs +the `windows-compat` job on a native `windows-2025` VM to keep that boundary +observable while [#251](https://github.com/KenTaniguchi-R/agentcall/issues/251) +implements it. A passing harness means the platform-neutral code and packed +CLI startup surface work; it does not mean setup, listening, or secret storage +are safe on Windows. + +## Automated coverage + +The job can be started with GitHub's **Run workflow** button and also runs for +pull requests and pushes to `main`. + +- Node 20, 22, and 24 each install the Linux-built shared and CLI tarballs into + a clean Windows prefix whose path contains spaces, then run the npm-generated + `agentcall.cmd` shim with `--version` and `--help`. +- The normal npm install is expected to fail with `EBADPLATFORM`. The job uses + `--force` only after proving that public-install boundary remains intact. +- Node 24 additionally runs the monorepo build and typecheck, every shared test, + and the CLI `test:windows` allowlist. That allowlist contains only + suites that do not construct unsupported machine paths, rely on POSIX mode + bits, launch Unix processes, or test Unix supervisors. +- `setup`, `doctor`, and `listen` are executed from the packed CLI and must all + stop at the same documented `Managed policy is not supported on win32` + boundary. A different error is an undocumented red test. + +## Blocker inventory + +All functional work below belongs to #251. The harness must keep a stable, +explicit expectation until that issue replaces it with positive coverage. + +| Area | Current evidence and required #251 replacement | +| --- | --- | +| npm `os` metadata | `packages/cli/package.json` permits `darwin` and `linux`; native installation must fail with `EBADPLATFORM`. Remove the restriction only when the functional gate is green. | +| Absolute managed-policy paths | `managedPolicyPath("win32")` rejects the platform. #251 must use a machine-owned absolute `%ProgramData%` location that `AGENTCALL_HOME` cannot relocate. | +| Unix-only fixture shebangs and executable bits | The full CLI suite creates executable shell fixtures and assumes shebang launch. `test:windows` names the portable allowlist; #251 must add `.cmd`/native fixtures and then retire the allowlist. | +| POSIX modes versus Windows ACLs | Stores use `chmod(0600/0700)`, which is not a Windows secrecy boundary. #251 must apply and diagnose owner-only Windows ACLs for credentials and local state. | +| launchd/systemd listener supervision | The service adapter has only launchd and systemd. #251 must add an idempotent per-user Windows supervisor, including install, status, restart, and uninstall. | +| Detached process groups and process-tree teardown | `runAgent` uses negative PIDs plus `SIGTERM`/`SIGKILL`. #251 must prove Windows cancellation, timeout, overflow, and shutdown terminate the entire child tree. | +| `.cmd` / `.exe` discovery | The packed smoke test proves npm's `agentcall.cmd` shim. #251 must cover agent discovery through `PATH`/`PATHEXT`, executable suffixes, and paths with spaces. | +| Shell and PowerShell quoting | The harness uses clean prefix and home paths containing spaces. #251 must cover user-controlled Windows paths, PowerShell argument boundaries, drive letters, UNC paths, case folding, and reparse points. | + +## Daytona/manual Windows VM probe + +Run this checklist on a fresh Windows VM from the #251 implementation branch; +record the Windows edition/architecture, Node and AgentCall versions, agent +binary/version, and the command transcript. Do not mark #251 supported from a +Docker-on-macOS run. + +1. Install the packed CLI into a non-admin prefix containing a space. Run + `agentcall.cmd --version` and `agentcall.cmd --help`. +2. Run `agentcall.cmd setup ...` with a disposable organization invite. Close + the setup shell and verify the per-user listener remains supervised. +3. Stop that supervisor and run `agentcall.cmd listen` for the foreground listen + probe. From a second enrolled handle, place one inbound call and + verify its reply. +4. Place a call whose fake/test agent exceeds its deadline; verify timeout + kills the full process tree and leaves no child behind. +5. Place another call and cancel it while running; verify cancellation reaches + the caller and leaves no child process behind. +6. Run `agentcall.cmd status ` and `agentcall.cmd doctor`; verify service, + PATH, auth, managed-policy, and ACL rows match the VM state. +7. Run `agentcall.cmd uninstall`, confirm the per-user supervisor is removed, + and record whether local identity/state was retained or explicitly purged. + +Today steps 2–7 are expected to stop at the #251 boundary. This checklist is +the positive acceptance probe once that implementation lands, not evidence +that the current release supports Windows. diff --git a/package.json b/package.json index 6f026beb..952ea6f3 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "build": "pnpm -r build", "typecheck": "pnpm -r typecheck", "docs:generate": "node scripts/generate-docs-site-cli.mjs && node scripts/generate-docs-site-protocol.mjs && node scripts/generate-docs-site-audit-events.mjs", - "docs:check": "node --test scripts/docs-site.test.mjs && node scripts/check-docs-site.mjs" + "docs:check": "node --test scripts/docs-site.test.mjs scripts/windows-compat.test.mjs && node scripts/check-docs-site.mjs" }, "packageManager": "pnpm@11.5.2" } diff --git a/packages/cli/package.json b/packages/cli/package.json index df653fbe..eda3f6a9 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -29,6 +29,7 @@ "scripts": { "build": "tsc -p tsconfig.json", "test": "vitest run", + "test:windows": "vitest run test/abuse-signals.test.ts test/prompt.test.ts test/queue.test.ts test/search-scope.test.ts test/telemetry.test.ts test/verbs.test.ts", "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json" }, "dependencies": { diff --git a/scripts/windows-compat.test.mjs b/scripts/windows-compat.test.mjs new file mode 100644 index 00000000..04d72b5b --- /dev/null +++ b/scripts/windows-compat.test.mjs @@ -0,0 +1,49 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { test } from "node:test"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const read = (path) => readFileSync(join(root, path), "utf8"); + +test("native Windows CI keeps the engines and unsupported-platform contracts explicit", () => { + const workflow = read(".github/workflows/ci.yml"); + const cliPackage = JSON.parse(read("packages/cli/package.json")); + + assert.match(workflow, /^ workflow_dispatch:/m); + assert.match(workflow, /^ windows-compat:/m); + assert.match(workflow, /runs-on: windows-2025/); + assert.match(workflow, /node: \[20, 22, 24\]/); + assert.match(workflow, /shell: pwsh/); + assert.match(workflow, /pnpm --filter @benree\/agentcall test:windows/); + assert.match(workflow, /#251/); + assert.match(workflow, /--force/); + assert.match(workflow, /agentcall\.cmd/); + assert.equal(typeof cliPackage.scripts["test:windows"], "string"); + assert.equal(cliPackage.os.includes("win32"), false); +}); + +test("the compatibility inventory names every required blocker and manual probe", () => { + const inventory = read("docs/windows-compatibility.md"); + for (const phrase of [ + "npm `os` metadata", + "managed-policy paths", + "fixture shebangs and executable bits", + "Windows ACLs", + "listener supervision", + "process-tree teardown", + "`.cmd` / `.exe` discovery", + "PowerShell quoting", + "setup", + "foreground listen", + "inbound call", + "cancellation", + "timeout", + "status", + "uninstall", + "#251", + ]) { + assert.match(inventory, new RegExp(phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i")); + } +}); From 27c6408f8d3234719f33462aeab4f2fc6488c833 Mon Sep 17 00:00:00 2001 From: RyuseiTaniguchi Date: Mon, 3 Aug 2026 16:38:36 -0700 Subject: [PATCH 2/4] ci: capture expected Windows command failures --- .github/workflows/ci.yml | 13 +++++++++++-- scripts/windows-compat.test.mjs | 2 ++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c742845..20b6f8d7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -173,8 +173,17 @@ jobs: if ($LASTEXITCODE -ne 0) { throw "agentcall --help failed" } function Assert-BlockedBy251([string] $name, [string[]] $commandArgs) { - $output = (& $agentcall @commandArgs 2>&1 | Out-String) - $code = $LASTEXITCODE + # GitHub's pwsh wrapper uses Stop globally. Expected stderr arrives + # as ErrorRecord objects, so relax only around this captured command + # and restore strict handling before evaluating the result. + $previousErrorAction = $ErrorActionPreference + try { + $ErrorActionPreference = "Continue" + $output = (& $agentcall @commandArgs 2>&1 | Out-String) + $code = $LASTEXITCODE + } finally { + $ErrorActionPreference = $previousErrorAction + } if ($code -eq 0) { throw "$name unexpectedly succeeded before #251" } if ($output -notmatch "Managed policy is not supported on win32") { throw "$name failed outside the documented #251 blocker: $output" diff --git a/scripts/windows-compat.test.mjs b/scripts/windows-compat.test.mjs index 04d72b5b..fa114f2a 100644 --- a/scripts/windows-compat.test.mjs +++ b/scripts/windows-compat.test.mjs @@ -20,6 +20,8 @@ test("native Windows CI keeps the engines and unsupported-platform contracts exp assert.match(workflow, /#251/); assert.match(workflow, /--force/); assert.match(workflow, /agentcall\.cmd/); + assert.match(workflow, /\$ErrorActionPreference = "Continue"/); + assert.match(workflow, /\$ErrorActionPreference = \$previousErrorAction/); assert.equal(typeof cliPackage.scripts["test:windows"], "string"); assert.equal(cliPackage.os.includes("win32"), false); }); From 95de9bf4110116ff009a6b0c283daa2777df70c2 Mon Sep 17 00:00:00 2001 From: RyuseiTaniguchi Date: Mon, 3 Aug 2026 16:42:59 -0700 Subject: [PATCH 3/4] ci: clear asserted Windows exit codes --- .github/workflows/ci.yml | 3 +++ scripts/windows-compat.test.mjs | 1 + 2 files changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20b6f8d7..434c3576 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -188,6 +188,9 @@ jobs: if ($output -notmatch "Managed policy is not supported on win32") { throw "$name failed outside the documented #251 blocker: $output" } + # The Actions pwsh wrapper exits with the last native code after + # this script returns. This nonzero code has just been asserted. + $global:LASTEXITCODE = 0 } Assert-BlockedBy251 "setup" @( "setup", "--invite", "unused", "--handle", "windows-probe", diff --git a/scripts/windows-compat.test.mjs b/scripts/windows-compat.test.mjs index fa114f2a..0c200e38 100644 --- a/scripts/windows-compat.test.mjs +++ b/scripts/windows-compat.test.mjs @@ -22,6 +22,7 @@ test("native Windows CI keeps the engines and unsupported-platform contracts exp assert.match(workflow, /agentcall\.cmd/); assert.match(workflow, /\$ErrorActionPreference = "Continue"/); assert.match(workflow, /\$ErrorActionPreference = \$previousErrorAction/); + assert.match(workflow, /\$global:LASTEXITCODE = 0/); assert.equal(typeof cliPackage.scripts["test:windows"], "string"); assert.equal(cliPackage.os.includes("win32"), false); }); From f170c004fef6ab81648a8da13a3158e491337b75 Mon Sep 17 00:00:00 2001 From: RyuseiTaniguchi Date: Mon, 3 Aug 2026 16:51:41 -0700 Subject: [PATCH 4/4] ci: run relay tests on native Windows --- .github/workflows/ci.yml | 1 + scripts/windows-compat.test.mjs | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 434c3576..97bd9cb4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,6 +134,7 @@ jobs: if: matrix.node == 24 run: | pnpm --filter @benree/agentcall-shared test + pnpm --filter @benree/agentcall-relay test pnpm --filter @benree/agentcall test:windows - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: diff --git a/scripts/windows-compat.test.mjs b/scripts/windows-compat.test.mjs index 0c200e38..7e91d4b3 100644 --- a/scripts/windows-compat.test.mjs +++ b/scripts/windows-compat.test.mjs @@ -16,6 +16,7 @@ test("native Windows CI keeps the engines and unsupported-platform contracts exp assert.match(workflow, /runs-on: windows-2025/); assert.match(workflow, /node: \[20, 22, 24\]/); assert.match(workflow, /shell: pwsh/); + assert.match(workflow, /pnpm --filter @benree\/agentcall-relay test/); assert.match(workflow, /pnpm --filter @benree\/agentcall test:windows/); assert.match(workflow, /#251/); assert.match(workflow, /--force/);