Commit Graph
17 Commits
Author SHA1 Message Date
Jinwoo Hong 9afd1101ff fix(orchestration): stop minting and printing the dispatch capability (#23994)
* fix(orchestration): authorize worker reports without the dispatch capability

Worker lifecycle reports and questions no longer depend on the per-dispatch
capability token that lives only in the agent's conversation. The host now:

- ignores capability_hash/capability_revoked_at for authorization on every
  row and checks the exact worker process instead (ask gains that check);
- refuses a report whose calling terminal is provably another orchestration
  party (a Run coordinator or another Dispatch's worker), treating env that
  names no live pane here as absent;
- applies one worker-state rule locally and remotely: a stop in flight
  refuses, while stop_unknown and start_unknown accept and settle.

Minting and printing the flag are unchanged, so an older host and older
preambles keep working.

* fix(orchestration): stop minting the dispatch capability

Dispatches no longer mint a per-Dispatch token, and preambles, the bundled
skill guide and the ask resume hint stop printing --dispatch-capability. The
consumer-generation bump and delivery fence that minting carried stay, now
as setDispatchConsumer. Readers that inferred meaning from capability_hash
read what they meant instead: worker-show's injected stage comes from the
attached consumer, and a failed start copies custody identity only when no
authority was ever attached. The CLI keeps accepting and forwarding the
flag for older hosts.

Cancelling a Task is recorded as failed with a reason; task-list now shows
that reason and the guide and task-update notes document the recipe.

* fix(orchestration): name the fenced party without implying which Dispatch it owns

* refactor(orchestration): one worker report rule, fence only a different party

- One module owns the unproven/settleable worker states and the refusal rule; local send records it, ask and remote throw it. A stale process is worker_identity_changed on every path.
- The caller fence passes the worker's own terminal when its --from handle went stale.
- Document the shared-tmux-server limit; drop the dead dispatch_capability_invalid rejection member; tests assert dispatch state, not the capability column.

* refactor(orchestration): drop setDispatchConsumer and the dead capability retention

- dispatch --inject no longer re-points the row createDispatchContext just wrote; worker-show reports every worker-less Dispatch as context_only, since Orca keeps no record of the paste. Tests re-point through a fixture.
- failWorkerStart always records when the lifecycle closed; nothing authorizes on it.
- Restore the ask resume hint's echo of a passed --dispatch-capability: an old host checks it before --resume.
- Move the cancellation convention to #23983.

* test(orchestration): drop capability-era assertions other tests already cover

* refactor(orchestration): drop the host-side capability field and no-op test fixtures

- RpcRequest and the SSH bridge stop carrying orchestrationCapability; the CLI's wire field stays for older hosts.
- Fixtures pass identity to createRootDispatch instead of re-pointing to the same values; drop absence checks for a flag that can no longer be produced.

* test(orchestration): cover a current process whose terminal moved to another pane

* chore(orchestration): finish the capability cleanup in test stubs and skill wording

* test(orchestration): drop needless response casts; mark the db stub cast safe
2026-09-30 13:26:06 -04:00
Brennan Benson 18bbf6f209 refactor(orchestration): resolve every caller and target to one orchestration party, keyed by the Orca session id (#22555)
* refactor(orchestration): resolve a session caller at the dispatch entry and bind it by actor

WIP: entry resolver on both dispatchers, caller identity through run scope,
actor-keyed Run binding and unbind, actor writes on bind/create/assign, and
actor-aware mail ownership exclusions.

* test(orchestration): pin actor-keyed Run binding, stale-actor precedence and actor mail ownership

Keeps the non-session dispatch path synchronous so terminal and session-tab
streams reach their handler without an extra async hop.

* feat(orchestration): resolve session callers before params parse and pin every verb on both routes

A session caller need not name itself in a param that requires a caller: the
entry binds the declared caller to the session before the schema runs. Session
refusal codes pass through the RPC error map, and DB row reads added here carry
their SAFETY rationale.

* test(orchestration): pin the SSH check's pane through the caller-identity lookup

* test(orchestration): pin a Run-less session's direct check and receipt binding without a caller param

Drops the actor clause from self-dispatch detection: a creator and assignee can
only share an actor when they already share a handle or pane.

* test(orchestration): pin a session's own Run for plain and group sends and the assignee-only mail sweep

* test(orchestration): name the party-naming field population for its role

* fix(orchestration): clear a worker actor an older binary's unbind leaves, and refuse a worker without its identity

An older binary unbinds a structured worker's Run by clearing handle and pane, which
leaves the actor looking like a handle-less chat binding. The every-open repair
clears that shape for actors recorded as structured workers only, and the resolver
refuses a worker session whose worker identity is gone, so this binary never
writes the shape itself.

* refactor(orchestration): read a Run's coordinator actor through its generation, and give a party's addresses one owner

The coordinator actor now counts only at the consumer generation it was written at, so a
Run binding matches a session by that rule alone. It replaces two mechanisms for the same
fact: the rule that an actor beside a handle it did not bind with never matches, and the
open-time repair that cleared a structured worker's actor an older binary's unbind left.
Every write of an older binary that rebinds or unbinds bumps the generation, so both
shapes stop counting by themselves, including a chat's actor after a rebind then an
unbind, which neither old mechanism caught. createRun and the same-coordinator actor
correction write the generation in the statement that writes the actor. The resolver
still refuses a structured worker whose worker identity is gone; its predicate moves
next to the worker identity lookup.

addressSpellingsOf is the one owner of the addresses a party is reachable at (a
structured worker's handle and session actor). createRun, bindRun, the coordinator
unbind and the declared-caller check use it instead of hand-built sets, and each has a
test at both of a worker's addresses.

* fix(orchestration): say a released session is not running, and scope the pane-key credential claim to requests without a session

A released lease is evicted, not ended: a user turn resumes the session, so the refusal
now says it is not running right now instead of that it has ended.

The worker pane-key comment claimed the random leaf is what stops anyone who learns a
session id from acting as the worker. On the same-host socket route the session id now
names the worker with no token by design; the pane key still matters where a request
names no session (a PTY agent's, or the paired-client route, which refuses session ids).

* test(orchestration): pin the same-coordinator actor correction's generation write on a row an older binary wrote

* refactor(orchestration): resolve session callers by the bare Orca session id, typed apart from its address

Carries the Orca session id rename into caller resolution, Run binding and Dispatch
creation. The caller identity holds the bare `orcaSessionId`; the `session:<id>`
spelling is derived by formatOrcaSessionAddress wherever mail needs it.

`OrcaSessionId` and `OrcaSessionAddress` are distinct branded strings. Only
isOrcaSessionId and parseOrcaSessionAddress produce an id, and only
formatOrcaSessionAddress produces an address, so comparing the two is a type error.
The Orca session id columns on the row types carry the id type.

Every reader that compares a stored id with a mail address now compares like with
like: the active-Dispatch ownership check formats the stored id
(orcaSessionAddressSql), and the stray-mail sweep, the creator nesting lookup and the
recorded-worker check bind a parsed or typed bare id. The Run-mailbox ownership
check keeps its existing handle comparison beside the session one.

* fix(orchestration): accept a worker's ask to every address its coordinator is reachable at

A worker's preamble names its coordinator as `session:<id>` when the coordinator is a structured session, but ask only accepted `run:<id>` or the coordinator's terminal handle. A chat coordinator has no handle, and a coordinating structured worker has two addresses, so ask --to the session address was refused as dispatch_run_mismatch. The check now takes the Run's current coordinator addresses from addressSpellingsOf(runCoordinatorKey(run)).

* refactor(orchestration): require every caller-identity entry point to be handed the resolved session

The resolved session parameter was optional on resolveRunScope, resolveOrchestrationCaller, orchestrationCallerIdentity, resolveDispatchCreator and resolveDispatchCallerWorktreeId, so a method that forgot to pass it would compile and silently treat a chat's session address as a terminal handle. It is now required and typed `OrchestrationSessionCaller | undefined`, so leaving it out is a type error. Every call site already passed it; no behavior changes.

* fix(orchestration): deliver mail sent to a session address to the mailbox that session reads

A send to session:<id> was resolved like a terminal handle: no live pane, so a chat's
current Run was missed (two Runs read as ambiguous), a Run-less chat was refused though
it reads its direct mailbox, and a structured worker's session address never reached
its Dispatch. Resolve a worker's session address as its handle, a chat's by its bound
Run, and fall back to the chat's durable direct mailbox while it runs on this host.

* refactor(orchestration): resolve every party through one resolver with one mailbox address

A structured worker is reachable at its handle and at its session address, and a chat only at its
session address. Callers and targets were each compared or resolved at their own site, some against
one spelling and some against every spelling, and the sites that did neither refused or misrouted.

Add orchestration-party: resolveOrchestrationParty (and resolveOrcaSessionParty for a bare id) is
now the only place an address becomes a party. The session caller, the declared-caller check,
ask's target and inbox's filter all resolve through it, so caller and recipient resolution cannot
disagree, including on a recorded worker whose identity this host lost. Every session-to-party step
passes through canonicalOrcaSessionId, the seam later lineage canonicalization plugs into.

mailboxAddressOf replaces addressSpellingsOf: a party has one mailbox address (a worker's handle
today), and Run binding remembers and reroutes only that.

A request with no session id that declares a session address as its caller now gets the party it
names: a worker's handle, as if it had named it, or a session_caller_chat_not_declarable refusal
for a chat, which is identified only by the session id its own environment sends.

* fix(orchestration): route dispatch and mail targets through the party they name

dispatch --to a worker's session address stored that address as the assignee handle, so the worker,
which reads by its handle, never saw the Dispatch. The assignee now resolves through the party
resolver: a worker's either spelling assigns its handle and records its Orca session id. A chat
cannot receive a dispatch yet, so dispatch --to a chat is refused with
session_chat_not_dispatchable before any row is written.

Recipient routing resolves the party once and, for any session-backed party, finds its Run by its
durable binding rather than by a live pane. A structured worker that coordinates a child Run while
assigned in its parent got mail at its parent Dispatch mailbox whenever its session was evicted,
which it never reads while bound to the child Run. Terminal handles keep the same live-pane lookup.

* refactor(orchestration): delete the SQL that matched a worker's second spelling

With every caller and target resolved to one mailbox address, no writer stores mail under a
structured worker's session address: sends resolve it to the handle, a declared session caller is
rewritten to the handle or refused, replies answer a stored canonical sender, and questions,
answers, escalations, federation and legacy mail write run:, dispatch: or handle addresses (legacy
rows are legacy_direct, which these queries never read). The branches that matched that spelling
were unreachable and are removed:

- the session-address OR in activeDispatchOwnsAddressSql, back to the assignee handle alone;
- the session branches in routeForeignDirectMessagesToOwnedMailboxes and
  findActiveDispatchForDirectMessageOwner, which return to the base-branch form.

Tests that inserted such rows directly now pin the behaviour through the verbs: both spellings of a
worker land in the one mailbox it reads.

* test(orchestration): pin one canonical address per party across every caller and target

- every target param in ORCHESTRATION_TARGET_PARAM, for a worker's handle and session address and
  a chat's session address: send, ask, dispatch (a chat refused with no row) and inbox;
- a declared session-address caller on a request with no session id: a worker acts as its handle,
  a chat is refused;
- a worker that coordinates a child Run while assigned in its parent gets mail in the child Run
  with no live pane;
- no mail writer stores to_handle or from_handle as a worker's session address;
- the caller a session id resolves to and the recipient its address resolves to agree in every
  party state, and every session-to-party step goes through canonicalOrcaSessionId.

* refactor(orchestration): drop the session caller's chat-to-terminal-view handoff remnants

The structured-chat terminal handoff is gone from the base branch: a session is owned natively only.

- the lease rule no longer speaks of either owner or a handoff keeping identity;
- an in-progress owner change (new-owner-proving, recovering, manual-recovery, which the base branch
  still produces) is refused as changing owners, not as switching between chat and terminal view;
- the native to terminal-view to native identity test is deleted, and the terminal-evidence test
  no longer describes that evidence as a terminal view's.

* fix(orchestration): read a declared caller param by typed access, not Reflect.get
2026-09-25 18:01:40 -07:00
Neil 13ba649c22 fix(terminal): let a runtime-created Windows terminal BE the requested shell (#20825)
* fix(terminal): let a runtime-created Windows terminal BE the requested shell

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Reported by an adversarial review of the branch.

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

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

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

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

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

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

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

The catalog walk under methods/ already skips *.test.ts, but the contract
directory glob took every .ts. terminal-create-shell-param.test.ts is the
first test to live there, so the bundle pulled vitest into a CJS build and
the generator threw on require(). Same exclusion, same reason.
2026-09-15 16:34:16 -07:00
Brennan Benson 8a07bbd8cf fix(orchestration): enforce nested worker depth instead of an accidental fence (#16668)
* fix(orchestration): enforce nested worker depth instead of an accidental fence

Orca documented that "dispatched workers cannot spawn their own sub-workers
(worker-start is coordinator-fenced)". No such check existed. What existed was a
single Run-binding check in the workerStart RPC: a worker's terminal is not bound
to a Run, so worker-start happened to fail. The rule was emergent, asserted by no
test, and written in no doc — and it leaked. A worker could run-create its own
Run, task-create, and worker-start: now bound, the check passed.

Replace it with a real, configurable depth cap.

Depth is derived from the caller's own active Dispatch rather than from Run
binding, which is what dissolves the run-create bypass: creating a Run does not
stop you being a worker. Enforcement lives in a single dispatch-row writer that
owns all three INSERTs that mint a live worker — the generic claim, the supervised
worker-start path (including every retry), and the remote attachment. Two of those
were missed by earlier drafts of this change, so `creator` and `maxDepth` are
required parameters: a new spawn path cannot compile without deciding, and a
boundary test refuses the SQL anywhere else.

Schema v30 adds depth to dispatch_contexts and remote_dispatch_attachments,
NOT NULL DEFAULT 1 and backfilled to 1 so an unstamped or pre-upgrade row fails
closed rather than reading as a root coordinator. The attachment pane indexes
widen to the five states in which a remote worker may still be running:
loss of contact is not evidence of process death, so an unverifiable worker still
counts as a nesting parent.

Also adds the caller-evidence assertion that workerStart was the only Run-scoped
verb to skip, so a declared --from cannot name another terminal's pane and inherit
its depth.

Default is 1, so behaviour is unchanged unless the new setting is raised. Two
limitations are deliberate and documented rather than papered over: this is a
guardrail and not a security boundary, since a caller whose launch evidence is
unverifiable (any ordinary restored terminal) can declare another handle; and it
is enforced at supervised dispatch creation, so a settled worker whose process is
still alive counts as a root again.

* fix(orchestration): share caller resolution and pin worker gaps

* refactor(orchestration): make the caller resolver's pane contract explicit

Overloads so requireStablePane callers get a non-null string instead of casting,
and rename the attestation opt-out to say what it means: the caller asserts it
itself. A flag called assertEvidence:false reads as "attestation optional",
which is the hole this helper exists to close.

* fix(orchestration): propagate dispatch depth to federated workers

* chore(cli): refresh bundled orchestration guide
2026-08-26 13:22:09 -07:00
Jinwoo Hong be07b43a2b fix(orchestration): enforce honest recipient routing (#14964) 2026-08-17 01:50:17 -07:00
Brennan Benson 39c3c58d55 perf(runtime): gate terminal.list visual layouts (#12450)
* perf(runtime): gate terminal.list visual layouts and stop the false writable claim

visualLayouts is ~31% of a large terminal.list payload (44,208 B of 137,412 B on a live 134-terminal remote runtime) and has exactly one consumer: the human-readable CLI formatter. Gate it behind an includeVisualLayouts request param that defaults to included, so pre-flag clients are unaffected, and have every --json/internal caller opt out.

Also drop the record-backed builder's writable, which was a verbatim copy of connected. terminal.show now states writability explicitly as exactly what terminal.send's PTY gate enforces.

* test(runtime): type the payload-size fixture arrays for tsc

* fix(runtime): preserve terminal list compatibility

* test(runtime): guard terminal list optimization

* fix(cli): preserve agent access to terminal layouts
2026-08-04 17:50:52 -07:00
650dd48ec9 feat(cli): add orca account add / account list for headless hosts (Claude + Codex) (#9177)
* feat(cli): add `orca account add` / `account list` for headless hosts

The desktop "Add account" UI is disabled when the renderer drives a remote
runtime (isRemoteAccountScope === kind:'environment'), so a headless server
reached from a remote desktop/web client has no way to register managed
Claude accounts. Add a host-local CLI path that reuses the existing capture
logic:

- ClaudeAccountService.addAccountFromConfigDir(): register a managed account by
  capturing credentials from an already-authenticated CLAUDE_CONFIG_DIR instead
  of spawning the interactive browser login (extracted persist/rollback helpers
  shared with the existing add flow)
- RPC accounts.addClaudeFromConfigDir, bridged via OrcaRuntime; rejected for
  mobile device tokens (host-local only)
- `orca account add` runs `claude login` in the user's own terminal into a temp
  CLAUDE_CONFIG_DIR, then registers it via the local runtime; `orca account list`
  lists managed accounts

Switching (select) already works from a remote client; only adding was blocked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(cli): support Codex in `orca account add` / `account list`

Mirror the Claude headless-account CLI for Codex:

- CodexAccountService.addAccountFromHome(): register a managed Codex account by
  importing auth.json from an already-authenticated CODEX_HOME, reusing a shared
  persist helper extracted from doAddAccount (no interactive login spawned here)
- RPC accounts.addCodexFromHome + OrcaRuntime.addCodexAccountFromHome bridge,
  rejected for mobile device tokens (host-local only)
- `orca account add --agent claude|codex` (default claude); `orca account list`
  now renders both Claude and Codex managed-account blocks

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test: cover headless account-add capture paths (Claude + Codex)

- ClaudeAccountService.addAccountFromConfigDir: registers a managed account by
  capturing an authenticated CLAUDE_CONFIG_DIR; rejects and rolls back when the
  dir has no .credentials.json
- CodexAccountService.addAccountFromHome: imports auth.json from an
  authenticated CODEX_HOME into a managed account; rejects when auth.json is
  missing

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review on headless account-add flows

- CLI login spawn uses a shell on Windows so `.cmd` agent shims resolve without
  ENOENT (args are fixed literals, no injection risk)
- Claude capture skips the `.credentials.json` precheck on macOS, where creds
  live in the Keychain and captureAuthFromConfigDir reads them
- Claude add rollback is best-effort: a failed rematerialization no longer skips
  managed-auth cleanup or masks the original add error
- Codex persist restores the prior account/selection if a post-write sync or
  rate-limit refresh fails, so a failure can't leave a dangling managed account
- Codex sync passes the account's selection target (correct runtime for WSL)
- Add JSDoc to the new public service methods and CLI functions

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(cli): harden headless account capture

* fix(cli): correct account command flag surface and interrupt cleanup

- `account` commands no longer accept or advertise the browser `--page`
  flag; `supportsBrowserPageFlag` allow-listed them by omission, so
  `orca account list --page x` was silently accepted and `--help`
  rendered a browser-only option
- account specs declare GLOBAL_FLAGS, so `--help`/`--json` render in the
  Options block like every other command
- `--agent` on `account add` documents the account provider instead of
  the terminal TUI-agent meaning inherited from the shared flag table
- a SIGINT/SIGTERM during the interactive login now removes the temp
  login dir (and restores the macOS Keychain item) before exiting 130;
  Node terminates without unwinding `finally`, which stranded live OAuth
  credentials on disk

* perf(cli): stop `account list` forcing a provider usage refresh

`accounts.list` awaited refreshAccountsForMobile(), which runs
fetchAll({ force: true }) — bypassing both the poll throttle and the
per-provider Retry-After gate — then O(N) serial per-account round
trips. `orca account list` renders only emails and the active ids, so
all of that work was discarded. The RPC now takes `refreshUsage`
(default true, so mobile and web keep the forced lane) and the CLI opts
out. Older hosts declare `params: null` and ignore the field, so a newer
CLI degrades to the previous behavior rather than failing.

Also documents on `account list` that `--environment` does not retarget
it, matching the host-local behavior of shouldIgnoreRemoteSelection.

* fix(cli): survive repeated and hangup signals during account add

withInterruptCleanup latched cleanup behind a boolean, so a second signal
got an already-resolved promise and its process.exit fired while the first
cleanup was still inside a Keychain call (3s each) — the temp dir's OAuth
credentials and the swapped macOS Keychain item both survived. Memoize the
cleanup promise so every signal awaits the same run, and register with
`on` instead of `once` so a second Ctrl-C cannot fall through to Node's
terminate-immediately default mid-cleanup.

Handle SIGHUP too. This flow exists for headless/SSH hosts, where the most
likely interrupt is the connection dropping, which hangs up the login's
terminal and previously ran no cleanup at all.

Warn when the interrupt lands after sign-in completed: the runtime finishes
the add independently of this process, so exiting 130 silently would tell
the user it was cancelled when the account may exist.

Reject a valueless `--agent`; the parser turns it into boolean true, which
silently ran a full OAuth login for Claude when the user asked for another
provider.

Also lock two behaviors the refactor changed but left uncovered: a WSL Codex
add must sync the WSL runtime lane rather than the default host lane, and
rename the account-spec help test to describe the Options block it actually
asserts rather than the usage string it never reads.

* fix(build): bundle the main modules the account CLI imports

electron-vite cleans out/main and emits only its declared entries, and
`build:desktop` runs it after `build:cli`, so the tsc-emitted copies of
`claude-accounts/keychain`, `codex-cli/command` and `win32-utils` were
deleted before packaging. Both `orca account add` and `orca account list`
then died at require time with "Cannot find module
'../../main/claude-accounts/keychain'" — reproduced against a real
`--serve` host. `agent-hooks/managed-agent-hook-controls` already carried
an entry for exactly this reason; these three were missing.

Adds a parity test so any future CLI import of a `src/main` module fails
in CI rather than at a user's shell after packaging.

* test: cover the desktop add-path behavior this PR changes

Both changes ride in the persist/rollback helpers the existing GUI add
flow shares with the new headless path, and neither had coverage:

- Claude: rollbackAddAccount now guards forceMaterializeCurrentSelection-
  ForRollback, so a rejecting rematerialization no longer replaces the
  real add error nor skips safeRemoveManagedAuth. Asserts the original
  error surfaces and the throwaway auth dir is gone.
- Codex: the desktop add now passes the account's selection target to
  syncForCurrentSelection, matching reauthenticate and select. Asserts
  the host target alongside the existing WSL assertion.

Both fail when the corresponding change is reverted.

* fix(cli): close the remaining account-add interrupt and preflight gaps

The round-1 interrupt fix detached the signal handlers before running the
finally-path cleanup, so the very window it was meant to protect — the two
serial 3s `security` calls plus rmSync on the success/error path — was
still covered only by Node's terminate-immediately default. Both review
lanes reproduced it independently. Await cleanup first, detach in a nested
finally, and stop a cleanup failure from replacing the error that actually
explains why the add failed.

Do not burn the interactive login when the runtime is unreachable. The
RuntimeClient is lazily constructed and the first call was the registration
RPC itself, so "Requires the Orca runtime to be running" was discovered
only after the user completed a full OAuth round trip. Preflight with the
now-cheap `accounts.list { refreshUsage: false }`.

Reject `--environment` / `--pairing-code` on `account add`.
shouldIgnoreRemoteSelection pins account commands to the local runtime, so
`orca account add --environment homelab` silently registered the account on
the laptop instead of the headless host it names.

Survive a daemon that cannot spawn `claude`. `allowFailure` is honored in
onClose but not onError, and unlike the GUI flow nothing has run `claude` in
the daemon before this point — so a launchd/systemd daemon with a minimal
PATH hard-failed an add the user had already signed in for, even though
identity resolves fine from the config dir's oauthAccount.

Also align the `--agent` help description with the global flag column.

* fix(cli): reject runtime selectors on `account list` too

`orca account list --environment homelab` was accepted and silently
listed the LOCAL machine's accounts, because shouldIgnoreRemoteSelection
pins account commands to the local runtime. Documenting that in --help
does not reach someone who already typed the flag, and answering with the
wrong host's accounts is the specific wrong answer they would act on.

`account add` already errors; this makes the new command group internally
consistent. The other groups in shouldIgnoreRemoteSelection keep their
existing silent-ignore behavior — changing those is not this PR's job.

* test: harden account-add signal tests and cover cleanup failure

- Identify the handler under test by set difference instead of
  `process.listeners(sig).at(-1)`. Vitest installs its own once-wrapped
  SIGINT teardown, so the positional lookup could grab the wrong listener;
  the helper also asserts exactly one new listener was added.
- Mock rmSync while keeping the real implementation by default, so the
  temp-dir assertions elsewhere stay honest.
- Cover that a cleanup failure in the `finally` does not replace the error
  explaining why the add failed. Fails when that guard is removed.

Completes the review loop's final round; the loop died on an API error
before it could commit this, and its `import()` type annotation would
have failed oxlint.

* fix(cli): harden interactive account add

* test(cli): make account cancellation coverage portable

* fix(cli): preserve merged skills runtime modules

---------

Co-authored-by: Dominik <marketing@gavaplast.sk>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
2026-07-30 12:50:07 -07:00
OrcaWinandOrcaWin 363e478909 fix(orchestration): preserve active workers across updates (#11271)
* fix(orchestration): preserve active workers across updates

* test(ssh): model absent legacy adoption

* test(orchestration): align compatibility contracts

* fix(windows): escape updater PowerShell booleans

* fix(windows): restore stock uninstall process check

* fix(orchestration): keep recovery off renderer startup barrier

* fix(orchestration): harden legacy recovery migration

* fix(orchestration): close recovery review gaps

* fix(orchestration): complete legacy worker cutover recovery

* fix(orchestration): preserve legacy workers across updates

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-29 11:31:35 -07:00
Jinwoo HongandOrcaWin 0d6f9195d8 fix(orchestration): reveal worker terminals reliably (#11142)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-07-28 01:59:49 -07:00
OrcaWin cd05f2ff93 Implement robust orchestration primitives and connected-server workers (#9925) 2026-07-27 12:31:37 -07:00
NeilandOrca aab112933e Revert "fix(memory): bound OOM-prone accumulators (#10179)" (#10255)
Co-authored-by: Orca <help@stably.ai>
2026-07-23 18:35:31 -07:00
Neil 8f40ddf328 fix(memory): bound OOM-prone accumulators (#10179) 2026-07-23 06:22:56 -07:00
Brennan Benson be066fe8e9 feat(linear): expose issue activity history (#9667) 2026-07-21 13:13:05 -07:00
Jinjing 52388c1ab7 Fix worker done pane identity (#8473)
* fix(orchestration): bind lifecycle sends to dispatched pane

* fix(orchestration): bind injected worker messaging

* Fix worker_done pane identity gating and rename SSH linear CLI files

- Lifecycle reconciliation now returns an explicit `rejected` action
  (instead of `ignored`) for persisted sender_not_assignee markers, and
  only treats a payload's `_orcaLifecycleRejection` as trusted state
  when it exactly matches that reserved shape, preventing caller-
  supplied markers from spoofing a rejected send as success.
- SSH legacy fallback CLI now fails closed with
  `no_active_sender_terminal` when a worker_done/heartbeat send has no
  resolvable sender identity, rejects mixed raw/structured payload
  flags, and reconstructs structured lifecycle payload fields
  (task/dispatch id, files-modified, report-path, phase) instead of
  dropping them — extracted into ssh-remote-orchestration-send.ts.
- Renamed ssh-remote-linear-argument-error.ts to
  ssh-remote-cli-argument-error.ts since RemoteCliArgumentError is now
  shared beyond the Linear CLI.

* Add re-read logging for already-converted lifecycle rejections

Send-path reconcile converts worker_done/heartbeat rejections with a
no-op logger, so the coordinator's later re-read is the only chance
to surface the rejection message. Log it there instead of dropping it.
2026-07-12 22:45:33 -07:00
Jinwoo HongandOrca 0784a7ea37 fix(ssh): bridge the full Orca CLI over the SSH relay instead of a hardcoded command switch (#7771)
The relay CLI shim on SSH remotes rejected every orchestration/mutation
command with 'Unsupported SSH Orca CLI command' because the host handled
relay CLI requests with a hand-rolled allowlist of five read-only-ish
commands. The host now runs the real bundled orca CLI entry (same entry
as the local shell command, via ELECTRON_RUN_AS_NODE) as a captured
subprocess, so remote invocations get the full command surface by
construction. Remote cwd is carried via ORCA_CLI_CWD so cwd-based
selectors (--worktree active) resolve against the caller's remote
directory; only Orca terminal-context env vars cross the bridge.

Host-interactive commands (serve, claude-teams, agent-teams-tmux) get a
targeted error, and the legacy in-process switch remains as a fallback
when the host CLI entry cannot be launched. Relay-side request timeouts
are raised to fit mutation and long-poll (--wait/--timeout-ms) commands,
and stdin forwarding now covers *-stdin payload flags.

Fixes #7716

Co-authored-by: Orca <help@stably.ai>
2026-07-08 13:59:42 -07:00
Brennan BensonandOrca cdc0ca5e53 Add read-only orca linear CLI with trusted launch-prompt pointer (V1) (#5126)
Co-authored-by: Orca <help@stably.ai>
2026-06-10 20:20:50 -07:00
Jinwoo Hong 4cc0147005 Fix orchestration mail from WSL and SSH terminals (#3918) 2026-05-31 15:13:15 -04:00