* feat(orcad): run --reclaim-stale-lock on a managed host's active slot
The desktop can now dry-run and confirm the stale instance-lock recovery
on a Linux managed host, serialized with launches, then start the server
again and reconnect the way the restart action does.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* feat(settings): offer Recover server for a managed host refused on a stale lock
When a managed orcad start names --reclaim-stale-lock, the SSH host row
offers Recover server. The dialog dry-runs the check, shows the host's
evidence, and clears the lock only after the user confirms no other
machine sharing the home runs Orca.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* feat(mobile): point a host refused on a stale lock to the desktop's Recover server
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(settings): mount the Recover server dialog per opening
A fresh mount starts each opening from a new check instead of resetting
state in an effect.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(orcad): end the readiness wait once the host proves the launch exited
A managed orcad refused at start (such as on a possibly stale instance
lock) left the client waiting out the full readiness deadline. Each
host-side wait now also checks the launch's PID file with kill -0 and
ends early on a proven exit, so the caller reports the same pending
readiness, with the orcad.log tail, within a poll. No PID, a denied
probe or a reused PID keeps the old deadline.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(orcad): refuse a managed slot without --reclaim-stale-lock before running it
An older orcad ignores the flag and starts serving in the foreground. The
same remote exec now checks that the slot's server bundle names the flag
and answers unsupported otherwise, without invoking orcad.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(settings): show the manual stale-lock command as literal mono text
The rendered dialog ran the command into the sentence, so it was hard to
read or copy. It now sits in its own mono block, like Details.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(settings): offer only a manual delete when the host can't reclaim, and format the lock's age
An unsupported host was told to run the very command it lacks. Main now
names the lock path for that case, and the dialog shows only a copyable
rm of it with the shared-home caution. The lock's age uses the UI's
relative-time formatter instead of a hand-rolled minutes count.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(orcad): pin a confirmed stale-lock reclaim to the record the user approved
--yes reclaimed whatever record held the lock at that moment, so a lock a
same-named machine wrote while the dialog sat open could be cleared.
orcad now takes --expect-nonce and refuses unless the lock still holds
it; the dry run reports the nonce, the desktop sends it back on confirm,
and the managed slot gate requires a build that knows the pin. A
reclaimed result carries the evidence it acted on.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(settings): keep the Recover server dialog open through its own reclaim
The dialog's mount was gated on the status that offered it, and the
reclaim clears that note before its answer arrives, so the result was
never shown and the card kept the dialog marked open. Only the triggers
are gated now; an open dialog keeps the server it opened for until the
user closes it.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(settings): put the Recover server confirmation on the security surface
It asks permission to run a remote command that clears an ownership lock
and starts server code, which STYLEGUIDE puts on that surface.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(orcad): trust a gone launch PID only on the machine that launched it
The PID file sits in a home other login nodes may share, so a wait exec
that lands on another node read a starting server as exited. Both launch
paths now record the launching machine's boot id and node name beside
the PID, and the readiness wait ends early only when its own exec reads
the same record. No record, a host without a boot id, or another machine
keeps the deadline. The PID path is named once in the launch command so
it stays within its sent-byte ceiling.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(orcad): keep a slot's unreadable stale-lock answer from offering a manual delete
A slot that passed the gate but gave no JSON, or a status this client
does not know, read as unsupported and offered rm of the lock. It now
reads as no-answer, which points to the slot's own reclaim command, and
unsupported is left to a slot without the command, a non-Linux host or
no active version.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* style(settings): use the text-2xs step for the stale-lock command block
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(mobile): offer the desktop-recovery hint on the same refusal the desktop does
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* test(orcad): name the extra fields a fake stale-lock answer carries
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(settings): end the Recover server flow once the lock is cleared, and keep an unpinned reclaim manual
After a reclaim whose restart failed, the dialog kept the old evidence,
the enabled confirmation and manual recovery for a lock that was gone.
It now shows only that the lock was cleared and why the server did not
start, with Close. A slot that has the reclaim but not the nonce pin is
now told apart from one without the reclaim, and is pointed to its own
reclaim command rather than rm.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(orcad): keep a stale-lock reclaim's result when the restart after it throws
A wake or tunnel rebuild that threw after the lock was cleared discarded
the reclaimed result, so the caller skipped its reconnect and note
clearing and the dialog kept a confirmation for a lock that was gone.
The restart's failure now becomes the result's notRestarted reason.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(orcad): report a reclaimed server as started when only its forward failed to follow
A tunnel rebuild that failed after the wake started the server was
reported as the server not starting. The rebuild now goes through
followLaunchedServerPort, as other relaunches do, which warns and leaves
the forward to the next connect. Also drops a caller test that only
covered behaviour the fix did not change.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* test(mobile): re-record the execution-target goldens for the recovery hint field
The SSH gate's projected state gained `needsDesktopServerRecovery`, which
is false for every recorded reply (no recorded host sends a managed
server refusal). Re-recorded with `pnpm --dir mobile rpc:record` for the
two pilot recordings and the four execution-target reply partitions; the
only change is that field, plus the value pool reordering around it.
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
---------
Co-authored-by: Jesse Sanford <108698+jessesanford@users.noreply.github.com>
* fix(ssh): stop local-only workspace metadata from blocking SSH access links
Unstamped floating and setup terminal metadata rows were projected onto every
execution host, so the dependent-state census refused SSH access links and
empty-host claims for every SSH target with orcad_migration_dependent_state.
- Local-only workspace ids project only onto the local host, are stamped local
on load, and their metadata writes always land on the local host.
- The census counts an unstamped legacy row only when its repo is not
registered solely on other hosts.
- The SSH access error now names the blocking state.
Builds on the approach in #31862 by Frank-zhu2000.
Fixes#31498
Co-authored-by: Frank-zhu2000 <Frank-zhu2000@users.noreply.github.com>
* fix(ssh): rehome remote-stamped local-only metadata, skip orphan rows, and word the SSH access error for a link
* fix(persistence): keep a local or unstamped floating row over a remote copy when rehoming
* fix(ssh): make the SSH access blocker copy switch exhaustive
---------
Co-authored-by: Frank-zhu2000 <Frank-zhu2000@users.noreply.github.com>
* fix(wsl-host): pair again from the distro when a WSL host refuses the enrolled token
A WSL host that lost its device registry, or revoked this desktop, refused the
enrolled token forever, and an update over it read as deferred. The connect now
re-pairs through the managed repair gate SSH uses (one repair at a time, a
capped number per window, only an offer under the pinned host key), reading the
host's published offer from the distro. The incumbent's terminal count uses the
same enrolled-or-live pairing the connect does.
* fix(wsl-host): retire a bridge whose pairing was refused, and let a user's Connect retry a repair
A refused or repaired-then-refused pairing on a reused bridge left its stale
token cached, and the failure status then saved it over the repair. That bridge
now closes first. A user-initiated Connect clears a standing repair failure or a
spent cap, as an explicit SSH connect does.
* fix(wsl-host): retire a refused bridge without cancelling a queued Update or Force
* fix(wsl-host): repair only a stored pairing, dispose the repair's transport directly, and log refusals neutrally
A host still being added has no stored record for the gate to re-pair, so its
live bridge is reused as before. The WSL repair deps are typed with the WSL
transport, and the shared refusal log no longer says SSH for WSL.
* fix(wsl-host): apply the SSH update rules to a WSL connect
A WSL connect deployed this app's orcad whenever the active build differed,
so a distro another, newer Orca had activated ended in an unconnectable
deferral, and a build the host had rolled back from was re-activated on
every connect. It now runs the same planManagedOrcadAutoUpdate decision SSH
uses and serves the active build when that plan skips.
* fix(wsl-host): serve the skipped-update incumbent only if it is still active
* fix(orcad): one update decision for SSH and WSL, applied again under the fence
The connect's update plan moves to its own module, and the fenced admission
every first deploy and WSL connect uses now applies it, so a transient
readiness failure or a race cannot reactivate a rolled-back build.
* refactor(wsl-host): adopt the serving build through the SSH first-deploy adoption
* fix(wsl-host): do not retry a candidate rejected earlier this launch
A rejected candidate is restored without recording rolledBackFrom, so WSL
retried it on every connect. SSH's launch-scoped failure memory moves to a
shared helper keyed per host, and WSL feeds it to the shared update decision.
A null adoption now re-reads the record before the deploy, as SSH does.
* fix(wsl-host): keep Force offered when a connect skips this app's build
The WSL connect's update admission moves to its own module, which returns a
forceable hint when it serves the incumbent over a rolled-back or failed build.
* fix(orcad): connect, Update and Force are the three update intents on SSH and WSL
Only a connect applies the skips; an explicit Update retries a skipped or failed
build (clearing the failure memory) behind the terminal census; only Force passes
the census. WSL's Update button now sends the update intent instead of a plain
connect. A connect that records a rejection serves the restored incumbent at once.
* fix(orcad): an explicit Update keeps the newer-host skip on SSH and WSL
Update lifts only a host's own skips (rolled back, failed before); Force lifts
all. SSH's explicit update now applies the same fenced admission, so stable
cannot replace a host a newer Orca activated without Force. A rejection retries
adoption only once the incumbent is back and no journal awaits recovery.
* fix(wsl-host): keep Update and Force offered on a deferred cold connect
A connect the host deferred with nothing serving now publishes its deferral, so
a host stopped after a rollback can still be retried from the UI. SSH's explicit
Update forgets a remembered failure only once it lands, and a restore that could
not be read keeps the rejection as reported.
* fix(wsl-host): offer only Force over a host a newer Orca runs, and keep failure memory until an Update lands
A host-newer refusal gets its own deferral code so the WSL row hides Update,
which can never pass it. An explicit Update no longer clears the launch failure
memory up front; only a landed deploy does, as on SSH.
* refactor(wsl-host): merge the intent type into the existing shared imports
Comments and one log line still said a host falls back to the relay, keeps
the pinned-relay ladder, or shares code with the relay's spawn and install
paths. No relay client exists; an unserved host fails its connect with a
reason, and the legacy bridge only keeps old relay terminals alive.
The SFTP/exec-stdin transfer helpers, their options type and the install
timeout still carried relay names from before orcad; they install the orcad
bundle and the Node runtime. The timeout's derivation from the relay's
native-deps budget is replaced by its value and the reason for it.
Five modules each declared the same all-null census for a daemon that could
not answer; they now use one frozen constant from the shared census module.
Removes the relay ladder's answered-failure predicate, the unused test-probe
joins, two dead renderer broadcast helpers, the bound-port reader the tunnel
no longer uses, the WSL terminal-verdict mapper the supervisor never called,
and an unused type.
* fix(orcad): hold terminal admission from a stop's start until exit; fence rollback and restart counts
Once orcad begins any stop (signal, managed stop request, idle exit), its terminal
admission stays closed until the process exits, past an update census's 60s fence
or a deferring desktop's reopen. Rollback and restart now take their count through
the same fenced recount the update uses, reopen admission when they leave the
server serving, and stop nothing once that count may have outlived its fence.
* fix(orcad): check the rollback's stop deadline after its transaction write
* fix(orcad): rollback and restart recount under the host lock; a reopen never wakes a server
Only the lock holder closes or reopens a server's terminal admission, so a
rollback or restart that loses the lock can no longer clear another update's
fence. A reopen skips the serving check, so it never starts or recovers a
stopped server, and a stop that begins before the runtime binds still holds.
* fix(orcad): reopen admission when a rollback's stop was withdrawn
* fix(orcad): keep admission closed after a stop that may still be on its way
* refactor(orcad): route update, rollback and restart through one fenced stop
runFencedStop is the single count, close, stop, reopen-on-abort sequence. The
update's activation, the rollback and the restart each supply only their own
refusal, journal step and stop; the reopen and deadline rules live in one place.
* perf(orcad): keep the server bundle ASCII so V8 stores it one byte per character
129 non-ASCII characters in regex literals (and three in a String.raw plugin
template) made V8 hold the whole ~11 MB orcad-server.js source as a two-byte
string. Write them as \uXXXX escapes and fail the build if any return.
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(orcad): keep license comments ASCII and name the module the guard trips on
esbuild gathers legal comments at the end of the bundle; escape that comment so
a dependency's license text cannot fail the ASCII guard, while licenses still
ship with the code. The guard now reports the offset and, from an unminified
rebuild, the module the character came from. A dark-switch build is covered.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(orcad): refuse new terminals between an update's last census and the stop
The last census before an SSH or WSL update stops the server now closes the
server's terminal admission first. Every spawn lease (acquireWorktreeTerminalSpawn,
which both spawn paths hold across the provider spawn) passes the fence, the
census waits for leases taken before the close, and an update that leaves the
incumbent serving reopens it. A reattach to a session the daemon still runs is
still admitted.
* fix(orcad): fence-exempt reattaches spawn attach-only; reopen admission when no stop was sent
* fix(orcad): stop nothing once the recount's admission fence may have lapsed
* fix(orcad): voice dictation on headless orca serve hosts
Mobile dictation against an orca serve host answered speech_unavailable
since serve moved to orcad: orcad never installed the speech service
factories, and the model manager and download imported Electron.
The model download now takes a host request opener (Chromium net on the
desktop, Node https on orcad, with redirects mapped to the same event),
the STT worker ships in the orcad slot, and orca serve hands orcad the
installed app's on-device recognizer. A host without a recognizer (an
SSH or WSL managed slot) lists only cloud models and sends a phone that
picked an on-device model to the setup sheet. Mobile names the fix when
an older server still has no speech at all.
The parity test now requires both roots to install speech.
Reported-by: delorean2020 (@delorean2020)
* fix(orcad): retry Node download drops, stage the recognizer on Windows, review fixes
- Node https errors (ECONNRESET, ETIMEDOUT, ...) are retryable, so an
interrupted orcad model download resumes (Codex P2).
- On Windows orca serve hands orcad a per-version copy of the recognizer
in the profile, so a loaded addon never locks the app's files against
an update (Opus).
- Drop a recognizer candidate path that could never match; one SAFETY
helper per test file instead of repeated casts; comments.
* fix(orcad): cloud speech is ready only with a key this host can decrypt; prune superseded recognizer copies
Codex round 2: a desktop-sealed OpenAI key exists on disk but orcad's
secret store cannot open it, so the catalog reported cloud models ready
and dictation failed at the end. orcad's model manager now proves the
key decrypts. The desktop keeps its existence check (no keychain prompt).
* fix(orcad): never prune staged recognizers before the profile lock; copy linked packages as real files
Codex round 3. ref-oss check: versioned copies are cleaned only by the
process that owns them, so the pre-lock prune is dropped rather than
patched.
* fix(orcad): remove a partial recognizer copy when staging fails (Codex round 4)
* fix(speech): never hand out another store's ciphertext as the OpenAI key (Codex round 5)
* refactor(orcad): no recognizer copy on Windows; Windows orcad keeps cloud dictation only
Design check after review round 5: every remaining finding came from the
Windows copy that dodges addon file locks. Dropping it shrinks the
surface to a path lookup; Linux and macOS serve keep on-device models.
* fix(speech): name the fix when an old server refuses a mic tap; always set the recognizer env; retry ENOTFOUND (final Opus review)
* fix(managed-server): probe install completeness relative to the slot dir
CI: shipping stt-worker.js added one more absolute slot path to the
install-complete probe, pushing it further past the exec-size budget.
Testing each artifact relative to the slot (inside a subshell cd) cuts
the probe from ~12.2 KB to ~3.6 KB, so it drops off the over-budget
list instead of raising a ceiling.
* fix(mobile): pin host.platform's read to a platform type, independent of which zod copy resolves
With mobile's own dependencies installed (which CI does for any PR that
touches mobile/), the e2e program resolves two zod copies, and the
salvaged platform arm from src/shared inferred as {}. The schema now
narrows through an explicit platform guard.
* fix(mobile): keep the host.platform read line intact for its RPC mutant; narrow in a second step
Mobile verify: the recorded-RPC mutant anchors on the read line, which
the previous guard rewrote, so the mutant could not apply.
* refactor(cli): app-executable resolution in its own module; drop the platform guard now zod is aligned
launch.ts went one line past max-lines with main; resolveAppRoot,
resolveForegroundOrcaExecutable, getExecutableAppArgs and
stripElectronRunAsNode move to orca-app-executable.ts (re-exported, so
callers are unchanged). With mobile and root on one zod (#31260), the
host.platform schema no longer needs the extra narrowing step.
* refactor(cli): import app-executable helpers from their module; no re-exports from launch.ts
* fix(orcad): run the remote daemon at the login umask so worktrees it creates are not owner-only (#31125)
* fix(orcad): keep refreshed Gemini OAuth tokens owner-only now that orcad runs at the login umask
The Gemini usage fetcher rewrote ~/.gemini/oauth_creds.json through a temp file and rename with
no mode, so the replacement took the process umask. Under the old inherited umask 077 that
happened to be 0600 on SSH hosts; at the login umask it would be 0644. Write it through
writeCredentialFileAtomic (0600 staging file, owner-only ACL on Windows).
The hostile-host matrix now checks that the deployed orcad runs at the SSH login's umask and that
its readiness file stays 0600.
* fix(orcad): keep rewritten agent configs at their mode and create the launch PID file owner-only
writeHooksJson now keeps an existing config's mode unless a caller opts out. Rewriting
~/.claude/settings.json (which can hold API keys) and the other agent configs through a temp file
used to take the process umask; inside orcad that was 077, and at the login umask it would widen an
owner-only file to 0644.
The launch runs `umask 077` in the foreground shell rather than inside the backgrounded nohup
list, so the PID file the foreground writes is created 0600 instead of relying on a racing chmod.
The Gemini credential test now pins umask 022 rather than inheriting the runner's.
* fix(agent-hooks): do not create a read-only temp file for a read-only Windows config
A preserved Windows mode can be read-only, which would leave an undeletable temp file behind when
the rename fails. POSIX keeps the preserved mode.
---------
Co-authored-by: Neil <neil@stably.ai>
* feat(settings): one-list Remote Orca Servers page and bulk-manageable SSH Hosts
Remote Orca Servers: replace the three mode cards, Advanced fold, duplicate
managed/SSH-access lists and troubleshooting accordion with a single list
(one row per server: status, one action, overflow menu), an always-visible
'New projects open on' row, and Share / Cloud VM cards that open dialogs.
SSH Hosts: group by source, filter, checkbox + shift-range selection, one
confirm for bulk remove (skipping hosts with a managed server or
workspaces), bulk Test. 'Sync ~/.ssh/config' no longer clears removal
tombstones; 'Restore removed hosts (n)' is a separate confirmed action.
* fix(settings): address review findings on servers and SSH hosts redesign
- SSH filter stays clearable when the list shrinks under the threshold
- Cmd/Ctrl-A only selects hosts when pressed in the list itself
- group legacy no-source hosts with main's import predicate (now shared)
- capture the shift-range anchor before queuing the selection update
- blank server names de-duplicate instead of colliding
- Share dialog Done refreshes link count and resets name editing
- Install / SSH-route dialogs reload SSH hosts when opened
- default-host row shows the selector when the default is a WSL host
- managed 'Not running' no longer masks connecting/revoked/update states
- servers without a dropped transport can be re-checked
* test(e2e): WSL host spec matches repo.rm ending a removed project's shells
repo.rm now stops the project's terminals on the host (#29964), so the
'orphan left by older builds' setup can no longer be built; use a shell
only the host knows of instead. Resolve the project path so macOS
/private/var matches what the host reports.
* fix(settings): carry 'Orca on <host>' naming and paired-twice warning into the new server list
Ports #31099 and #31100 into the redesigned rows, install dialog and SSH
host dialogs.
* test(e2e): keep the WSL spec's project path as created
* fix(ssh): say where Orca runs instead of "managed server" in the UI
Users read "managed server" as hosting run by someone else, so they declined
moving their SSH hosts. Every user-facing string now says "Orca on <host>" or
"this host's Orca server", in all six locales; Settings > "Orca on your SSH
hosts" and the setup line say the code and files stay where they are and only
Orca's list of projects, tabs and terminals moves to the host. CLI command names,
logs and identifiers are unchanged.
Fixes#30880
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(ssh): reassure only when something moves; name the host's label in its row
Review follow-ups: the reassurance shows only while Orca sets itself up or
moves a host, not on every reconnect; the converting line no longer repeats it;
the row names the SSH host's current label; the section description and the
Move anyway dialog stop repeating themselves; translate the Restart dialog's
buttons; drop "managed" from the remaining raw causes and the external
automation refusal.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
* fix(ssh): recover a tunnel port a ControlMaster still holds
A forward made through a ControlMaster leaves its local listener on the
master: the `-N` client exits at once and the master keeps the port. No
process of Orca's holds it then, so the orphan sweep finds no argv to
match and a managed tunnel's persisted port stays bound - every later
connect fails with `Local port 127.0.0.1:<port> is not available:
EADDRINUSE`, until the user finds and stops that master by hand.
Ask the master to drop just this forward (`ssh -O cancel -L`) when the
orphan sweep comes up empty, then rebind. OpenSSH refuses a spec no
master registered, so another forward on the port, and a port held by
anything but a master, are left alone - as are the master's own
sessions and its other forwards.
* fix(ssh): read a refused forward cancel off stderr, not the exit code
`ssh -O cancel` exits 0 whether or not a master held the spec, reporting
a refusal only as `master cancel forward request failed` on stderr. So a
spec no master registered read as a release that freed the port: the
recovery logged a cancel it never made and let the caller rebind a port
still held, turning a clear refusal into a second bind failure.
Treat a request failure on stderr as nothing released, so a port held by
anything but a matching master keeps reporting the original bind error.
* docs(ssh): document the port reservation contract and its recovery limits
State on each entry point what it returns and what it will not do: that
reservation only ever recovers a port already in use and only from what
the caller supplies, that the bind reports a busy port through the
error cause, and that a forward on port 0 carries no recovery at all
because the port it lands on was just free.
* fix(ssh): any managed-tunnel forward that fails to start falls back to the stdio bridge
* fix(ssh): skip the master cancel on Windows; note the forward's bridge fallback
* test(ssh): an abandoned forward's late exit uses a real close reason
* fix(ssh): cancel only Orca's forward on the master, and bridge a forward that never listens
Codex: -O cancel also cancels every forward the loaded ssh_config declares, so the
cancel now reads the control socket from ssh -G and sends the request with no config.
Opus: a forward still waiting on its own login never binds; that now takes the bridge.
* fix(ssh): pass the master's ControlPath through -S so a path with spaces stays whole
* fix(ssh): keep ssh's own exit reason when the forward also never listened
* test(ssh): the real sshd fixture waits for its processes before removing its temp dir
---------
Co-authored-by: zzzpyyy <167217540+zzzpyyy@users.noreply.github.com>
Co-authored-by: zhangpengyun <zhangpengyun@zpydeMacBook-Pro-2.local>
* 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
* 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
* 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
* 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
* 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
* 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
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.
* 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
* 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>
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>
* 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
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.
* 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
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>
* 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
* 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
* 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>
* 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
* 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>
* 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>
* 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>
* 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>
* 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>
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>
* 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>
* 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>
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.