mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-16 02:40:39 +09:00
Sanitize remote execution envs at the boundary (#5325)
## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - Adapters spawn CLIs against local, SSH, and sandbox targets, threading a runtime env through `runAdapterExecutionTargetProcess` and the SSH/sandbox runners > - Host identity vars (HOME, TMPDIR, XDG_*, NVM_DIR, PATH) routinely leak into the env we send to remote targets — sometimes via test probes, sometimes via runtime config — and break sandboxed/SSH'd CLIs whose own profiles set those values correctly > - The sanitization logic existed but lived alongside other helpers in `server-utils.ts` and was applied piecemeal at adapter callsites, so it was easy to bypass > - This pull request lifts the sanitization into a standalone `remote-execution-env.ts`, applies it at the SSH and sandbox runtime boundary so every remote spawn goes through it, and removes the duplicated callsite-level filtering > - The benefit is identity-bound host env stops leaking across SSH/sandbox transports regardless of which adapter calls in ## What Changed - `packages/adapter-utils/src/remote-execution-env.ts`: new module — single source of truth for which env keys are identity-bound and how to strip them when the value matches the host's value - `packages/adapter-utils/src/server-utils.ts`: remove the inline sanitization (now in `remote-execution-env.ts`) - `packages/adapter-utils/src/execution-target.ts`: apply sanitization at the sandbox runtime boundary - `packages/adapter-utils/src/ssh.ts`: apply sanitization at the SSH spawn boundary - `packages/adapters/opencode-local/src/server/test.ts`: drop now-redundant callsite filtering - `packages/adapters/pi-local/src/server/test.ts`: drop now-redundant callsite filtering - New tests `execution-target.test.ts` and `execution-target-sandbox.test.ts` cover the sanitizer flow at both transports, including positive cases (host-shaped path stripped) and explicit-override preservation ## Verification - `pnpm vitest run --no-coverage --project @paperclipai/adapter-utils --project @paperclipai/adapter-opencode-local --project @paperclipai/adapter-pi-local` - `pnpm typecheck` clean ## Risks Low–medium. The sanitization is now applied at one layer (boundary) instead of N (callsites), so behavior is more consistent. Any adapter that previously relied on a leaked host var landing on the remote shell would now see it stripped — but those reliances were what this change exists to fix. ## Model Used Claude Opus 4.7 (1M context) ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable — new tests at both transports - [x] If this change affects the UI, I have included before/after screenshots — N/A (no UI) - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge
This commit is contained in:
parent
36eaf9778f
commit
f6bad8f6bf
8 changed files with 306 additions and 72 deletions
|
|
@ -721,6 +721,7 @@ export async function runSshCommand(
|
|||
config: SshConnectionConfig,
|
||||
remoteCommand: string,
|
||||
options: {
|
||||
env?: Record<string, string>;
|
||||
timeoutMs?: number;
|
||||
maxBuffer?: number;
|
||||
} = {},
|
||||
|
|
@ -730,12 +731,33 @@ export async function runSshCommand(
|
|||
const auth = await createSshAuthArgs(config);
|
||||
cleanup = auth.cleanup;
|
||||
const sshArgs = [...auth.args];
|
||||
const envEntries = Object.entries(options.env ?? {})
|
||||
.filter((entry): entry is [string, string] => typeof entry[1] === "string");
|
||||
for (const [key] of envEntries) {
|
||||
if (!isValidShellEnvKey(key)) {
|
||||
throw new Error(`Invalid SSH environment variable key: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Mirror buildSshSpawnTarget: source login profiles first, then run
|
||||
// `env KEY=VAL cmd` so user-supplied identity overrides win over anything
|
||||
// a profile re-exports. Without this, a remote profile that resets HOME
|
||||
// / NVM_DIR / etc. would silently undo the explicit env passed in here.
|
||||
const envArgs = envEntries.map(([key, value]) => `${key}=${shellQuote(value)}`);
|
||||
const remoteScript = [
|
||||
'if [ -f "$HOME/.profile" ]; then . "$HOME/.profile" >/dev/null 2>&1 || true; fi',
|
||||
'if [ -f "$HOME/.bash_profile" ]; then . "$HOME/.bash_profile" >/dev/null 2>&1 || true; fi',
|
||||
'if [ -f "$HOME/.zprofile" ]; then . "$HOME/.zprofile" >/dev/null 2>&1 || true; fi',
|
||||
envArgs.length > 0
|
||||
? `exec env ${envArgs.join(" ")} sh -c ${shellQuote(remoteCommand)}`
|
||||
: `exec sh -c ${shellQuote(remoteCommand)}`,
|
||||
].join(" && ");
|
||||
|
||||
sshArgs.push(
|
||||
"-p",
|
||||
String(config.port),
|
||||
`${config.username}@${config.host}`,
|
||||
remoteCommand,
|
||||
`sh -lc ${shellQuote(remoteScript)}`,
|
||||
);
|
||||
|
||||
return await execFileText("ssh", sshArgs, {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue