Skip to content

Start the CopilotKit CLI through cmd.exe on Windows - #793

Open
Caldas wants to merge 1 commit into
CopilotKit:mainfrom
Caldas:fix/GH-792_setup-learning-windows-spawn
Open

Caldas wants to merge 1 commit into
CopilotKit:mainfrom
Caldas:fix/GH-792_setup-learning-windows-spawn

Conversation

@Caldas

@Caldas Caldas commented Oct 14, 2026

Copy link
Copy Markdown

What this changes

Fixes #792. On Windows, bun scripts/setup-learning.ts stopped at once with spawn npx.cmd EINVAL and wrote nothing to .env. The cause is the hardening that followed CVE-2024-27980: Node and Bun refuse to spawn a .cmd without a shell, and the refusal is a synchronous throw. The server's own Learning setup at boot (ensureLearningContainer, server/src/index.ts:360) goes through the same runCli, so it could not run the CLI on Windows either.

In server/src/learning/ensure-container.ts:

  • npxCommand(platform) returns npx.cmd with shell: true on Windows, and npx without a shell everywhere else. macOS and Linux are unchanged.
  • cmdLineSafe(args): with a shell, the arguments become one unquoted cmd.exe line. A & or | would start another command, and a % would expand a variable. Not every argument is a literal: setup-learning.ts passes the project id it reads from .copilotkit/project.json. On the shell path, runNpx refuses any argument outside [A-Za-z0-9@._:/+-] without starting anything. Every argument any caller passes today fits. setup-learning.ts checks the project id where it reads it, so a bad id is named instead of surfacing later as "CopilotKit returned an invalid setup response".
  • stopProcessTree(child, shell): on Windows, child.kill() ends cmd.exe only. The CLI under it keeps the pipes open, so close (and the promise) waited for it, and the two-minute limit did not hold. The timer now runs %SystemRoot%\System32\taskkill.exe /pid <pid> /T /F, falling back to child.kill(). It does nothing once cmd.exe has exited, so it cannot act on a reused pid.
  • runNpx(args, directory): the spawn moved here from runCli, which now calls it with --yes copilotkit@latest. That lets the tests run the real spawn path.

Tests

server/tests/learning-ensure-container.test.ts and scripts/setup-learning.test.ts, 35 pass on Windows 11 with Bun 1.4.3:

  • npxCommand and cmdLineSafe are tested on every platform.
  • These tests are Windows-only (test.if(process.platform === "win32")), because the behaviour does not exist elsewhere:
    • runNpx(["--version"]) really starts and exits 0, where before it threw EINVAL.
    • runNpx refuses x&echo INJECTED without starting anything.
    • stopProcessTree ends a ping -n 30 under cmd.exe within 4 s. With child.kill() in its place, the test times out: close took 6.2 s for an 8-ping run.
    • setup-learning names a 12&calc project id and never calls the CLI.
  • test:ci only enforces a floor of 400 tests, so skipping the Windows tests on Linux cannot lower the count below it.
  • bun run format:check and bun run lint are clean on the changed files, and so is biome check. tsc --noEmit in server/ exits 0.
  • End to end on Windows: bun scripts/setup-learning.ts now exits 0 and writes INTELLIGENCE_API_KEY and CPK_INTELLIGENCE_LEARNING_CONTAINER_ID.

Not changed: with a UNC working directory (\\server\share), cmd.exe falls back to C:\Windows. This is rare, it existed before this change, and the checks that follow fail closed.

Where it runs

  • New state that outlives a request? None. The change is confined to one child process per CLI call, as before.
  • What happens on the second replica? Nothing new. Each process runs its own CLI call, as before, and only on Windows does anything differ.
  • Anything serialised? No.
  • Anything fanned out to a browser? No.
  • New listener, port, or schedule? No.

Boundary and audit

  • Every acting call still goes through the gateway: this touches only the local CopilotKit CLI used to set up Learning, not a Bot action.
  • New refusals and new failures each write a row: not applicable. The new refusal is a local setup error, reported the way runCli failures already are.
  • Nothing new is trusted from the client: the one non-literal argument (the project id from a local file) is now checked before it reaches a shell.

Changelog

  • A line in CHANGELOG.md under Unreleased: "setup-learning.ts runs on Windows".

On Windows, spawning npx.cmd without a shell throws a synchronous EINVAL
(Node/Bun hardening after CVE-2024-27980), so scripts/setup-learning.ts
stopped with "spawn npx.cmd EINVAL" and the server's Learning setup at
boot could not run the CLI either.

- npxCommand() starts npx.cmd with shell: true on Windows only.
- cmdLineSafe() refuses an argument cmd.exe would run or expand (the
  project id read from .copilotkit/project.json is not a literal), and
  setup-learning.ts names such an id instead of failing later.
- stopProcessTree() ends the whole tree with taskkill /T /F on timeout;
  child.kill() ended only cmd.exe and left the CLI holding the pipes.

Fixes CopilotKit#792

This branch has not been deployed

No deployments
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.

setup-learning.ts fails on Windows with "spawn npx.cmd EINVAL"

1 participant