Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -99,3 +100,103 @@ 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-relay 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) {
# 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"
}
# 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",
"--agent", "claude", "--relay", "https://relay.example",
"--skip-service", "--no-snippet", "--no-verify"
)
Assert-BlockedBy251 "doctor" @("doctor")
Assert-BlockedBy251 "listen" @("listen")
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
69 changes: 69 additions & 0 deletions docs/windows-compatibility.md
Original file line number Diff line number Diff line change
@@ -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 <peer>` 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.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
"landing:check": "node --test scripts/landing-site.test.mjs"
},
"packageManager": "pnpm@11.5.2"
Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
53 changes: 53 additions & 0 deletions scripts/windows-compat.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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-relay test/);
assert.match(workflow, /pnpm --filter @benree\/agentcall test:windows/);
assert.match(workflow, /#251/);
assert.match(workflow, /--force/);
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);
});

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"));
}
});