* fix(artifacts): gate agent artifact publishing behind an off-by-default capability
Public artifact sharing was reachable by any agent through `orca artifacts
share`: the Artifacts settings toggle only controlled sidebar visibility, and
nothing in the main process checked a capability before minting a public URL.
Add `artifactSharingEnabled` (default off) and enforce it in
ArtifactCloudService.share/update — before auth, network, or the share-record
write — so the CLI, relay-forwarded remote CLI, and IPC paths are all denied.
The denial carries a stable `artifact_sharing_disabled` code plus next steps
through the RPC error allowlist, so the CLI prints actionable guidance.
list, unshare, and delete stay ungated: turning publishing off must not strand
already-published links. The capability is absent from the `settings.update`
RPC schema, so an agent cannot grant it to itself — only the desktop UI can.
Co-authored-by: Orca <help@stably.ai>
* fix(artifacts): gate agent artifact publishing behind an off-by-default
Publishing is blocked until enabled in Settings → Artifacts. CLI preflights the capability before reading files to avoid unnecessary uploads. RPC surface rejects capability grants so callers cannot self-grant. UI shows opt-in workflow and recovery path when publishing is off. Web clients mirror the host's setting read-only.
---------
Co-authored-by: Orca <help@stably.ai>
* fix(serve): recognize CLI-form serve args on the Electron process
When the binary is launched as `… serve --port …` without the CLI rewrite
that injects `--serve`, normalize argv so isServeMode, headless GPU flags,
and serve option parsing all engage.
Preserves existing `--serve*` flag behavior for the CLI-spawned path.
Fixes#12677
* fix(serve): treat only CLI subcommand position as serve
Parse bare `serve` as the first positional token after flags/values so an
option value named `serve` cannot enable headless mode.
Addresses CodeRabbit on #12818.
* fix(serve): keep CLI redirects ahead of the serve argv rewrite
Rewriting argv before maybeRedirectAppImageCliLaunch replaced the `serve`
positional with `--serve`, so the redirect's command-name lookup saw a port
number and bailed — dropping AppImage serve launches out of the CLI path.
Also translate `--port=6768` (the CLI accepts it, getServeOptions only reads
the next token) and the mixed `--serve --port` form, so a security-shaped flag
like `--no-pairing` can no longer read as accepted while pairing stays on.
Map lookups replace `in` on object literals, which turned a stray `serve
toString` positional into a function spliced onto argv.
* fix(serve): close the CLI-form serve gaps found in review
second-instance: shouldActivateDesktopForSecondInstance matched only `--serve`,
so a duplicate `<binary> serve --port …` — the ExecStart shape documented in
docs/reference/headless-linux-server.md — promoted the live headless server to a
desktop window, un-fixing #11935 on exactly the launch shape this PR legitimizes.
findServeSubcommandIndex consumed a flag's value unconditionally while the
rewrite consumed it only when the next token was not flag-shaped. The two could
disagree and swallow the `serve` token, leaving `--serve` uninjected: #12677
again in a new shape (`--port --port serve`, `--port -- serve`). Both scans now
share one definition of value consumption.
`<binary> serve --help` / `serve help` bound a network-exposed runtime server
with pairing on and printed nothing; the AppImage redirect already routes those
three tokens to the CLI, so refuse them here too.
`--no-pairing=false` translated to `--serve-no-pairing` with the value dropped,
disabling pairing for an operator who asked for the opposite. The CLI reads its
serve booleans as `flags.get(name) === true`, so a boolean is now translated only
in its bare form and the `=` form rides through as the CLI treats it.
Tests: spec-derived parity between src/cli/specs/serve.ts and the rewrite,
covering both ends of the contract (serveOrcaApp and getServeOptions); a
source-text lock on the index.ts redirect/rewrite ordering, which reverted
silently green before; an exhaustive self-consistency property test; and the
real GUI launch argv shapes that must never enter serve mode.
---------
Co-authored-by: Jinwoo-H <Jinwoo-H@users.noreply.github.com>
* fix(agent-hooks): give resumed Claude sessions a sidebar row at SessionStart (STA-3386)
Claude's hook set never registered SessionStart and normalizeClaudeEvent
dropped it at ingest, so a resumed session that idled produced zero hook
traffic and earned no sidebar agent row until the first prompt.
- Register SessionStart in CLAUDE_EVENTS (local + remote installs).
- Map lead SessionStart (startup/resume/clear) to an idle 'done' row,
resetting stale roster/task/cron/tool/prompt state like the Codex path;
compact restarts and child-attributed SessionStart stay dropped.
- Thread hookEventName through the agent-status IPC payload so the
completion coordinator can tell a session connect from a turn result;
a SessionStart 'done' no longer raises agent-task-complete.
* fix(agent-hooks): mark SessionStart rows as session boundaries, not completions (STA-3386)
Review follow-up: represent the idle connect as a first-class
sessionBoundary flag on the status payload instead of gating one
renderer consumer on hookEventName.
- sessionBoundary rides AgentStatusPayload/AgentStatusEntry (done-only,
clamped like interrupted); drops the hookEventName IPC threading.
- Completion-reactive consumers ignore session boundaries: the
completion coordinator (task-complete notifications), automation
dispatch observers (a connecting agent no longer completes the run
and closes its tab), activity unread counts, and the dashboard
finished timestamp; the status slice keeps boundaries out of
stateHistory and preserves the flag across done->done repaints.
- SessionStart sources are allowlisted (startup/resume/clear) so
compact restarts or unknown sources fail closed mid-turn.
- A live SessionStart now un-retires a reusable pane like a fresh
prompt, so resume-in-reused-pane earns its row too.
* fix(agent-hooks): keep session-boundary dones out of teardown and completion history (STA-3386)
Review round 2:
- A boundary done no longer deletes the pane's launch-config registry
entry, so a resumed idle TUI keeps its registered-launch-agent
identity evidence.
- A boundary landing on a REAL done pushes that completion into
stateHistory so the finished timestamp and unread badge survive a
resume//clear right after a finish.
- The done->done flag carry yields to turn evidence (assistant message
or changed prompt) so a genuine completion can never be suppressed.
- Star-nag value-moment observer and the server's OSC-equivalence
dedupe now discriminate the flag.
* fix(agent-hooks): keep a displaced completion unread in the sidebar badge (STA-3386)
Review round 3: sidebar-badge mode counts only the live entry, so a
session boundary landing on an unacknowledged completion silently
dropped the sidebar badge while the agent-events count kept it. Count
the displaced completion from history for boundary rows, and pin the
behavior with countActivityUnread tests.
* fix(agent-hooks): prevent SessionStart completion side effects (STA-3386)
* fix(agent-hooks): preserve SessionStart through renderer IPC (STA-3386)
* 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
* perf(runtime): keep PowerShell ACL work and a second auth off the remote command path
Two costs sat on the remote authentication path on Windows:
- The E2EE handshake persisted `lastSeenAt` inline, and every secure-file write
spawns PowerShell synchronously twice to reapply the registry ACL, so the
client's `e2ee_authenticated` waited on both spawns.
- Every remote CLI command except `status.get` opened a second full WebSocket
connection just to re-read status for the protocol-compat check, doubling the
authentications per command.
The first sighting of a device still persists inline (rotation drops entries
disk says were never scanned); later refreshes update memory now and coalesce
onto one deferred write. The compat verdict is saved against the runtime's
per-launch `runtimeId`, so a restarted or upgraded runtime retires it.
* fix(runtime): preserve compatibility on one remote auth
* fix(runtime): flush registry after transport shutdown
* feat(sidebar): link Linear issues from Edit Worktree Details
The Issue field only accepted GitHub numbers, so a workspace tracking a
Linear issue had no way to say so from the dialog — the link could only be
set at creation time or through `orca worktree set --linear-issue`.
Replaces the field with one provider-aware row: a chip suffix inside the
input selects GitHub or Linear, and pasting a URL flips the chip to match.
A bare key never steers the provider — Linear and Jira issue keys are
byte-identical in shape, so shape alone cannot decide one.
One issue per workspace. A changed field displaces the other provider's
slot and the row names what Save is about to unlink. GitLab and Jira links
are left alone: the row cannot display them, and nothing else in the UI
could restore one it dropped.
- Folder workspaces read-only (their link is creation-time only)
- Remote runtimes assert the capability before writing or clearing, since
`worktree.set` parses in strip mode and would silently drop the keys
- `updateWorktreeMeta` now reports failure so the dialog can stay open
instead of closing over a save that refetch reverted
- Parses are length-bounded — `matchGitHubItemPath` strips trailing
slashes with an unanchored regex that is quadratic on a large paste
* fix(sidebar): respect one-issue-per-workspace rule conditionally
Only clear displaced issue links when they actually existed, preventing
unnecessary Linear keys in GitHub-only workspaces. Skip comment updates
when unchanged to avoid workspace reordering. Add accessibility to
displacement messages and improve folder workspace error handling.
* fix(sidebar): resolve workspace ambiguity and improve Linear issue linki
The same workspace ID can exist under multiple hosts — the owner index reports
this as ambiguous rather than guessing. Dialog callers now pass their repoId so
lookups are unambiguous. Linear identifiers without an org key are resolved
across all workspaces (not just the active organization). Added race-condition
protection for async issue lookups and better change detection to avoid clearing
work-item titles when re-saving an identifier in different spelling.
Bound exclusive host navigation to a generation-aware latest-wins
single-flight so bulk open and switch fan-out stay responsive on large
remote fleets. Add freeze repro harnesses and navigated settlement.
Destructive worktree removal proves every PTY is dead before touching the filesystem. When a stop
RPC failed, it re-listed the provider to check whether the PTY had already exited — but on the
same deadline the sweeps had just spent, so it timed out without ever asking and read "could not
verify" as "still live". The sweep spends that budget every run, making the refusal deterministic;
--force never reached the gate, so the workspace was unremovable forever.
- Verification gets its own budget instead of an exhausted remainder.
- Verdicts split into exited / live / unverifiable; the error names the blocking PTY ids and why.
- A reachable escape hatch: allowUnverifiedPtyStop, set only by genuine Force Delete affordances
and the CLI's --force — never by the force the ordinary delete confirmation already sets — with
an 'unstopped-pty' classifier reason so the desktop actually offers the button.
- Force also survives a sweep that cannot complete; the non-force path still fails fast.
Fixes#11960
* chore(dead-code): drop 2k lines of unreachable exports and orphan modules
Ran knip across every build entry (main, preload, renderer, popout, web,
cli, relay, workers, forked sidecars, config scripts) and removed what no
entry graph can reach.
- 11 orphan modules nothing imported, plus one test that only covered them
- 159 unused exports/types, with their now-dead helpers, imports and tests
Each candidate was verified against dynamic references before deletion.
42 knip hits were false positives and are kept: shared modules consumed by
the mobile/ workspace, the src/shared/plugins/** public API, vendored
shadcn primitives, and relay wire-protocol constants held for compatibility.
Adds knip.json + `pnpm audit:dead-code` so this stays measurable.
Verified: pnpm typecheck, pnpm lint, and 2081 tests across the 73 affected
test files all pass.
* chore(dead-code): move knip config under config/
Root-level additions are blocked by the root directory guard.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* chore: condense code comments
* chore: shorten more code comments
* clarify PTY agent session descendant cleanup behavior
Refine the comment on ptyAgentSessionIds to more accurately describe
when agent sessions sweep their descendant process trees and note the
exception on immediate Windows shutdown.
* fix(orchestration): gate methods route calls to the caller's Run with `f
Gates are Run-scoped state; every gate command now resolves the caller's active Run
(via pane binding or explicit --from flag) and authorizes within that Run's scope.
Settled adopted work no longer requires --takeover-legacy, and the legacy coordinator
fence respects both binding-based and attestation-based proof of authority.
* fix(orchestration): gate methods route calls to the caller's Run with at
Gate and run methods now verify that declared terminal handles match the caller's
attested identity, preventing spoofing of other coordinators. Extracted shared
`resolveRunScope` to enforce one authorization rule across all orchestration
mutations. Added comprehensive regression tests for #11745.
The move of hasUnsafeWindowsBatchSyntax into src/shared/windows-batch-spawn.ts
silently added `(` and `)` to the cmd.exe denylist, so every .cmd shim or
argument path containing parentheses became unspawnable across nine call sites.
Parentheses only group commands and cannot chain one without a separator the
guard already rejects, so they are dropped again.
The rejected character set is now the single source for the user-facing error
strings, and `orca account add` translates the sentinel into a real message.
Co-authored-by: Orca <help@stably.ai>
* 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>
Adds `orca skills install` and `orca skills update` so skills can be set up without the GUI — SSH hosts, containers, CI. Previously `orca skills` had only `list` and `get`, so there was no headless path.
**Agent targeting is scoped explicitly rather than delegated to detection.** The `skills` CLI decides which agents to install into, and with `-y` and zero detected agents it takes `targetAgents = validAgents` — all ~75. That is not a corner case for a headless CLI: a fresh SSH box or container with no agent installed is the normal starting state. Measured on a bare host, the unscoped command created **52 top-level agent directories and 54 junctions** (one real payload in `~/.agents/skills`, the rest links) on Windows, and 52/53 on macOS.
The CLI now passes `--agent` derived from Orca's own detection, mapped to the `skills` key namespace, plus `universal`. Supplying `--agent` makes `runAdd` use it directly and never call `detectInstalledAgents()`, so the fan-out branch is unreachable. On a bare host it now refuses with `No coding agent detected on this host` and exit 1, creating nothing. Same command with scoping: **1 directory, 0 junctions.**
`universal` alone would under-install — Claude Code is not in that set, and 19 of 28 mapped keys write agent-private homes `universal` never touches. `--agent '*'` is the bug itself. The mapping is hedged three ways: `null` for any agent whose key could not be confirmed, `satisfies Record<TuiAgent, …>` so a new Orca agent is a compile error, and a test pinning every mapped key against the CLI's own valid list.
Fixed during review — two holes that each restored the full fan-out through a different door:
- `--agent ','` trimmed to nothing, which skipped the refusal *and* emitted no `--agent`.
- `--agent -y` passed an emptiness check, and the vendor CLI silently drops `-`-leading values, re-emptying its list.
The real invariant is argument *shape*, not emptiness, and it is now enforced at the choke point in `buildAgentFeatureSkillInstallArgs`, so no caller can emit `-y` without a usable target. `*` remains allowed — asking for every agent explicitly is a choice, not an accident. Verified with 51 hostile inputs through the built binary, each recorded argv replayed through the vendor's own parser.
Also fixed: the `ORCA_CLI_CWD` refusal now runs before target resolution (it was quoting the wrong host's agent list), and `--dry-run` is refused in a forwarded shell rather than printing a command naming the wrong machine.
Validated on a real Windows host across PowerShell 7, PowerShell 5.1, cmd.exe and Git Bash: `.cmd` shims route through `cmd.exe` and `.exe` shims spawn directly (proved with instrumented shims, not inferred), the ENOENT path produces an actionable error rather than a silent failure, and `skills update` genuinely restores a corrupted skill byte-for-byte.
Known, not addressed here — both upstream behaviours this only forwards: a partial install failure exits 0, and "no installed skills found" exits 0. Both are invisible to the headless callers this feature exists for.
Co-authored-by: scastanoh21 <scastanoh21@gmail.com>
src/cli/index.ts was the only eager value-import of RuntimeClient, and five
other eager modules imported just RuntimeClientError / RuntimeRpcFailureError
from the runtime-client barrel -- dragging in client -> pairing -> zod -> ws
-> e2ee on every invocation. Those error classes live in runtime/types.ts,
which has zero children, so the five imports now point there and the client
loads through the existing (already lazy by design) ctx.client getter.
Eager modules 199 -> 46, with node_modules dropping 94 -> 0.
`orca --help` 2.04x (59.6 -> 29.2 ms); the same for help, no-args, and both
error paths, which return before constructing a client. Commands that DO
construct one still gain 1.10-1.12x from not eagerly parsing the transport
the local path never uses.
Correction to an earlier note: websocket-transport alone is ~24 modules /
~8 ms, not the 107 / 28 ms once recorded -- that figure wrongly charged it
for zod, which enters through shared/pairing on a different edge. Marginal
cost, never isolated cost.
Co-authored-by: Orca <help@stably.ai>
* fix(runtime): refuse SSH hosts in project setup instead of acting locally
projectHostSetup.clone and .setupExistingFolder threaded executionHostId all
the way down but never used it for routing: cloneRepo runs a local mkdir plus
a local gitSpawn, and addRepo probes the path with existsSync/statSync. An
`ssh:` host therefore cloned and validated on the *local* machine and then
registered the result as living on the SSH host.
It only failed loudly here because the remote path did not exist locally. With
a plausible destination the clone succeeds and writes a setup record pointing
at the wrong machine.
Nothing legitimate sends `ssh:` to these RPCs: the renderer maps every ssh
host (including ephemeral-VM `ssh:runtime-ssh-*`) to the desktop IPC path,
which dispatches to addRemoteRepoFromPath/cloneRemoteRepo, and the IPC handler
symmetrically rejects `runtime:`. Only the CLI can reach here with `ssh:`.
Fail closed until the RPC learns to route through the SSH providers.
* test(runtime): make the SSH guard test observe the corruption it names
The test asserted `gitSpawn` was never called and no repo was registered, but
neither assertion could fail. `/home/brennan` is unwritable on macOS, so the
pre-guard clone died at `mkdir` before reaching `gitSpawn`, and
`/home/brennan/orca` failed `isGitRepo` before reaching `addRepo` — the exact
side effects under test were unreachable either way. `rejects.toThrow` also
aborted the test before those lines ran.
Use a real temp destination and a real temp git repo, await both calls via
`.catch`, and assert the side effects before the wording. With the guard
disabled the test now fails on `gitSpawn` being called once with a real
`git clone`, and on a repo registered stamped `executionHostId: 'ssh:openclaw'`
— the silent local-clone-recorded-as-remote defect itself. `gitSpawn` is
stubbed so a regression records the call instead of hitting the network.
Also document the SSH restriction on `project setup-existing-folder`, which the
guard now rejects. `setup-clone` already carried that note; its sibling did not.
The 30-min clamp was silent: the ask result carried no timeout figure, so
the CLI printed the value the caller *sent*. A worker passing
--timeout-ms 3600000 was told "ask timeout after 3600000ms" after only
30 min of real waiting — off by 2x, and accurate before this PR added the
clamp. Echo the effective budget on every ask return and print that.
Additive optional field; older clients fall back to the requested value.
* fix(cli): explain SIGABRT serve exits instead of naming the signal (#10461)
`orca serve` reported only "Orca serve exited via SIGABRT", which sent a P0
investigation down a code-signature path while a diagnostic crash report sat
unread on disk. On darwin + SIGABRT the signal-exit path now names the macOS
application-startup abort, its usual sandbox/SSH/CI causes, and points at
~/Library/Logs/DiagnosticReports/Orca-*.ips via the existing nextSteps channel.
Other platforms and signals get a clear message with no invented cause.
* fix(cli): stop asserting the SIGABRT exit happened at startup
* fix(cli): stop steering macOS SIGABRT users away from SSH serve
* Revert "Enable accessibility tree (`ax`) command on iOS emulator sessions (#10007)"
This reverts commit 43ae014a64.
* fix(emulator): expose iOS accessibility tree
* fix(emulator): support device-only iOS AX
* fix(emulator): normalize iOS ax to 0..1 and heal missing axUrl
serve-sim's helper /ax reports element frames in absolute pixels, but
tap/gesture take normalized 0..1 coords. Normalize the raw AX node tree
into a compact nested shape whose frames are 0..1 over the device screen
(first root's frame), mirroring serve-sim's own normalizeAxTree, so agents
can feed ax output straight back into input commands.
Also heal sessions that were registered without an axUrl: #9924 only
derived /ax at parse time, so already-active sessions had no endpoint.
The bridge now derives it from the session's mjpeg stream URL, guarded to
the /stream.mjpeg suffix so a non-mjpeg URL never fabricates a bogus /ax.
* docs(emulator): mark ax working on iOS with correct raw-AX-tree shape
Both skill guides and the CLI summary described iOS ax as unsupported (or,
via the reverted #10007, as a normalized "screen + elements" shape that
never matched the endpoint). ax works on both backends: Android via
uiautomator, iOS via the serve-sim helper. Document the real iOS output —
a raw AX node tree (labels, roles, nested children) with frames normalized
to 0..1 — and regenerate the bundled skill guides.
* chore(skills): regenerate skill bundle manifests
CI verify failed because generated skill artifacts were stale after version/skill revision bumps.
* fix(emulator): read ax from explicit device without active session
Fall back to udid-keyed session lookup when a worktree has no active emulator,
allowing `--device` targeting to work the same way for ax as it does for tap/type.
Also clarify in docs that AX frames are normalized 0..1 with top-left origin,
and show how to tap an element at its frame center (x+width/2, y+height/2).
* fix(emulator): cap iOS AX tree at 500 nodes
Unbounded accessibility trees can flood agent output. Enforce a 500-node limit (matching serve-sim's snapshot cap) and mark truncated parents so consumers know the tree was cut.
---------
Co-authored-by: 5Hyeons <ohs2251@naver.com>
* Enable accessibility tree (`ax`) command on iOS emulator sessions
Fetch the accessibility tree from serve-sim's /ax endpoint, which requires an
active session but provides the same UI snapshot capability as Android's
uiautomator output. Derive the endpoint from the stream URL when not explicitly
provided by the helper, and route through the bridge to pass session context to
the backend.
* Add ax command routing and backend integration tests
Tests verify accessibility tree routes through EmulatorBridge,
Android backend ignores iOS-specific ax URLs, and ax endpoints
are derived from serve-sim stream URLs.
* feat(linear): add MCP-style save issue
* fix(linear): harden save issue parity
* fix(linear): close save issue contract gaps
* docs(linear): bundle project discovery with save issue