mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 00:19:42 +02:00
fix(review): harden + wire remote-host SSH cases end-to-end (PR #145)
- UI: add the missing data-tab="case-remote" tab button; dispatch it through submitCaseModal()/switchCaseModalTab() to linkRemoteCase() (was dead code). - Restore: restoreMuxSessions() now passes remote (muxSession.remote ?? savedState.remote) into the Session constructor, so remote metadata round-trips on restart instead of reattaching from a local cwd / respawning LOCAL / being erased from state.json. Recovery tests added. - Run flows: runClaude()/runShell() route remote cases through /api/quick-start (POST /api/sessions stat-validates workingDir locally); run*() skip the /api/*/status pre-check and omit inert config/env for remote cases. - Quick-start: resolve the remote case BEFORE the local CLI availability gates and skip isCodex/Gemini/OpenCodeAvailable() when remote; REJECT envOverrides/effort/codex/gemini/openCode config for remote (they don't cross ssh) instead of silently dropping them. - Injection: reject $, backtick, $( in remotePath + identityFile at the schema layer (they survive shellescape into the bash -c launch double-quote layer). Regression tests for $(...) and backtick payloads added. - Remote socket/name: launch on a DEDICATED -L codeman-remote socket under a codeman-ssh-<id> name that fails a remote Codeman's SAFE_MUX_NAME_PATTERN, so a remote instance can't adopt the session; scope tmux set-options per-session (never -g) so they don't mutate other sessions. - Kill: best-effort ssh 'tmux -L codeman-remote kill-session' on remote session kill (fire-and-forget, never blocks/throws the local kill) so the remote agent isn't orphaned forever. - Probe: wire checkRemoteTmuxAvailable() into POST /api/quick-start (structured OPERATION_FAILED) and as courtesy validation in remote-link; add a default -o ConnectTimeout=10 to buildSshConnectionArgs (overridable via extraSshOptions). - Command default: remote claude default is now 'exec claude --dangerously-skip-permissions' (per-host override stays the escape hatch), mirroring local non-interactive semantics. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+16
-8
@@ -289,6 +289,13 @@ const RemoteCommandOverridesSchema = z
|
||||
const NO_SHELL_INJECTION = /^[^\n\r\0`]*$/;
|
||||
const noCommandSubstitution = (s: string) => !s.includes('$(');
|
||||
|
||||
// `remotePath`/`identityFile` are shell-escaped, then the whole launch command is
|
||||
// embedded via `JSON.stringify(...)` inside `bash -c "..."` (tmux-manager). That
|
||||
// outer DOUBLE-quote layer re-exposes `$(...)`, backticks, and `$VAR` even though
|
||||
// the inner value is single-quoted — so a `$(cmd)` in the path would run LOCALLY at
|
||||
// launch. Reject `$` and backtick (and newline/CR/NUL) entirely at the boundary.
|
||||
const NO_SHELL_META = /^[^\n\r\0`$]*$/;
|
||||
|
||||
export const RemoteHostSchema = z.object({
|
||||
id: z.string().regex(/^[a-zA-Z0-9_-]+$/, 'Invalid remote host id'),
|
||||
label: z.string().min(1).max(100),
|
||||
@@ -303,13 +310,9 @@ export const RemoteHostSchema = z.object({
|
||||
.max(100)
|
||||
.regex(/^[a-zA-Z0-9._-]+$/, 'Invalid SSH username'),
|
||||
port: z.number().int().min(1).max(65535).optional(),
|
||||
// Identity (private-key) file PATH only — never key bytes. No newline/NUL.
|
||||
identityFile: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(4096)
|
||||
.regex(/^[^\n\r\0]*$/, 'Invalid identity file path')
|
||||
.optional(),
|
||||
// Identity (private-key) file PATH only — never key bytes. Reject shell
|
||||
// metacharacters ($, backtick) that survive into the `bash -c` launch layer.
|
||||
identityFile: z.string().min(1).max(4096).regex(NO_SHELL_META, 'Invalid identity file path').optional(),
|
||||
// SOCKS5 proxy as host:port (e.g. 127.0.0.1:1080).
|
||||
socksProxy: z
|
||||
.string()
|
||||
@@ -348,7 +351,12 @@ export const RemoteHostSchema = z.object({
|
||||
export const RemoteCaseLinkSchema = z.object({
|
||||
name: z.string().regex(/^[a-zA-Z0-9_-]+$/, 'Invalid case name format'),
|
||||
hostId: z.string().regex(/^[a-zA-Z0-9_-]+$/, 'Invalid remote host id'),
|
||||
remotePath: z.string().min(1).max(2000).regex(/^\//, 'Remote path must be absolute'),
|
||||
remotePath: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(2000)
|
||||
.regex(/^\//, 'Remote path must be absolute')
|
||||
.regex(NO_SHELL_META, 'Invalid characters in remote path'),
|
||||
});
|
||||
|
||||
// ========== Quick Start ==========
|
||||
|
||||
Reference in New Issue
Block a user