Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions server/compute-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ 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');
const LEGACY_CONFIG_DIR = path.join(os.homedir(), '.openclaw');

// ─── ID generation ───

Expand All @@ -23,7 +24,19 @@ async function loadRawConfig() {
const data = await fs.readFile(CONFIG_FILE, 'utf8');
return JSON.parse(data);
} catch (e) {
return { nodes: [], activeNodeId: null };
// Migrate from legacy ~/.openclaw/ if it exists
const legacyFile = path.join(LEGACY_CONFIG_DIR, 'compute-node.json');
try {
const legacyData = await fs.readFile(legacyFile, 'utf8');
const parsed = JSON.parse(legacyData);
// Copy to new location
await fs.mkdir(CONFIG_DIR, { recursive: true });
await fs.writeFile(CONFIG_FILE, legacyData, { mode: 0o600 });
console.log('[compute-node] Migrated config from ~/.openclaw/ to ~/.dr-claw/');
return parsed;
} catch {
return { nodes: [], activeNodeId: null };
}
}
}

Expand Down Expand Up @@ -213,7 +226,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);
}
}

Expand All @@ -226,12 +241,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);
}
}

Expand Down
17 changes: 13 additions & 4 deletions server/routes/community-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,28 @@ 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 });

let existing = {};
try {
existing = JSON.parse(await fs.readFile(configPath, 'utf8'));
} catch {
// file doesn't exist yet
// Migrate from legacy ~/.openclaw/ if it exists
const legacyPath = path.join(os.homedir(), '.openclaw', 'community-tools.json');
try {
const legacyData = await fs.readFile(legacyPath, 'utf8');
existing = JSON.parse(legacyData);
await fs.writeFile(configPath, legacyData, { mode: 0o600 });
console.log('[community-tools] Migrated config from ~/.openclaw/ to ~/.dr-claw/');
} catch {
// Neither file exists — start fresh
}
}

// Merge keys into config
Expand All @@ -112,7 +121,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 });
}
Expand Down
9 changes: 6 additions & 3 deletions server/routes/compute.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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 || '',
Expand Down
15 changes: 13 additions & 2 deletions src/components/compute-dashboard/NodeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 || '~',
Expand Down Expand Up @@ -168,6 +168,15 @@ export default function NodeForm({
<div className="flex items-center gap-2">
<label className="text-xs text-muted-foreground">Authentication</label>
<div className="flex gap-2">
<Button
type="button"
variant={form.authType === 'agent' ? 'secondary' : 'outline'}
size="sm"
className="rounded-xl h-7 text-xs"
onClick={() => update('authType', 'agent')}
>
SSH Agent
</Button>
<Button
type="button"
variant={form.authType === 'key' ? 'secondary' : 'outline'}
Expand All @@ -188,7 +197,9 @@ export default function NodeForm({
</Button>
</div>
</div>
{form.authType === 'key' ? (
{form.authType === 'agent' ? (
<p className="text-xs text-muted-foreground">Uses system SSH agent (1Password, ssh-agent, etc.) — no key or password needed.</p>
) : form.authType === 'key' ? (
<div className="space-y-1">
<label className="text-xs text-muted-foreground">SSH Key (path or content)</label>
<textarea
Expand Down
4 changes: 2 additions & 2 deletions src/components/compute-dashboard/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export type NodeFormData = {
host: string;
user: string;
port: string;
authType: 'key' | 'password';
authType: 'agent' | 'key' | 'password';
key: string;
password: string;
workDir: string;
Expand All @@ -80,7 +80,7 @@ export const defaultFormData: NodeFormData = {
host: '',
user: '',
port: '22',
authType: 'key',
authType: 'agent',
key: '',
password: '',
workDir: '~',
Expand Down
Loading