* fix(security): put every trust dialog on a host-owned token surface
Plugin themes write CSS tokens on the document root, and dialogs portal to
body, so any trust surface without plugin-security-chrome inherits them; a
theme could hide what is being approved. The orca.yaml hook trust dialog,
notebook run confirmation, plugin install, remove and rollback dialogs were
missing it. DialogContent gains surface="security", which applies the
host-owned tokens, and a scrollable prop for the plugin dialogs' existing
viewport-height scrolling; every trust dialog uses the surface and a test
lists them. The style guide says when to use each.
* fix(security): keep inline trust decisions on the host-owned surface
The agent command approval card (Allow/Deny) and the browser certificate
override sit outside a dialog, so plugin theme tokens still reached them.
A SecuritySurface primitive puts its child on the same host-owned token set
as DialogContent surface="security"; both use it, and the security surface
test now lists inline trust surfaces as well as dialogs.
The root moved to pnpm 12 in #17156 but these two isolated workspaces were
left on pnpm 10, so CI and deploys carried a second pnpm and pnpm/action-setup.
- Pin the root's integrity-checked pnpm 12.14.0 and regenerate both
lockfiles (only the pnpm 12 package-manager document is added; the
dependency graph is unchanged).
- Move settings pnpm 12 ignores out of .npmrc and package.json#pnpm into
each pnpm-workspace.yaml; onlyBuiltDependencies becomes allowBuilds, and
cloud keeps esbuild's script skipped as pnpm 10 did. package-manager-strict
is gone in pnpm 12, whose own version pinning supersedes it.
- docs/site drops --ignore-workspace: under pnpm 12 it skips the workspace
file, losing the overrides and failing the frozen install. vercel.json now
pins install/build so a stale dashboard command cannot reintroduce it.
- Cloud and docs workflows use pnpm/setup@v2 like the root; the relay
unit-test step uses the root's pnpm instead of npx pnpm@10.
* Add telemetry for directional tab close requests
Measure how often people use Close Tabs To The Left and Right, without recording tab titles, paths, or workspace identifiers.
- Sample a stable 10% of installs by hashing the install ID, so membership survives restarts. The app-open event carries the sampled flag so the group can be analysed.
- Cap each direction to one event per app session, tracked in main, so repeated clicks or extra windows cannot inflate volume.
- Count the request before the close flow runs, so cancelled save prompts still count.
- Route both directions through one helper that counts eligible unpinned tabs in visible order.
* Count settings opt-in sessions in tab-close sampling denominator
Enabling telemetry from settings now fires `app_opened` for that session, not only when the opt-in came from the pending banner. Before, settings opt-ins never emitted `app_opened`, so those sessions were missing from the adoption denominator for directional tab close events.
- `trackAppOpenedOnce` now marks the session as tracked only when `track()` accepts the event. An opt-out or environment consent override no longer uses up the session's denominator.
- Tests cover sampling for settings opt-in and the `DO_NOT_TRACK` override.
* feat(cli): set the title of one terminal pane
Adds `orca terminal set-pane-title --terminal <handle> --title <text>`; `--title ""`
clears it. The runtime resolves the live handle to its saved leaf and asks the
desktop window to name that pane through a new optional notifier hook
(`ui:setPaneTitle`). Mounted panes apply it through the same local setter the GUI
uses; parked panes get it in the saved layout their next mount reads. The tab's
own name is never touched.
Rebuilt on current main from #24755.
Co-authored-by: stevelliu <stevelliu@tencent.com>
* fix(renderer): a main-owned layout takes a CLI pane title before the window shows it
* fix(renderer): a CLI pane title reaches a never-mounted tab with a rootless layout
---------
Co-authored-by: stevelliu <stevelliu@tencent.com>
* Accept host aliases and infer the host for a single-setup project
`orca worktree create --project scalp-it` failed with "Project is not set
up on the selected host" even though the project has exactly one ready
setup, and `--host trader-local` was rejected as an invalid value even
though `orca host list` prints that name. Three separate causes:
- `--project` was matched against the stored `projectId` only, which is a
provider-scoped string (`github:owner/repo`). The display name shown by
`orca project list` never matched, so the lookup found nothing and the
failure was reported as a missing host.
- With no `--host`, the host fallback existed but was unreachable, because
the project lookup above had already filtered to zero candidates.
- `parseExecutionHostId` accepts only `local`, `ssh:<id>` and
`runtime:<id>`, so a bare alias could not parse at all. `ssh:<label>`
already resolved labels; only the unprefixed spelling was missing.
Now:
- `--project` takes the project id or the display name, resolving through
`project.list` and falling back to the setup's own display name for
hosts that predate that method. Two projects sharing a name are refused
rather than guessed.
- `--host` is only required when the project is set up on more than one
host. A single setup is used directly; an ambiguous one fails with every
host and the flag to select it.
- `--host <alias>` resolves against both axes: paired servers by name
before routing, SSH targets by label once a client exists. An alias that
names a machine on both is refused with the prefixed flag for each.
Errors now echo the flag the caller typed instead of an `ssh:` spelling
they did not use, and carry the hosts to retry with in `nextSteps`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Refuse a host alias that collides with duplicated SSH labels
assertAliasNamesOneHost asked findSshTargetByName whether the alias also
named an SSH target, but that function returns undefined for a label two
targets share — deliberately, so an ambiguous label never resolves. Read
as a boolean it says "no SSH target by that name", so an alias naming one
paired server and two same-labelled SSH targets looked conflict-free and
resolved to the server.
The opposite direction was already covered: a unique SSH target against
two servers sharing a name fails through assertEnvironmentNameUnambiguous.
Fall back to ambiguousSshTargets so both axes refuse, and list every
colliding target in nextSteps rather than only the first.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Only fall back to setup display names on a server without project.list
findProjectsByName caught every error from project.list and returned an
empty list, so a network failure, a permission error, or a genuine server
fault was indistinguishable from "no project by that name" — and the
caller was then told `Project is not set up on the selected host` for a
project that exists and is set up.
Narrow the catch to method_not_found, the only code that means the weaker
fallback is warranted, matching callProjectHostSetup and listSshTargets in
the same layer. The function now returns null for that version gap and an
empty array for a real miss, so the two cases stay distinguishable at the
call site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Refuse a legacy display-name match that spans two projects
The pre-project.list fallback matches ProjectHostSetup.displayName, which
is the repo's folder name rather than the project's name, so two projects
holding repos with the same folder name both match. selectSetupOnHost
would then have taken the first candidate — targeting a project the caller
never named.
Reject when the matched setups span more than one projectId and name each
one, the same rule the project.list path already applies to duplicate
display names. A single project with several setups is unaffected; the
host selection below still decides between them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Spell the --host placeholder as <host> in the project setup usage lines
The worktree create usage was updated to <host> when aliases became
valid, but the four project setup-* lines kept <host-id> in both the
command specs and the root help text they mirror. Those commands accept
an alias too, so the narrower placeholder hid it from anyone reading help.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Document host names and the optional --host in the CLI docs
The published CLI reference described `--host` as taking only `local`,
`ssh:<target-id>` or `runtime:<environment-id>`, and the automations page
showed `--host <hostId>` as required alongside `--project`. Both now
understate what the commands accept, which is how the original report
started: `orca host list` prints a name, the docs never said you could
type it, so the generated id looked mandatory.
Spell out that `--host` takes the bare name from `host list` on either
axis, that a name resolves only when unique and is otherwise refused with
the prefixed flag for each candidate, and that `--host` is unnecessary
when the project has a single setup. Also note `--project` accepts the
display name from `orca project list`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Name the runtime host id as the paired-server tie-breaker in the docs
The host-selection page told readers to fall back to ids when a name is
ambiguous but only ever showed `--environment <server-name>` for paired
servers — and that selector is exactly the one that cannot separate two
servers sharing a name. Spell out the pair the resolver actually emits in
its nextSteps: `--host ssh:<target-id>` and `--host runtime:<environment-id>`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Spell the automations create usage as --project <id-or-name> [--host <host>]
`automations create` resolves --project and --host through the same
alias-capable resolver as `worktree create`, and its own notes already say
--project takes a display name and --host takes a bare host name. The usage
line still read `--project <id> [--host <id>]`, the last spot left on the old
spelling (flagged by CodeRabbit after the main merge).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Give the fake runtime client cast in the host alias tests its SAFETY note
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): accept every name host list and project list print, everywhere --project/--host is taken
Extends the alias work to the commands it did not reach and the names it
missed:
- `project setups --project <display name>` filtered by projectId only.
- `project setup-existing-folder|setup-clone|setup-create --project <name>`
now send the project id that name resolves to (pass-through for ids and
for servers without project.list).
- `--host "this machine"` (the local row's name) resolves to local, and is
refused if an SSH target or paired server shares it.
- One pair of wrapping quotes is ignored, since cmd.exe passes single
quotes through literally.
- With --environment/--pairing-code, a bare --host name is a machine the
routed server reaches, not a different locally paired server.
- An unknown project is reported as a project miss, not a host miss.
Co-authored-by: Younghwan Chae <chyohw97@gmail.com>
* fix(cli): keep exact project ids over names, and refuse an unverifiable host name
An exact --project id whose only setup is not ready no longer falls through
to a project whose display name equals that id. A bare --host name that
matches a paired server now routes there only once this machine's SSH
targets were actually read, and under --environment/--pairing-code a bare
name always means a machine the routed server reaches.
Co-authored-by: Younghwan Chae <chyohw97@gmail.com>
---------
Co-authored-by: younghwan <chyohw97@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(mobile): wall hosts that cannot serve the page instead of opening native screens
* refactor(mobile): drop the native-route state and the shell fallback prop; unknown routes say unavailable
* test(mobile): read the nullable page route refusal in its policy test
* refactor(mobile): every build is the OTA shell; drop the native/OTA build switch
The owner retired the native-screen fallback with no emergency native build, so
EXPO_PUBLIC_MOBILE_SHELL, mobileShellBuildKind, shellSwitchDecision, the Metro
cache-key suffix and both release workflows' shell input go. Every host route
mounts the shell; a route naming no screen the shell could open gets the
existing refusal screen instead of a native screen.
Claude-Session: https://claude.ai/code/session_6cea92f6
* refactor(mobile): delete native screens the OTA shell no longer reaches
With every build the OTA shell, these native siblings run nowhere: the
native-executed closure (index.js plus every non-web route) does not reach
them, and the page resolves their .web siblings. Each .web sibling takes the
plain name, its web-overrides entry goes, and the native-only rich Markdown
editor and Mermaid WebView generators and their outputs go with them.
Kept: native siblings the node test suite still renders shared screens
through (terminal WebView, browser frame path, clipboard, dictation, media
picker, live input, snapshot budget, input styles).
Claude-Session: https://claude.ai/code/session_6cea92f6
* test(mobile): drop the phone-preview comparison the deleted native sibling held
Claude-Session: https://claude.ai/code/session_6cea92f6
* refactor(mobile): drop the dead web route, flag-era comments and the one-value shell kind
The host route files no longer switch, so their comments say what they do now:
a route naming no screen shows the unavailable screen. app/h/[hostId]/web.tsx
duplicated index.tsx and nothing linked to it, so it and its stack entry go
(the bundle sweep is re-measured: 77 scripts at 15 routes). Diagnostics print
the bridge protocol on its own instead of a shell kind that has one value.
Claude-Session: https://claude.ai/code/session_6cea92f6
* refactor(mobile): one host for the rich editor, plain Text, and drop two unused native deps
MobileSelectableText was react-native's Text, so callers use Text. The rich
Markdown editor's host-facing seams (messages, URL prompt, keyboard inset) are
required now that the page is its only host; the WebView defaults go. Comments
that described deleted native components are rewritten, and
expo-image-manipulator and react-native-uitextview leave package.json and the
lockfile.
Claude-Session: https://claude.ai/code/session_6cea92f6
* feat(mobile): draw the host Accounts screen from the desktop's page
Accounts was the last host screen the shell still drew natively, so its
changes needed a phone build. It is now a page route like Tasks: the
screen moves to src/accounts/MobileAccountsScreen.tsx, accounts.web.tsx
renders it on the page, and accounts.tsx mounts the shell for it.
What React Native Web needed: Alert.alert is a no-op there, so the
reset confirmation, its outcome and a failed account switch are
ConfirmModal drawers (ConfirmModal can now drop Cancel for a notice);
Back goes through the route handoff, which pops the shell's stack; and
the reset idempotency key is built from getRandomBytes, since
randomUUID needs a secure context the iOS custom scheme is not.
edit.tsx edits the phone's own pairing and never renders on the page,
so the route manifest leaves it out and the page's host stack does not
declare it. The script sweep is re-measured for the 14-route tree.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(accounts): answer a same-offer Codex reset retry with its recorded outcome; mobile mirrors the desktop
The desktop keeps no client journal for a reset credit: the host's ledger
owns the attempt. A remote client that lost a reply and retried the same
offer under a new key was refused with "already attempted" (or "unknown
outcome" while the provider call was unresolved), which is why the phone
carried its own durable journal to replay the old key.
The coordinator now answers a request for an offer an earlier key already
claimed as that attempt: the settled outcome, the running attempt, or a
resume of the pending one under its original key. A different offer for
the account is still refused while an outcome is unknown, so nothing can
spend twice. The reply shape is the one a same-key replay always had, so
installed clients with their journal read it unchanged.
With that, the Accounts page sends a fresh key per attempt like the
desktop and keeps nothing on the phone: the AsyncStorage journal, its
client singleflight and the retry-record warnings are deleted, and the
journal-resume recording goes with them.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(accounts): resume an account's unresolved Codex reset when the retry names a newer offer
Review follow-ups:
- Host: after an unknown provider outcome the usage refresh moves the
offer, so a retry names a newer one. The coordinator now answers it as
the account's unresolved attempt (resumed under its own key and scope),
as the desktop's consumeCurrent does, instead of refusing forever. One
ledger lookup finds own key, offer claimant, then unresolved attempt.
- Mobile: a reply is matched on target, account and account revision,
not the offer, so the resumed attempt's scope is accepted.
- The key is minted inside requestCodexResetCredit from randomV4Uuid, the
session helper moved to src/platform/random-v4-uuid.ts.
- RefreshControl goes; the header refresh stays. ConfirmModal's
onConfirm is optional for an OK-only notice.
- Stale journal comments and the mobile docs line are corrected.
- The edit.tsx page exclusion is dropped from this PR; the script sweep
is re-measured for 15 routes.
- Root oxlint: escape the apostrophe; zod v4 strictObject.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): secure UUID fallback from expo-crypto; pin reset retry routing
- randomV4Uuid never falls back to Math.random: without crypto.randomUUID
it builds the v4 UUID from ExpoCrypto.getRandomBytes, as the old reset
key did. Tests get Node's secure bytes for expo-crypto from the setup
file, since the native module has no Node build.
- Host tests: a new offer after a settled attempt spends again rather
than replaying, and a retry naming a newer offer joins a resumed
attempt in flight (one provider call, original key).
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* chore: restore main's bytes for files the merge's pre-commit format touched
The merge commit's lint-staged run reformatted 27 files this PR does not
change. Committed without the hook so they match origin/main exactly.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): stand in for expo-crypto with a Node alias, not a setup-file mock
Anti-slop forbids module mocking outside test files. The vitest config
now aliases expo-crypto to a faithful Node implementation of the names
shared code reaches; a test file's own mock still wins.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* docs(mobile): match the bridged-parity comments to the measured 795-golden tally
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(orcad): keep the integration credential key only in the owner-only key file
Managed servers no longer create, read or wait on the macOS login Keychain or
a Secret Service keyring for their credential master key. A record an hourly
build pointed at the keyring is migrated once: the key is copied into the key
file when it reads back without UI within 5s, otherwise a new key file is
started and old credentials show the usual reconnect error. Removes the
locked-Keychain status fields, retry, the integrationCredentials.moveKeyToFile
RPC and the Settings notice.
* fix(orcad): serialize the keyring-record migration so concurrent starts agree on one key
* docs(orcad): note the migration lock
* fix(orcad): publish the migration lock with its PID and bound every wait
* refactor(orcad): reuse the file transaction lock for the keyring-record migration
* Run automated workspace setup in a dedicated tab
* Preserve native-chat setup route coverage after placement retirement
* Preserve Setup tab design and independent review evidence
* Classify activation test fixtures consistently for routing checks
* Sync runtime English catalog after removing setup preferences
* Update listener savings model after retiring setup action
* fix(orcad): tell the user when a locked macOS Keychain hides Linear and Jira, and recover without a restart
A managed macOS host reached only over SSH keeps its login Keychain locked
after a reboot, so the server's credential key failed closed and the app
asked the user to reconnect a token that was fine. Exit 36 from
security find-generic-password (errSecInteractionNotAllowed) is now a
'locked' lookup: the server logs it once, retries the Keychain on each
credential use and before each Linear/Jira status read, and status carries
an optional credentialErrorReason: 'keychain_locked' that the app shows in
place of the generic text. Once unlocked, Settings offers a one-click move
of the same key into the owner-only key file.
* fix(orcad): keep retrying a locked Keychain after a timeout, fence the key move to the host that asked, report the lock only for credentials that need the key
* fix(settings): offer the file-storage switch to a paired browser's implicit server too
* fix(settings): route the Keychain notice and key move by the host that reported the status; sweep stale key-record staging; say the moved key is not machine-bound
* fix(orcad): sweep only this machine's key-record staging, so a shared home's other hosts keep theirs
* perf(build): remove unnecessary development and config work
* feat(dev): reclaim inactive worktree Vite caches
---------
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
ORCA_TERMINAL_MEMORY_MAX and ORCA_TERMINAL_MEMORY_SWAP_MAX set MemoryMax and
MemorySwapMax on each terminal's systemd user scope. Off by default, applied only
after the probe scope shows the kernel enforcing them, and dropped (logged) where
the host cannot, so terminal scopes never turn off because of them.
Refs STA-3430
* fix(orcad): seal integration credentials on the headless server instead of storing them in plaintext
The Node SecretStore always reported encryption unavailable, so every
credential writer on orcad fell back to plaintext. orcad now seals with
AES-256-GCM using a host-owned master key kept in the OS keyring (macOS
Keychain, Linux Secret Service) when reachable at first start, else in a
0600 profile file bound to the machine ID. A key that cannot load fails
closed instead of downgrading. Existing plaintext credentials are sealed
on startup without loss.
* docs: describe how the headless server seals credentials and its limits
* fix(orcad): unique credential staging files, no keyring items for temp profiles, isolate startup test
* fix(orcad): keep the credential key with the credentials, one record per machine
* fix(orcad): never re-key over an unreadable record, serialize first-start key creation, refuse an unrestricted key file
* fix(orcad): verify a key record's Windows ACL and wait out a concurrent first start's keyring write
* fix(orcad): keep a keyring claim and fail closed when its store result is uncertain
* fix(credentials): restrict the staged file's Windows ACL before it replaces a credential
* fix(orcad): a temporary profile never creates a key in, or re-seals, the real credential home
* ci: baseline the two single-file credential ACL restrictions
* fix(credentials): stage Windows credentials born restricted and verify the key record asynchronously; document rollback
* test(bitbucket): inject the write failure into the Windows staging writer too
* fix(ssh): flag a macOS host whose managed server outlived its launching SSH session
macOS Local Network privacy pins a nohup-launched orcad to the sshd-session
that started it; once that session ends, its terminals lose LAN access (#25934).
A macOS launch now records that session, a connect reports it when it proved
exited, and the host status offers a restart when no terminals are running.
* fix(ssh): retire the idle terminal daemon on a launch-session restart
* fix(ssh): keep the fence on an unsettled restart stop, and report a restart that can't hold its session
* fix(ssh): carry the launch session forward while the terminal daemon survives; never redial for the check
* chore(i18n): add the launch-session warning and restart strings to the English catalog
* Recover retired macOS daemon trees during terminal restart
* fix(daemon): never let retired-job recovery failure block restart
A wedged retired job (unkillable descendant, launchctl timeout, recycled
root) previously aborted every untargeted Restart Terminal Daemon, leaving
no way to replace the current daemon short of a reboot. Log the failure
and continue with the ordinary restart instead.
* fix(browser): agent typing into a cross-site frame no longer borrows the user's focus
Keys and text for a focused cross-site iframe went through the focus-borrowing
fallback, because widget input reaches only the guest's own renderer. They now
go to that frame's own CDP target session, found by following the page's focus
through frame elements, which reaches the frame without touching the user's
focus. The proxy keeps those Orca-owned sessions' events from the agent's client.
* fix(browser): retry frame focus emulation after a failed enable
* fix(browser): retry a failed frame focus-emulation enable before sending input
* Run safe worktree cleanup on dev and add interactive pnpm reclaim
* Rename storage cleanup command to reclaim-space
* Keep real stream constructors in transcript dependency tests
* Detach cleanup output and align historical cache previews
Support scoped Jira Cloud API tokens through the configured gateway while keeping self-hosted routing, redirects, diagnostics, and selected-host capabilities bounded.
Co-authored-by: Jerome Würf <oss@scharom.de>
Accept positive safe-integer line and column flags through the existing file-open path. Reject invalid or incomplete positions before changing the editor. Bind deferred reveals to the requested workspace and tab, including floating panels, and clamp oversized positions without moving another editor or stealing late focus.
Co-authored-by: Nikita Maketryuk <n.maketryuk@lince.studio>
* feat(windows): add administrator PowerShell terminal setting
* fix(settings): localize administrator terminal search keywords
* fix(windows): restrict elevated terminal host to Orca's runtime and requesting account
* fix(windows): harden administrator terminal routing, inventory and restore
* fix(build): build the Windows agent hook when electron-builder packages Windows
* fix(build): build the packaged Windows agent hook for the target architecture
* fix(windows): accept the development terminal runtime and test real elevation in CI
* feat(windows): show administrator status and restart instructions
* fix(settings): localize administrator mode search keywords
* test(settings): isolate the desktop permission section in shell picker tests
* Add Windows administrator restart and simplify Terminal settings
* Keep administrator restart silent and settings actions within narrow panes
* Keep Windows shell choices and permission guides usable at minimum width
* Prevent later restarts from replaying administrator terminal reset
* Fix CI route resolution and transient Windows fixture cleanup
On a validated AppImage the desktop's daemon launch first runs <generation>/resources/node-runtime/bin/node
on the generation's daemon-entry.js when the extracted copy for the file at $APPIMAGE is complete, carries
the bundled Node and the entry, and is the running release. It then falls back to the AppImage's own mount
and the current mount, logging every fallback. The copy rung records $APPIMAGE/<entry> like the own-mount
rung, so either daemon is adopted. Generation leases recognize the bundled Node. macOS, Windows, deb and
rpm never reach it.
Co-authored-by: m4air <m4air@Mac.localdomain>
* fix(terminal): preserve preparation and discovery for delayed agent launches
* refactor(agents): centralize launch intent recognition
* refactor(agents): keep launch intent in one module apart from process recognition
Move recognizeAgentLaunchCommand next to the sleep-prefix scanner so the strict
process-table matcher is untouched, name the module away from the agent-launch
funnel files, share one executable-name normalizer, and colocate the
preparation tests with their modules.
* fix(linux): keep extracted AppImage copies that something still runs from
New builds extract into a namespace that pre-lease builds never prune.
Pruners now delete a generation only when no same-user process runs from
it and no lease names a live process (pid, start ticks, boot id).
Co-Authored-By: Claude <noreply@anthropic.com>
* fix(linux): skip non-dumpable processes in the AppImage residency scan
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>
* Share immutable historical source across local cross-version test caches
* Add preview-first cleanup of old historical test caches
* Include historical test cache cleanup in worktree space maintenance
* Report and retain read-only Electron installs during cleanup
* Protect read-only installs under root and isolate cleanup CLI fixtures
* fix(native-chat): put chat text on one type scale that follows Text size
Sizes were picked per component, so the same kind of row came out 12, 13 or
14px and the Text size setting reached only a few of them.
* fix(native-chat): scale chat spacing and icons with Text size
Icons and gaps stayed fixed while text grew, so at larger sizes they looked
undersized and sat off-centre.
* fix(native-chat): implement PR #26685 review fixes
- Scale row gap with text size so controls don't overlap
- Clamp 'Match terminal interface' to 12-20 for labels/controls
- Use existing --status-warning token for warning color
- Rewrite stale test for scaled typography tokens
- Use app colors for in-progress tasks in composer dock
* fix(native-chat): leave the task list's dock colour as it was
* fix(native-chat): message text follows the terminal size; controls and spacing stay in 12-20
* test(native-chat): labels and spacing follow the bounded chat size; main's new notice test uses the chat colour
---------
Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
* fix(skills): judge official skill updates by ownership, not snapshot matching
Orca decided whether an installed official skill was pristine by matching its
bytes against a shipped list of past snapshots, falling back to the skills CLI's
lock hash. Copies installed between releases, CRLF copies, and installs whose lock
recorded a sha256 hash all read "doesn't match the official version" and could not
be updated (#26841), and older content could be labelled newer and never offered.
Now an official skill is Orca's when the skills.sh global lock says it came from
stablyai/orca, and it is current only when its listed files equal the bundled
manifest (line endings and sidecars ignored); anything else is outdated. Install
and update are one pinned `skills add .../tree/v<appVersion>` command, used by the
in-app runner, `orca skills update`, and the Settings rails. The update dialog
preselects Orca-owned rows and lists same-named skills from elsewhere unchecked.
Removes the snapshot registry, release mapping, git-tree identity, lock trust and
convergence logic, and the release-cut provenance step.
* ci(skills): run the pinned-add round trip on a skills CLI that can fetch a commit
* fix(skills): let orca skills update target the shared folder on a bare host
Update became the same pinned add as install, so it inherited install's
refusal when no agent is detected. That broke the bare-host update the old
command guaranteed, and the built-CLI check (update --dry-run, no --agent) on
CI runners. The in-app runner already fell back to universal; both now share
one detectSkillsCliUpdateAgentKeys rule.
Also brings the CLI skills docs off `npx skills update`.
* refactor(skills): derive the update selection from the grouped rows
The selection hook took the live and the retained inventory, re-derived Orca
ownership from installations beside the grouping's own orcaOwned, and kept a
separate display count. Groups already carry both eligibility and ownership,
so the hook reads them once; the button's existing live-scan gate is the only
authorization check. The unselected line is inlined into the headline.
* refactor(skills): drop helpers the ownership design made redundant
- isOwnerManagedSkillScope only negated into the participation check once
plugin caches stopped participating; fold it in.
- Eligibility re-checked resolvedPath/physicalIdentity, which observation
already turns into 'inaccessible'.
- The manifest verifier probed access() before a readFile that throws anyway.
- orchestration-install-command.ts only forwarded one constant from the lib
module its three importers already import.
* i18n(skills): stop naming npx skills update in four skipped-copy strings
The es/ja/ko/zh cantUpdateReason and tipDuplicate strings still named
`npx skills update`, which Orca no longer runs for its skills. Say 'the update
command' instead.
* refactor(skills): inline the single-use pinned source URL helper
orcaSkillsSourceUrl was exported but only called by the builder beside it.
* fix(skills): let an update choice lapse when the install source changes
The selection froze every checked name, Orca's preselection included, the
moment the user ticked anything. A re-check that then attributed a name to
another source kept it checked, replacing it without consent. Store only the
user's explicit choices, keyed by name and lock source, and default to
ownership otherwise.
* fix(skills): label the empty update action 'Update skills', not 'Update 0 skills'
The button stays whenever a row is updatable, so unticking every row (or a
dialog with only non-Orca copies outdated) doesn't reflow the footer. At zero
selected it stays disabled and reads 'Update skills' instead of a zero count.
The label lives beside the dialog's other count copy.
* fix(skills): let keyboard focus open the Not from Orca tooltip
The source of a non-Orca copy is only in the tooltip, and its trigger was a
plain span. Make it focusable like Orca's other tooltip-on-text triggers
(tabIndex 0, cursor-help, the row primitive's focus ring), as in the agreed
mock.
* test(skills): hold the round-trip add equal to the shared install builder
The CI round trip hardcoded its skills add argv, so it could drift from what
Orca runs. A .mjs script can't import the TS builder (extensionless imports),
so the argv moves into an exported roundTripAddArgs and a unit test asserts it
is the builder's unattended add with the tag swapped for the PR commit. The
script runs under the same main guard as the manifest generator.
* i18n(skills): translate the skill row's location count and duplicate reason
es, ko and zh still carried English for SkillUpdateRow.oneLocation and
manyLocations, and es and ko for SkillFreshnessRow.skippedReasonDuplicate.
* fix(skills): pin official skill installs to the build commit and skills CLI
`skills add .../tree/v<version>` only works for stable and RC builds: hourly,
daily and adhoc builds publish elsewhere, so stablyai/orca has no tag for their
version and both install and update failed there. Every build is made from a
commit that exists in stablyai/orca, so the build now stamps `git rev-parse HEAD`
and the one shared builder pins to `tree/<commit>`, which equals the bundled
skills by construction. Main, renderer, web and vitest get it as
ORCA_BUILD_COMMIT (replacing ORCA_APP_VERSION); the tsc-built CLI reads it from
the compiled out/package.json boundary. A missing or malformed commit fails the
build or the command with a clear error instead of building a bad URL.
The builder also pins the skills CLI (`npx skills@1.7.2`): 1.5.x cannot fetch a
commit, and a bare `npx skills` can reuse a stale cache or resolve to another
binary (#18687). The CI round-trip script reuses the same constant.
* ci(skills): run the round trip on the shared pinned skills CLI version
* docs(skills): document the commit-pinned skills add
* refactor(skills): keep the pinned skills CLI version in the install builder
skills-cli-version.ts existed only so the plain-Node round-trip script could
strip-load a .ts file, which needed --disable-warning in CI. The version now
lives beside the builder that uses it; the script keeps a literal, and the
existing round-trip test already asserts its argv, skills@<version> included,
equals the builder's.
* refactor(skills): validate the build commit once, where it is resolved
resolveBuildSourceCommit already refuses anything but a full hash, and both the
define and the CLI's stamped package.json come from it. The builder's second
check could only fire as a module-load throw in the renderer's command
constants. Also drops the resolver's unused cwd parameter.
* test: stamp a fixed build commit instead of running git in the vitest config
Tests only interpolate ORCA_BUILD_COMMIT, so they need not read HEAD (or fail
outside a git checkout) on every run.
* refactor(settings): match only skills add in the Windows npx preflight
Every official skill command is now the builder's skills@<version> add; no
rail emits skills update any more.
* fix(skills): fail an update that leaves a broken skill link behind
Eligibility already skips names with a broken link before the run, so one
afterwards is the run's own damage; report it instead of claiming success.
* ci(release): refuse to tag when the skill manifest is stale
The removed --release step implicitly checked the committed manifest against
skills/. Keep that guarantee with a verify-only run; it writes nothing.
---------
Co-authored-by: Orca Worker <orca-worker@localhost>
* fix(macos): preserve terminal privacy identity across app updates
* test(macos): make launchd fixtures portable across host platforms
* fix(macos): probe protected contents and close handles on denial
* fix(i18n): register terminal permission status labels
* fix(macos): fork the daemon when no stable-bundle job can claim the endpoint
A copy, signature, plist or deadline failure before bootstrap, a bootstrap
launchd refused with the job absent, a job whose daemon exited before
answering, and a foreign endpoint all threw out of the launcher. The app then
lost its persistent daemon and fell back to local PTYs, where main forks one.
These now fall through to the existing fork path, which also adopts a foreign
occupant normally instead of as a degraded endpoint. A timed-out bootstrap or
a job still running/unreadable stays fatal, so no second daemon races it.
* feat(telemetry): split daemon origin events by stable-copy launch method
daemon_adopted, daemon_pty_cwd_denied and daemon_pty_cwd_readable gain a
closed-enum launch_method (stable-copy / app-fork / unknown), read from the
spawner path the daemon recorded, so the update fix is measurable per daemon
even when an old forked daemon is adopted across updates. The reliability
gate is a test manifest with no runtime switch, so there is no gate state to
report; spawner_path_class reports the private copy as 'other' alongside
other non-/Applications installs.
* refactor(telemetry): report the stable copy as a spawner path class
The separate launch_method property duplicated what the recorded spawner
path already says. spawner_path_class gains `stable-copy`, classified from
the same path under the private runtime root, and launch_method, its enum
and classifier are removed from all three .strict() daemon origin events.
A deleted copy still reports `missing`.
* refactor(settings): gate terminal-host Full Disk Access on macOS once
The permissions handler already asks for the terminal-host status only on
macOS, so the status function's own platform check and the pane's
'unsupported' branch could never fire. The off-macOS guarantee is now
tested at the handler.
* fix(macos): keep the fork fallback inside the startup PTY gate
The stable launch could run until 55 s after the launch started, so a late
"unavailable" verdict left the fork fallback's own 10 s readiness wait to
finish after the 60 s gate had already moved the app to local PTYs.
The launcher now gives the stable attempt one handoff deadline: the gate
minus the fork's readiness timeout and the 5 s lease/adapter allowance
(45 s). Copy, signature check and plist conversion share one abort signal
that ends early enough for bootstrap and the job's own readiness wait to
meet that deadline; every command otherwise keeps a fixed timeout. An
unavailable verdict past the deadline is reported instead of forking a
daemon that cannot arrive in time. The per-step budget module is gone (its
deletion landed in the previous commit by a staging slip).
The launch body is now two blocks: preparation (plist, plutil, deadline
check) fails as unavailable after removing the copy and job file, and
bootstrap runs in its own try/finally that always deletes the job file.
Retirement is simpler and claims more orphans: one in-flight collection per
process instead of a per-root map and rotating 20-record scan, and the job
record (written before copying) no longer carries a bundle name because
lsof +D scans the whole runtime directory (measured equal cost, same
result on a live copy). A never-submitted copy is retired once its producer
has exited and its job is missing, so a bootstrap timeout or a crash
mid-copy no longer leaves a copy forever; the open-file check still guards
deletion. Collection now starts from the launcher after the attempt.
* docs(macos): ship the daemon launch rules, not the investigation
The signed investigation harness left the tree in the previous telemetry
commit by a staging slip; its results and pinned link stay in the PR. This
trims the reference doc to why the copy and launchd exist, the fallback
states and the cleanup rule, trims the reliability gate to what its
validator needs, and points AGENTS.md at the reference doc next to the
Windows relocation entry.
* fix(macos): hand off to the fork as soon as the stable job exits
A copied daemon refused at exec (for example by Gatekeeper) never answers,
so every fresh start paid the full 10 s readiness wait before the fork
fallback. The readiness loop now reads the job state every 500 ms and
abandons as soon as the job is stopped or missing; a running or
unreadable job is still waited out and never forked beside.
Also drops the dead forkEntryPath the stable launch never read (its
options type now omits it) and un-exports the handoff constant.
* fix(macos): keep daemon copies out of Spotlight and unregister retired ones
Each runtime copy is a full Orca.app, so Spotlight indexed it as an app
and LaunchServices kept a record for it that outlived the copy. A
registered copy claims orca: links and Markdown/CSV files, and becomes
their default handler whenever /Applications/Orca.app is not registered.
New copies go under runtime-*/app.noindex/, which Spotlight skips and which
stops the record LaunchServices made while the bundle was being copied.
Retirement now runs lsregister -u on every bundle in the copy (both the
app.noindex layout and the earlier runtime-*/Orca.app layout) once lsof
shows it unused, then deletes it. A running copy stays registered,
because Local Network access may be resolved through that record.
Collection and stable-copy telemetry match the runtime-* directory, so
copies made before this change are still adopted, classified and retired.
* docs(macos): record the LaunchServices trade-off and the rollback plan
The reference doc now explains why copies sit in a .noindex folder, why
only retired copies are unregistered, and how to turn the copy launch off:
make launchMacDaemonFromStableBundle return null, and keep the retirement
code until no copy-launched daemon can remain, because the old code never
deletes copies.
A test pins that rollback: with the stable launcher returning null, the
launcher forks and still runs the copy collection.
* fix(macos): keep daemon copies registered so Local Network can resolve them
The `.noindex` folder kept copies out of Spotlight and LaunchServices, but
Local Network access is resolved from the signing identifier through
LaunchServices to the executable's Mach-O UUID. After an update that changes
Electron only the copy carries the running daemon's UUID, so an unregistered
copy risks the same Local Network loss main has today. Copies go back to
`runtime-*/Orca.app`; retirement still unregisters each copy before deleting
it, so deleted copies leave no LaunchServices entry. The remaining trade-off:
while `/Applications/Orca.app` is missing, a running registered copy can be
picked for `orca:` links and documents.
* fix(macos): hide daemon copies from Spotlight and tie the job to Orca
Spotlight listed each runtime copy as a second Orca app. New copies go
back under runtime-*/app.noindex/Orca.app, which Spotlight skips.
Retirement and stable-copy telemetry match the runtime-* directory, so
copies in the earlier runtime-*/Orca.app layout still retire and classify
the same way.
Local Network access identifies the daemon by its code signature and
main-executable UUID, not by a LaunchServices record, so an unregistered
copy keeps it. The launchd job now sets AssociatedBundleIdentifiers to
com.stablyai.orca, which is how a job not installed through SMAppService
names the app it belongs to. Copies are never registered explicitly;
retirement still runs lsregister -u before deleting each one.
* feat(macos): package a signed terminal host helper that runs the daemon on the pinned Node
Contents/Helpers/Orca Terminal Host.app carries the pinned Node, the daemon's
out/main JS and this slice's node-pty, signed inside-out as com.stablyai.orca.
afterPack boots it through one PTY round trip, then fails the build on layout,
Info.plist, macOS floor (NODE_RUNTIME_DARWIN_MINIMUM_OS vs Node's minos),
architecture or signature drift.
* ci(macos): cache and prefetch the pinned Node before packaging
Packaging now fetches the darwin Node from nodejs.org, whose download has no
retry. Each mac workflow caches it with the electron-builder downloads, keyed on
the pin, and warms both darwin targets inside the existing retry wrapper.
* feat(macos): launch the daemon from a copy of the terminal host helper, not the whole app
The stable launch now clones Contents/Helpers/Orca Terminal Host.app (~124 MiB
instead of ~628 MiB) and runs its Node on its daemon entry, without
ELECTRON_RUN_AS_NODE. The copy must still match the running app's designated
requirement. A missing helper falls back to the fork, and macOS below the
helper's Node floor forks without copying.
* docs(macos): describe the terminal host helper, its floor and its identity
* fix(macos): bound the terminal host boot check by its deadline and always clean up
A daemon that stayed silent hung packaging past the 20 s deadline; one that closed the
connection left the hello unsettled so Node exited 0 without cleanup; a malformed line threw
uncaught. Every connect, hello and PTY wait now races the deadline, close/error/parse failures
reject, and the daemon and scratch dir are removed on every path.
* ci(macos): cache the pinned Node on its own key with no restore-keys fallback
Sharing the electron-builder cache let old Node archives accumulate through restore-keys.
Also moves release-mac-build's Electron cache comment back onto its step.
* fix(macos): reject a universal slice for the terminal host and pin its name in one test
packageMacTerminalHost turned an unknown arch into darwin-undefined; it now throws through the
shared mac arch mapping. A test asserts the packager and the desktop agree on the helper bundle
and executable names, replacing a Linux/Windows test that could not fail.
* fix(macos): pass the launchd adoption lease its options object after the main merge
* test(macos): accept a reset as the daemon closing the boot-check connection; keep only passing gate commands
* fix(daemon): bump the terminal daemon protocol to v45 so new macOS tabs start in the terminal host helper
* fix(macos): ship the daemon's required packages in the terminal host and boot it outside the build tree
* style(macos): use a template literal in the terminal host resolution check
* fix(macos): derive the terminal host's packages from the daemon's own module graph
* fix(macos): treat any non-literal daemon load as computed, follow directory loads, and keep nested dependencies
* refactor(macos): move terminal-host telemetry, Settings status and Helper folder strings to a follow-up
Keeps this change to the update-safe terminal host itself. The follow-up restores:
- the `stable-copy` spawner path class in daemon adoption telemetry;
- the per-terminal-host Full Disk Access row in Settings and its daemon RPC;
- writing folder usage descriptions into the app's Electron Helpers for the fork fallback.
The helper still takes its Desktop, Documents and Downloads descriptions from
`macos-folder-usage-descriptions.cjs`, so that constant stays.
* feat(macos): report terminal-host Full Disk Access, classify stable-copy daemons, and give Helpers folder strings
Restores what a32018836b moved out of the terminal host change:
- daemon adoption telemetry classifies a daemon spawned from the private copy as `stable-copy`;
- Settings shows the terminal hosts' own Full Disk Access verdict beside the app's, through a
new `fullDiskAccessStatus` daemon request that older daemons answer as unknown;
- packaging writes the Desktop, Documents and Downloads descriptions into the app's Helper
bundles, which still host terminals when the fork fallback runs.
* fix(i18n): restore en.json to this branch's base, not newer main
The previous commit took the file from a newer main, which pulled unrelated strings into this PR.
* refactor(macos): publish one terminal-host copy per helper build under a fixed launchd label
Name the copy after the helper's CDHash, reuse it when present, and
publish a new one by cloning into a staging dir, verifying it once
against the app's designated requirement and renaming it into place.
One launchd label per profile and protocol replaces per-launch labels,
so job.json, producer pids and per-label collection go away. Retirement
deletes any other copy lsof shows unused. The stable launcher returns
null for every fork fallback and logs its reason.
* fix(packaging): copy terminal-host dependencies from where Node resolves them
A nested package's own dependencies were never walked, so a hoisted
dependency of a nested package was left out of the helper. Resolve each
dependency through the node_modules lookup Node performs from its parent
and copy it to the same relative place, unless a copied package already
carries it.
* fix(packaging): fail release and CI mac builds that cannot boot a terminal-host slice
An arm64 runner without Rosetta skipped the x64 helper's PTY boot check
and shipped the slice untested. Release channels and CI now fail there;
local cross-arch builds still skip with a log line.
* docs(macos): describe the per-build terminal-host copy and its fallback reasons
* docs(macos): keep the codesign command on one line
* test(macos): type the launchd test state without an assertion
* docs(macos): name what the login-session flag gates in the stable launcher
* fix(macos): wait for launchd to drop a booted-out terminal job before reusing its label
* refactor(daemon): keep the health request types in their own module
types.ts hit the line limit once the Full Disk Access status request joined the union.
---------
Co-authored-by: Neil <neil@stably.ai>
* fix(macos): preserve terminal privacy identity across app updates
* test(macos): make launchd fixtures portable across host platforms
* fix(macos): probe protected contents and close handles on denial
* fix(i18n): register terminal permission status labels
* fix(macos): fork the daemon when no stable-bundle job can claim the endpoint
A copy, signature, plist or deadline failure before bootstrap, a bootstrap
launchd refused with the job absent, a job whose daemon exited before
answering, and a foreign endpoint all threw out of the launcher. The app then
lost its persistent daemon and fell back to local PTYs, where main forks one.
These now fall through to the existing fork path, which also adopts a foreign
occupant normally instead of as a degraded endpoint. A timed-out bootstrap or
a job still running/unreadable stays fatal, so no second daemon races it.
* feat(telemetry): split daemon origin events by stable-copy launch method
daemon_adopted, daemon_pty_cwd_denied and daemon_pty_cwd_readable gain a
closed-enum launch_method (stable-copy / app-fork / unknown), read from the
spawner path the daemon recorded, so the update fix is measurable per daemon
even when an old forked daemon is adopted across updates. The reliability
gate is a test manifest with no runtime switch, so there is no gate state to
report; spawner_path_class reports the private copy as 'other' alongside
other non-/Applications installs.
* refactor(telemetry): report the stable copy as a spawner path class
The separate launch_method property duplicated what the recorded spawner
path already says. spawner_path_class gains `stable-copy`, classified from
the same path under the private runtime root, and launch_method, its enum
and classifier are removed from all three .strict() daemon origin events.
A deleted copy still reports `missing`.
* refactor(settings): gate terminal-host Full Disk Access on macOS once
The permissions handler already asks for the terminal-host status only on
macOS, so the status function's own platform check and the pane's
'unsupported' branch could never fire. The off-macOS guarantee is now
tested at the handler.
* fix(macos): keep the fork fallback inside the startup PTY gate
The stable launch could run until 55 s after the launch started, so a late
"unavailable" verdict left the fork fallback's own 10 s readiness wait to
finish after the 60 s gate had already moved the app to local PTYs.
The launcher now gives the stable attempt one handoff deadline: the gate
minus the fork's readiness timeout and the 5 s lease/adapter allowance
(45 s). Copy, signature check and plist conversion share one abort signal
that ends early enough for bootstrap and the job's own readiness wait to
meet that deadline; every command otherwise keeps a fixed timeout. An
unavailable verdict past the deadline is reported instead of forking a
daemon that cannot arrive in time. The per-step budget module is gone (its
deletion landed in the previous commit by a staging slip).
The launch body is now two blocks: preparation (plist, plutil, deadline
check) fails as unavailable after removing the copy and job file, and
bootstrap runs in its own try/finally that always deletes the job file.
Retirement is simpler and claims more orphans: one in-flight collection per
process instead of a per-root map and rotating 20-record scan, and the job
record (written before copying) no longer carries a bundle name because
lsof +D scans the whole runtime directory (measured equal cost, same
result on a live copy). A never-submitted copy is retired once its producer
has exited and its job is missing, so a bootstrap timeout or a crash
mid-copy no longer leaves a copy forever; the open-file check still guards
deletion. Collection now starts from the launcher after the attempt.
* docs(macos): ship the daemon launch rules, not the investigation
The signed investigation harness left the tree in the previous telemetry
commit by a staging slip; its results and pinned link stay in the PR. This
trims the reference doc to why the copy and launchd exist, the fallback
states and the cleanup rule, trims the reliability gate to what its
validator needs, and points AGENTS.md at the reference doc next to the
Windows relocation entry.
* fix(macos): hand off to the fork as soon as the stable job exits
A copied daemon refused at exec (for example by Gatekeeper) never answers,
so every fresh start paid the full 10 s readiness wait before the fork
fallback. The readiness loop now reads the job state every 500 ms and
abandons as soon as the job is stopped or missing; a running or
unreadable job is still waited out and never forked beside.
Also drops the dead forkEntryPath the stable launch never read (its
options type now omits it) and un-exports the handoff constant.
* fix(macos): keep daemon copies out of Spotlight and unregister retired ones
Each runtime copy is a full Orca.app, so Spotlight indexed it as an app
and LaunchServices kept a record for it that outlived the copy. A
registered copy claims orca: links and Markdown/CSV files, and becomes
their default handler whenever /Applications/Orca.app is not registered.
New copies go under runtime-*/app.noindex/, which Spotlight skips and which
stops the record LaunchServices made while the bundle was being copied.
Retirement now runs lsregister -u on every bundle in the copy (both the
app.noindex layout and the earlier runtime-*/Orca.app layout) once lsof
shows it unused, then deletes it. A running copy stays registered,
because Local Network access may be resolved through that record.
Collection and stable-copy telemetry match the runtime-* directory, so
copies made before this change are still adopted, classified and retired.
* docs(macos): record the LaunchServices trade-off and the rollback plan
The reference doc now explains why copies sit in a .noindex folder, why
only retired copies are unregistered, and how to turn the copy launch off:
make launchMacDaemonFromStableBundle return null, and keep the retirement
code until no copy-launched daemon can remain, because the old code never
deletes copies.
A test pins that rollback: with the stable launcher returning null, the
launcher forks and still runs the copy collection.
* fix(macos): keep daemon copies registered so Local Network can resolve them
The `.noindex` folder kept copies out of Spotlight and LaunchServices, but
Local Network access is resolved from the signing identifier through
LaunchServices to the executable's Mach-O UUID. After an update that changes
Electron only the copy carries the running daemon's UUID, so an unregistered
copy risks the same Local Network loss main has today. Copies go back to
`runtime-*/Orca.app`; retirement still unregisters each copy before deleting
it, so deleted copies leave no LaunchServices entry. The remaining trade-off:
while `/Applications/Orca.app` is missing, a running registered copy can be
picked for `orca:` links and documents.
* fix(macos): hide daemon copies from Spotlight and tie the job to Orca
Spotlight listed each runtime copy as a second Orca app. New copies go
back under runtime-*/app.noindex/Orca.app, which Spotlight skips.
Retirement and stable-copy telemetry match the runtime-* directory, so
copies in the earlier runtime-*/Orca.app layout still retire and classify
the same way.
Local Network access identifies the daemon by its code signature and
main-executable UUID, not by a LaunchServices record, so an unregistered
copy keeps it. The launchd job now sets AssociatedBundleIdentifiers to
com.stablyai.orca, which is how a job not installed through SMAppService
names the app it belongs to. Copies are never registered explicitly;
retirement still runs lsregister -u before deleting each one.
* feat(macos): package a signed terminal host helper that runs the daemon on the pinned Node
Contents/Helpers/Orca Terminal Host.app carries the pinned Node, the daemon's
out/main JS and this slice's node-pty, signed inside-out as com.stablyai.orca.
afterPack boots it through one PTY round trip, then fails the build on layout,
Info.plist, macOS floor (NODE_RUNTIME_DARWIN_MINIMUM_OS vs Node's minos),
architecture or signature drift.
* ci(macos): cache and prefetch the pinned Node before packaging
Packaging now fetches the darwin Node from nodejs.org, whose download has no
retry. Each mac workflow caches it with the electron-builder downloads, keyed on
the pin, and warms both darwin targets inside the existing retry wrapper.
* feat(macos): launch the daemon from a copy of the terminal host helper, not the whole app
The stable launch now clones Contents/Helpers/Orca Terminal Host.app (~124 MiB
instead of ~628 MiB) and runs its Node on its daemon entry, without
ELECTRON_RUN_AS_NODE. The copy must still match the running app's designated
requirement. A missing helper falls back to the fork, and macOS below the
helper's Node floor forks without copying.
* docs(macos): describe the terminal host helper, its floor and its identity
* fix(macos): bound the terminal host boot check by its deadline and always clean up
A daemon that stayed silent hung packaging past the 20 s deadline; one that closed the
connection left the hello unsettled so Node exited 0 without cleanup; a malformed line threw
uncaught. Every connect, hello and PTY wait now races the deadline, close/error/parse failures
reject, and the daemon and scratch dir are removed on every path.
* ci(macos): cache the pinned Node on its own key with no restore-keys fallback
Sharing the electron-builder cache let old Node archives accumulate through restore-keys.
Also moves release-mac-build's Electron cache comment back onto its step.
* fix(macos): reject a universal slice for the terminal host and pin its name in one test
packageMacTerminalHost turned an unknown arch into darwin-undefined; it now throws through the
shared mac arch mapping. A test asserts the packager and the desktop agree on the helper bundle
and executable names, replacing a Linux/Windows test that could not fail.
* fix(macos): pass the launchd adoption lease its options object after the main merge
* test(macos): accept a reset as the daemon closing the boot-check connection; keep only passing gate commands
* fix(daemon): bump the terminal daemon protocol to v45 so new macOS tabs start in the terminal host helper
* fix(macos): ship the daemon's required packages in the terminal host and boot it outside the build tree
* style(macos): use a template literal in the terminal host resolution check
* fix(macos): derive the terminal host's packages from the daemon's own module graph
* fix(macos): treat any non-literal daemon load as computed, follow directory loads, and keep nested dependencies
* refactor(macos): move terminal-host telemetry, Settings status and Helper folder strings to a follow-up
Keeps this change to the update-safe terminal host itself. The follow-up restores:
- the `stable-copy` spawner path class in daemon adoption telemetry;
- the per-terminal-host Full Disk Access row in Settings and its daemon RPC;
- writing folder usage descriptions into the app's Electron Helpers for the fork fallback.
The helper still takes its Desktop, Documents and Downloads descriptions from
`macos-folder-usage-descriptions.cjs`, so that constant stays.
* fix(i18n): restore en.json to this branch's base, not newer main
The previous commit took the file from a newer main, which pulled unrelated strings into this PR.
* refactor(macos): publish one terminal-host copy per helper build under a fixed launchd label
Name the copy after the helper's CDHash, reuse it when present, and
publish a new one by cloning into a staging dir, verifying it once
against the app's designated requirement and renaming it into place.
One launchd label per profile and protocol replaces per-launch labels,
so job.json, producer pids and per-label collection go away. Retirement
deletes any other copy lsof shows unused. The stable launcher returns
null for every fork fallback and logs its reason.
* fix(packaging): copy terminal-host dependencies from where Node resolves them
A nested package's own dependencies were never walked, so a hoisted
dependency of a nested package was left out of the helper. Resolve each
dependency through the node_modules lookup Node performs from its parent
and copy it to the same relative place, unless a copied package already
carries it.
* fix(packaging): fail release and CI mac builds that cannot boot a terminal-host slice
An arm64 runner without Rosetta skipped the x64 helper's PTY boot check
and shipped the slice untested. Release channels and CI now fail there;
local cross-arch builds still skip with a log line.
* docs(macos): describe the per-build terminal-host copy and its fallback reasons
* docs(macos): keep the codesign command on one line
* test(macos): type the launchd test state without an assertion
* docs(macos): name what the login-session flag gates in the stable launcher
* fix(macos): wait for launchd to drop a booted-out terminal job before reusing its label
---------
Co-authored-by: Neil <neil@stably.ai>