Merge pull request #1054 from tyler6204/fix/imsg-remote-attachments

iMessage: Add remote attachment support for VM/SSH deployments
This commit is contained in:
Peter Steinberger
2026-01-17 00:37:21 +00:00
committed by GitHub
6 changed files with 108 additions and 6 deletions

View File

@@ -1,9 +1,11 @@
import { spawn } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { ensureSandboxWorkspaceForSession } from "../../agents/sandbox.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { logVerbose } from "../../globals.js";
import { CONFIG_DIR } from "../../utils.js";
import type { MsgContext, TemplateContext } from "../templating.js";
export async function stageSandboxMedia(params: {
@@ -29,7 +31,11 @@ export async function stageSandboxMedia(params: {
sessionKey,
workspaceDir,
});
if (!sandbox) return;
// For remote attachments without sandbox, use ~/.clawdbot/media (not agent workspace for privacy)
const remoteMediaCacheDir = ctx.MediaRemoteHost ? path.join(CONFIG_DIR, "media", "remote-cache", sessionKey) : null;
const effectiveWorkspaceDir = sandbox?.workspaceDir ?? remoteMediaCacheDir;
if (!effectiveWorkspaceDir) return;
const resolveAbsolutePath = (value: string): string | null => {
let resolved = value.trim();
@@ -46,7 +52,8 @@ export async function stageSandboxMedia(params: {
};
try {
const destDir = path.join(sandbox.workspaceDir, "media", "inbound");
// For sandbox: <workspace>/media/inbound, for remote cache: use dir directly
const destDir = sandbox ? path.join(effectiveWorkspaceDir, "media", "inbound") : effectiveWorkspaceDir;
await fs.mkdir(destDir, { recursive: true });
const usedNames = new Set<string>();
@@ -69,9 +76,15 @@ export async function stageSandboxMedia(params: {
usedNames.add(fileName);
const dest = path.join(destDir, fileName);
await fs.copyFile(source, dest);
const relative = path.posix.join("media", "inbound", fileName);
staged.set(source, relative);
if (ctx.MediaRemoteHost) {
// Always use SCP when remote host is configured - local paths refer to remote machine
await scpFile(ctx.MediaRemoteHost, source, dest);
} else {
await fs.copyFile(source, dest);
}
// For sandbox use relative path, for remote cache use absolute path
const stagedPath = sandbox ? path.posix.join("media", "inbound", fileName) : dest;
staged.set(source, stagedPath);
}
const rewriteIfStaged = (value: string | undefined): string | undefined => {
@@ -111,3 +124,32 @@ export async function stageSandboxMedia(params: {
logVerbose(`Failed to stage inbound media for sandbox: ${String(err)}`);
}
}
async function scpFile(remoteHost: string, remotePath: string, localPath: string): Promise<void> {
return new Promise((resolve, reject) => {
const child = spawn(
"/usr/bin/scp",
[
"-o",
"BatchMode=yes",
"-o",
"StrictHostKeyChecking=accept-new",
`${remoteHost}:${remotePath}`,
localPath,
],
{ stdio: ["ignore", "ignore", "pipe"] },
);
let stderr = "";
child.stderr?.setEncoding("utf8");
child.stderr?.on("data", (chunk) => {
stderr += chunk;
});
child.once("error", reject);
child.once("exit", (code) => {
if (code === 0) resolve();
else reject(new Error(`scp failed (${code}): ${stderr.trim()}`));
});
});
}