Skip to content

Commit 5c2b87f

Browse files
committed
fix(auto-reply): resolve scp via PATH instead of hardcoded /usr/bin/scp (#78677)
stageRemoteFileIntoRoot -> scpFile spawned /usr/bin/scp directly, which is POSIX-FHS-only and fails with ENOENT on Windows native (OpenSSH ships scp.exe outside /usr/bin), Homebrew/Nix prefixes, and any layout where scp lives elsewhere on PATH. Pass the bare command 'scp' so child_process.spawn resolves the OS-appropriate binary via PATH while keeping the existing BatchMode + StrictHostKeyChecking + '--' separator argv (no shell expansion, normalizeScpRemoteHost/normalizeScpRemotePath still guard host/path inputs, no widened attack surface). Adds a targeted regression test that asserts spawn is called with 'scp' (not '/usr/bin/scp') so a future revert reintroduces the cross-platform regression visibly.
1 parent 8e17910 commit 5c2b87f

3 files changed

Lines changed: 39 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ Docs: https://docs.openclaw.ai
142142

143143
### Fixes
144144

145+
- Auto-reply/staging: spawn `scp` via PATH lookup instead of a hardcoded `/usr/bin/scp` path so remote inbound media staging works on Windows native (`OpenSSH\scp.exe`), Homebrew/Nix, and other non-FHS layouts. Fixes #78677.
145146
- Gateway/sessions: clear cached skills snapshots during `/new` and `sessions.reset` so long-lived channel sessions rebuild the visible skill list after skills change. (#78873) Thanks @Evizero.
146147
- fix(auto-reply): gate inline skill tool dispatch [AI]. (#78517) Thanks @pgondhi987.
147148
- Canvas plugin: keep legacy root `canvasHost` configs valid until `openclaw doctor --fix` migrates them into `plugins.entries.canvas.config.host`, move Canvas/A2UI clients to gateway protocol v4 plugin surfaces, and refresh the generated A2UI bundle hash so normal builds stay clean.

src/auto-reply/reply.stage-sandbox-media.scp-remote-path.test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,35 @@ describe("stageSandboxMedia scp remote paths", () => {
117117
}
118118
});
119119
});
120+
121+
it("invokes scp via PATH instead of a hardcoded /usr/bin/scp absolute path (#78677)", async () => {
122+
await withSandboxMediaTempHome("openclaw-triggers-", async (home) => {
123+
const { cfg, workspaceDir, sessionKey } = createRemoteStageParams(home);
124+
const remotePath = "/Users/demo/Library/Messages/Attachments/ab/cd/photo.jpg";
125+
const { ctx, sessionCtx } = createRemoteContexts(remotePath);
126+
childProcessMocks.spawn.mockImplementation(() => {
127+
// Stop before any actual ssh handshake. We only assert spawn argv.
128+
throw new Error("stop before scp");
129+
});
130+
131+
await stageSandboxMedia({
132+
ctx,
133+
sessionCtx,
134+
cfg,
135+
sessionKey,
136+
workspaceDir,
137+
});
138+
139+
// OpenSSH lives outside `/usr/bin` on Windows native (typically
140+
// `C:\\Windows\\System32\\OpenSSH\\scp.exe`) and on Homebrew/Nix-based
141+
// macOS/Linux installs. The fix routes through PATH lookup by passing
142+
// the bare command name `scp` so `child_process.spawn` resolves the
143+
// OS-appropriate binary instead of failing with `ENOENT` on a hardcoded
144+
// FHS path.
145+
expect(childProcessMocks.spawn).toHaveBeenCalled();
146+
const command = childProcessMocks.spawn.mock.calls[0]?.[0];
147+
expect(command).toBe("scp");
148+
expect(command).not.toBe("/usr/bin/scp");
149+
});
150+
});
120151
});

src/auto-reply/reply/stage-sandbox-media.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,8 +318,14 @@ async function scpFile(remoteHost: string, remotePath: string, localPath: string
318318
throw new Error("invalid remote path for SCP");
319319
}
320320
return new Promise((resolve, reject) => {
321+
// Resolve `scp` via PATH so OpenSSH installs that live outside `/usr/bin`
322+
// (Windows OpenSSH, Homebrew, Nix, custom prefixes) can stage remote
323+
// attachments. The argv (BatchMode + StrictHostKeyChecking + `--` separator)
324+
// already prevents shell-injection, so PATH lookup does not widen the
325+
// attack surface beyond what the surrounding `normalizeScpRemote*` helpers
326+
// already guard. (#78677)
321327
const child = spawn(
322-
"/usr/bin/scp",
328+
"scp",
323329
[
324330
"-o",
325331
"BatchMode=yes",

0 commit comments

Comments
 (0)