Adopt group routing and unknown-address diagnostics from PR #20549; retain current generated guide content and omit the documentation enumeration ratchet.
Co-authored-by: OrcaWin <orcawin@users.noreply.github.com>
Adopts native usage parsing and display from PR #20797, adding a pre-invocation version guard and legacy group labels.
Co-authored-by: Abhishek Tiwari <69499+abti@users.noreply.github.com>
* fix(omp): join prompt submit with large paste
* test(omp): cover joined submit timing
* ci: rerun PR checks after timing test fix
* test(omp): acknowledge joined submit activity
* fix(runtime): detect a same-size terminal artifact swap the granted stat cannot see
A local terminal-artifact grant pinned the file as `dev:ino:nlink:size:mtimeMs`.
On Linux every one of those can survive an unlink+recreate: ext4 reuses the
just-freed inode (measured: 100% of the time), nlink and size are unchanged for a
same-size replacement, and the mtime clock is tick-quantized to 1ms, so a swap
inside one tick produces a byte-identical identity string. The grant then served
the attacker's bytes as if nothing had changed.
Local grants now also pin a sha256 of the artifact's content, taken from the same
handle as the stat so nothing can swap the file between them, and every local
read, preview and write re-checks it before returning or committing content.
The stat identity string itself is unchanged: the relay recomputes it verbatim to
honour `expectedStatIdentity`, so its format is a wire contract. Remote grants
keep the stat-only check and are untouched.
This is also the mechanism behind the intermittent
`orca-runtime-files-terminal-artifact-io.test.ts` failure on
`rejects stale absolute terminal artifact previews before returning changed
content`: it replaces an 8-byte artifact with 8 different bytes, so whenever the
two writes shared a 1ms tick the product genuinely could not tell them apart.
* docs(runtime): record what the terminal artifact grant checks do not close
The digest makes the same-size swap detectable; it does not make the sequence
atomic. A reader arriving at the access module would reasonably assume otherwise,
so write down the measured limits of the stat identity, why the identity string
cannot change, and the four windows that stay open — the write path's surviving
rename() gap above all.
The relay control socket closes with the reason `signed-out` so the cell can
tell paired phones to sign in rather than reporting a bare "host offline".
Only the auth coordinator computed that reason. The broker's renewal tick
called `closeNow()` with no reason when its token refresh came back empty, and
`closeNow` is idempotent, so whenever that tick observed the lost session
first — the common case for a session revoked or expired while Orca runs — the
coordinator's later `closeNow(SIGNED_OUT)` was a no-op and the cause never left
the desktop.
`refreshAccessToken` now returns a refusal that carries the reason the
coordinator already derives, and both close paths read it from one place. A
refusal for any other cause still names nothing: a present-but-unentitled
desktop is signed in, and a superseded refresh leaves the close to whoever
superseded it.
* feat(github): bind projects to a specific gh account
Adds per-project `Repo.ghAccount` so repo-scoped gh calls (create-worktree
issue/PR search, work items, hosted-review reads and mutations) run as the bound
account via ephemeral child-env token injection instead of the globally active
gh login. Multi-account resolution is capability-gated (gh >= 2.40) and fails
closed when the bound account or host is unavailable; Project View stays
ambient by design.
Repository settings gains a section for selecting or clearing a keyring-backed
account (shadcn `Select`), with mixed-version "not enforced" handling for older
remote runtimes. Attached `-Rhost/owner/repo` forms are covered by the host-drift
guard and its tests; es/ja/ko/zh catalogs carry the section's strings.
`getLocalProjectGhExecOptions` centralizes the binding lookup so every gh
execution path picks it up, including the Electron `hostedReview:*` handlers
that previously stayed on the ambient login. `gh auth token` (a keyring read)
is exempt from the rate-limit breaker gate so a tripped bucket cannot turn a
bound-token resolve into a false "unavailable".
The `ghAccount` update field and the two binding RPC methods live in the shared
RPC params contract; the generated catalog is regenerated.
Fixes#13612
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012B3QEP5iP4WGGEpPLtkHqA
* fix(settings): make GitHub account refresh secondary
* fix(github): satisfy strict casting quality checks
* test(rpc): use runtime fixture for repo binding
* fix(github): preserve project account for PR worktree lookups
* test(rpc): avoid incomplete runtime settings fixture
* fix(i18n): add GitHub account refresh label
* fix(i18n): refresh runtime required catalog
* fix(windows): preserve mobile patch bytes
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Neil <neil@stably.ai>
* fix(terminal): keep a client copy of a parked remote pane's scrollback
A remote-runtime pty's bytes never transit the client's main process, so the pane's
xterm buffer is the only client-side copy. The ordinary cold-park unmounted that pane
without capturing it, licensed by TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY — a static
build string that says nothing about whether the host retained this pty's buffer. On
reveal, a host that answers 'no-serializable-buffer' (or stays silent past the request
timeout) collapses to a null snapshot and the pane paints blank: tabs and splits survive,
the scrollback is gone.
Capture before every park, not only the retention-budget force-park, so the reveal has a
copy to replay when the host cannot answer. An unverifiable host answer is not proof the
pane was empty; keep the buffer, never discard it.
Adds ORCA_E2E_FORCE_REMOTE_TERMINAL_SNAPSHOT_UNAVAILABLE so an e2e can reproduce the
host-retains-nothing state, mirroring the existing forced-truncation lever.
* test(terminal): prove a parked remote pane survives a host that answers nothing
The oracle is a token the test types into the terminal before the park and the fixture
echoes back. Nothing replays stdin, so a respawned command cannot reproduce that line —
only the pre-park buffer can. An earlier argv marker passed vacuously for exactly that
reason.
The control ('host retains the buffer') is insensitive to the fix and fails if the harness
never parks, never reveals, or never echoed the token, so the regression case cannot be
green for a harness reason.
* refactor(terminal): validate the paired host terminal RPC shape instead of casting it
The merge-commit consistent-type-assertions gate flags every new `as`. Two were fixture
shapes that a type annotation states directly, and the third hid an unchecked RPC payload —
readCreatedTerminalTab now fails with the shape named rather than surfacing later as an
undefined surface id.
* fix(terminal): let a park capture survive an unhydrated repo catalog
Reading state.repos unguarded threw out of the cold-park effect whenever the catalog was
absent, which would break parking itself. Capture is best-effort evidence; an empty catalog
also fails open in shouldPreserveTerminalScrollbackBuffers, the safe direction for a park.
* docs(terminal): pin why the two unhydrated-catalog fallbacks point opposite ways
shouldPreserveTerminalScrollbackBuffers fails open toward 'remote' because a worktree wrongly
judged local parks with no copy at all. worktree-runtime-owner.ts resolves the same unhydrated
catalog to 'local', which is safe there and would be data loss here. A reader pattern-matching
'fail open' across the two gets one of them backwards.
* fix(terminal): keep a parked pane's scrollback across a reconnect merge
The direct-SSH pull replaces a replaced tab's layout wholesale, and a park capture does not
bump tab.generation — so a just-parked tab is not in locallyPreservedTabIds and the only
client-side copy of its remote scrollback went with the layout it replaced. That is the same
data loss this branch already fixes, one layer down, and it is the layer that decides whether
the fix survives the app update the user actually performed.
Carry the client's leaf-keyed scrollback into the host's layout, filtered to the host's own
root leaves. Structure stays the host's verbatim, so a split it added while we were away still
wins and a leaf it retired still drops its bytes. Local wins a conflict: neither copy is then
the only one, but remote-wins would overwrite the tail captured since the last upload and
propagate that backwards on the next replace-session patch.
Not a generation bump: the pane key is `${tab.id}-${tab.generation}`, so bumping would remount
the pane and destroy the very buffer the capture just serialized, lift the recovery-storm
ledger ceiling, and let a stale local ptyId win through preserveNewerLocalTerminalFields.
* fix(terminal): carry a parked pane's scrollback through the mirrored-layout rebuild
Found in review of this PR by rc-ssh-remoting. chooseRemoteTerminalLayout rebuilds a
mirrored tab's layout from the host's picture and never carried buffersByLeafId or
scrollbackRefsByLeafId forward, though it already receives existingLayout. The host
publishes no scrollback of its own, so ANY session-inventory frame landing between park and
reveal dropped the only client-side copy: the rebuild is bufferless, terminalLayoutEqual
compares buffers so the write is not bailed out, and apply-terminal-records assigns it
wholesale.
Measured before the fix: 336 bytes captured at park, 0 after one forced frame, blank pane on
reveal. After: 411 bytes survive the frame and the reveal repaints.
The e2e passed either way because no frame happened to land in its window, so it was not
covering the destroying event. It now forces one inside the park -> reveal window and asserts
the capture survives it.
An identical fix was written and reverted earlier in this branch as 'no measurable effect' —
that measurement ran on a harness deleting the client profile between launches, so nothing
downstream of persistence could register. It was never actually tested.
* feat(session): add a local-only home for ordinary-park scrollback
localOnlyScrollbackByTabId is a top-level session field, tabId -> leafId -> buffer, that never
rides the remote projection: exportRemoteWorkspaceSession is an explicit allowlist of named
top-level fields, so a new one is omitted for free, whereas anything added to
TerminalLayoutSnapshot is copied whole. It is also outside the two records the mirrored-tab apply
rewrites, so a host inventory frame cannot wipe it.
Registered in every exhaustive session registry ('tabKeyed'), hydrated and scoped like the layout
map, dropped with its tab on close/removal/purge/repo removal/mirrored retirement, copied on profile
transfer, emitted by the incremental patch builder, and capped by pruneLocalTerminalScrollbackBuffers
alongside the shared home — with a per-home test so an uncapped path cannot go unnoticed.
Known ceiling, not widened here: the field routes through the partition router that falls back to
'local' when the repo catalog is unknown at write time (#21295).
* fix(terminal): keep ordinary-park scrollback off the upload, and read both homes through one resolver
The ordinary cold park fires on every workspace hide. Its capture now splits: structure (root,
ptyIds, titles) stays in the shared layout, bytes go to localOnlyScrollbackByTabId. Force-park,
hibernate, sleep and shutdown keep writing buffersByLeafId, because that copy is what a second
desktop cold-restores from; a shared capture clears the local copy so the two homes never hold two
versions of one leaf.
resolveLeafScrollbackBuffers is the only read across the two homes (local wins a conflict: it is
the later write by construction). restoreTerminalPaneLayout no longer reads buffersByLeafId
directly, the capture's merge prior comes from the resolver, and the post-replay release covers
both homes.
Measured with the projection at 20 tabs x 2 panes at the per-leaf cap: the shared-layout shape
exports ~22 MiB per replace-session; the local-only shape exports the bufferless baseline.
* test(sync): pin that the mirrored rebuild carries the client scrollback refs
The carry-through added in f210dece83 keeps scrollbackRefsByLeafId for leaves the host still
names (a ref is the only pointer to a local scrollback file), so the rebuilt layout equals the
stored one and the write bails. The old assertion expected the refs to be dropped and has been red
on this branch since that commit.
* test(e2e): assert where a park's bytes land, and re-point the inventory-frame check at the force-park
Once ordinary parks stop writing buffersByLeafId, the existing survivedInventoryFrame assertion
passes trivially — there is nothing in the layout to wipe. The ordinary scenarios now assert the
store-level upload contract (bytes in the local-only home, shared home empty) and that the local
home is out of a host frame's reach; a third scenario reaches a force-park (host without paired
parking, client retention limit 1) and asserts the shared capture survives the forced frame, which
is where the mirrored-layout carry-through is load-bearing.
* test(e2e): leave the force-park reveal out of the assertion instead of matching it loosely
expect.any(Boolean) reads as a check but cannot fail. The force-park reveal is served by the host
tail on a host without paired parking, so it is logged for diagnosis and the assertion carries the
three fields that are deterministic on this topology.