Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
bc8140e
feat(config): Add remoteFileSystem zod object shape to tools schema
MarvelNwachukwu Feb 14, 2026
867b06e
feat(config): Make remoteFileSystem optional with sensible defaults
MarvelNwachukwu Feb 14, 2026
fd1205d
feat(config): Add remoteFileSystemEnabled to AppConfig interface
MarvelNwachukwu Feb 14, 2026
5038880
feat(config): Map remoteFileSystemEnabled in toAppConfig
MarvelNwachukwu Feb 14, 2026
2cfa55e
feat(tools): Scaffold remoteFileSystemTools module with disabled check
MarvelNwachukwu Feb 14, 2026
a8fd2fd
feat(tools): Add base SSH MCP transport with StrictHostKeyChecking
MarvelNwachukwu Feb 14, 2026
1d37312
feat(tools): Add BatchMode=yes to prevent SSH password prompt hangs
MarvelNwachukwu Feb 14, 2026
e3b4ea8
feat(tools): Add SSH key path, port, and host args from config
MarvelNwachukwu Feb 14, 2026
5f88826
feat(tools): Run MCP filesystem server on remote machine via npx
MarvelNwachukwu Feb 14, 2026
05064e6
feat(tools): Add debug logging for SSH tunnel connections
MarvelNwachukwu Feb 14, 2026
8dca07d
feat(tools): Export getRemoteFileSystemTools from barrel index
MarvelNwachukwu Feb 14, 2026
0b983c0
feat(delegate): Add remoteFilesystem to tool group names
MarvelNwachukwu Feb 14, 2026
e16ae80
feat(delegate): Document remoteFilesystem in tool groups description
MarvelNwachukwu Feb 14, 2026
d0a32eb
feat(agent): Import getRemoteFileSystemTools
MarvelNwachukwu Feb 14, 2026
516101a
feat(agent): Initialize remote filesystem tools after local filesystem
MarvelNwachukwu Feb 14, 2026
7f5ce4a
feat(agent): Pass remote filesystem tools to delegate toolGroups
MarvelNwachukwu Feb 14, 2026
c1de78f
feat(agent): Register remote filesystem tools with agent builder
MarvelNwachukwu Feb 14, 2026
115d465
feat(remote): Prefix remote tools with remote_ and add system prompt …
MarvelNwachukwu Feb 18, 2026
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
6 changes: 6 additions & 0 deletions src/agents/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { buildSkillsPrompt } from "../skills/SkillService.js";
import {
createDelegateTaskTool,
getFileSystemTools,
getRemoteFileSystemTools,
loadPrivateTools,
scheduleTools,
shellTools,
Expand Down Expand Up @@ -83,6 +84,9 @@ export async function createClawdAgent(options: CreateAgentOptions = {}) {
// Get filesystem tools for workspace access
const fileSystemTools = await getFileSystemTools();

// Get remote filesystem tools (SSH tunnel, empty array when disabled)
const remoteFileSystemTools = await getRemoteFileSystemTools();

// Memory tools shared with subagents (no ForgetMemoryTool for safety)
const memoryTools = [new RecallMemoryTool(), new WriteMemoryTool()];

Expand All @@ -94,6 +98,7 @@ export async function createClawdAgent(options: CreateAgentOptions = {}) {
toolGroups: {
shell: shellTools,
filesystem: fileSystemTools,
remoteFilesystem: remoteFileSystemTools,
memory: memoryTools,
},
});
Expand All @@ -116,6 +121,7 @@ export async function createClawdAgent(options: CreateAgentOptions = {}) {
...scheduleTools,
...shellTools,
...fileSystemTools,
...remoteFileSystemTools,
...privateTools,
delegateTool,
)
Expand Down
28 changes: 26 additions & 2 deletions src/agents/config/agent.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import * as fs from "node:fs/promises";
import * as path from "node:path";
import matter from "gray-matter";
import { configExists, getConfig } from "../../config/index.js";
import { configExists, getConfig, getRawConfig } from "../../config/index.js";
import { HEARTBEAT_OK, SILENT_REPLY_TOKEN } from "../../lib/tokens.js";
import { buildSkillsPrompt } from "../../skills/SkillService.js";
import type {
Expand Down Expand Up @@ -184,7 +184,31 @@ You have FULL read/write access to the workspace directory. Use these tools to m
- Extra flags/arguments ARE allowed: \`git log --author=Marvel --since=yesterday --oneline\` works because \`git log\` is allowlisted.
- Avoid shell metacharacters (\`; & | $ !\`). Use simple commands without pipes or chaining.
- For dates with spaces, use dotted format: \`--since=2.days.ago\` instead of \`--since="2 days ago"\`.
- \`gh\` (GitHub CLI) is available for GitHub API queries: PRs, issues, commits, etc.`;
- \`gh\` (GitHub CLI) is available for GitHub API queries: PRs, issues, commits, etc.
${buildRemoteFileSystemSection()}`;
}

/**
* Build the remote filesystem tools section (only when enabled)
*/
function buildRemoteFileSystemSection(): string {
try {
const rawConfig = getRawConfig();
const remoteFsConfig = rawConfig.tools.remoteFileSystem;
if (!remoteFsConfig?.enabled) return "";

const host = remoteFsConfig.sshHost;
const paths = remoteFsConfig.allowedPaths.join(", ");

return `\n**Remote PC Tools (via SSH to \`${host}\`):**
- These tools access files on the user's *remote machine* over SSH — they are NOT local workspace tools.
- Remote host: \`${host}\` | Accessible paths: \`${paths}\`
- All remote tools are prefixed with \`remote_\`: \`remote_read_file\`, \`remote_write_file\`, \`remote_list_directory\`, \`remote_search_files\`, \`remote_get_file_info\`, \`remote_create_directory\`, \`remote_move_file\`
- When the user asks about files on "my PC", "my computer", "my desktop", or "remote machine", use these \`remote_*\` tools — NOT the local workspace tools.
- The remote tools operate on the remote machine's filesystem at the allowed paths listed above.`;
} catch {
return "";
}
}

/**
Expand Down
22 changes: 22 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,22 @@ const configFileSchema = z.object({
enabled: z.boolean(),
allowedCommands: z.array(z.string()),
}),
remoteFileSystem: z
.object({
enabled: z.boolean(),
sshHost: z.string(),
sshKeyPath: z.string(),
sshPort: z.number().default(22),
allowedPaths: z.array(z.string()),
})
.optional()
.default({
enabled: false,
sshHost: "",
sshKeyPath: "",
sshPort: 22,
allowedPaths: [],
}),
Comment on lines +77 to +92
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current schema allows remoteFileSystem to be enabled with an empty sshHost or sshKeyPath, which will cause runtime errors when the SSH command is executed. You should add a .refine() validation to the Zod schema to ensure these fields are non-empty strings when enabled is true.

    remoteFileSystem: z
      .object({
        enabled: z.boolean(),
        sshHost: z.string(),
        sshKeyPath: z.string(),
        sshPort: z.number().default(22),
        allowedPaths: z.array(z.string()),
      })
      .refine(
        (data) => !data.enabled || (data.sshHost !== "" && data.sshKeyPath !== ""),
        {
          message: "sshHost and sshKeyPath must be provided when remoteFileSystem is enabled.",
        },
      )
      .optional()
      .default({
        enabled: false,
        sshHost: "",
        sshKeyPath: "",
        sshPort: 22,
        allowedPaths: [],
      }),

}),
skills: z.object({
enabled: z.boolean(),
Expand Down Expand Up @@ -119,6 +135,9 @@ export interface AppConfig {
githubEnabled: boolean;
githubToken: string;

// Remote Filesystem
remoteFileSystemEnabled: boolean;

// Features
memoryEnabled: boolean;
skillsEnabled: boolean;
Expand Down Expand Up @@ -173,6 +192,9 @@ function toAppConfig(file: ConfigFile): AppConfig {
githubEnabled: file.channels.github?.enabled ?? false,
githubToken: file.channels.github?.token ?? "",

// Remote Filesystem
remoteFileSystemEnabled: file.tools.remoteFileSystem?.enabled ?? false,

// Features
memoryEnabled: file.memory.enabled,
skillsEnabled: file.skills.enabled,
Expand Down
9 changes: 7 additions & 2 deletions src/tools/delegateTools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ import { createLogger } from "../lib/logger.js";

const log = createLogger("Delegate");

const toolGroupNames = ["shell", "filesystem", "memory"] as const;
const toolGroupNames = [
"shell",
"filesystem",
"remoteFilesystem",
"memory",
] as const;
type ToolGroupName = (typeof toolGroupNames)[number];

/** Max LLM round-trips per subagent to keep delegation fast */
Expand All @@ -38,7 +43,7 @@ const taskSchema = z.object({
.array(z.enum(toolGroupNames))
.default(["filesystem"])
.describe(
"Tool groups the subagent needs: shell (exec commands), filesystem (read/write files), memory (recall/write memories)",
"Tool groups the subagent needs: shell (exec commands), filesystem (read/write files), remoteFilesystem (read files on remote machine via SSH), memory (recall/write memories)",
),
});

Expand Down
1 change: 1 addition & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { createDelegateTaskTool } from "./delegateTools.js";
export { getFileSystemTools } from "./fileSystemTools.js";
export { loadPrivateTools } from "./private-loader.js";
export { getRemoteFileSystemTools } from "./remoteFileSystemTools.js";
export { scheduleTools, setSchedulerDeps } from "./scheduleTools.js";
export { execShellTool, shellTools } from "./shellTools.js";
68 changes: 68 additions & 0 deletions src/tools/remoteFileSystemTools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Remote file system tools for SSH-based access to a remote machine
* Uses the same @modelcontextprotocol/server-filesystem via SSH stdio tunnel
* Tools are prefixed with "remote_" to avoid conflicts with local filesystem tools
*/

import { type McpConfig, McpToolset } from "@iqai/adk";
import { getConfig, getRawConfig } from "../config/index.js";

/** Prefix applied to all remote filesystem tool names to distinguish from local tools */
const REMOTE_TOOL_PREFIX = "remote_";

/**
* Get remote filesystem MCP tools via SSH tunnel
* Returns empty array when disabled — zero impact on existing functionality
*/
export async function getRemoteFileSystemTools() {
const rawConfig = getRawConfig();
const remoteFsConfig = rawConfig.tools.remoteFileSystem;

if (!remoteFsConfig?.enabled) {
return [];
}

const mcpConfig: McpConfig = {
name: "Remote Filesystem MCP Client",
description: "Access to files on remote machine via SSH",
transport: {
mode: "stdio",
command: "ssh",
args: [
"-o",
"StrictHostKeyChecking=accept-new",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-critical critical

Using StrictHostKeyChecking=accept-new poses a significant security risk as it makes the SSH connection vulnerable to Man-in-the-Middle (MITM) attacks. This option automatically trusts new host keys without verification. For a more secure implementation, it's recommended to remove this option and require users to add the host key to their known_hosts file manually before the first connection.

"-o",
"BatchMode=yes",
"-i",
remoteFsConfig.sshKeyPath,
"-p",
String(remoteFsConfig.sshPort),
remoteFsConfig.sshHost,
"npx",
"-y",
"@modelcontextprotocol/server-filesystem",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Executing npx -y without pinning the package version creates a supply chain security risk. This command will always fetch the latest version of @modelcontextprotocol/server-filesystem, which could be a compromised version in the future. It is strongly recommended to pin this to a specific, known-good version. Please replace 0.0.0 in the suggestion with the actual version you intend to use.

Suggested change
"@modelcontextprotocol/server-filesystem",
"@modelcontextprotocol/server-filesystem@0.0.0",

...remoteFsConfig.allowedPaths,
],
env: { PATH: process.env.PATH || "" },
},
retryOptions: { maxRetries: 2, initialDelay: 500 },
debug: getConfig().debug,
};

if (getConfig().debug) {
console.log(
`[RemoteFileSystemTools] SSH tunnel to ${remoteFsConfig.sshHost}, paths: ${remoteFsConfig.allowedPaths.join(", ")}`,
);
}

const mcpToolset = new McpToolset(mcpConfig);
const tools = await mcpToolset.getTools();

// Prefix tool names with "remote_" to avoid collisions with local filesystem tools
for (const tool of tools) {
tool.name = `${REMOTE_TOOL_PREFIX}${tool.name}`;
tool.description = `[Remote PC] ${tool.description}`;
}

return tools;
}
Loading