mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* 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>
122 lines
6.9 KiB
TypeScript
122 lines
6.9 KiB
TypeScript
import { REMOTE_SERVER_UPDATE_CAPABILITY } from './remote-server-update'
|
|
|
|
// Why: declares the Orca runtime RPC compatibility contract. Desktop,
|
|
// headless server, CLI, and mobile builds may drift in app version, but
|
|
// they must agree on this protocol range before runtime RPCs are allowed.
|
|
//
|
|
// Bump RUNTIME_PROTOCOL_VERSION when:
|
|
// - You remove an RPC method or required parameter that clients use.
|
|
// - You change the meaning (units, nullability) of an existing field
|
|
// clients read.
|
|
// - You change encrypted framing, terminal stream framing, or auth.
|
|
// Do NOT bump for:
|
|
// - Adding new RPC methods.
|
|
// - Adding new optional fields on existing methods.
|
|
// - Adding new ignorable event types.
|
|
//
|
|
// Bump MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION when a runtime server must
|
|
// refuse older clients. Bump MIN_COMPATIBLE_RUNTIME_SERVER_VERSION when
|
|
// this client build requires a newer server. Exact app-version equality is
|
|
// never required; these numbers define the supported compatibility window.
|
|
|
|
export const RUNTIME_PROTOCOL_VERSION = 3
|
|
export const MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION = 2
|
|
export const MIN_COMPATIBLE_RUNTIME_SERVER_VERSION = 2
|
|
|
|
export const PROJECT_HOST_SETUP_RUNTIME_CAPABILITY = 'project-host-setup.v1' as const
|
|
export const TASK_SOURCE_CONTEXT_RUNTIME_CAPABILITY = 'task-source-context.v1' as const
|
|
export const WORKSPACE_RUN_CONTEXT_RUNTIME_CAPABILITY = 'workspace-run-context.v1' as const
|
|
export const WORKTREE_LINKED_WORK_ITEM_CONTEXT_RUNTIME_CAPABILITY =
|
|
'worktree.linked-work-item-context.v1' as const
|
|
export const REMOTE_RUNTIME_SHARED_CONTROL_CAPABILITY = 'remote-runtime.shared-control.v1' as const
|
|
export const ORCHESTRATION_FEDERATION_RUNTIME_CAPABILITY = 'orchestration.federation.v1' as const
|
|
export const ORCHESTRATION_FEDERATION_CONTROL_MAIL_RUNTIME_CAPABILITY =
|
|
'orchestration.federation-control-mail.v1' as const
|
|
export const ORCHESTRATION_FEDERATION_CONTROL_MAIL_PROTOCOL_VERSION = 2 as const
|
|
export const ORCHESTRATION_CONTRACT_VERSION = 1 as const
|
|
export const ORCHESTRATION_CONTRACT_RUNTIME_CAPABILITY = 'orchestration.contract.v1' as const
|
|
export const FOLDER_WORKSPACE_PATH_STATUS_RUNTIME_CAPABILITY =
|
|
'folder-workspace.path-status.v1' as const
|
|
export const LINEAR_ISSUE_ATTRIBUTE_FILTER_RUNTIME_CAPABILITY =
|
|
'linear.issue-attribute-filter.v1' as const
|
|
// Why: signals the host exposes the Agent Session History scanner over RPC
|
|
// (aiVault.listSessions). Registered unconditionally for every build, so it is a
|
|
// STATIC capability advertised by getStatus() automatically — NOT a runtime
|
|
// conditional like browser.headless.v1.
|
|
export const AI_VAULT_RUNTIME_CAPABILITY = 'aiVault.v1' as const
|
|
// Why: signals a host owns browser pages with no renderer (headless serve via the
|
|
// offscreen backend). Advertised only when that backend is actually available, so
|
|
// clients never fall back to a local desktop browser tab for a remote-owned page.
|
|
export const BROWSER_HEADLESS_RUNTIME_CAPABILITY = 'browser.headless.v1' as const
|
|
export const BROWSER_CERTIFICATE_TRUST_RUNTIME_CAPABILITY = 'browser.certificate-trust.v1' as const
|
|
// Why: hosts without this strip terminal.send's inputKind (zod object drops
|
|
// unknown keys), so a mobile xterm query reply would land as ordinary
|
|
// floor-taking input. Mobile must not forward replies unless advertised.
|
|
export const TERMINAL_QUERY_REPLY_INPUT_RUNTIME_CAPABILITY =
|
|
'terminal.query-reply-input.v1' as const
|
|
// Why: paired clients may unmount xterm only when the host can return a
|
|
// bounded, sequenced scrollback snapshot for lossless reveal.
|
|
export const TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY = 'terminal.paired-parking.v1' as const
|
|
// Why: older hosts lack the targeted settings RPCs and strip agentPrompt from
|
|
// terminal creation, so mobile must hide Quick Commands unless both are present.
|
|
export const TERMINAL_QUICK_COMMANDS_RUNTIME_CAPABILITY = 'terminal.quick-commands.v1' as const
|
|
// Why: older hosts strip worktree.create's clientMutationId, so mobile must only
|
|
// replay ambiguous cutovers when the host advertises idempotent create support.
|
|
export const WORKTREE_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY =
|
|
'worktree.create-idempotency.v1' as const
|
|
export const CODEX_RESET_CREDIT_RUNTIME_CAPABILITY = 'accounts.codex-reset-credit.v1' as const
|
|
export const ACCOUNT_IMPORT_RUNTIME_CAPABILITY = 'accounts.import-host-credentials.v1' as const
|
|
// Why: older hosts cannot reconcile terminal.create's mutation after losing the reply, so clients may only retry unknown outcomes when advertised.
|
|
export const TERMINAL_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY =
|
|
'terminal.create-idempotency.v2' as const
|
|
export const SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY = 'session-tabs.close-intent.v1' as const
|
|
export { REMOTE_SERVER_UPDATE_CAPABILITY } from './remote-server-update'
|
|
export const AGENT_SESSION_HOST_AUTHORITY_RUNTIME_CAPABILITY =
|
|
'agent-session.host-authority.v1' as const
|
|
export const AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY =
|
|
'agent-session.omp-resume-path.v1' as const
|
|
// Why: older runtimes strip mutation owner fields, so clients must fence writes before RPC.
|
|
export const FILE_MUTATION_OWNERSHIP_RUNTIME_CAPABILITY = 'files.mutation-ownership.v1' as const
|
|
export const FILE_MUTATION_OWNERSHIP_UPDATE_REQUIRED_MESSAGE =
|
|
'Remote file changes require a newer Orca server. Update the HUB and try again.'
|
|
|
|
export const RUNTIME_CAPABILITIES = [
|
|
'runtime.status.compat.v1',
|
|
'runtime.environments.v1',
|
|
REMOTE_RUNTIME_SHARED_CONTROL_CAPABILITY,
|
|
ORCHESTRATION_FEDERATION_RUNTIME_CAPABILITY,
|
|
ORCHESTRATION_FEDERATION_CONTROL_MAIL_RUNTIME_CAPABILITY,
|
|
ORCHESTRATION_CONTRACT_RUNTIME_CAPABILITY,
|
|
'browser.screencast.v1',
|
|
'terminal.binary-stream.v1',
|
|
'terminal.multiplex.v1',
|
|
'workspace-ports.v1',
|
|
'mobile.tasks.v1',
|
|
PROJECT_HOST_SETUP_RUNTIME_CAPABILITY,
|
|
TASK_SOURCE_CONTEXT_RUNTIME_CAPABILITY,
|
|
WORKSPACE_RUN_CONTEXT_RUNTIME_CAPABILITY,
|
|
WORKTREE_LINKED_WORK_ITEM_CONTEXT_RUNTIME_CAPABILITY,
|
|
FOLDER_WORKSPACE_PATH_STATUS_RUNTIME_CAPABILITY,
|
|
LINEAR_ISSUE_ATTRIBUTE_FILTER_RUNTIME_CAPABILITY,
|
|
AI_VAULT_RUNTIME_CAPABILITY,
|
|
TERMINAL_QUERY_REPLY_INPUT_RUNTIME_CAPABILITY,
|
|
TERMINAL_PAIRED_PARKING_RUNTIME_CAPABILITY,
|
|
TERMINAL_QUICK_COMMANDS_RUNTIME_CAPABILITY,
|
|
WORKTREE_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY,
|
|
TERMINAL_CREATE_IDEMPOTENCY_RUNTIME_CAPABILITY,
|
|
SESSION_TAB_CLOSE_INTENT_RUNTIME_CAPABILITY,
|
|
REMOTE_SERVER_UPDATE_CAPABILITY,
|
|
AGENT_SESSION_HOST_AUTHORITY_RUNTIME_CAPABILITY,
|
|
AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY,
|
|
FILE_MUTATION_OWNERSHIP_RUNTIME_CAPABILITY,
|
|
ACCOUNT_IMPORT_RUNTIME_CAPABILITY,
|
|
CODEX_RESET_CREDIT_RUNTIME_CAPABILITY
|
|
] as const
|
|
|
|
export type RuntimeCapability = (typeof RUNTIME_CAPABILITIES)[number] | (string & {})
|
|
|
|
// COMPAT(mobileProtocolAliases): added 2026-05-15 for mobile builds that
|
|
// still read desktop/mobile names; remove once mobile reads runtime names.
|
|
export const DESKTOP_PROTOCOL_VERSION = RUNTIME_PROTOCOL_VERSION
|
|
export const MIN_COMPATIBLE_MOBILE_VERSION = MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION
|