fix(pi): close stdin before remote print-mode turns

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-10 06:10:32 -05:00
parent 04242dd77e
commit 65127dff98
3 changed files with 24 additions and 3 deletions

View File

@ -135,6 +135,9 @@ describe("pi remote execution", () => {
const options = processSpy.mock.calls[0][4];
expect(options.env.HOME).toBe(workFolderHome ?? runtimeRootDir);
expect(options.env.PAPERCLIP_WORKSPACE_CWD).toBe("/home/daytona/repos/main");
// Pi consumes piped stdin before starting its turn, even with a prompt argument.
// An empty input asks the sandbox transport to deliver EOF instead of an open pipe.
expect(options.stdin).toBe("");
} finally {
vi.restoreAllMocks();
}

View File

@ -723,6 +723,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
const proc = await runAdapterExecutionTargetProcess(runId, runtimeExecutionTarget, command, args, {
cwd,
env: executionTargetIsRemote ? env : runtimeEnv,
// Pi reads piped stdin before starting print mode. Remote transports must
// deliver EOF; leaving the session input pipe open stalls the first turn.
stdin: executionTargetIsRemote ? "" : undefined,
timeoutSec,
graceSec,
onSpawn,

View File

@ -1503,6 +1503,21 @@ describe("Daytona sandbox provider plugin", () => {
code: "PROCESS_NOT_FOUND", statusCode: 404,
});
it("redirects empty stdin so a session command receives EOF", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox();
mockGet.mockResolvedValue(sandbox);
await plugin.definition.onEnvironmentExecute?.(sessionExecParams({ stdin: "" }));
expect(sandbox.fs.uploadFile).toHaveBeenCalledWith(
Buffer.alloc(0), expect.stringMatching(/^\/tmp\/paperclip-stdin-/), 1,
);
const [, params] = sandbox.process.executeSessionCommand.mock.calls[0]!;
expect(params.command).toMatch(/< '\/tmp\/paperclip-stdin-/);
expect(sandbox.fs.deleteFile).toHaveBeenCalledWith(expect.stringMatching(/^\/tmp\/paperclip-stdin-/));
});
it("replaces a disappeared session only when dispatch was rejected", async () => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox();
@ -2272,7 +2287,7 @@ describe("Daytona sandbox provider plugin", () => {
expect(mockGet).toHaveBeenCalledTimes(1);
});
it("stages stdin in the sandbox filesystem when execution needs redirected input", async () => {
it.each(["input payload", ""])("stages redirected stdin, including EOF-only input (%j)", async (stdin) => {
process.env.DAYTONA_API_KEY = "host-key";
const sandbox = createMockSandbox();
mockGet.mockResolvedValue(sandbox);
@ -2290,12 +2305,12 @@ describe("Daytona sandbox provider plugin", () => {
command: "cat",
args: [],
cwd: "/workspace",
stdin: "input payload",
stdin,
timeoutMs: 1000,
});
expect(sandbox.fs.uploadFile).toHaveBeenCalledWith(
Buffer.from("input payload", "utf8"),
Buffer.from(stdin, "utf8"),
expect.stringMatching(/^\/tmp\/paperclip-stdin-/),
1,
);