Commit Graph
34 Commits
Author SHA1 Message Date
Neil e0cf91c62a feat(ssh): one-click Node.js mirror when neither machine can download Node (#31069)
* feat(ssh): offer a Node.js mirror when neither machine can download Node

* fix(ssh): put the mirror action beside Details on the host row

* fix(ssh): offer the mirror only where this computer's setting applies, and word main's error by mirror state

* test(ssh): type the refusal case

* test(ssh): one checked settings fixture for the mirror dialog
2026-10-20 01:55:13 -07:00
Neil ba6599eabb fix(ssh): pair again over SSH when a managed server drops this desktop, instead of reconnecting forever (#30775)
* fix(ssh): say the host dropped this desktop's pairing, not that another Orca holds the port

* fix(ssh): pair again over SSH when our own managed server refuses this desktop's token

* fix(ssh): key re-pairs by the refused revision, cap attempts, retry on explicit connect

* fix(ssh): keep a newer pairing stored mid-repair, count only completed reads, and offer Connect, not an access link, for managed servers

* fix(ssh): judge a re-pair only against the pairing still stored; Check connection retries a managed server's pairing

* fix(ssh): a pane whose managed server refuses this desktop says why and offers Reconnect, instead of spinning

* test(e2e): live failure path for a refused managed pairing

* fix(ssh): a Connect that re-paired answers from the new pairing, not the retired one

* fix(ssh): only an explicit Connect lifts a managed re-pair's hold and cap; say SSH access can pair again

Startup restore reaches ssh:connect with automatic: true and went through the same
ensureTunnel that cleared the hold, so repeated automatic reconnects could bypass the
three-attempt cap. The reset now lives in connectTarget's explicit branch.

The Shared Server Access footnote now says revoking there doesn't stop someone with
SSH access to this computer from pairing again.

* ci(e2e): route the managed pairing-repair spec with the managed-host remote sources

* fix(ssh): the repair gate decides whether a managed tunnel builds, not the build

A refused shared-control open rebuilt the tunnel (SSH execs, forward, handshake, wake)
before the held failure or the attempt cap was consulted, so reopens on a blocked
server rebuilt without limit. buildWithPairingRepair now asks the gate first: a failure
stands for the refused pairing revision for 15 s, or through the window once the cap is
spent, and every caller gets it without reaching the host.

- A connect during an in-flight repair keeps its reset (per-environment reset epoch).
- An unpaired answer fails at once; it is not a moved port, so no re-read or wake.
- A refusal that only rebuilt the tunnel no longer reports "pairing changed".
- WSL and cloud VMs no longer say Orca tried to pair over SSH, and Check connection
  only re-pairs servers managed over SSH (runtimePairingRepairRoute).
- Translate the new strings; route the pane-banner files to the managed-SSH e2e lane.

* fix(ssh): fence a status re-pair follow-up to the same server, and hold a refused repair under its own revision

- getRuntimeEnvironmentStatus followed any pairing change mid-probe, past a caller's
  expectedEnvironmentPairingRevision fence. It now follows only for an unpinned caller and only
  a new pairing of the same SSH-managed server under its pinned host key.
- recordRefusedAfterRepair stood under whatever revision was stored now, so a newer pairing
  stored mid-refusal was blocked untried; it stands under the repaired revision.
- The WSL restart test expects the refused-pairing classification main's WSL path now reaches.

* fix(ssh): a re-pair hands its callers the new credentials, and a failed one judges only the pairing it tried

- ensureOrcadManagedTunnel returns the environment as stored once the tunnel stands. The
  terminal census and a deploy's already-current result use it, so after a re-pair they
  no longer authenticate with the refused token, which read as an unverifiable census and
  refused restarts, updates and stops.
- The shared-control refusal handler blocks the status owner only while the failure stands
  for the pairing stored now (managedOrcadPairingFailureStands). A newer pairing stored
  meanwhile keeps its owner and retries.

* fix(ssh): migrations commit with the pairing the tunnel stands on, and a Connect mid-handshake stays reset

- Conversion binds the environment ensureOrcadManagedTunnel returns, so the destination, the
  server project reads and the result use the re-paired credentials (the migration mark is kept).
- A delta move gets its destination only from connectDestination, which opens the tunnel and
  builds the destination from what it returns; a pre-ensure destination can no longer exist. A
  tunnel that cannot open keeps the delta journaled and the host marked for the next move.
- buildWithPairingRepair takes the reset epoch when it starts, so a refusal after the re-pair
  cannot reinstate a failure that a Connect pressed during the handshake cleared.

* refactor(ssh): one module owns a shared control socket's environment policy, keeping request-connections under its line limit
2026-10-19 22:45:25 -07:00
Neil e278bc9a17 feat(orcad-migration): unsaved drafts stay on this desktop and are renamed to the server on commit (#30894)
* feat(orcad-migration): unsaved drafts stay on this desktop and are renamed to the server on commit

* fix(orcad-migration): refuse clashing drafts from the rename's own collection; stricter live spec

* fix(orcad-migration): stamp renamed drafts with the server, rename at startup after a cut-off commit, keep draft refusals

* fix(orcad-migration): retain only after a startup rename is durable; startup survives an unreadable journal

* fix(orcad-migration): a cut-off commit is renamed and retained at startup, so the rename runs once

* fix(orcad-migration): one retain per migration at a time; no rollback that could drop newer edits

* test(e2e): check the visible draft in whichever editor a large file opens in

* fix(orcad-migration): startup retains only migrations of hosts this profile owns

* ci(e2e): route the editor-drafts migration spec to the sources it covers
2026-10-19 22:44:43 -07:00
Neil 66d37178da feat(ssh): let a nodejs.org mirror serve the pinned Node on this computer and the host (#30981)
* feat(ssh): let a nodejs.org mirror serve the pinned Node on both machines

* fix(ssh): share one deadline across every host download URL

* fix(ssh): give each host download URL its share of the deadline and cap its size

* refactor(ssh): one tool per host, labelled sources and a portable wget watchdog

* fix(ssh): quote this computer's short download reason in the combined error

* fix(ssh): fetch only validated mirror URLs on the host

* fix(ssh): drop an empty query or fragment from the mirror URL

* fix(ssh): keep a cancel or a local error as it is after the mirror fails
2026-10-19 20:52:44 -07:00
Neil c822123b43 fix(ssh): a forward edit cut short by Disconnect keeps the host's saved port forwards (#30932)
* fix(ssh): a port forward never starts on a connection its teardown already disposed

* fix(ssh): a Disconnect-cancelled edit still resyncs the renderer, and tests model a disposed connection faithfully
2026-10-19 19:57:47 -07:00
Neil b6eee3e981 fix(ssh): download the pinned Node on the host when this computer can't reach it (#30945)
* fix(ssh): let the host download the pinned Node when this computer can't

* fix(ssh): keep both host download tools inside one time budget

* fix(ssh): use timeout for wget only where it runs as expected
2026-10-19 19:56:11 -07:00
Neil bfdf16b253 style: format three files #29042 landed before the format check (#30948)
Its CI ran before #30863 added the check. Formatting pushed wsl-orcad-connection.ts past its line
limit, so its port-fallback listen moves next to the stdio bridge it wraps.
2026-10-19 18:46:58 -07:00
Brennan Benson 65d671ecc9 fix(wsl): keep the desktop pairing across host restarts (#29042)
* fix(wsl): retain the paired desktop identity across host restarts

* refactor(wsl): resolve enrolled pairing in the identity module

* test(wsl): supply the required host readiness state

* fix(wsl): resolve registered pairings through the saved authority
2026-10-19 18:27:48 -07:00
Neil c9e7147f84 fix(managed-server): keep the tunnel manager under its line limit after #30888 and #30891 crossed (#30933)
The build's same-pairing check moves next to managedTunnelAccess, which it already wraps.
2026-10-19 17:56:49 -07:00
NeilandClaude 6f96efb662 fix(orcad-migration): a host fenced on a manifest no request can carry can be cancelled by its id (#30864)
* fix(orcad-migration): a source fenced on a manifest no request can carry can be aborted by its identity

A desktop that fenced a manifest just under the export cap before #30836
could never send its abort: every catalog request carries the whole
manifest and refuses it before sending. The abort now asks the server by
migration id and digest instead (orcad.migration.abortCatalogByReference,
behind a new capability), and releases only on the server's durable
absent answer. A committed server still refuses the release.

Co-authored-by: Claude <noreply@anthropic.com>

* test(orcad-migration): cover the durable abort by reference and its RPC method

Co-authored-by: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 17:25:47 -07:00
Jinwoo HongandE2E Test e7d6ee645b chore(lint): bring orcad-managed-tunnel-manager.ts back under max-lines (#30921)
Move the 'recorded tunnel still matches this build' check into
orcad-managed-tunnel-active.ts as activeTunnelMatches(). No behaviour change.

Co-authored-by: E2E Test <e2e@test.local>
2026-10-19 19:17:28 -04:00
Neil 0a3d203f61 fix(ssh): Disconnect closes the managed server tunnels the SSH session carried (#30891)
* fix(ssh): Disconnect closes the managed server tunnels the SSH session carried

The managed tunnels live in their own manager, so the session teardown closed ordinary
forwards and the connection but left a system-SSH tunnel's ssh -L process authenticated and
listening. The teardown now closes every managed tunnel built or building on the target.

* fix(ssh): a session teardown also closes forwards still starting or awaiting their identity check

* fix(ssh): a connection teardown waits for every forward of it to settle, starts and closes under way included

* fix(ssh): bound a teardown's wait on starts, skip edit rollback after one, and never record a tunnel a teardown overtook

* fix(ssh): an edit a Disconnect cancelled leaves the target's saved forwards alone
2026-10-19 16:00:38 -07:00
Neil 98ee268825 fix(ssh): a managed tunnel follows the host's bound port after another desktop restarts the server (#30888)
The tunnel was rebuilt only when this desktop's own check saw the server start. When another
desktop restarted it onto a new port, the old forward stayed and every check answered
unverifiable. A check that finds the server not answering now reads the port the host's
readiness record names and, when it moved, rebuilds the tunnel there.
2026-10-19 15:39:54 -07:00
Neil 209e468a7c fix(ssh): an SSH server update counts terminals again just before it stops the server (#30874)
* fix(ssh): an SSH server update counts terminals again just before it stops the server

The SSH update passed the terminal census taken before the artifact upload and no recount,
so a terminal opened during the upload was missed and, on a host running terminals inside
orcad, ended by the restart. The recount is now a required deploy option, so every runtime
(SSH, WSL, first deploy) supplies one and the stop step always recounts.

* fix(ssh): a recount that throws reads as unverifiable and defers the update
2026-10-19 15:34:16 -07:00
NeilandClaude 2b07f0f3b6 fix(managed-server): snapshot capture and restore run on BusyBox before 1.30 (#30840)
The state mutation wrapper ran `timeout -s KILL 900 sh -c ...`, which old
BusyBox (`timeout [-t SECS] [-s SIG] PROG`) reads as running a program
named 900. The script never ran, the capture answered nothing and parsed
as failed, so every update stopped the server and rolled back. Probe the
syntax as the room check (#30618) already does.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 12:52:55 -07:00
Neil 43abe329c5 refactor(orcad): every snapshot state op fits the gateway exec budget in shell; the state lock sends its script once (#30744)
* refactor(orcad): every snapshot state op fits the gateway exec budget in shell; the state lock sends its script once

* test(orcad): a restore that stops part way reports FAILED and reruns from the archive
2026-10-19 07:54:34 -07:00
Neil 5a9e26ef9f fix(ssh): say a noexec home can't run Orca's server, instead of "install an older version" (#30724)
* fix(ssh): name a noexec home instead of calling the host unsupported

* test(ssh): measure the stage exec probe in the host-op byte budget

* fix(ssh): the connect error names a noexec home too

* chore(i18n): add the noexec status line to the English catalog

* fix(ssh): only the shell's exec refusal reads as noexec; other probe failures reach the self-test

* fix(ssh): a stage the probe can't write skips the probe, so a full disk still reads as full

* fix(ssh): read the probe's exit 126 as noexec in any locale
2026-10-19 05:45:39 -07:00
NeilandClaude e72d8f926a fix(ssh): say the host's disk is full when a managed-server install runs out of space (#30679)
* fix(ssh): say the host's disk is full when a managed-server install write fails for lack of space

OpenSSH's sftp-server reports ENOSPC and EDQUOT as the generic SSH_FX_FAILURE, so a deploy onto a
full home failed with the bare detail "Failure" under "Try connecting again". After a failed
install write, probe the host's free space where the write was going; when it cannot hold the
write, fail with an error that names the filesystem, how much is free and how much is needed.
Lost or aborted transfers and Windows hosts keep their original error.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ssh): diagnose only SFTP's generic failure on an exhausted filesystem

Codex review: a partial upload may spend the room the rest needed, so compare against an empty
filesystem, not the bundle size, and keep explicit refusals such as permission denied.

Co-Authored-By: Claude <noreply@anthropic.com>

* refactor(ssh): reuse formatKb and size an upload the way uploadDirectory walks it

Opus review: the shared formatKb already exists, and following symlinks inside the tree could
recurse; uploadDirectory follows the root and skips links below it.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(ssh): measure the free-space probe in the host-op byte budget

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 04:20:09 -07:00
Neil 4f425a2861 fix(cli): refuse server names --environment cannot tell apart, and say how to pick one of twin projects (#30676)
* fix(cli): refuse a paired server name --environment cannot tell apart, and say how to pick one of twin projects

* fix(cli): point an ambiguous --worktree at its host-qualified identity

* fix(cli): keep the ambiguity listing on the paired server the command chose

* fix(cli): compare saved server names the way --environment does, and route cross-host twin projects to --project --host
2026-10-19 03:12:30 -07:00
NeilandClaude Opus 5.5 912cb06398 fix(managed-server): repair main typecheck after the byte-budget and snapshot-room PRs crossed (#30680)
* fix(managed-server): import newestStateMtimeCommand from its new module

#30635 and #30618 merged in parallel: #30618 moved newestStateMtimeCommand
to orcad-state-newest-mtime.ts while #30635's byte-budget test imported it from
orcad-state-snapshot.ts, breaking typecheck on main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(managed-server): measure the snapshot-space probe and record #30618's capture growth

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 02:29:45 -07:00
NeilandClaude 79b7d702be test(managed-server): 6 KB wire-size budget for every POSIX host-op command (#30635)
* test(managed-server): byte budget for every POSIX host-op command

Builds each exported POSIX host-op builder at realistic maximal arguments and
asserts the raw command stays under 6 KB, under the 8,000-byte compression
threshold. Ops already over budget are listed with ceilings that may only
shrink until they move onto the host script (round 4 R4-10).

Co-Authored-By: Claude <noreply@anthropic.com>

* test(managed-server): budget the wrapped, fence-guarded command on Linux and macOS

Measures what a gateway sees on the plain fallback (fence guard + uncompressed
POSIX wrapper), takes the larger of Linux and macOS, launches the SSH way (nohup),
and also discovers the top-level src/main/ssh builders. Exports three builders
that are sent as their own exec request so they can be measured.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(managed-server): count the stdout fence; scan ssh transport subfolders

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 01:55:29 -07:00
NeilandClaude a451f472c3 fix(ssh): make room for the pre-activation snapshot on remote hosts, and say why a capture failed (#30618)
* fix(ssh): make room for the pre-activation snapshot, and say why a capture failed

Every update left its own full state copy behind, and a failed capture its partial archive, so a
small or quota-limited home filled until no update could snapshot. The capture now prunes every
pre-* snapshot except the one the activation record names, removes its own partial archive on
failure, and prints tar's error. Before stopping the running server, the activation checks that
the snapshot fits, so a host without room keeps its server.

Refs #30491

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ssh): the room refusal says what is free apart from what a prune reclaims

It also stops claiming the recorded server is running, and a test pins that the record's
snapshot survives both the room check and the prune.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ssh): measure snapshot room before the last terminal recount, within a deadline

A slow scan after the recount could let a terminal opened meanwhile be stopped. The scan now runs
first, gives up after 12 s per du (UNMEASURED reads as unknown), and a lost probe channel leaves
the room unknown rather than failing the update. The newest-mtime probe moves to its own module
to keep the snapshot module under its line limit.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(ssh): pick timeout's syntax for the room probe, and pin its failure paths

BusyBox before 1.30 takes `timeout -t SECS`; plain `timeout 12` disabled the probe there.
Tests cover a timeout on the old-snapshot scan and a lost fence from the probe.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 01:35:18 -07:00
NeilandClaude 36ed236394 fix(persistence): repo writes take the row's identity, never a bare id (#30616)
* fix(persistence): repo writes take the row's identity, never a bare id

updateRepo(target, updates) now takes Pick<Repo,'id'|'connectionId'|'executionHostId'>
(RepoHostTarget) and resolves the host from it, so a caller that holds the row can no
longer drop its host and write a same-id sibling on another host. The id-only callers
(project host setup controller and IPC handler, folder-to-Git upgrade watch, both
clone paths, registration adoption, fork backfill) pass the row they resolved.

Co-Authored-By: Claude <noreply@anthropic.com>

* test: migrate the remaining Store.updateRepo callers to row targets

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 00:33:50 -07:00
21af18af35 fix(ssh): managed servers install through SSH gateways that cut long commands (#30491) (#30549)
* fix(ssh): preserve activation commands through SSH gateways

* fix(ssh): fall back when a host cannot decode a long command, and read claims and snapshot verdicts through gateway noise

On top of the gateway fix (#30503):

- A host without base64 or gzip gets a long command plain: the compressed wrapper
  checks for its decoders before reading anything, exits 125 with a marker, and
  execCommand resends once with compressCommand: false and remembers that host.
  `base64 -D` covers macOS before 13, whose base64 refuses `-d`.
- Snapshot capture, restore, presence and compare answers read the last line that is
  a verdict, so shell noise a stderr-merging gateway appends after it is ignored.
- The machine claim reads the claim line by line, and every failed step now prints
  CLAIM_FAILED <step> <error> with the df line, so "orcad could not claim this host's
  .orca-remote" says what failed and how to fix it instead of exiting 65 with the raw
  command or failing with no reason. A failed write no longer leaves a partial file.

Refs #30491

* test(ssh): size the simulated gateway cut from the command, not a fixed 10 KiB

The snapshot command repeats its temp path ~40 times, so on CI's short /tmp it fell under 10 KiB
and the truncation precondition failed.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: zzzpyyy <167217540+zzzpyyy@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-19 00:03:01 -07:00
NeilandClaude 24e218c101 refactor(shared): move orcad-migration-* into src/shared/orcad-migration/ (#30536)
Pure git mv plus import and path-literal rewrites. Two files #29339 edits stay put:
orcad-migration-dormant-state-validation.ts and orcad-migration-manifest-contract.test.ts.

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-18 21:28:03 -07:00
NeilandClaude 6328a104bd refactor(ssh): split ssh-connection-utils into concretely named modules (#30488)
* refactor(ssh): split ssh-connection-utils into concretely named modules

- classifiers -> ssh-reconnect-error-classification.ts
- retry/timeout constants and sleep -> connection/ssh-connection-timing.ts
- buildConnectConfig -> connection/ssh-connect-config.ts
- credential callbacks -> connection/ssh-connection-callbacks.ts
- exec options and abort error -> connection/ssh-exec-options.ts
- wrapRemoteCommandForPosixShell -> ssh-posix-command-wrapper.ts
- shellEscape and shared/wsl-login-shell-command quotePosixShell (identical) -> shared/shell-argument-quoting.ts quotePosixShell

No re-exports are left behind.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: point the WSL git benchmark and the WSL lane route at shell-argument-quoting

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-18 20:48:05 -07:00
NeilandClaude 139bac88b6 refactor(ssh): move the ssh-connection and ssh-target cluster into ssh/connection/ and ssh/targets/ (#30479)
* refactor(ssh): move the ssh-connection and ssh-target cluster into ssh/connection/ and ssh/targets/

Pure git mv plus import and path-literal rewrites. ssh-connection-utils stays put; the next PR splits it.

Co-Authored-By: Claude <noreply@anthropic.com>

* test(e2e): keep the release-checkout import at the v1.4.218 path

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-18 19:23:21 -07:00
Neil b1adb89157 refactor: import renderer runtime, shared remote-runtime and orcad helpers from their owning modules (#30370)
Removes the re-export lines in runtime-rpc-result (hasRuntimeRpcErrorCode), sync-runtime-graph,
sync-key, the terminal orphan-recovery modules, orcad-idle-exit, remote-runtime-subscription-transport,
two orcad modules and the ssh-connection test harness; importers and test mocks name the real modules.
2026-10-18 09:14:07 -07:00
Neil 1938bd055a refactor(ipc): group SSH and runtime-environment IPC handlers into ipc/ssh/ and ipc/runtime-environments/ (#30362)
git mv plus import, vi.mock and path-literal rewrites; e2e route regexes and the Windows host lane
filter follow the moved handlers.
2026-10-18 08:11:52 -07:00
Neil 658953dc23 refactor(ssh): group system-ssh transport, ssh config and remote exec into ssh/{system-transport,config,remote-exec}/ (#30355)
git mv plus import, vi.mock and path-literal rewrites; one e2e route regex follows the moved
dynamic-forward process.
2026-10-18 07:26:10 -07:00
Neil d7aea6dc28 refactor(managed-server): move host state (state locks, Windows host ops, machine homes) into src/main/managed-server/host-state/ (#30347)
orcad-state-*, orcad-windows-host-* and orcad-machine-* leave src/main/ssh. git mv plus import and
path-literal rewrites, including the Windows host lane's test path in its workflow and PowerShell
runner.
2026-10-18 06:39:56 -07:00
Neil 506aa03195 refactor(managed-server): move the managed server's lifecycle into src/main/managed-server/lifecycle/ (#30343)
orcad-managed-*, orcad-runtime-*, orcad-stdio-bridge-*, orcad-decommission-* and orcad-rollback-*
leave src/main/ssh. git mv plus import and path-literal rewrites; two e2e route regexes learn the
new folder. orcad-runtime-maintenance stays for now because open #29834 edits it.
2026-10-18 06:01:46 -07:00
Neil d38299290d refactor(managed-server): move the managed server's install and deploy code into src/main/managed-server/install/ (#30335)
orcad-remote-*, remote-install-*, remote-node-runtime-store-*, remote-versioned-install and
remote-deploy-timing leave src/main/ssh. git mv plus import and path-literal rewrites; one e2e route
regex learns the new folder. orcad-remote-deploy and orcad-remote-node-runtime-report stay for now
because open PRs #29834 and #29829 edit them.
2026-10-18 05:20:19 -07:00
Neil 0627cd2bd6 refactor(managed-server): move orcad activation into src/main/managed-server/activation/ (#30320)
First slice of the desktop side of managed orcad leaving src/main/ssh. git mv plus import and
path-literal rewrites; host-lane workflow filters and the cross-host prefix list gain the new folder.
2026-10-18 03:59:34 -07:00