mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 23:49:41 +02:00
feat(remote): support password-authenticated SSH hosts, storing the password optionally
The remote path always passed `-o BatchMode=yes`, which disables every interactive prompt, so password-only hosts could never be used. BatchMode is not an oversight: Codeman launches ssh non-interactively from a service process with no terminal and nobody watching, and without it ssh hangs on a password prompt no one will ever answer — a dead pane, which is worse than an error. So the password goes through sshpass, handed to ssh in the SSHPASS environment variable: never in argv (same-host users can read /proc) and never in a temp file. The variable itself is injected into the pane with socket-scoped `tmux setenv`, the same rule every other secret here follows. ⚠️ One thing measured, and a naive implementation will hit it: BatchMode=yes and sshpass are mutually exclusive. The former disables the password prompt, and answering that prompt is exactly how sshpass works, so using both yields `Permission denied (publickey,password)` — which reads like a wrong password rather than wrong arguments. With a password we therefore send BatchMode=no plus NumberOfPasswordPrompts=1, the latter so a wrong password fails immediately instead of hanging (also measured). ⚠️ The preflight probe runs in the server process, not in the pane, so `tmux setenv` does not reach it and that path passes the variable through the child environment instead. A missing sshpass is reported as a named prerequisite during the probe as well; otherwise it surfaces as a pane dying with "sshpass: command not found", which reads like a broken host. Storage and exposure: - remote-hosts.json now holds a secret, so it is written 0600, and an existing file is explicitly tightened once (writeFile's mode only applies on create) - the API always redacts, returning only a `passwordSet` boolean - updates merge the stored password, because a redacted host posted back carries no `password` and a straight write would silently erase it; an explicit empty string still means "clear" - the schema deliberately does not apply NO_SHELL_META to `password`: a password legitimately contains `$` and backticks, and unlike the path fields it is never interpolated into a shell string
This commit is contained in:
@@ -2110,6 +2110,30 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
/**
|
||||
* Configure Gemini-specific environment on a tmux session.
|
||||
*/
|
||||
/**
|
||||
* Hand a remote host's stored SSH password to the pane as `SSHPASS`.
|
||||
*
|
||||
* `buildSshConnectionArgs` launches such a host through `sshpass -e`, which
|
||||
* reads exactly this variable. Injected with socket-scoped `tmux setenv` so the
|
||||
* secret never appears in `ps` — the same discipline the CLI API keys follow —
|
||||
* and inherited by `respawn-pane`, so a respawned wrapper reconnects without the
|
||||
* server having to re-send anything.
|
||||
*
|
||||
* ⚠️ A user's own `envOverrides` cannot reach this variable: `SSHPASS` matches
|
||||
* no entry in ALLOWED_ENV_PREFIXES / ALLOWED_ENV_KEYS, so unlike the DeepSeek
|
||||
* case there is no later `applyEnvOverrides` pass that could override it.
|
||||
*/
|
||||
private _configureSshPassword(muxName: string, remote?: SessionRemote): void {
|
||||
if (!remote?.password) return;
|
||||
try {
|
||||
execSync(`${this.tmux()} setenv -t "${muxName}" SSHPASS ${shellescape(remote.password)}`, { stdio: 'ignore' });
|
||||
} catch {
|
||||
// Best-effort like the sibling _configure* helpers: a failed setenv surfaces
|
||||
// as an auth failure in the pane, which is visible, rather than as a thrown
|
||||
// session-create that hides the real cause.
|
||||
}
|
||||
}
|
||||
|
||||
private _configureGemini(muxName: string): void {
|
||||
setGeminiEnvVars(this.tmux(), muxName);
|
||||
}
|
||||
@@ -2373,6 +2397,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
|
||||
// Apply user-supplied env overrides (e.g., CLAUDE_CODE_EFFORT_LEVEL) via tmux setenv
|
||||
// so secret values stay off the bash command line. Must run before respawn-pane.
|
||||
// A password-authenticated remote host needs SSHPASS in the pane (see
|
||||
// _configureSshPassword); a no-op for key auth and every non-remote session.
|
||||
this._configureSshPassword(muxName, remote);
|
||||
|
||||
this.applyEnvOverrides(muxName, envOverrides);
|
||||
|
||||
// Replace the shell with the actual command (no echo in terminal). Keep
|
||||
@@ -2602,6 +2630,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
|
||||
}
|
||||
|
||||
// Re-apply user env overrides before respawn so the new shell inherits them.
|
||||
// A password-authenticated remote host needs SSHPASS in the pane (see
|
||||
// _configureSshPassword); a no-op for key auth and every non-remote session.
|
||||
this._configureSshPassword(muxName, remote);
|
||||
|
||||
this.applyEnvOverrides(muxName, envOverrides);
|
||||
|
||||
// -c /tmp + cd bounce — see createSession() for rationale (stale FUSE state).
|
||||
|
||||
Reference in New Issue
Block a user