Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ project adheres to [Semantic Versioning](https://semver.org/).
- Playwright setup note in the Quick start section of `README.md`.

### Fixed
- The act stage now reads the rail bundle file with the same byte cap and
symlink refusal as a saved case. The previous read followed a link and
accepted a file larger than the child stdout cap.
- Bootstrap and provenance now refuse a `deps` directory that is a symlink.
Checkout and later reads followed the link, so a planted `deps` link was
accepted as the component root.
Expand Down
4 changes: 3 additions & 1 deletion bin/aas.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,9 @@ export function runAct(
}
if (scratch !== null) {
try {
payload.rail_bundle = JSON.parse(readFileSync(bundlePath, "utf8"));
// Same bounds as a saved case file. readFileSync followed a symlink
// and accepted a bundle larger than the child stdout cap.
payload.rail_bundle = readBoundedCaseJson(bundlePath);
} catch (error) {
throw attachChildDiagnostics(
new Error(`act did not persist a readable rail bundle: ${error.message}`),
Expand Down
28 changes: 28 additions & 0 deletions test/stack.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1251,6 +1251,34 @@ test("runAct persists the rail bundle only when asked", () => {
assert.equal("rail_bundle" in plain.raw, false);
});

test("runAct rejects a rail bundle file past the child output cap", () => {
const runner = (_bin, args) => {
const out = args[args.indexOf("--out") + 1];
writeFileSync(out, Buffer.alloc(CHILD_JSON_LIMIT + 1, 0x78));
return { status: 0, stdout: '{"outcome":"settled","state":"CLOSED","fault":"none","action_id":"a"}\n', stderr: "", error: null };
};
assert.throws(
() => runAct("none", { depsDir: "deps", runner, persistRailBundle: true }),
/byte limit/,
);
});

test("runAct does not follow a symlinked rail bundle file", (t) => {
const probe = tempRoot();
if (!linkOrSkip(t, join(probe, "missing"), join(probe, "link"))) return;
const runner = (_bin, args) => {
const out = args[args.indexOf("--out") + 1];
const target = join(out, "..", "target.json");
writeFileSync(target, JSON.stringify({ action: { action_id: "leaked" }, settlement_receipt: { outcome: "settled" } }));
symlinkSync(target, out);
return { status: 0, stdout: '{"outcome":"settled","state":"CLOSED","fault":"none","action_id":"a"}\n', stderr: "", error: null };
};
assert.throws(
() => runAct("none", { depsDir: "deps", runner, persistRailBundle: true }),
/symbolic link/,
);
});

test("demo rail mode records the review binding and fails closed without a bundle", async () => {
const outputRoot = mkdtempSync(join(tmpdir(), "agent-action-stack-rail-"));
const bundle = railBundleFixture();
Expand Down
Loading