* fix(setup): correct Windows sequencing wrapper quoting
* test(setup): preserve spaced Windows batch paths
* refactor(setup): dedupe PowerShell encoder, clarify wrapCmd comment
Route the Windows setup-sequencing and Hermes startup planners through the
shared renderer-safe encodePowerShellCommand instead of two verbatim btoa
copies, and make that shared encoder renderer-safe (Buffer is unavailable in
the sandboxed renderer where both planners also run). Reword the wrapCmd
comment so it describes the current single-outer-quote behavior instead of the
old quote-doubling bug.
* test(setup): cover Windows metacharacter paths
* fix(setup): keep Windows runner paths out of cmd source
* test(setup): preserve Windows setup failures
* docs(setup): explain safe cmd path handoff
---------
Co-authored-by: OrcaWin <alpha-eng@stably.ai>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* fix(plugins): close trust-boundary holes in the plugin system
Move five security decisions to their chokepoints rather than leaving them
enumerated at individual call sites.
- Kill-list revocation reaches content packs: PluginContentPackRegistry now
takes an isKilled predicate and intersects it with any caller-supplied
approval, so a killed plugin's VM recipes can no longer reach
spawn(..., { shell: true }) through either reconcile() call site.
- Bound kill-list generatedAt to a 24h future skew at the parse chokepoint.
A far-future timestamp previously made every genuine later list look
"older" and disabled revocation permanently, persisted across restarts.
- Protect the whole auto.components.settings.Plugin* translation subtree
instead of an enumerated prefix list, so language packs cannot forge the
consent provenance badge or rewrite install-error security copy.
- Resolve manifest panel icons by own-key only; "constructor"/"__proto__"
previously yielded non-component prototype members that crashed the
right sidebar to its error boundary.
- Give panel liveness frames a reserved control budget so a panel that
saturates its action budget can still answer the watchdog.
Co-authored-by: Orca <help@stably.ai>
* fix(plugins): keep the kill-list future bound off the cache read path
The schema-level generatedAt bound re-judged the on-disk cache against the
device clock at every launch, so a client whose clock ran behind the last
genuine publication discarded its whole cached kill list and started with
zero revocations. Move the bound to the two fetch chokepoints instead.
Co-authored-by: Orca <help@stably.ai>
* fix(plugins): remove the reserved-lane starvation window and the revocation TOCTOU
Review follow-ups on the trust-boundary fixes:
- The reserved liveness lane had a per-window count equal to the ping
interval, so a panel's own pong-shaped traffic could spend it and drop
the next genuine reply — reintroducing the starvation the lane exists to
prevent. The lane is now size-bounded only; rate stays bounded because
every pong is also charged to the data budget.
- Only schema-valid pongs take the lane now, so near-miss pong-shaped junk
cannot drain it. readPanelPongId replaces the zod parse on this
guest-controlled path (a rejected safeParse allocates an issue list, ~90x
the accepted-path cost) and is pinned to the schema by a parity test.
- Re-read the kill list inside approveAtomically: approvedKeys is snapshotted
before an awaited verification phase, so a plugin killed during that wait
could still publish VM recipes and language packs.
- Assert the curated icon resolves to FileText; the old equality also passed
when both sides fell back to Plug.
Co-authored-by: Orca <help@stably.ai>
* fix(plugins): match zod's safe-integer bound in the pong reader
readPanelPongId used Number.isInteger, but zod's .int() rejects anything
above 2**53-1, so pingIds like 1e100 took the reserved lane the schema
would have refused. The parity test never probed that boundary.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix: stop notification loss, credentialed cache reuse, and clipboard clobber
Mobile catch-up (#8591): fetchMissed swallowed the RPC failure while
deliverLive kept advancing and persisting lastDeliveredSeq, so the next
successful catch-up asked from above the abandoned range and the desktop
cut it. Sessions are module-scope, so an unchanged epoch never resets it.
Quarantine the watermark at the last contiguously-delivered seq and hold
it there until some later catch-up actually drains — not just one retry.
A batch cut short by a teardown quarantines at the last event it settled.
Jira attachment cache: currentEpoch summed two independent counters, so a
site at siteEpoch 1 read the same value before and after a global clear.
The mid-flight guard passed and re-inserted credentialed image bytes that
"disconnect all" had just purged — resident for the process lifetime since
pruneExpired has no timer. One monotonic ticker, compared by max.
Web copy fallback: the handler registered in the capture phase, so xterm's
bubble-phase listener overwrote text/plain with the terminal selection
afterwards; served was already true, so the copy reported success. Every
Orca copy affordance over plain HTTP (Copy Pane ID, Copy Path, commit SHA,
PR URL) pasted the terminal selection. Bubble phase with
stopImmediatePropagation. Covers the secure-context retry branch too,
which shares the same helper.
* fix: roll back the persisted watermark on catch-up failure; cover stopImmediatePropagation
Adversarial review of a98d7f4d5d found two gaps.
1. The quarantine clamped only writes made AFTER the failure. getMissedSince
waits up to 30s, so a live event routinely persists a higher seq while the
request is still outstanding; that value stayed on disk, and the next launch
read it back and resumed past the abandoned range -- the original bug,
reached through a restart. quarantineCatchUpWatermark now re-persists the
clamped seq, so the stored value never outlives the gap it guards.
2. web-clipboard-copy-terminal-selection's second test registered its "late"
document handler BEFORE the fallback's, so it lost on registration order
alone and stopImmediatePropagation was never exercised -- the test passed
with that line deleted. Bubbling reaches the document before the window, so
a window-level listener is what actually requires it.
* fix(mobile): mark a notification seen only once its show lands
A pre-marked seen key made a rejected show unrecoverable: the next
catch-up re-fetched the seq and the dedup guard dropped it, and the
first later event to drain the batch lifted the quarantine past it.
Also contains the rejection so it does not escape the un-awaited
'ready'/live handlers as an unhandled rejection.
Co-authored-by: Orca <help@stably.ai>
* test(web-clipboard): pin stopImmediatePropagation with a same-target handler
Both existing cases passed with plain stopPropagation, and with the listener
back in the capture phase — neither half of the fix was actually pinned. The
window-level clobber is on a different target, so stopPropagation suppresses
it too. Registering the clobber on the document, ordered after the fallback's
own listener, is the only shape stopPropagation cannot cover.
Addresses the review comment posted after the last commit.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
After a successful headless update the CLI installs source-repo HEAD,
which legitimately runs ahead of any shipped bundle. The scan classified
those bytes 'unrecognized', so the row went amber ('may be modified…
remove it') seconds after our own Update button ran, and the advice
looped: remove + reinstall lands the same newer content.
Scan half: a canonical/alias placement whose observed git tree sha
equals the updater lock's skillFolderHash is the CLI's own install, not
a user edit — reclassify it 'newer-known'. Display half: 'newer-known'
is recognized official content ahead of this build with nothing to fix,
so it no longer marks the copy blocked. Eligibility is deliberately
unchanged: ahead of the bundle means there is nothing this build can
update to, and offering one risks the provably-unperformable update
(#11110) when source HEAD still equals the lock.
Copies whose sha does not match the lock, copies with no lock entry,
same-name copies outside the placements the command writes, and
plugin-cache behavior all stay flagged exactly as before.
* fix(ui): preference sync, picker arming, zoom, chat status, and reverted locales
7.1 ui.set rejected whole preference payloads on enum drift. The new
AssertNoMissingKeys guard is key-only, so it could not see that
LegacyWorktreeCardProperty omitted 'cli' (in DEFAULT_WORKTREE_CARD_PROPERTIES)
or that rightSidebarTab omitted 'workspaces'/'pr-checks' and every plugin tab.
UiUpdate is .strict(), so one bad value failed the entire batch and silently
dropped sidebarWidth/groupBy/sortBy/filterRepoIds riding the same debounced
write. Both enums now derive from the shared unions, AssertNoMissingValues
catches value drift by name, and UiUpdate drops an unknown value instead of
rejecting the batch around it. Unknown KEYS still reject.
7.2 The SSH shell-ready fallback moved from first-output to spawn, so a remote
shell needing >1.5s to prompt got the bracketed-paste startup command before
readline armed it, with no recovery afterward. The short deadline now applies
only once output proves the shell is talking; a silent-since-spawn shell gets a
longer budget and still delivers eventually.
7.3 The project picker armed in rank order but rendered in section order, so
with a folder group present the BOTTOM row was armed on open and Enter created
the workspace in the wrong place. Row keys now derive from the same sections
that render. The folders bucket also gains the recent-exclusion guard the
projects bucket has; that duplicate was unreachable, so this is symmetry, not a
live bug fix.
7.4 setBrowserPageZoomLevel now compares before writing, so a pane reasserting
a level the host already holds no longer emits a redundant host-wide
HostZoomMap write. The user-applied level also moved to a module-level map
keyed by page id: the guest webview outlives its React pane, so the pane-local
ref re-seeded from the shared Settings default on every remount and let a later
default retroactively hijack an already-zoomed tab. See PR notes on the part of
this finding that could not be fixed as prescribed.
7.5 A non-null sessionId short-circuited the live-work escape hatch, forcing
'loading' over hook 'working' and rendering an idle pane mid-turn: Send instead
of Stop, no typing indicator, no streaming preview. Status stays 'working'; the
empty-transcript loading SURFACE moves to selectNativeChatViewState, which keeps
7.6 #10770 merged from a base predating #8549, reverting 182-187 translated
strings per locale to English (es 182, ja/ko/zh 187) plus en.json's recipesHelp.
Restored by script, only where the English source is unchanged between the two
shas, so later legitimate edits are preserved: 0 keys added or removed, every
value sourced from 97e4776dfe, and the four other English-source changes since
7.7 Match highlighting indexed by UTF-16 code unit but rendered by code point,
so an emoji-named folder showed marks one glyph late. Cosmetic.
Co-authored-by: Orca <help@stably.ai>
* fix(ssh): keep fast startup delivery on the short fallback deadline
The 15s no-output budget added for the shell-ready fallback was applied to
every SSH launch, including 'fast' delivery. Fast delivery waits for no
marker and pastes nothing prompt-sensitive, so it gained a 10x startup
delay for nothing.
Co-authored-by: Orca <help@stably.ai>
* fix(rpc): generalize the ui.set value-parity guard to every shared key
Naming worktreeCardProperties and rightSidebarTab left the next field to drift
exactly as unguarded: dropping 'pr-status' from groupBy typechecked clean.
Check the value domain of every shared key instead, against z.input (what a
client may send) rather than z.infer (post-transform).
Also move the pure mergeNativeChatLiveSession suite beside the module it covers;
the hook's test file owns an IO harness and was at the max-lines cap.
Co-authored-by: Orca <help@stably.ai>
* fix(i18n): re-apply only the locale strings still reverted at HEAD
#10770 merged from a base predating #8549, so its stale locale copies
overwrote ~185 already-translated strings per locale back to English. A
present catalog value always beats the English translate() fallback, so
those strings render English with nothing to signal the loss.
Since that finding was written, #11205 and other upstream translation
passes independently re-covered most of ja/ko/zh. Replaying #8549's
catalogs wholesale would now overwrite that newer work, so this re-applies
a key ONLY where all of the following hold at origin/main: it was
translated at 97e4776dfe, #10770 reverted it, its English source is
unchanged since, and no upstream commit has touched it since the revert.
es 182 ja 32 ko 17 zh 55
Everything else is left to upstream. Verified zero upstream translations
reverted: every changed key still matches its #10770 value at main. Keys
upstream deleted are not resurrected, and keys whose English source was
edited since are skipped as legitimate source changes rather than reverts
(this is what keeps zh CPU on #11205's deliberate "CPU" over #8549's
"中央处理器"). Key count and order are unchanged in all five catalogs.
en.json's own recipesHelp was reverted by the same stale base and no
upstream commit has touched it since, so it is restored to match the live
source at EphemeralVmsPane.tsx:252.
* fix(rpc): keep null in the ui.set value-parity guard
NonNullable stripped null as well as undefined, so dropping .nullable()
from a `| null` field passed the guard while still rejecting the batch at
runtime -- the exact drift class the guard exists to catch. Proven: making
visibleWorkspaceHostIds non-nullable typechecked clean before, now errors
by name. Also pins the 15s silent-shell budget so it cannot silently
shrink back toward the short deadline.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
* fix(perf): correct three 07-27 perf regressions
Traversal capacity cap no longer scales with worker concurrency
(#11026). retainWorkspaceSpaceScanEntry charged a traversal-wide entry
counter, so N workers each holding a listing multiplied the live charge.
At concurrency 48 a 48x2,100 tree (100,848 entries) hit the 100,000 cap
while 100x1,500 (150,100 entries, 50% more) passed, and scanLocalWorktree
treats the capacity error as terminal, reporting an intact worktree as
"Unavailable" with sizeBytes 0. The cap is now per directory listing --
the only quantity fixed by directory shape -- restoring the invariant
docs/workspace-space-scan-resource-bounds.md already states. Aggregate
live retention stays bounded by the unchanged 64 MiB byte cap.
Note: releasing each entry's charge at dispatch (the originally suggested
fix) was measured and does not help; the peak is set at admission, before
any entry is dispatched.
Repo image icons are no longer fully base64-decoded on every snapshot
publish (#11012). sanitizeRepoIcon reached decodeBase64Prefix, which
sized its buffer to the whole payload to read a 24-byte header, running
synchronously inside ipcMain.handle at a 250 ms throttle. Validation is
now memoized on source+src in a BoundedMap. Measured for 10 icons x
256 KB: 37.34 ms -> 0.67 ms per publish.
One over-long card label no longer discards the entire snapshot (#11012).
isDashboardSnapshot was all-or-nothing and dashboard-popout returned
early with no log while replaying lastSnapshot, so `orca terminal rename
--title "<1025+ chars>"` froze the pop-out board on its last good paint
with nothing surfaced. Labels are truncated at the producer, the
validator drops only the offending card, and both the rejection and the
drop are logged. The bound now lives in the shared snapshot contract so
producer and validator cannot drift.
Co-authored-by: Orca <help@stably.ai>
* fix(perf): charge a scan listing's parent path once, not per entry
The 4.1 fix made the entry cap per-listing but left the 64 MiB byte cap
charging parentPath.length for every entry in a listing. Because a
listing's entries all share one parent-path string, that multiplied the
path by the directory's width, so the byte cap measured checkout depth
rather than live heap.
The reported symptom therefore still reproduced at the production default
limits: 48 x 2,100 @ concurrency 48 raised a capacity error once the
worktree path passed ~58 characters, while the same layout at concurrency
1 succeeded. The shipped regression test could not see this because it
passes maxRetainedBytes: Number.MAX_SAFE_INTEGER, disabling the only cap
still in play. Measured at a real 65-char worktree root, 3 of the report's
4 documented layouts still failed.
The parent path is now charged once per listing, with its first entry, so
an empty listing strands no charge. Per-entry overhead is unchanged at
512 B + name, which still dominates the estimate, so the OOM protection
the original PR added is preserved.
Adds a production-default-limits case covering the report's layouts under
a deep root, plus an assertion that a short and a deep root reach the same
verdict -- the path independence docs/workspace-space-scan-resource-bounds.md
requires and which no existing test enforced.
Co-authored-by: Orca <help@stably.ai>
* fix(perf): prove the icon cache by decode count, not wall clock
The caching test asserted a per-publish millisecond budget, which failed
on CI at 5.64 ms against a 5 ms ceiling. Any threshold flakes on a loaded
box, so count real sanitizeRepoIcon entries instead: 10 repos x 20
publishes is 200 icon checks against exactly 1 decode. Added cases pin
the cache key (payload and source both re-decode; a cached image verdict
never answers for an emoji) and that a rejection is cached too.
Also drops budget.entries, which the per-listing cap left as a
traversal-wide counter no check reads -- exactly the shape a future
guard could reintroduce the concurrency bug from.
Co-authored-by: Orca <help@stably.ai>
* fix(dashboard): bound the project filter label the whole board rides on
#11042 added snapshot-level filterOptions whose project labels are
repo.displayName -- the same unbounded source this PR already bounds for
card.repoName, but one level up where dropping a card cannot recover it.
An over-long project name would fail isDashboardFilterOptions and take
the entire snapshot with it, which is the exact frozen-board failure the
per-card drop was added to end. Workspace-status labels are already
capped at 32 by workspace-statuses.ts, so only projects needed this.
Co-authored-by: Orca <help@stably.ai>
* fix(dashboard): disambiguate the repo icon cache key
The memoization key joined `source` and `src` with a space, but the
sanitizer's base64 pattern admits whitespace inside a valid `src`. A
rejected icon can therefore split the same concatenation differently and
inherit an accepted icon's cached verdict, reaching the pop-out's
`<img src>` without ever being sanitized. Length-prefix the source.
Co-authored-by: Orca <help@stably.ai>
---------
Co-authored-by: Orca <help@stably.ai>
Re-lands #11129, which was merged into #11128's branch rather than main and
so never reached main. Content is identical to the reviewed and live-QA'd
head ac5ec5b0b0 (1775d83cf6 + ac5ec5b0b0, minus the intermediate merge).
* fix(skills): stop the skill review dialog contradicting the badge that opens it
A skill whose only fault was an edited copy or one Orca could not read turned the
setup-rail badge amber and offered Details — and Details opened a dialog headlined
"All installed Orca skills are up to date." over an empty list. The badge says
something is wrong, the dialog it points at says nothing is.
The grouping only returned skills with an out-of-date copy, so those two states
produced no row and the summary fell through to the all-clear headline. Include a
skill when a copy needs attention as well, using one shared predicate so the badge
and the dialog cannot disagree again. A plugin's own copy of a same-named skill
stays out: that is the vendor's, not the user's drift.
* test(skills): pin that a routine outdated copy raises no attention marker