diff --git a/bin/aas.mjs b/bin/aas.mjs index c7fa65e..cd8d86d 100644 --- a/bin/aas.mjs +++ b/bin/aas.mjs @@ -1424,18 +1424,17 @@ export function writeAtomicFile( const temporary = `${target}.${process.pid}.${randomUUID()}.tmp`; let written = false; try { - writeFile(temporary, data, { encoding: "utf8", flag: "wx" }); + const fd = openSync(temporary, "wx"); written = true; + try { writeFile(fd, data, { encoding: "utf8" }); } + finally { closeSync(fd); } if (replace) rename(temporary, target); else link(temporary, target); } catch (error) { throw error; } finally { if (written) { - // Best-effort cleanup. Only attempt unlink when we know the - // temporary was created; if the write itself failed, the file - // does not exist and unlink would just throw ENOENT we have to - // swallow. Rethrow the original error above either way. + // Clean up only the temporary we created, including partial writes. try { unlink(temporary); } catch (cleanupError) { diff --git a/docs/release-readiness.md b/docs/release-readiness.md index 0d4574a..8fb0a50 100644 --- a/docs/release-readiness.md +++ b/docs/release-readiness.md @@ -14,8 +14,8 @@ Local checks may install dependencies and write caches or temporary files; see - [ ] Review the final raw tree and changed-file list. - [ ] Confirm `stack-lock.json` still contains the approved public URLs and commits: - Constitutional Agent Testbench: `16b2faa71b0f92b9afa15b13afad8c48da8132f4` - - Consequence Rail: `9f60ab3223970c22371c20d3584e8330674d997c` ([PR #67](https://github.com/EauDoon/consequence-rail/pull/67)) - - MandateBound: `b51fe137958afe26eee052c5a129e5481ccae560` ([PR #102](https://github.com/EauDoon/mandatebound/pull/102)) + - Consequence Rail: `c430383c0a0931f0dcf17845d6f0e8ccf328615a` ([PR #68](https://github.com/EauDoon/consequence-rail/pull/68)) + - MandateBound: `708256d4e48babeb13079fac7589d172920a5c95` ([PR #103](https://github.com/EauDoon/mandatebound/pull/103)) - [ ] Run bootstrap from a clean workspace and verify detached, clean, exact dependency checkouts. - [ ] Confirm no private repository, credential, or production endpoint is referenced. diff --git a/docs/stack-lock.md b/docs/stack-lock.md index 1b7d37b..e2b5d60 100644 --- a/docs/stack-lock.md +++ b/docs/stack-lock.md @@ -69,6 +69,13 @@ synthetic evidence whose currency contradicts the proposal. The historical MandateBound `e526c4c`; it pins replay compatibility for an ordinary synthetic refund. Its public demonstration signatures establish no real-world provenance. +The current Rail pin also binds a receipt's close time to its terminal event, +so a clock advancing between reads still produces evidence that survives the +same-case handoff and replay. The MandateBound pin rejects weak Ed25519 keys in +caller-pinned CasePack checkpoint trust snapshots using its existing strict key +validator. Ordinary valid evidence retains the same format; neither update +rewrites old artifacts. The testbench runtime pin is unchanged. + An older artifact with numeric-looking object keys may contain signatures made with the former Rail canonical ordering. The current verifier does not try that obsolete ordering after verification fails. Preserve the original artifact and diff --git a/stack-lock.json b/stack-lock.json index c86d915..960f062 100644 --- a/stack-lock.json +++ b/stack-lock.json @@ -13,7 +13,7 @@ { "name": "consequence-rail", "repository": "https://github.com/EauDoon/consequence-rail.git", - "commit": "9f60ab3223970c22371c20d3584e8330674d997c", + "commit": "c430383c0a0931f0dcf17845d6f0e8ccf328615a", "expected_entrypoints": [ "package.json", "cmd/crctl.js" @@ -22,7 +22,7 @@ { "name": "mandatebound", "repository": "https://github.com/EauDoon/mandatebound.git", - "commit": "b51fe137958afe26eee052c5a129e5481ccae560", + "commit": "708256d4e48babeb13079fac7589d172920a5c95", "expected_entrypoints": [ "package.json", "package-lock.json", diff --git a/test/component-compatibility.test.mjs b/test/component-compatibility.test.mjs index 575d16d..e338ca1 100644 --- a/test/component-compatibility.test.mjs +++ b/test/component-compatibility.test.mjs @@ -3,7 +3,7 @@ import { readFileSync } from "node:fs"; import test from "node:test"; import { replayBundle, runProveRail } from "../bin/aas.mjs"; import { canonicalJson, digest } from "../deps/consequence-rail/src/canonical.js"; -import { createDemoRuntime, buildRefundProposal, runRefundDemo } from "../deps/consequence-rail/src/demo.js"; +import { createDemoRuntime, buildRefundProposal, prepareRefund, runRefundDemo } from "../deps/consequence-rail/src/demo.js"; import { validateSettlementBundle } from "../deps/consequence-rail/src/bundle-validation.js"; import { MemoryEventStore } from "../deps/consequence-rail/src/event-store.js"; import { createDemoSigner, signArtifact } from "../deps/consequence-rail/src/signing.js"; @@ -63,3 +63,20 @@ test("ordinary synthetic case exported by the previous pins still replays", () = assert.equal(result.ok, true, JSON.stringify(result)); assert.ok(result.checks.every((check) => check.passed)); }); + +test("moving-clock rail evidence survives the same-case handoff and replay", async () => { + let tick = Date.parse("2035-01-01T00:00:00.000Z"); + const runtime = createDemoRuntime({ clock: { now: () => new Date(tick++).toISOString() } }); + const { actionId } = prepareRefund(runtime); + await runtime.rail.execute(actionId, { fault: "duplicate" }); + await runtime.rail.verifyOutcome(actionId); + await runtime.rail.remediate(actionId); + const bundle = runtime.rail.exportBundle(actionId, { profile: "audit" }); + const proved = runProveRail(bundle); + assert.equal(proved.ok, true); + const replayed = replayBundle({ + report: { run_id: "moving-clock" }, + stages: { act: { action_id: actionId, rail_bundle: bundle }, prove: proved.raw }, + }); + assert.equal(replayed.ok, true, JSON.stringify(replayed)); +}); diff --git a/test/stack.test.mjs b/test/stack.test.mjs index 29815dc..695df46 100644 --- a/test/stack.test.mjs +++ b/test/stack.test.mjs @@ -60,7 +60,7 @@ const PROVENANCE = [ { name: "consequence-rail", repository: "https://github.com/EauDoon/consequence-rail.git", - commit: "9f60ab3223970c22371c20d3584e8330674d997c", + commit: "c430383c0a0931f0dcf17845d6f0e8ccf328615a", origin: "https://github.com/EauDoon/consequence-rail.git", detached: true, clean: true, @@ -69,7 +69,7 @@ const PROVENANCE = [ { name: "mandatebound", repository: "https://github.com/EauDoon/mandatebound.git", - commit: "b51fe137958afe26eee052c5a129e5481ccae560", + commit: "708256d4e48babeb13079fac7589d172920a5c95", origin: "https://github.com/EauDoon/mandatebound.git", detached: true, clean: true, @@ -783,10 +783,16 @@ test("act and prove child-process errors keep the run isolated", async () => { test("atomic writes leave no partial target or temporary file after a write failure", () => { const outputRoot = tempRoot(); const target = join(outputRoot, "atomic", "manifest.json"); + mkdirSync(join(outputRoot, "atomic")); + writeFileSync(target, "original evidence"); assert.throws(() => writeAtomicFile(target, "payload", { - writeFile: () => { throw new Error("simulated partial write"); }, + writeFile: (fd) => { + writeFileSync(fd, "partial evidence"); + throw new Error("simulated partial write"); + }, }), /partial write/); - assert.equal(readdirSync(join(outputRoot, "atomic")).length, 0); + assert.equal(readFileSync(target, "utf8"), "original evidence"); + assert.deepEqual(readdirSync(join(outputRoot, "atomic")), ["manifest.json"]); }); test("persistRunBundle refuses a final-directory collision", () => {