fix(runner): bound SSH output for verified remote files
Allow one maximum-size base64 deliverable through the native SSH command transport while retaining a finite output cap. Exercise the real SSH adapter subprocess limit with exact-size and overflow controls. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
7552b5deaa
commit
2582bf814d
|
|
@ -78,7 +78,7 @@ import {
|
|||
type NativeSessionGoalControl,
|
||||
} from "../../vendor/paperclip-runner/index.js";
|
||||
import type { AdapterExecutionTarget } from "@paperclipai/adapter-utils/execution-target";
|
||||
import { createSshCommandManagedRuntimeRunner } from "@paperclipai/adapter-utils/ssh";
|
||||
import { createNativeSshCommandRunner } from "./native-ssh-command-runner.js";
|
||||
import type { CommandManagedRuntimeRunner } from "@paperclipai/adapter-utils/command-managed-runtime";
|
||||
import {
|
||||
resolvePaperclipRunnerTransport,
|
||||
|
|
@ -9919,7 +9919,7 @@ async function createRunnerdBackendWithinSessionClaim(
|
|||
const remoteTarget = target.kind === "remote" ? target : null;
|
||||
const remoteCommandRunner = remoteTarget
|
||||
? remoteTarget.transport === "ssh"
|
||||
? createSshCommandManagedRuntimeRunner({
|
||||
? createNativeSshCommandRunner({
|
||||
spec: remoteTarget.spec,
|
||||
defaultCwd: remoteTarget.remoteCwd,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import { createHash } from "node:crypto";
|
||||
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createNativeSshCommandRunner } from "./native-ssh-command-runner.js";
|
||||
import { MAX_REMOTE_DELIVERABLE_BYTES, readVerifiedRemoteWorkspaceFile } from "./remote-deliverable-file.js";
|
||||
|
||||
describe("native SSH deliverable output budget", () => {
|
||||
let root: string;
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(join(tmpdir(), "paperclip-ssh-output-"));
|
||||
// Exercise the real SSH command adapter and its execFile output limit,
|
||||
// replacing only the network executable with a deterministic byte source.
|
||||
const executable = join(root, "ssh");
|
||||
await writeFile(executable, `#!${process.execPath}\nprocess.stdout.write(require('node:fs').readFileSync(${JSON.stringify(join(root, "response"))}));\n`);
|
||||
await chmod(executable, 0o700);
|
||||
vi.stubEnv("PATH", `${root}:${process.env.PATH}`);
|
||||
});
|
||||
afterEach(async () => {
|
||||
vi.unstubAllEnvs();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const createRunner = () => createNativeSshCommandRunner({
|
||||
spec: {
|
||||
host: "fixture.invalid", port: 22, username: "fixture",
|
||||
remoteWorkspacePath: "/workspace", remoteCwd: "/workspace",
|
||||
privateKey: null, knownHosts: null, strictHostKeyChecking: true,
|
||||
},
|
||||
defaultCwd: "/workspace",
|
||||
});
|
||||
|
||||
it.each([64, MAX_REMOTE_DELIVERABLE_BYTES])("returns exact verified bytes for a %i-byte file through the SSH adapter", async (byteSize) => {
|
||||
const body = Buffer.alloc(byteSize, 65);
|
||||
await writeFile(join(root, "response"), body.toString("base64"));
|
||||
const result = await readVerifiedRemoteWorkspaceFile({
|
||||
runner: createRunner(), workspaceRoot: "/workspace", contentRef: "result.md",
|
||||
byteSize, sha256: createHash("sha256").update(body).digest("hex"),
|
||||
});
|
||||
expect(result.equals(body)).toBe(true);
|
||||
});
|
||||
|
||||
it("stops a remote command that exceeds the maximum encoded envelope", async () => {
|
||||
await writeFile(join(root, "response"), Buffer.alloc(4 * Math.ceil(MAX_REMOTE_DELIVERABLE_BYTES / 3) + 1, 65));
|
||||
const result = await createRunner().execute({ command: "node", args: ["unused"], timeoutMs: 10_000 });
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { createSshCommandManagedRuntimeRunner } from "@paperclipai/adapter-utils/ssh";
|
||||
import { MAX_REMOTE_DELIVERABLE_BYTES } from "./remote-deliverable-file.js";
|
||||
|
||||
/** The command transport used by a native session on an operator-bound SSH host. */
|
||||
export function createNativeSshCommandRunner(
|
||||
input: Pick<Parameters<typeof createSshCommandManagedRuntimeRunner>[0], "spec" | "defaultCwd">,
|
||||
) {
|
||||
return createSshCommandManagedRuntimeRunner({
|
||||
...input,
|
||||
// The verified reader returns base64. Bound the command output to one
|
||||
// maximum-size encoded file; the adapter's 1 MiB default truncates it.
|
||||
maxBufferBytes: 4 * Math.ceil(MAX_REMOTE_DELIVERABLE_BYTES / 3),
|
||||
});
|
||||
}
|
||||
Loading…
Reference in New Issue