Skip to content

feat(ssh): a paired client can manage the server's own SSH hosts (ssh.target-management.v1) - #27329

Open
OrcaWin wants to merge 2 commits into
mainfrom
OrcaWin/d2q1-ssh-target-management
Open

OrcaWin wants to merge 2 commits into
mainfrom
OrcaWin/d2q1-ssh-target-management

Conversation

@OrcaWin

@OrcaWin OrcaWin commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 5 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​459 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​458
Prod 15 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​724 $\color{#cf222e}{\Huge{\mathbf{−}}}$​344 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​380

ELI5

An Orca server can keep its own list of SSH hosts and run projects on them. Until now, a computer or browser paired with that server could only see those hosts. It could not add, edit or remove them, look inside their folders, or add a project on them. This PR gives the server a safe way to do those things when a paired client asks. The next PR builds the screens on top.

What Changed

  • Before: the server answered only "which SSH hosts do you have, and are they connected". Managing them, browsing their folders, and adding a project on one were desktop-only (Electron IPC on the server's own window). A paired client had no path ([Bug]: Paired Desktop cannot manage or use headless server SSH targets #8489), and a headless orca serve had none at all.
  • After: a server that advertises the new capability ssh.target-management.v1 answers these methods, all with the workspace permission:
    • ssh.listEditableTargets, ssh.addTarget, ssh.updateTarget, ssh.removeTarget
    • ssh.browseDir: lists a folder on one of the server's SSH hosts.
    • repo.addRemote: adds an existing folder on one of the server's SSH hosts as a project.
  • Mechanism:
    • No parallel implementation. The desktop's add/update/remove handlers are now plain functions. The IPC handlers and the new RPC methods call the same functions through the existing SSH target registry, which already lets the runtime use the SSH stack without loading Electron. The same rules therefore apply to both: a managed server's host cannot be removed, Orca's internal VM hosts cannot be edited, main-owned fields are stripped, and orphaned projects are re-adopted. The folder listing moved out of the IPC file unchanged. repo.addRemote uses the registration the server already uses for projectHostSetup.setupExistingFolder on SSH hosts.
    • Secrets are never readable. The server never stores SSH passwords or key passphrases; it asks for them when it connects. The input schema is strict, so a password or passphrase sent by a client is refused instead of being silently dropped. The client can never believe the server kept one. Main-owned fields are refused the same way. Listing uses an explicit allowlist of fields, held in step with the shared field list by the type checker, so a stray key in a stored row (or a field added later) never reaches a client. No client event carries target data. Tests cover each of these.
    • Capability-negotiated per docs/reference/remote-wire-compatibility.md. The new methods are distinct, so an old server can never strip an unknown field and act locally. For example, repo.add with a connectionId on an old server would have registered a path on the server's own disk. The renderer client module checks the capability before every call. An old server gives a typed "update this server" error and nothing falls back to the client's own SSH hosts.
    • Who may call: workspace per the owner's decision that a paired client counts as "shell as the user" (docs(rpc): a paired client runs commands as you; say so when pairing #27317). Phones are refused because the methods are not on the mobile allowlist. An SSH host's orca CLI is refused unless that host has remote control enabled.
    • Repo re-adoption after an add now also notifies paired clients, not just the local window.

Why

The owner chose that paired workspace access equals shell access (D2 Q1 = b). A client that can open a terminal can already edit ~/.ssh/config, so a narrower tier here would protect nothing. Reusing the desktop's own handlers keeps one set of rules for local and remote management. Distinct methods behind a capability are the only shape that is safe when the client and server are on different versions.

Considered: adding an optional connectionId to repo.add. Rejected, because an old server strips the field and registers a path on its own disk instead. Also considered: accepting a password and keeping it in memory for the next connect. Rejected, because the server has no secret store today, and a write path with nothing behind it would only mislead. Remote credential prompts are separate work.

Linked Issue

Server half of #8489 and #25887. Both close with the UI in the follow-up PR.

Ideas and tests harvested from the community PR #8492 by @jae-heo: a strict, bounded target schema, a reusable browseSshDirectory, and the repo.addRemote RPC with its test. Thank you.

Visual Proof

N/A: server and RPC only, no UI in this PR.

Testing

  • pnpm tc, pnpm lint (owner-routing ratchet stays at 0, runtime-Electron ratchet stays at 0, RPC params catalog regenerated), pnpm run check:code-quality:changed

  • New: src/main/runtime/rpc/methods/ssh-target-management.test.ts, which covers:

    • the allowlisted view, and that secrets and main-owned keys never appear in list, add or update results;
    • password and passphrase refused on add and update;
    • replace semantics on update;
    • an unknown target reports an error;
    • remove refusal surfaces;
    • browse;
    • no client event is published;
    • permission rows: paired client allowed, phone and unopted SSH-host CLI refused.
  • New: src/renderer/src/runtime/runtime-ssh-target-management.test.ts, a mixed-version test. An old server refuses every operation with "update" and nothing is called locally; a current server is called by environment.

  • Updated: ssh-target-crud-handlers-ownership.test.ts (paired clients get the same managed-host rules), repo.test.ts, runtime-project-host-setup-controller.test.ts.

  • Focused suites: src/main/runtime/rpc, src/main/ipc, src/main/ssh, src/main/startup, src/renderer/src/runtime.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

Review

P0/P1 review by two independent reviewers; see comments.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • SSH execution boundary: listing reports connectionStatus only when the server has a state for the target; absence stays absent, never "disconnected". Browse on a target that isn't connected fails with "not found" and makes no verdict. Remove tears down through the existing lifecycle queue.
  • Mobile: unchanged; the new methods are not on its allowlist.
  • Windows and SSH hosts: browse keeps the existing POSIX-then-PowerShell fallback.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@coderabbitai

coderabbitai Bot commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

⛔ Files ignored due to path filters (1)
  • src/shared/rpc-contract/rpc-params-catalog.generated.ts is excluded by !**/*.generated.*

⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 0ea9772d-5034-4d52-a7ef-bc8d25264a6d

📥 Commits

Reviewing files that changed from the base of the PR and between f361c91 and 0d11eb9.


⛔ Files ignored due to path filters (1)
  • src/shared/rpc-contract/rpc-params-catalog.generated.ts is excluded by !**/*.generated.*

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough
📝 Walkthrough
📝 Walkthrough
📝 Walkthrough

Walkthrough

The changes add runtime RPC operations for SSH target management and directory browsing. Shared schemas define editable target inputs and remote repository parameters. The server applies target ownership checks and returns allowlisted target data. The renderer checks runtime capability before making management calls. The runtime also adds SSH-hosted repositories through remote registration.








Priority: ⬇️ Low

Merge Risk: 🔵 Low · up to 7fa83

Paired clients can see an incorrect filename when the last listed file ends in a space. This is a narrow issue with a localized fix; the change is otherwise mergeable with owner awareness.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 25.64% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 39 functions across 19 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check Passed The title clearly identifies the primary change: capability-negotiated SSH host management for paired clients. It is specific and concise.
Description check Passed The description covers the required sections, including the user impact, implementation, rationale, linked issues, testing, visual proof, compatibility notes, and checklist. It clearly notes that manu…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.







✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR





🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR







  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 9e664587-889f-4d7f-a7b7-262a773a755e
📥 Commits

Reviewing files that changed from the base of the PR and between 68519e6 and 7fa838a.

⛔ Files ignored due to path filters (1)
  • src/shared/rpc-contract/rpc-params-catalog.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (19)
  • src/main/ipc/repos/repos-changed-notification.ts
  • src/main/ipc/ssh-browse.ts
  • src/main/ipc/ssh-target-crud-handlers-ownership.test.ts
  • src/main/ipc/ssh-target-crud-handlers.ts
  • src/main/ipc/ssh.ts
  • src/main/runtime/rpc/methods/repo.test.ts
  • src/main/runtime/rpc/methods/repo.ts
  • src/main/runtime/rpc/methods/ssh-target-management.test.ts
  • src/main/runtime/rpc/methods/ssh.ts
  • src/main/runtime/runtime-project-host-setup-controller.test.ts
  • src/main/runtime/runtime-project-host-setup-controller.ts
  • src/main/runtime/runtime-repository-command-surface.ts
  • src/main/ssh/ssh-remote-directory-browse.ts
  • src/main/ssh/ssh-target-registry.ts
  • src/renderer/src/runtime/runtime-ssh-target-management.test.ts
  • src/renderer/src/runtime/runtime-ssh-target-management.ts
  • src/shared/protocol-version.ts
  • src/shared/rpc-contract/ssh-params.ts
  • src/shared/ssh-target-management.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review.

Comment on lines +201 to +218
const lines = stdout.trim().split(/\r?\n/)
if (lines.length === 0) {
rejectOnce(new Error('Empty response from remote'))
return
}

const resolvedPath = lines[0]
const entries: RemoteDirEntry[] = []

for (let i = 1; i < lines.length; i++) {
const line = lines[i]
if (!line || line === './' || line === '../') {
continue
}
if (line.endsWith('/')) {
entries.push({ name: line.slice(0, -1), isDirectory: true })
} else {
entries.push({ name: line, isDirectory: false })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

git rev-parse --verify eaf5dbed9e5fec6046eb8b4835303c40125b8012
git show eaf5dbed9e5fec6046eb8b4835303c40125b8012:src/main/ipc/ssh-browse.ts | rg -n -C 5 'trim\(|split\(|readStdout|sortDirEntries'
git diff eaf5dbed9e5fec6046eb8b4835303c40125b8012 7fa838a866e29bfed5f5b0e1ef9f3f3c9301ac2a -- src/main/ipc/ssh-browse.ts src/main/ssh/ssh-remote-directory-browse.ts

Repository: stablyai/orca

Length of output: 24495


🏁 Script executed:

set -eu
printf '%s\n' '--- head RPC browse references ---'
rg -n -F --glob '*.ts' -- 'browseSshDirectory' src/main src/renderer || test "$?" -eq 1
printf '%s\n' '--- head ssh RPC method context ---'
git show 7fa838a866e29bfed5f5b0e1ef9f3f3c9301ac2a:src/main/runtime/rpc/methods/ssh.ts | nl -ba | sed -n '145,185p'
printf '%s\n' '--- merge-base ssh RPC browse references ---'
git grep -n -F 'browseSshDirectory' eaf5dbed9e5fec6046eb8b4835303c40125b8012 -- src/main src/renderer || test "$?" -eq 1
printf '%s\n' '--- RPC diff for browse-related additions ---'
git diff --unified=12 eaf5dbed9e5fec6046eb8b4835303c40125b8012 7fa838a866e29bfed5f5b0e1ef9f3f3c9301ac2 -- src/main/runtime/rpc/methods/ssh.ts src/main/runtime/rpc

Repository: stablyai/orca

Length of output: 26711


Preserve trailing spaces in RPC browse results.

The extracted parser already used stdout.trim(), but this PR adds ssh.browseDir, which exposes the parser to paired clients. If the final POSIX entry is a regular file named file , trimming changes it to file. The RPC response then contains and displays the wrong filename.

Suggested fix
--- "a/src/main/ssh/ssh-remote-directory-browse.ts"
+++ "b/src/main/ssh/ssh-remote-directory-browse.ts"
@@ -198,7 +198,10 @@
       }
 
       // Why: Windows OpenSSH exec emits CRLF; split on \r?\n so a trailing \r doesn't defeat the endsWith('/') dir check or leave a stray CR in names.
-      const lines = stdout.trim().split(/\r?\n/)
+      const lines = stdout.split(/\r?\n/)
+      while (lines.length > 0 && lines[lines.length - 1] === '') {
+        lines.pop()
+      }
       if (lines.length === 0) {
         rejectOnce(new Error('Empty response from remote'))
         return
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const lines = stdout.trim().split(/\r?\n/)
if (lines.length === 0) {
rejectOnce(new Error('Empty response from remote'))
return
}
const resolvedPath = lines[0]
const entries: RemoteDirEntry[] = []
for (let i = 1; i < lines.length; i++) {
const line = lines[i]
if (!line || line === './' || line === '../') {
continue
}
if (line.endsWith('/')) {
entries.push({ name: line.slice(0, -1), isDirectory: true })
} else {
entries.push({ name: line, isDirectory: false })
const lines = stdout.split(/\r?\n/)
while (lines.length > 0 && lines[lines.length - 1] === '') {
lines.pop()
}
if (lines.length === 0) {
rejectOnce(new Error('Empty response from remote'))
return
}
const resolvedPath = lines[0]
const entries: RemoteDirEntry[] = []
for (let i = 1; i < lines.length; i++) {
const line = lines[i]
if (!line || line === './' || line === '../') {
continue
}
if (line.endsWith('/')) {
entries.push({ name: line.slice(0, -1), isDirectory: true })
} else {
entries.push({ name: line, isDirectory: false })

@OrcaWin
OrcaWin force-pushed the OrcaWin/d2q1-ssh-target-management branch 2 times, most recently from 9f1a4dc to f361c91 Compare October 10, 2026 23:51
@OrcaWin
OrcaWin force-pushed the OrcaWin/d2q1-ssh-target-management branch from f361c91 to 0d11eb9 Compare October 11, 2026 00:50

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant