Files
orca/pnpm-workspace.yaml
T
Neil 13ba649c22 fix(terminal): let a runtime-created Windows terminal BE the requested shell (#20825)
* fix(terminal): let a runtime-created Windows terminal BE the requested shell

`orca terminal create --environment <windows-host> --command 'cmd.exe'` never
created a cmd terminal. `--command` is text the provider TYPES into whatever
shell it spawned, so the PTY stayed the host's default shell with cmd running
inside it. Captured on `awin`, whose default is Git Bash:

    $ orca terminal create --environment awin --command 'cmd.exe' --json
    $ orca terminal send --environment awin --terminal term_10656cf7... \
        --text exit --enter
    $ orca terminal read --environment awin --terminal term_10656cf7... --screen
      neil@awin MINGW64 ~/orca/orca ((30f820708f...))
      $ cmd.exe
      Microsoft Windows [Version 10.0.26200.9445]
      C:\Users\neil\orca\orca>exit
      neil@awin MINGW64 ~/orca/orca ((30f820708f...))
      $

The handle is alive the whole time and `terminal list` shows one healthy
terminal, because the PTY never changed — so the only symptom is that the
caller's terminal is now a shell it never asked for, and every later `send` is
quoted for the wrong one. On `win-lowspec` (default pwsh) the same create lands
cmd inside PowerShell.

Root cause
----------
There are two spawn preflights and they are twins:

- `src/main/ipc/pty/ipc/spawn-preflight.ts` — renderer/IPC spawns, i.e. a
  terminal tab opened in the app.
- `src/main/ipc/pty/runtime/spawn-preflight.ts` — runtime spawns: the CLI's
  `terminal.create`, headless `orca serve`, and every paired remote
  environment.

Only the IPC twin read the caller's requested shell. The runtime twin passed a
literal `requestedShellOverride: undefined`, so a runtime-created terminal on
Windows could only ever be the host default. Everything downstream of that
point — `spawn-options`, the daemon, `resolvePtyShellOverride` in the relay,
`local-pty-launch-plan` — already honoured `shellOverride`; nothing upstream
could supply one.

Change
------
- Thread `shellOverride` through the runtime lane: `RuntimePtySpawnArgs` ->
  runtime `spawn-preflight` -> `RuntimePtyController.spawn` ->
  `TerminalCreateOptions` -> the `terminal.create` RPC's new `shell` param ->
  `orca terminal create --shell`.
- Thread it through the renderer-backed lane too (`createDesktopTerminal` ->
  `terminal:requestTabCreate` -> `store.createTab`), so `--shell --focus` is not
  silently dropped on a local Windows app.
- An agent launch quotes its startup command for the shell it will actually run
  in, so a requested shell now owns the startup-shell family instead of the
  global `terminalWindowsShell` setting.
- Lift the relay's `ALLOWED_WINDOWS_SHELL_OVERRIDES` into
  `isSupportedWindowsShellOverride` in `src/shared/windows-terminal-shell.ts`
  (membership unchanged) so the CLI, the zod param schema, and the relay refuse
  the same names. `--shell` therefore cannot carry a path or a command line into
  `pty.spawn`; only allowlisted bare shell names pass.
- Gate on `TERMINAL_CREATE_SHELL_SELECTION_RUNTIME_CAPABILITY`. An older host
  strips the unknown `shell` param and answers with a healthy terminal running
  its default shell — a reply indistinguishable from success — so the CLI
  refuses before creating anything rather than creating the wrong shell quietly.

`--shell` stays Windows-only; macOS and Linux hosts spawn the login shell and
the relay drops the value off win32 rather than honouring it half-way. A WSL
project runtime still outranks it, unchanged.

Tests
-----
- `pty-spawn-shell-override-parity.test.ts` pins both preflights against the
  exact drift that caused this (verified failing with the fix reverted).
- `createTerminal` passes `shellOverride` to `ptyController.spawn` with no
  startup command.
- CLI: sends `shell`, refuses a shell the host cannot spawn, and refuses a host
  without the capability — in both refusals without making the round trip.
- Allowlist and `terminal.create` schema accept/refuse cases, including paths
  and appended arguments.

* fix(terminal): refuse a requested shell the execution host cannot apply

The first commit made `--shell` reach the spawn, but only a LOCAL win32
execution host applies it: `spawn-options` gates the override on
`process.platform === 'win32' && !args.connectionId`. So `--shell cmd.exe`
against an SSH-routed worktree, or against a macOS/Linux host, still returned a
healthy terminal running that host's default shell — the same
indistinguishable-from-success reply the capability gate exists to prevent, one
layer down.

Refuse instead, before anything spawns. The check sits at the top of
`resolveAgentTerminalCreateOptions`, which every create lane funnels through, so
neither lane has to remember it; the desktop lane additionally refuses a
worktree-less create, which has no execution host to resolve a shell on.

An SSH host's platform and installed shells are not visible to this runtime, and
a POSIX host has no Windows shell to pick. Neither can honour the request, and
saying so is the whole point of the flag.

Docs and the CLI spec now say "refused", not "ignored".

* fix(terminal): refuse a shell that contradicts the project execution runtime

`resolveLocalWindowsTerminalRuntimeOptions` does not merely rank the project's
execution runtime above a per-terminal pick -- it REWRITES the pick, in both
directions, and says nothing:

- a WSL project forces `wsl.exe`, discarding `--shell cmd.exe`;
- a Windows-host project discards a WSL name and falls back to `COMSPEC`
  (`getHostShellForProjectRuntime`), so `--shell wsl.exe` spawns cmd. That is
  the common case, not an edge: `resolveProjectExecutionRuntime` resolves
  `windows-host` for every project that is not WSL, while a repo belonging to no
  project honours `wsl.exe` -- so the same flag behaved differently depending on
  whether the repo was in a project.

Either rewrite returns a healthy terminal running a shell the caller did not ask
for, which is the failure `--shell` exists to remove.

It also split an agent launch's quoting from the shell that receives it. The
previous commit made the startup-shell family follow the REQUESTED shell, so
`--shell wsl.exe --command codex` on a Windows-host project typed POSIX-quoted
launch args into cmd. Refusing the contradiction removes that case rather than
papering over it.

Refuse instead, alongside the SSH and non-Windows refusals, from the same
`resolveAgentTerminalCreateOptions` seam every create lane funnels through.

Also from review:
- the allowlist test looped the list against itself; spell the members out.
- the runtime spec case claimed to prove the pty's shell when it asserts the
  controller received the field; name it for what it checks.

Reported by an adversarial review of the branch.

* fix(terminal): canonicalize --shell and refuse a WSL-path rewrite

Review of the --shell create path turned up two ways the terminal could
still end up being a shell the caller never asked for -- the exact failure
--shell exists to remove.

Bare and mixed-case spellings passed the allowlist but reached consumers
that exact-match the canonical name: resolveWindowsShellStartupFamily
classified `cmd` as the PowerShell family, resolveWindowsShellLaunchArgs
fell through to empty shellArgs (no `chcp 65001`, no OSC 133 bootstrap that
Windows foreground status depends on), and resolveWindowsGitBashShellPath
compares case-sensitively so `Git-Bash` spawned a literal `Git-Bash`.
The allowlist is now one canonical-name map and terminal.create canonicalizes
on parse, so the spawn path only ever sees `.exe` spellings. `pwsh` and
`powershell` stay distinct binaries.

A `\\wsl$\<distro>\...` cwd made the providers force wsl.exe regardless of
the request, and terminalShellOverrideRefusal only inspected the project
runtime -- undefined for a folder workspace with no project. Refuse on the
resolved cwd and the workspace path, judging what the PTY actually gets.

Also: the capability gate reported an unreachable host as too old rather
than unavailable; the SSH CLI shim dropped capabilities from status, so
--shell there blamed the host version instead of naming SSH; and --shell
had no help entry, rendering bare in `orca terminal create --help`. Adding
that entry crossed help.ts's max-lines cap, so the flag table moved to
flag-help-text.ts rather than suppressing the rule.

Adds a behavioural test for the runtime preflight (the one-line fix was
pinned only by a source-text scan), plus coverage for the startup-command
quoting family, the no-workspace refusal, and the WSL-path refusal.

* fix(build): keep tests out of the RPC params catalog bundle

The catalog walk under methods/ already skips *.test.ts, but the contract
directory glob took every .ts. terminal-create-shell-param.test.ts is the
first test to live there, so the bundle pulled vitest into a CJS build and
the generator threw on require(). Same exclusion, same reason.
2026-09-15 16:34:16 -07:00

71 lines
3.0 KiB
YAML

# The desktop app is a single-project install. `mobile/` is a separate workspace
# with its own pnpm-workspace.yaml and lockfile, so keep it out of the root
# package graph. Without this file `pnpm -r` auto-discovers mobile/ and its
# nested package, and the root's patchedDependencies fail as ERR_PNPM_UNUSED_PATCH.
#
# native/windows-registry is listed explicitly rather than reached by a glob: as a `file:`
# dependency pnpm re-linked it on every install (including --frozen-lockfile), and that
# virtual-store churn raced node-gyp reading the same tree, which lost @vscode/windows-process-tree's
# binding.gyp mid-rebuild on Windows CI. A workspace link is resolved once instead.
packages:
- native/windows-registry
minimumReleaseAge: 4320
# 6.3.289 was published 2026-08-29T12:48Z; it clears the 3-day gate on 2026-09-01T12:48Z.
# zod 4.5.4 was published 2026-08-29T17:55Z; it clears the 3-day gate on 2026-09-01T17:55Z.
minimumReleaseAgeExclude:
- pdfjs-dist@6.3.289
- zod@4.5.4
- electron@43.7.0
- '@shadcn/lint@0.1.0'
shamefullyHoist: true
# Orca always launches the user's own resolved Claude CLI via
# pathToClaudeCodeExecutable, so the SDK's bundled ~95 MB-per-platform CLI
# binaries must never be installed. Excluding them is what makes the path
# override mandatory rather than merely preferred.
ignoredOptionalDependencies:
- '@anthropic-ai/claude-agent-sdk-darwin-arm64'
- '@anthropic-ai/claude-agent-sdk-darwin-x64'
- '@anthropic-ai/claude-agent-sdk-linux-arm64'
- '@anthropic-ai/claude-agent-sdk-linux-arm64-musl'
- '@anthropic-ai/claude-agent-sdk-linux-x64'
- '@anthropic-ai/claude-agent-sdk-linux-x64-musl'
- '@anthropic-ai/claude-agent-sdk-win32-arm64'
- '@anthropic-ai/claude-agent-sdk-win32-x64'
# Development and test jobs need only their host. Cross-target packaging opts in
# with pnpm install:release (or explicit pnpm install --os / --cpu targets).
supportedArchitectures:
os:
- current
cpu:
- current
allowBuilds:
'@parcel/watcher': true
'@swc/core': false
'@vscode/windows-process-tree': false
agent-browser: false
cpu-features: true
electron-winstaller: false
esbuild: true
msw: false
node-pty: true
sherpa-onnx: true
ssh2: false
'@orca/windows-registry': false
overrides:
monaco-editor>dompurify: 3.4.14
patchedDependencies:
node-pty@1.1.0: config/patches/node-pty@1.1.0.patch
'@xterm/addon-ligatures@0.11.0-beta.300': config/patches/@xterm__addon-ligatures@0.11.0-beta.300.patch
'@xterm/addon-webgl@0.20.0-beta.299': config/patches/@xterm__addon-webgl@0.20.0-beta.299.patch
'@xterm/addon-search@0.17.0-beta.300': config/patches/@xterm__addon-search@0.17.0-beta.300.patch
'@xterm/addon-serialize@0.15.0-beta.300': config/patches/@xterm__addon-serialize@0.15.0-beta.300.patch
'@xterm/xterm@6.1.0-beta.303': config/patches/@xterm__xterm@6.1.0-beta.303.patch
lint-staged@16.4.0: config/patches/lint-staged@16.4.0.patch
'@vscode/windows-process-tree@0.8.0': config/patches/@vscode__windows-process-tree@0.8.0.patch