Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/safe-bash/scripts/integration-inputs.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,7 @@ function assertSource7Discovery(files) {
"tests/commands/input.test.ts",
"tests/commands/network/mounted-output.test.ts",
"tests/commands/network/aggregate-deadline.test.ts",
"tests/commands/network/response-body-mode.test.ts",
"tests/contracts/value.test.ts",
"tests/shell/value-state.test.ts",
"tests/shell/byte-values.test.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/safe-bash/src/commands/network/curl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ async function transfer(context: CommandContext, args: CurlArguments, input: str
})();
try {
response = await operation.acquire(async () => {
const acquired = await transport({ url: current.href, method, headers, signal,
const acquired = await transport({ url: current.href, method, headers, signal, responseBodyMode: args.head ? "omit" : args.fail ? "omit-on-http-error" : "read",
registerCleanup: operation.registerCleanup, ...policy, ...(upload ? { body: upload } : {}) });
let cleanup: Promise<void> | undefined;
return { ...acquired, dispose() { cleanup ??= Promise.resolve().then(() => acquired.dispose()); return cleanup; } };
Expand Down
4 changes: 4 additions & 0 deletions packages/safe-bash/src/commands/network/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ export interface HttpRequest {
readonly method: string;
readonly headers: HttpHeaders;
readonly body?: ByteSource;
/** Consumer body intent, independent of method. Omitted means "read".
* "omit" permits an empty body; "omit-on-http-error" permits it only for
* status >= 400. Neither changes the HTTP request or response status/headers. */
readonly responseBodyMode?: "omit" | "omit-on-http-error" | "read";
readonly signal: AbortSignal;
readonly registerCleanup?: (cleanup: InvocationCleanup) => void;
readonly denyPrivateNetworks?: true;
Expand Down
113 changes: 113 additions & 0 deletions packages/safe-bash/tests/commands/network/response-body-mode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import assert from "node:assert/strict";
import test from "node:test";
import { Shell, MemoryFileSystem, toByteSource } from "../../../src/index.js";
import { networkCommands, type HttpRequest } from "../../../src/commands/network/index.js";

for (const profile of [
{ args: "", method: "GET", mode: "read" },
{ args: "-i", method: "GET", mode: "read" },
{ args: "-I", method: "HEAD", mode: "omit" },
{ args: "-I -X GET", method: "GET", mode: "omit" },
{ args: "-X HEAD", method: "HEAD", mode: "read" },
{ args: "-f", method: "GET", mode: "omit-on-http-error" },
{ args: "--fail-with-body", method: "GET", mode: "read" },
{ args: "-I -f", method: "HEAD", mode: "omit" },
] as const) {
for (const path of ["direct", "redirect", "retry"] as const) {
test(`curl ${profile.args || "GET"} preserves response body intent across ${path}`, async () => {
const requests: HttpRequest[] = [];
const disposed: number[] = [];
let reads = 0;
const shell = new Shell({ fs: new MemoryFileSystem() }).use(networkCommands({
authorize: () => true,
async transport(request) {
requests.push(request);
const index = requests.length - 1;
const status = index === 0 && path !== "direct" ? path === "redirect" ? 302 : 503 : 200;
return {
status, statusText: "Fixture",
headers: status === 302 ? [["Location", "/next"]] : [],
body: (async function* () { reads++; yield* toByteSource("payload"); })(),
async dispose() { disposed.push(index); },
};
},
}));
try {
const flags = path === "redirect" ? "-L" : path === "retry" ? "--retry 1 --retry-delay 0.001" : "";
const result = await shell.exec(`curl ${profile.args} ${flags} https://offline.invalid/start`);
assert.equal(result.exitCode, 0, result.stderr.toString());
assert.deepEqual(requests.map(({ method, responseBodyMode }) => ({ method, responseBodyMode })),
Array.from({ length: path === "direct" ? 1 : 2 }, () => ({ method: profile.method, responseBodyMode: profile.mode })));
assert.deepEqual(disposed, requests.map((_, index) => index));
assert.equal(reads, profile.mode === "omit" ? 0 : path === "retry" && profile.mode === "read" ? 2 : 1);
assert.equal(result.stdout.toString().includes("payload"), profile.mode !== "omit");
} finally { await shell.dispose(); }
});
}
}

for (const profile of [
{ flags: "-f", method: "GET", mode: "omit-on-http-error", reads: 0, output: true },
{ flags: "--fail-with-body", method: "GET", mode: "read", reads: 1, output: true },
{ flags: "-I -f", method: "HEAD", mode: "omit", reads: 0, output: false },
{ flags: "-I -X GET -f", method: "GET", mode: "omit", reads: 0, output: false },
{ flags: "-I --fail-with-body", method: "HEAD", mode: "omit", reads: 0, output: false },
] as const) {
for (const status of [399, 400, 404, 500]) test(`curl ${profile.flags} handles HTTP ${status} with the requested body intent`, async () => {
const fs = new MemoryFileSystem();
await fs.writeFile("/out", Buffer.from("existing"));
let writes = 0;
let reads = 0;
const writeFile = fs.writeFile.bind(fs);
const writeStream = fs.writeStream.bind(fs);
fs.writeFile = async (...args) => { writes++; await writeFile(...args); };
fs.writeStream = async (...args) => { writes++; await writeStream(...args); };
const requests: HttpRequest[] = [];
const shell = new Shell({ fs }).use(networkCommands({
authorize: () => true,
async transport(request) {
requests.push(request);
return {
status, statusText: "Fixture", headers: [],
body: (async function* () { reads++; yield* toByteSource("payload"); })(),
async dispose() {},
};
},
}));
try {
const result = await shell.exec(`curl ${profile.flags} ${profile.output ? "-o /out" : ""} https://offline.invalid/missing`);
const expectedReads = status < 400 && profile.mode === "omit-on-http-error" ? 1 : profile.reads;
assert.equal(result.exitCode, status >= 400 ? 22 : 0, result.stderr.toString());
assert.deepEqual(requests.map(({ method, responseBodyMode }) => ({ method, responseBodyMode })),
[{ method: profile.method, responseBodyMode: profile.mode }]);
assert.equal(reads, expectedReads);
assert.equal(writes > 0, profile.output && expectedReads > 0);
assert.equal(Buffer.from(await fs.readFile("/out")).toString(), profile.output && expectedReads > 0 ? "payload" : "existing");
} finally { await shell.dispose(); }
});
}

for (const flags of ["-f --fail-with-body", "--fail-with-body -f", "-I -f --fail-with-body", "-I --fail-with-body -f"]) {
test(`curl rejects mutually exclusive ${flags} before authorization or file access`, async () => {
const fs = new MemoryFileSystem();
await fs.writeFile("/out", Buffer.from("existing"));
let authorizations = 0;
let requests = 0;
let writes = 0;
const writeFile = fs.writeFile.bind(fs);
const writeStream = fs.writeStream.bind(fs);
fs.writeFile = async (...args) => { writes++; await writeFile(...args); };
fs.writeStream = async (...args) => { writes++; await writeStream(...args); };
const shell = new Shell({ fs }).use(networkCommands({
authorize: () => { authorizations++; return true; },
async transport() { requests++; throw new Error("unexpected transport"); },
}));
try {
const result = await shell.exec(`curl ${flags} -o /out https://offline.invalid/missing`);
assert.equal(result.exitCode, 2);
assert.ok(result.stderr.toString().includes("mutually exclusive"));
assert.deepEqual({ authorizations, requests, writes }, { authorizations: 0, requests: 0, writes: 0 });
assert.equal(Buffer.from(await fs.readFile("/out")).toString(), "existing");
} finally { await shell.dispose(); }
});
}
1 change: 1 addition & 0 deletions scripts/fixtures/safe-packages-browser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Shell, agentCommands, createAgentCommands, createMemoryFileSystem, eval
import { checksumWorkflows, expectedAgentCommandNames, nullDeviceWorkflows, runNestedCommands, verifyCmpCommands, verifyFmtCommands, verifyShufCommands, verifyNumfmtCommands, verifyTruncateCommands, verifyZipCommands, verifyCsplitCommands, verifyNullDeviceView } from "./safe-packages-mixed-entry-runtime.mjs";
import { FsError as CoreFsError, createDeviceFileSystem } from "@poe-platform/safe-fs/core";
import { FsError as CompatibilityFsError } from "@poe-platform/safe-js/fs/core";
import "./safe-packages-response-body-mode.mjs";

if (FsError !== CoreFsError) throw new Error("Browser filesystem identity diverged");
if (FsError !== CompatibilityFsError) throw new Error("Compatibility filesystem identity diverged");
Expand Down
70 changes: 70 additions & 0 deletions scripts/fixtures/safe-packages-response-body-mode.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Shell, createMemoryFileSystem } from "@poe-platform/safe-bash";
import { networkCommands } from "@poe-platform/safe-bash/commands/network";

for (const [flags, method, mode, errorExit] of [
["", "GET", "read", 0], ["-I", "HEAD", "omit", 0],
["-I -X GET", "GET", "omit", 0], ["-X HEAD", "HEAD", "read", 0],
["-f", "GET", "omit-on-http-error", 22], ["--fail-with-body", "GET", "read", 22],
["-I -f", "HEAD", "omit", 22], ["-I --fail-with-body", "HEAD", "omit", 22],
]) {
for (const scenario of ["direct", "redirect", "non-error", "error"]) {
let calls = 0;
let reads = 0;
let disposed = 0;
let writes = 0;
const fs = createMemoryFileSystem();
await fs.writeFile("/out", new Uint8Array([9]));
const writeFile = fs.writeFile.bind(fs);
const writeStream = fs.writeStream.bind(fs);
fs.writeFile = async (...args) => { writes++; await writeFile(...args); };
fs.writeStream = async (...args) => { writes++; await writeStream(...args); };
const output = scenario === "error" && mode !== "omit";
const expectedReads = mode === "omit" || (scenario === "error" && mode === "omit-on-http-error") ? 0 : 1;
const shell = new Shell({ fs }).use(networkCommands({
authorize: () => true,
async transport(request) {
if (request.method !== method || request.responseBodyMode !== mode) {
throw new Error(`Unexpected body intent: ${JSON.stringify({ method: request.method, mode: request.responseBodyMode })}`);
}
calls++;
const status = scenario === "error" ? 400 : scenario === "non-error" ? 399 : scenario === "redirect" && calls === 1 ? 302 : 200;
return {
status, statusText: "Fixture", headers: status === 302 ? [["Location", "/next"]] : [],
body: (async function* () { reads++; yield new Uint8Array([1, 2, 255]); })(),
async dispose() { disposed++; },
};
},
}));
try {
const result = await shell.exec(`curl ${flags} ${scenario === "redirect" ? "-L" : ""} ${output ? "-o /out" : ""} https://offline.invalid/start`);
if (result.exitCode !== (scenario === "error" ? errorExit : 0) || calls !== (scenario === "redirect" ? 2 : 1) || disposed !== calls || reads !== expectedReads) {
throw new Error(`Packed curl body intent failed: ${JSON.stringify({ flags, scenario, exitCode: result.exitCode, calls, reads, disposed })}`);
}
if ((writes > 0) !== (output && expectedReads > 0)) throw new Error("Unexpected output-file acquisition");
const bytes = [...await fs.readFile("/out")].join(",");
if (bytes !== (output && expectedReads > 0 ? "1,2,255" : "9")) throw new Error("Unexpected output-file bytes");
} finally { await shell.dispose(); }
}
}

for (const flags of ["-f --fail-with-body", "--fail-with-body -f", "-I -f --fail-with-body", "-I --fail-with-body -f"]) {
const fs = createMemoryFileSystem();
await fs.writeFile("/out", new Uint8Array([9]));
let effects = 0;
const writeFile = fs.writeFile.bind(fs);
const writeStream = fs.writeStream.bind(fs);
fs.writeFile = async (...args) => { effects++; await writeFile(...args); };
fs.writeStream = async (...args) => { effects++; await writeStream(...args); };
const shell = new Shell({ fs }).use(networkCommands({
authorize: () => { effects++; return true; },
async transport() { effects++; throw new Error("Unexpected transport"); },
}));
try {
const result = await shell.exec(`curl ${flags} -o /out https://offline.invalid/missing`);
if (result.exitCode !== 2 || effects !== 0 || [...await fs.readFile("/out")].join(",") !== "9") {
throw new Error("Conflicting failure flags must reject before network or file effects");
}
} finally { await shell.dispose(); }
}

console.log("Packed curl preserves body intent, HTTP-error omission and conflicting-flag admission");
1 change: 1 addition & 0 deletions scripts/fixtures/safe-packages-smoke.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import "./safe-packages-named-mutations.mjs";
import "./safe-packages-console-override.mjs";
import "./safe-packages-callback-phases.mjs";
import "./safe-packages-curl-output.mjs";
import "./safe-packages-response-body-mode.mjs";
import { Budget, run } from "@poe-platform/safe-js";
import { FsError, createDeviceFileSystem, createMemoryFileSystem, createReadOnlyFileSystem } from "@poe-platform/safe-fs";
import { FsError as CompatibilityFsError } from "@poe-platform/safe-js/fs";
Expand Down
Loading