From 854a9e24b67f9662accb1cedc3f7852c3ef82d0c Mon Sep 17 00:00:00 2001 From: Daniel Oon Date: Sun, 30 Aug 2026 02:26:08 +0800 Subject: [PATCH] fix: validate child success flags --- bin/aas.mjs | 12 ++++++++++-- test/stack.test.mjs | 13 +++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/bin/aas.mjs b/bin/aas.mjs index 079fc36..9d12e43 100644 --- a/bin/aas.mjs +++ b/bin/aas.mjs @@ -133,6 +133,14 @@ export function parseJsonOutput(text, label) { throw new Error(`${label} did not return JSON: ${parseError.message}`); } +function booleanField(payload, field, label) { + if (!payload || typeof payload !== "object" || Array.isArray(payload) + || typeof payload[field] !== "boolean") { + throw new Error(`${label} did not return a boolean ${field} field`); + } + return payload[field]; +} + function pythonCandidates() { if (process.platform === "win32") { return [["py", ["-3"]], ["python", []], ["python3", []]]; @@ -195,7 +203,7 @@ export function runDecide( throw childProcessError("decide", result); } const evaluation = parseJsonOutput(result.stdout, "decide"); - return { ok: Boolean(evaluation.passed), raw: evaluation, status: 0 }; + return { ok: booleanField(evaluation, "passed", "decide"), raw: evaluation, status: 0 }; } throw new Error(`Python not found for decide stage${lastError ? ` (${lastError.code ?? "spawn-error"})` : ""}`); } @@ -231,7 +239,7 @@ export function runProve( ); if (result.status !== 0) throw childProcessError("prove", result); const payload = parseJsonOutput(result.stdout, "prove"); - return { ok: Boolean(payload.ok), raw: payload, status: 0 }; + return { ok: booleanField(payload, "ok", "prove"), raw: payload, status: 0 }; } export function createRunId(now = new Date(), nonce = randomUUID()) { diff --git a/test/stack.test.mjs b/test/stack.test.mjs index f327413..5e8e4d3 100644 --- a/test/stack.test.mjs +++ b/test/stack.test.mjs @@ -10,7 +10,9 @@ import { persistRunBundle, resolveComponentProvenance, runAct, + runDecide, runDemo, + runProve, writeAtomicFile, } from "../bin/aas.mjs"; @@ -140,6 +142,17 @@ test("act rejects a zero-exit payload without a valid outcome", () => { ); }); +test("decide and prove reject non-boolean success fields", () => { + const runner = () => ({ + status: 0, + stdout: '{"passed":"false","ok":"false"}\n', + stderr: "", + error: null, + }); + assert.throws(() => runDecide("unused", { runner }), /boolean passed field/); + assert.throws(() => runProve("unused", { runner }), /boolean ok field/); +}); + test("child output parser accepts logged pretty-printed JSON", () => { assert.deepEqual( parseJsonOutput('starting child\n{\n "ok": true,\n "result": { "count": 2 }\n}\n', "fixture"),