feat: remote host SSH cases (CRUD, case-link, session launch, injection hardening)#145
Open
aakhter wants to merge 7 commits into
Open
feat: remote host SSH cases (CRUD, case-link, session launch, injection hardening)#145aakhter wants to merge 7 commits into
aakhter wants to merge 7 commits into
Conversation
… hatch)
The Remote case form could only reach port-22, default-identity, directly
SSH-able hosts. Add an escape-hatch set of SSH connection options so Codeman
can reach a host like aa-desktop (custom port 2222, ed25519 identity, cloudflared
SOCKS5 ProxyCommand) the way ssh-aa-desktop does — without shelling out to that
wrapper.
- Model (types/session.ts): new optional RemoteSshOptions (identityFile,
socksProxy, jumpHost, extraSshOptions) on RemoteHost AND SessionRemote; all
absent = today's behavior. toSessionRemote() carries them case->session.
- Shared buildSshConnectionArgs(remote) in remote-hosts.ts: pure, exported,
ordered ssh connection tokens (-o BatchMode=yes, -p, -i <abs identity with
~/$HOME expanded + shellescaped>, -J, -o ProxyCommand=nc -X 5 -x <socks>
%h %p emitted as ONE shellescaped token so %h %p reach ssh literally, then
each extraSshOptions -o). Both buildRemoteLaunchCommand (tmux-manager.ts) and
buildRemoteTmuxCheckCommand now use it, so the prereq probe and the real
launch connect identically. checkRemoteTmuxAvailable widened to accept the
options (callers already pass the full host).
- Validation (schemas.ts): identityFile (no newline/NUL), socksProxy
(host:port), jumpHost (no shell metachars), extraSshOptions (KEY=VALUE,
reject newline/NUL/backtick/$() — defense-in-depth on operator-entered config.
- UI (index.html + session-ui.js): SSH Port field + collapsible "Advanced SSH"
section (identity, SOCKS proxy, jump host, extra -o options one per line);
wired into the remote-host create payload.
Empty-options remotes emit byte-identical ssh to before (pinned by test).
Tests: test/remote-ssh-options.test.ts (buildSshConnectionArgs +
buildRemoteLaunchCommand + buildRemoteTmuxCheckCommand for the aa-desktop set,
escaping/%h %p/identity-~ expansion, byte-identical back-compat); case-routes
schema tests (advanced options round-trip; malformed extraSshOptions/socksProxy
rejected). tsc/eslint/frontend-syntax/prettier/build clean.
Acceptance (real remote, no wrapper): the emitted command connected to
aa-desktop through the cloudflared SOCKS proxy and created a durable remote
tmux session (verified independently via ssh-aa-desktop: CONNECTED_NO_WRAPPER,
STILL_ALIVE_AFTER_DETACH); checkRemoteTmuxAvailable over the proxy returned
{ok:true, tmuxPath:/usr/local/bin/tmux}; test session cleaned up.
…mmand-injection) buildSshConnectionArgs interpolated jumpHost raw while its siblings (identityFile/socksProxy/extraSshOptions) were shellescaped. The token array is joined and run via execAsync (/bin/sh -c), so a jumpHost like "x; touch /tmp/pwned" executed. The Zod denylist only blocked backtick/newline/$( and let ;|& and spaces through. - shellescape jumpHost in buildSshConnectionArgs (primary fix) - replace jumpHost denylist with a structural allowlist: [user@]host[:port], comma-separated multi-hop, bracketed IPv6; no shell metachar can appear - update/extend tests: escaped -J assertion + injection-safety case Verified: remote-ssh-options (11) + case-routes (33) pass, tsc --noEmit clean, regex accepts valid forms / rejects 8 injection payloads. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…egration - src/remote-hosts.ts: add missing execAsync = promisify(exec) that was implied by intermediate commits not in the cherry-pick set - src/web/routes/session-routes.ts: add getDataDir import and readRemoteCases/readRemoteHosts/toSessionRemote for remote case support in quick-start; narrow casePath string|null via resolvedCasePath cast - test/routes/session-routes.test.ts: add vi.hoisted remoteStore mock for remote-hosts.js; fix 'creates session from remote case' test to use /api/quick-start (remote cases are not supported on /api/sessions) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… class \[ inside [...] doesn't need backslash — ESLint no-useless-escape.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Introduces the remote host cases feature: a domain for managing named SSH remote hosts, CRUD endpoints + case-link, and a session-UI picker that lets users start Claude sessions on a remote host via SSH. Includes the COD-107 injection-hardening fix that closes a command-injection path in
-J jumpHosthandling.Key files:
src/remote-hosts.ts— new domain:RemoteHost,RemoteCase,readRemoteHosts,readRemoteCases,buildSshConnectionArgs(fully shell-escaped),checkRemoteTmuxAvailablesrc/types/session.ts—SessionRemote,RemoteCommandMode,defaultRemoteCommandForMode;SessionConfig.remotefieldsrc/web/routes/case-routes.ts—GET /api/remote-hosts,POST /api/remote-hosts,PUT/DELETE /api/remote-hosts/:id,POST /api/cases/remote-linksrc/web/schemas.ts—RemoteHostSchema,RemoteCaseSchema,RemoteCaseLinkSchemasrc/tmux-manager.ts—buildRemoteLaunchCommandfor SSH-backed sessions (ssh → remote tmux new-session); all SSH tokens viabuildSshConnectionArgssrc/session.ts,src/web/routes/session-routes.ts— wireremotethrough session create + quick-start; remote case resolves without localstatvalidationsrc/web/public/session-ui.js— remote host picker in the new-session flowsrc/web/public/index.html— remote host selector markupSecurity:
buildSshConnectionArgsshell-escapes every user-supplied field (host,user,-p port,-i identityFile,-J jumpHost,-o socksProxy,extraSshOptions). The-Jflag was previously interpolated raw (command-injection); this commit closes that. A structural allowlist validates jumpHost format ([user@]host[:port], multi-hop, bracketed IPv6) so no shell metacharacter can appear.Scope note: This PR covers the original remote-host cases surface (COD-24) plus the injection-hardening fix. It does not include durable remote tmux, discover/attach of existing sessions, or auto-reconnect — those are a follow-on.
Remote cases vs
/api/sessions: Remote case support is wired into/api/quick-start.POST /api/sessionswithcaseNameworks for local cases only; remote case resolution skips localfs.statand usesreadRemoteCasesinstead.Tests
test/remote-hosts.test.ts— domain unit tests (2 tests)test/routes/case-routes.test.ts— CRUD + case-link route tests (33 tests)test/routes/session-routes.test.ts— quick-start with remote case (55 tests)test/tmux-manager.test.ts— SSH command building + injection-safety (45 tests)All 135 tests pass.
tsc --noEmitclean.🤖 Generated with Claude Code