mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
* perf(claude): run the Windows Claude hook as a native orca-hook.exe
Each Claude hook event on Windows started cmd.exe (plus the user's cmd
AutoRun) and then curl.exe on top of the agent's own shell. A small signed
Rust binary now does the same synchronous POST in one process, under Git
Bash, PowerShell and direct CreateProcess alike.
- native/windows-agent-hook: std-only crate, same contract as the .cmd hook
(prints {}, env guards before stdin, endpoint re-read, 0.5s/1.5s caps,
waits for the listener reply, always exit 0). Its installed file name
selects the agent profile, so the registration stays a bare path.
- Packaged as resources/bin/orca-hook.exe (SignPath signs it with the other
inner PEs); afterPack fails on a missing or wrong-machine binary.
- Claude install copies it verbatim to ~/.orca/agent-hooks/claude-hook.exe
(temp + rename, running copy moved aside) and registers it; it falls back
to the .cmd hook when the bundle is missing, unsigned or cannot be copied.
Only Claude opts in, via windowsNativeHookExecutable.
- The managed-command matcher sweeps .exe entries alongside .cmd/.ps1/.sh.
- The CLI launcher and hook builds share windows-rust-executable-build.mjs,
which now also checks the PE machine.
Refs #20913
* fix(build): publish native Windows binaries atomically so parallel builds cannot collide
The Windows CI lane runs every orca-hook.exe suite in one vitest
invocation, each building through build-windows-agent-hook.mjs. The build
copied straight onto native/windows-agent-hook/.build/orca-hook.exe, and
CopyFile opens its destination unshared, so a second build (or a scanner
still holding the fresh image) failed with EBUSY.
- windows-rust-executable-build.mjs skips the publish when the output
already has the built bytes, otherwise stages a per-process temp file and
renames it over the output, retrying briefly while another handle holds it.
- The test fixture hands each suite a private copy of the build output, so
no suite reads or starts the shared file while another build replaces it.
Refs #20913
* refactor(claude): simplify the native Windows hook to one Claude binary
Review follow-up on the native hook (#20913): the same behaviour with less
machinery.
- The binary is built, shipped and installed as claude-hook.exe, so the
installed name matches its PE OriginalFilename. It is Claude-only, so the
file-name profile table and the per-agent opt-in flag are gone; the
registration is gated by the existing Claude-only usesWindowsEntry check.
- Install and refresh reuse writeManagedScript / refreshManagedScriptIfPresent
(now accepting bytes): temp file, rename, skip when identical. A copy whose
hook is still running cannot be replaced; install then registers the .cmd
hook and refresh keeps the old build until a later start, instead of the
rename-aside and *.retired sweep.
- The install-time Authenticode-presence check is dropped. Release CI now
requires resources\bin\claude-hook.exe to be signed (release-cut and the
signing rehearsal), next to resources\bin\orca.exe.
- getStatus counts either Windows form through findManagedEventHookForms in
hook-settings.ts, replacing managed-event-hook-status.ts.
- Rust: plain Claude constants, one endpoint-file lookup (last assignment
wins, as under `call`), one request write and a deadline-bounded drain.
Guard order and form encoding are unchanged.
- Build: PR CI builds the hook once before the Windows tests, so the suites
only read it; the launcher publish goes back to copyFileSync and the retry
loop and its test are gone. The PE-machine check lives only in afterPack.
Cargo.lock joins both crates' build fingerprints, and the launcher's alias
exports are removed.
Refs #20913
* test(ci): run the managed hook stdin-lifecycle suite in the Windows lane
Its claude-hook.exe case (and the other Windows-only cases) are gated per
test, so the lane-registration guard did not require the file and it ran
only on Linux, where those cases skip. Register it in both Windows lists.
Refs #20913
* feat(agent-hooks): ship the native hook as orca-agent-hook.exe <agent>
The native Windows hook (#20913) becomes one general binary that stays
Claude-only in behaviour.
- Named orca-agent-hook.exe everywhere: build output, PE OriginalFilename
and InternalName, resources\bin, the afterPack check, the installed copy
in ~/.orca/agent-hooks, and the release signed-target lists.
- It takes the agent as its first argument and Claude registers
`<path> claude`. The CLAUDE_JOB_DIR exit and the Grok/Devin drain belong
to that agent; an unknown or missing agent answers {} without reading
stdin. The managed-command matcher owns `orca-agent-hook.exe claude` and
still owns the old claude-hook.exe entry, so installs migrate; install
deletes a leftover claude-hook.exe when nothing runs it.
- A copy still running from the previous build no longer forces the .cmd
fallback. Identical bytes are not rewritten; otherwise the temp file is
renamed over the target, and when Windows refuses because the image is
mapped, the running copy is renamed to orca-agent-hook.exe.old first.
Install deletes a stale .old; only when that copy is also still running
does the install fall back to .cmd. writeManagedScript and
refreshManagedScriptIfPresent take the aside path, sharing one helper.
Refs #20913
* fix(agent-hooks): roll back a failed aside rename and keep an installed hook exe
Design-review follow-up on the native hook (#20913).
- If the running copy was moved to orca-agent-hook.exe.old but the new
file then could not take its name, the copy is renamed back before the
error is rethrown, so settings.json never points at a missing exe. The
rename helper now owns the `.old` name, next to the comment that
explains it; a mocked-fs unit test covers both the sync and async paths.
- A failed write, or a build with no bundled exe, keeps registering a copy
that is already installed (every build knows `claude`), so a dev checkout
and an installed build no longer flip settings.json between forms.
- The claude-hook.exe migration is gone: no release ever shipped that
name, so the legacy sweep, the `.exe` matcher needle and their test are
removed.
Refs #20913
* fix(agent-hooks): derive the native hook's agent with win32 path rules
getWindowsNativeHookAgent took the host's basename of the managed script
path, so on a POSIX host (Linux CI) a Windows path was not split and the
registered command became `orca-agent-hook.exe C:\...\claude`. It now takes
the path and applies path.win32.basename itself. The install destination
and the `.old` aside stay on host paths: they name files the host opens.
Refs #20913
---------
Co-authored-by: Orca Worker <orca-worker@localhost>
201 lines
10 KiB
YAML
201 lines
10 KiB
YAML
name: SSH Windows hosts
|
|
|
|
# Managed orcad against a real Win32-OpenSSH server on 127.0.0.1 (inbox capability and the pinned
|
|
# 10.0.0.0p2-Preview release), on x64 and arm64. Each job provisions a private sshd service and one
|
|
# standard account per cell (config/ci/windows-ssh-provider/), hides the host toolchain from SSH
|
|
# sessions behind logging shims, and deploys orcad on the pinned node.exe in both DefaultShell cases.
|
|
on:
|
|
pull_request:
|
|
types: [opened, synchronize, reopened, ready_for_review]
|
|
paths:
|
|
- 'src/main/ssh/ssh-relay-*'
|
|
- 'src/main/ssh/*runtime*'
|
|
- 'src/main/ssh/orcad-*'
|
|
- 'src/main/orcad/orcad-artifact-materializer.ts'
|
|
- 'src/main/orcad/orcad-host-unavailable.ts'
|
|
- 'src/main/node-runtime/**'
|
|
- 'src/main/ssh/remote-install-*'
|
|
- 'src/main/ssh/ssh-remote-*'
|
|
- 'src/main/ssh/ssh-hostile-host-*'
|
|
- 'src/main/ssh/ssh-windows-host-*'
|
|
- 'src/wsl-guest/**'
|
|
- 'src/packages/process-host/**'
|
|
- 'pnpm-workspace.yaml'
|
|
- 'src/shared/node-runtime-pin.ts'
|
|
- 'src/shared/orcad-artifacts.ts'
|
|
- 'config/scripts/build-orcad-*.mjs'
|
|
- 'config/scripts/orcad-prebuild-*.mjs'
|
|
- 'config/scripts/orcad-windows-process-tree.mjs'
|
|
- 'config/scripts/orcad-windows-cli-launcher.mjs'
|
|
- 'config/scripts/build-windows-cli-launcher.mjs'
|
|
- 'config/scripts/windows-rust-executable-build.mjs'
|
|
- 'native/windows-cli-launcher/**'
|
|
- 'src/main/orcad/orcad-cli-launcher.ts'
|
|
- '.github/actions/prepare-windows-cli-launcher/**'
|
|
- 'config/patches/node-pty*'
|
|
- 'config/patches/@vscode__windows-process-tree*'
|
|
- 'config/scripts/build-windows-process-tree-addon.mjs'
|
|
- 'config/scripts/windows-process-tree-prepared-addon*.mjs'
|
|
- 'config/scripts/windows-process-tree-gyp-rebuild.mjs'
|
|
- 'src/shared/windows-breakaway-launch*.ts'
|
|
- 'src/main/ipc/ssh-host-server-*.ts'
|
|
- '!src/**/*.test.ts'
|
|
- 'src/main/ssh/orcad-windows-host-lane.test.ts'
|
|
- 'tests/e2e/ssh-orcad-auto-convert.spec.ts'
|
|
- 'tests/e2e/helpers/orcad-convert-host.ts'
|
|
- 'tests/e2e/helpers/orcad-convert-flow.ts'
|
|
- 'tests/e2e/helpers/orcad-upgrade-profile.ts'
|
|
- 'tests/e2e/ssh-orcad-windows-cli-matrix.spec.ts'
|
|
- 'tests/e2e/helpers/compiled-orca-cli.ts'
|
|
- 'tests/e2e/helpers/windows-host-orcad-processes.ts'
|
|
- 'config/ci/windows-ssh-provider/**'
|
|
- '.github/workflows/ssh-windows-hosts.yml'
|
|
- '.github/actions/prepare-orcad-prebuilds/**'
|
|
- '.github/actions/restore-pnpm-verification/**'
|
|
- 'config/scripts/orcad-windows-prebuild-cache.mjs'
|
|
workflow_dispatch:
|
|
inputs:
|
|
cells:
|
|
description: Comma-separated cell ids from src/main/ssh/ssh-windows-host-cells.ts; empty runs the default set (orcad-cli-* cells run only when named).
|
|
required: false
|
|
default: ''
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: ssh-windows-hosts-${{ github.event.pull_request.number || github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
jobs:
|
|
hosts:
|
|
# A draft carries no verdict; readiness re-triggers this workflow.
|
|
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft != true }}
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- arch: x64
|
|
runner: windows-2022
|
|
server: inbox
|
|
- arch: x64
|
|
runner: windows-2022
|
|
server: preview
|
|
archive: OpenSSH-Win64.zip
|
|
- arch: arm64
|
|
runner: windows-11-arm
|
|
server: inbox
|
|
- arch: arm64
|
|
runner: windows-11-arm
|
|
server: preview
|
|
archive: OpenSSH-ARM64.zip
|
|
runs-on: ${{ matrix.runner }}
|
|
# Installing the inbox capability alone can take several minutes on a fresh image; the CLI matrix
|
|
# cells launch the e2e app several times each.
|
|
timeout-minutes: ${{ contains(github.event.inputs.cells || '', 'orcad-cli') && 160 || 75 }}
|
|
env:
|
|
ORCA_BACKGROUND_LAUNCH: '1'
|
|
ORCA_ISOLATED_SSH_CI: '1'
|
|
steps:
|
|
- uses: actions/checkout@v6
|
|
with:
|
|
persist-credentials: false
|
|
# Keep complete server/test trees; missing future imports fail the unchanged builds.
|
|
# All of src: the orcad-convert cell builds the full e2e app and the bundled CLI.
|
|
sparse-checkout: |
|
|
.github
|
|
config
|
|
native
|
|
resources
|
|
tests
|
|
src
|
|
- name: Self-test the provisioning scripts before touching the machine
|
|
shell: pwsh
|
|
run: |
|
|
foreach($file in @(Get-ChildItem -Recurse -Filter *.ps1 config/ci/windows-ssh-provider)){
|
|
$errors=$null;$tokens=$null
|
|
[Management.Automation.Language.Parser]::ParseFile($file.FullName,[ref]$tokens,[ref]$errors)|Out-Null
|
|
if($errors.Count){throw "PowerShell parse failed: $($file.FullName)"}
|
|
}
|
|
& config/ci/windows-ssh-provider/preview-ssh/test-preview-diagnostics.ps1
|
|
# ARM inbox servicing can finish while the independent Node artifacts are prepared.
|
|
- name: Prepare the Windows inbox SSH capability
|
|
id: inbox-capability
|
|
background: true
|
|
shell: pwsh
|
|
run: |
|
|
if('${{ matrix.server }}' -eq 'inbox' -and '${{ matrix.arch }}' -eq 'arm64'){
|
|
$receipts=Join-Path $pwd '.build/ssh-windows-host-receipts'
|
|
New-Item -ItemType Directory -Force -Path $receipts | Out-Null
|
|
. config/ci/windows-ssh-provider/preview-ssh/windows-ssh-capability.ps1
|
|
Initialize-WindowsInboxSshCapability -Arch '${{ matrix.arch }}' -Receipt (Join-Path $receipts 'inbox-capability-preparation.json')
|
|
}
|
|
- uses: ./.github/actions/install-node-dependencies
|
|
id: dependencies
|
|
with:
|
|
native-runtime: node
|
|
cache-pnpm-store: 'false'
|
|
# The deploy materializes rung A from this template; only this runner's slot exists here.
|
|
# The process-tree addon carries the launcher that starts orcad outside sshd's job; the orcad
|
|
# slot stages it, and a standard-user host has no other launch route.
|
|
- name: Build this runner's Windows process-table addon
|
|
shell: bash
|
|
env:
|
|
REUSE_PREPARED_RUNTIME: ${{ github.event_name == 'pull_request' && steps.dependencies.outputs.native-cache-hit == 'true' }}
|
|
run: |
|
|
reuse_args=()
|
|
if [ "$REUSE_PREPARED_RUNTIME" = true ]; then
|
|
reuse_args+=(--reuse-prepared-runtime)
|
|
fi
|
|
node config/scripts/build-windows-process-tree-addon.mjs --arch=${{ matrix.arch }} "${reuse_args[@]}"
|
|
- uses: ./.github/actions/prepare-orcad-prebuilds
|
|
with:
|
|
resolve-windows-cache: ${{ github.event_name == 'pull_request' }}
|
|
restore-windows-cache: ${{ github.event_name == 'pull_request' }}
|
|
- uses: ./.github/actions/prepare-windows-cli-launcher
|
|
with:
|
|
arches: ${{ matrix.arch }}
|
|
- name: Build the win32 template
|
|
shell: bash
|
|
run: |
|
|
pnpm build:orcad-prebuilds --require-slots "win32-${{ matrix.arch }}"
|
|
node config/scripts/build-orcad-template.mjs --targets "win32-${{ matrix.arch }}"
|
|
- wait: inbox-capability
|
|
- name: Run the Windows host cells against a private ${{ matrix.server }} sshd
|
|
shell: pwsh
|
|
timeout-minutes: ${{ contains(github.event.inputs.cells || '', 'orcad-cli') && 130 || 50 }}
|
|
env:
|
|
CELLS: ${{ github.event.inputs.cells || '' }}
|
|
run: |
|
|
$tools=Join-Path $pwd 'config/ci/windows-ssh-provider'
|
|
$sourceRoot=$pwd.Path
|
|
$receipts=Join-Path $pwd '.build/ssh-windows-host-receipts'
|
|
New-Item -ItemType Directory -Force -Path $receipts | Out-Null
|
|
$cells=@($env:CELLS -split ',' | ForEach-Object {$_.Trim()} | Where-Object {$_})
|
|
if(-not $cells.Count){$cells=@('orcad-cmd','orcad-powershell')}
|
|
# One Windows host runs the app-level conversion; it is last because it switches native modules.
|
|
if(-not $env:CELLS -and '${{ matrix.arch }}' -eq 'x64' -and '${{ matrix.server }}' -eq 'inbox'){$cells+='orcad-convert'}
|
|
# App cells reach the managed server through an SSH local forward, which provisioning grants to the last accounts only.
|
|
$appCellIds=@('orcad-convert','orcad-cli-managed','orcad-cli-convert')
|
|
$cells=@($cells | Where-Object {$appCellIds -notcontains $_})+@($cells | Where-Object {$appCellIds -contains $_})
|
|
$forwarding=@($cells | Where-Object {$appCellIds -contains $_}).Count
|
|
$archive=''
|
|
$preparation=''
|
|
if('${{ matrix.server }}' -eq 'inbox' -and '${{ matrix.arch }}' -eq 'arm64'){$preparation=Join-Path $receipts 'inbox-capability-preparation.json'}
|
|
if('${{ matrix.server }}' -eq 'preview'){
|
|
$archive=Join-Path $env:RUNNER_TEMP 'preview-${{ matrix.archive }}'
|
|
# The provisioning script refuses the archive unless its sha256 and every binary's match the pin.
|
|
& "$env:WINDIR\System32\curl.exe" --fail --location --connect-timeout 15 --max-time 90 --output $archive 'https://github.com/PowerShell/Win32-OpenSSH/releases/download/10.0.0.0p2-Preview/${{ matrix.archive }}'
|
|
if($LASTEXITCODE -ne 0){throw 'SSH archive fetch failed'}
|
|
}
|
|
$callback={param($context)
|
|
& (Join-Path $tools 'invoke-windows-host-cells.ps1') -SourceRoot $sourceRoot -Context $context -Target 'win32-${{ matrix.arch }}' -ReceiptRoot $receipts -Cells $cells
|
|
}.GetNewClosure()
|
|
& (Join-Path $tools 'preview-ssh/prove-preview-openssh.ps1') -Archive $archive -Arch '${{ matrix.arch }}' -Server '${{ matrix.server }}' -Receipt (Join-Path $receipts 'provider-server.json') -InboxPreparationReceipt $preparation -Accounts $cells.Count -ForwardingAccounts $forwarding -HiddenTools @('npm','npx','node-gyp','gcc','g++','cc','c++','make','cl','clang','clang++','msbuild','cmake') -HostCellProbe $callback 2>&1 | Tee-Object (Join-Path $receipts 'provision.log')
|
|
- uses: actions/upload-artifact@v7
|
|
if: always()
|
|
with:
|
|
name: ssh-windows-host-${{ matrix.arch }}-${{ matrix.server }}-receipts
|
|
path: .build/ssh-windows-host-receipts/
|
|
retention-days: 7
|