From 3ac138defb30cd935f7cb1454d97cfb5844376dc Mon Sep 17 00:00:00 2001 From: cc Date: Wed, 15 Apr 2026 16:19:31 -0400 Subject: [PATCH 1/2] feat: add SSH agent auth support for compute nodes - Add "SSH Agent" as a third authentication option alongside SSH Key and Password, supporting 1Password, ssh-agent, and other key agents - Fall back to SSH agent in execSsh/execRsync instead of throwing an error when no explicit key or password is configured - Move config directory from ~/.openclaw to ~/.dr-claw for consistency with the rest of the app's config paths - Update NodeForm UI with SSH Agent button and explanatory text - Fix configured check to not require explicit key/password Co-Authored-By: Claude Opus 4.6 (1M context) --- server/compute-node.js | 13 +++++++------ server/routes/community-tools.js | 6 +++--- server/routes/compute.js | 9 ++++++--- src/components/compute-dashboard/NodeForm.tsx | 15 +++++++++++++-- src/components/compute-dashboard/types.ts | 4 ++-- 5 files changed, 31 insertions(+), 16 deletions(-) diff --git a/server/compute-node.js b/server/compute-node.js index f62ba65e..2340c082 100644 --- a/server/compute-node.js +++ b/server/compute-node.js @@ -5,7 +5,7 @@ import os from 'os'; import pty from 'node-pty'; import crypto from 'crypto'; -const CONFIG_DIR = path.join(os.homedir(), '.openclaw'); +const CONFIG_DIR = path.join(os.homedir(), '.dr-claw'); const CONFIG_FILE = path.join(CONFIG_DIR, 'compute-node.json'); // ─── ID generation ─── @@ -213,7 +213,9 @@ async function execSsh(nodeConfig, remoteCmd) { const cmd = `${sshBase} ${nodeConfig.user}@${nodeConfig.host} ${JSON.stringify(remoteCmd)}`; return await execWithPassword(cmd, nodeConfig.password); } else { - throw new Error('No authentication method configured (need SSH key or password)'); + // Fall back to SSH agent (e.g. 1Password, ssh-agent) — no explicit key or password + const cmd = `${sshBase} ${nodeConfig.user}@${nodeConfig.host} ${JSON.stringify(remoteCmd)}`; + return await execLocal(cmd); } } @@ -226,12 +228,11 @@ async function execRsync(nodeConfig, src, dst, excludes = '') { const cmd = `rsync -avz ${excludes} -e "${sshCmd}" ${src} ${dst}`; - if (nodeConfig.keyPath) { - return await execLocal(cmd); - } else if (nodeConfig.password) { + if (nodeConfig.password && !nodeConfig.keyPath) { return await execWithPassword(cmd, nodeConfig.password, 120000); } else { - throw new Error('No authentication method configured'); + // keyPath or SSH agent — both work via execLocal + return await execLocal(cmd); } } diff --git a/server/routes/community-tools.js b/server/routes/community-tools.js index 99a711c0..8acab396 100644 --- a/server/routes/community-tools.js +++ b/server/routes/community-tools.js @@ -77,11 +77,11 @@ router.post('/configure', async (req, res) => { const results = { steps: [], errors: [] }; - // ── Step 1: Write API keys to ~/.openclaw/community-tools.json ── + // ── Step 1: Write API keys to ~/.dr-claw/community-tools.json ── // (NOT project .env — writing .env triggers Vite restart and kills the fetch) if (apiKeys && Object.keys(apiKeys).length > 0) { try { - const configDir = path.join(os.homedir(), '.openclaw'); + const configDir = path.join(os.homedir(), '.dr-claw'); const configPath = path.join(configDir, 'community-tools.json'); await fs.mkdir(configDir, { recursive: true }); @@ -112,7 +112,7 @@ router.post('/configure', async (req, res) => { .join('\n') + '\n'; await fs.writeFile(rcPath, rcContent, { mode: 0o600 }); - results.steps.push({ step: 'env', status: 'ok', message: `Saved ${Object.keys(apiKeys).length} key(s) to ~/.openclaw/community-tools.json` }); + results.steps.push({ step: 'env', status: 'ok', message: `Saved ${Object.keys(apiKeys).length} key(s) to ~/.dr-claw/community-tools.json` }); } catch (err) { results.errors.push({ step: 'env', error: err.message }); } diff --git a/server/routes/compute.js b/server/routes/compute.js index fc252c9a..ea9ecaaf 100644 --- a/server/routes/compute.js +++ b/server/routes/compute.js @@ -93,6 +93,9 @@ router.put('/nodes/:id', async (req, res) => { updated.password = password; } // If no new password provided, keep existing + } else if (authType === 'agent') { + delete updated.keyPath; + delete updated.password; } // Slurm config @@ -340,11 +343,11 @@ router.get('/config', async (req, res) => { return res.json({ configured: false, host: '', user: '', workDir: '~', authType: 'key', keyPath: '', hasPassword: false }); } res.json({ - configured: !!(node.host && node.user && (node.keyPath || node.password)), + configured: !!(node.host && node.user), host: node.host || '', user: node.user || '', workDir: node.workDir || '~', - authType: node.keyPath ? 'key' : (node.password ? 'password' : 'key'), + authType: node.keyPath ? 'key' : (node.password ? 'password' : 'agent'), keyPath: node.keyPath || '', hasPassword: !!node.password, type: node.type || 'direct', @@ -498,7 +501,7 @@ router.get('/local/monitor', async (_req, res) => { router.get('/status', async (req, res) => { try { const node = await getActiveNode(); - const configured = !!(node && node.host && node.user && (node.keyPath || node.password)); + const configured = !!(node && node.host && node.user); res.json({ configured, host: node?.host || '', diff --git a/src/components/compute-dashboard/NodeForm.tsx b/src/components/compute-dashboard/NodeForm.tsx index d0220b6d..6815d200 100644 --- a/src/components/compute-dashboard/NodeForm.tsx +++ b/src/components/compute-dashboard/NodeForm.tsx @@ -12,7 +12,7 @@ function formFromNode(node: ComputeNode): NodeFormData { host: node.host || '', user: node.user || '', port: String(node.port || 22), - authType: node.keyPath ? 'key' : node.hasPassword ? 'password' : 'key', + authType: node.keyPath ? 'key' : node.hasPassword ? 'password' : 'agent', key: '', password: '', workDir: node.workDir || '~', @@ -168,6 +168,15 @@ export default function NodeForm({
+
- {form.authType === 'key' ? ( + {form.authType === 'agent' ? ( +

Uses system SSH agent (1Password, ssh-agent, etc.) — no key or password needed.

+ ) : form.authType === 'key' ? (