fix: refresh preserved harness receipt facts without copying archives (#20322)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - Volume-free deployment must preserve historical controller files
before removing the old disk.
> - A WEB preservation receipt binds an archive and source files to the
selected session and database facts.
> - Later metadata changes can make those facts stale while the
preserved files and archive remain unchanged.
> - The existing owner refuses every stale receipt, so preservation
cannot converge.
> - This pull request refreshes only facts freshness after it verifies
the same source bytes, checkpoints and archive.
> - Existing locks and an old-receipt digest comparison prevent a
concurrent replacement from being overwritten.

## Linked Issues or Issue Description

Refs #19814 for WEB evidence preservation without settling remote
recovery. Refs #19832 for bounded larger inventories. Related harness
receipt PRs were searched; no existing change provides this guarded
refresh.

**What happened?**

A valid WEB preservation receipt becomes stale after later run metadata
changes. Its original-volume owner refuses another preservation attempt
because an invalid receipt already exists. This leaves an eligibility
blocker even when the source and saved archive are unchanged.

**Expected behavior**

The original-volume owner can refresh stale facts only after it proves
every other persisted binding unchanged. It retains the prior receipt
and remote recovery state.

**Steps to reproduce**

1. Preserve a settled session's WEB files and controller evidence.
2. Add later run metadata so its database facts digest changes.
3. Preserve the same scope again. The previous implementation refuses at
the existing-receipt check.

**Paperclip version or commit**

The failure was reproduced on master
`12523fb677585fcd6da929f868014514a4e96e87`. This branch includes master
`1b7fe33432016b3b27ccdfbf7b8944da4b40726d`.

**Deployment mode**

Managed Linux server with PostgreSQL and durable object storage.

## What Changed

- Reuse the exact prior archive for existing WEB receipts. Recheck
current source bytes, controller history, checkpoint material and
archive bindings. Read back ciphertext and verify its size and hash.
- Permit only database/local-observation freshness changes. Selected
session, source run, controller, archive, WEB entries, and assets must
still match the prior proof. Local inode and timestamp observations may
change across attempts; preserved bytes, paths and modes must match.
- Recheck current facts and source files under the existing finalizer,
task and run fences. Accept a matching concurrent winner or require the
exact previous receipt digest before replacement.
- Retain the prior database and local facts digests, witness
fingerprint, observation time and receipt digest in the same
company-scoped activity transaction. Require those four fields to
reconstruct the exact prior proof from unchanged current bindings before
replacement. Normal audit redaction stays enabled.
- Preserve current-key checks and archive reads on ordinary idempotent
calls. Keep active-source, changed-evidence and cancellation refusals.
- Document the refresh contract. No schema, saved-run format or public
API change.

## Verification

- Genuine RED: the new real PostgreSQL/encrypted-archive case failed at
the old previous-invalid gate.
- Before the audit review follow-up, the full local owner suite passed
**94/94 integration cases in 50.01 seconds**. It covers same-archive
refresh with no PUT, audit history, current-key failures, unreadable
archives, cancellation, changed files, replaced volume, active sources,
matching concurrent winners and conflicting receipt replacement.
- The cases also cover a source becoming active during an idempotent
readback. Earlier 91- and 93-case runs preceded these final review
guards and are not the final result.
- Server typecheck, module boundaries and diff checks passed on the
final production/test bytes before the clean master merge. Required CI
supplies final-head breadth. Four narrow post-merge
refresh/current-key/idempotent regressions passed in 5.70 seconds on
`82e270f29fff98442c4184b96082f72f18a88721`.
- The fixtures use a real disposable PostgreSQL database and actual
local encrypted archives/storage. Controlled faults affect the stated
storage or concurrency boundary. They do not prove a hosted refresh or
archive plaintext cold restore.
- The existing integration file now exceeds 1,000 lines. Keeping these
cases with its private original-volume, controller, archive and
PostgreSQL fixture avoids duplicating that setup.
- Initial worktree preflight stalled on the global package-manager
wrapper. Preflight completed with the existing pinned toolchain. No
setup failure is counted as a product test pass.
- Review found that activity redaction could change a full prior-receipt
copy. A new regression enabled the real username censor and reproduced
that mismatch. The fix stores only the four changed scalar fields and
their original proof digest; a prepublication canonical reconstruction
guard rejects dropped or unknown fields.
- On the audit follow-up, 16 affected real PostgreSQL cases passed in
11.98 seconds; 80 unchanged cases were intentionally skipped. They
include real redaction with a username-containing WEB path,
reconstruction across two refreshes, unknown prior fields, CAS, active
sources, current-key failures, unavailable archives and cancellation.
Final server typecheck and boundaries passed. This was an intermediate
revision; the final-head results appear below.
- A second review found that comparing old inode/timestamp observations
also broke ordinary valid retries. Three real cases reproduced
valid-receipt touch/inode failures and stale-facts touch failure. The
fix permits metadata freshness changes across attempts while keeping
exact byte/path/mode/checkpoint/archive coverage and the existing locked
within-attempt metadata fence.
- Final local owner suite on the metadata follow-up: **102/102
integration cases passed in 44.04 seconds**, including valid
touch/new-inode retries, stale refresh with the same archive and no PUT,
four-scalar audit reconstruction, changed bytes/mode/entries, and a
touch during archive readback that must refuse. Server typecheck, module
boundaries and diff checks passed. The final-head CI and review results
appear below.
- Final head `c81475c927d0862dcb8d2c2356dc41ab4a27e63f`: [PR workflow
attempt
5](https://github.com/paperclipai/paperclip/actions/runs/42667287803/attempts/5)
completed successfully at 19:24:28 UTC on 2026-11-09. All 51 jobs are
accounted for: 49 passed and 2 were skipped. The actual [final ci /
verify](https://github.com/paperclipai/paperclip/actions/runs/42667287803/job/129382061558)
and [E2E
aggregate](https://github.com/paperclipai/paperclip/actions/runs/42667287803/job/129381486342)
passed. Typecheck, build and the selected test lanes passed. The
exact-head rollup is successful with no pending or failed checks.
- Attempt 5 resolved both trusted workflows to
`3a2f71db31aa99633610dc241f55f34ac65124de`, after #20339 merged. Its
routing gate passed and real jobs started. This verifies successful
startup and completion of this rerun; it does not exercise the
empty-runner fallback because routing succeeded.
- Earlier CI failures on `82e270f29fff98442c4184b96082f72f18a88721` and
attempts 1–4 of this final-head workflow remain nonpasses. Attempt 1 had
interrupted leaf jobs and failed aggregates. Attempts 2 and 3 ended in
workflow failure without a final verify job. Attempt 4 failed startup
with zero jobs and empty-runner errors. Those failures are not counted
as passes or labeled unrelated.
-
[Greptile](https://github.com/paperclipai/paperclip/pull/20322#issuecomment-6746395090)
and
[Ponytail](https://github.com/paperclipai/paperclip/pull/20322#issuecomment-6746327827)
reviewed the unchanged final head at **5/5**. Both historical review
threads are resolved; no inline thread remains open.
- Hosted application to an actual stale scope remains pending normal
release delivery.

## Risks

- Refresh must not bless different preservation evidence. The
unchanged-binding checks, fresh file and checkpoint proof, exact archive
reuse, source capability and locked receipt comparison remain mandatory.
- A fresh capability has a new witness fingerprint and expiry. The audit
retains the old fingerprint, database/local facts digests and
observation time. All unchanged proof bindings remain on the current
receipt. Their canonical reconstruction must match the old digest. The
new capability is validated at entry and publication.
- Readback verifies ciphertext size/hash and current-key checkpoint
material. This change does not claim a new archive GCM/plaintext
restore.
- Archive identity and rows remain unchanged, so retention ownership
does not change. Failed refreshes retain the previous receipt, original
files and unresolved remote recovery obligations.
- Older receipt shapes are not rewritten automatically. Normal
validation and named preservation refusals remain in force.

## Model Used

OpenAI Codex, GPT-6.1 Sol (`gpt-6.1-sol`), with reasoning, repository
tools and code execution. Context-window size is not exposed by the
runtime.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
This commit is contained in:
Dotta authored and GitHub committed 2026-11-09 13:26:27 -06:00
1 parent 908d2aae7a
commit 0eacad0d0c
3 files changed
+262 -13

No files matched your search

+19
View File
@@ -122,6 +122,25 @@ original-volume anchor or filesystem read. These validations add bounded
per-retained-scope database reads during startup. They do not add a worker or a
poll. Missing or invalid byte evidence remains an enrollment blocker.
The original-volume owner can refresh a WEB receipt whose database facts digest
has become stale only when fresh source inspection, current-key checkpoint
validation and ciphertext hash/size readback prove the preserved bindings unchanged.
It reuses the same archive and takes the existing finalizer, task and run fences before
comparing the prior receipt digest and replacing it. The company activity log
retains the prior database and local facts digests, source witness fingerprint,
observation time and receipt digest. Those four changed fields must reconstruct
the exact prior proof from the unchanged current bindings before publication. No source paths
enter the audit delta; normal activity redaction remains enabled. Changed
evidence, concurrent invalid receipts and active sources remain blocked; remote
recovery obligations are unchanged.
Local inode and timestamp observations may change between preservation attempts
without changing byte coverage. Valid receipts remain idempotent after those
changes; stale database facts can refresh with the same archive and fresh local
observations. During one attempt, the local metadata digest must remain stable
from capture through the locked publication check. Changed bytes, paths or modes
still invalidate preservation evidence.
Receipt validation adds bounded per-scope database reads to startup eligibility.
It does not add a recurring scan or network work. Qualification must measure the
selected cohort's startup latency on the serving revision before enrollment.
@@ -1,12 +1,12 @@
import { createHash, randomUUID } from "node:crypto";
import { chmod, cp, lstat, mkdir, mkdtemp, readFile, realpath, rename, rm, symlink, truncate, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { chmod, copyFile, cp, lstat, mkdir, mkdtemp, readFile, realpath, rename, rm, symlink, truncate, utimes, writeFile } from "node:fs/promises";
import { tmpdir, userInfo } from "node:os";
import { basename, join } from "node:path";
import { and, eq } from "drizzle-orm";
import type { CompanySkillVersionFileInventoryEntry } from "@paperclipai/shared";
import { afterAll, afterEach, beforeAll, expect, it, vi } from "vitest";
import { activityLog, agentTaskSessions, agents, companies, companySkills, companySkillVersions, companySkillRepositorySnapshots, createDb, environmentLeases,
heartbeatRuns, issues, nativeHarnessBackups, nativeRunnerCheckpoints, nativeRunFinalizations, nativeRunResults } from "@paperclipai/db";
heartbeatRuns, instanceSettings, issues, nativeHarnessBackups, nativeRunnerCheckpoints, nativeRunFinalizations, nativeRunResults } from "@paperclipai/db";
import { describeEmbeddedPostgres } from "../../__tests__/helpers/native-restart.js";
import { startEmbeddedPostgresTestDatabase } from "../../__tests__/helpers/embedded-postgres.js";
import { createLocalDiskStorageProvider } from "../../storage/local-disk-provider.js";
@@ -33,6 +33,7 @@ import { nativeToolContractFingerprintForTarget } from "./native-session-resume.
import { nativeCheckpointRetirement } from "./native-runner-checkpoint-retirement.js";
import { nativeSha256 } from "./canonical.js";
import { nativeControllerEvidenceDescriptor, NATIVE_CONTROLLER_WEB_LIMITS, type NativeControllerHistoryManifestEntry } from "./native-controller-archive.js";
import { logActivity } from "../activity-log.js";
vi.mock("../../build-commit.js", () => ({ readBuildCommit: () => "a".repeat(40) }));
@@ -388,6 +389,208 @@ describeEmbeddedPostgres("owner-verified native harness filesystem independence"
expect(await hasValidNativeHarnessFilesystemIndependence(db, f.input)).toBe(false);
});
async function staleWebFixture() {
const f = await webFixture(), first = await preserveNativeHarnessFilesystemIndependence(db, f.input);
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const previous = run!.runnerProfileJson!.nativeHarnessFilesystemIndependence as Record<string, unknown>;
await db.update(heartbeatRuns).set({ runnerProfileJson: { ...run!.runnerProfileJson, retainedWarning: "later preservation metadata" } })
.where(eq(heartbeatRuns.id, f.runId));
expect(await hasValidNativeHarnessFilesystemIndependence(db, f.input)).toBe(false);
return { ...f, first, previous };
}
it.each([false, true])("refreshes stale WEB facts with the same verified source and archive while auditing the prior receipt (touched: %s)", async touched => {
const f = await staleWebFixture();
if (touched) { const stat = await lstat(f.supervisorPath); await utimes(f.supervisorPath, stat.atime, new Date(stat.mtimeMs + 5_000)); }
const runs = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.companyId, f.input.companyId));
const leases = await db.select().from(environmentLeases).where(eq(environmentLeases.companyId, f.input.companyId));
const finalizers = await db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.companyId, f.input.companyId));
const backups = await db.select().from(nativeHarnessBackups).where(eq(nativeHarnessBackups.companyId, f.input.companyId));
const put = vi.spyOn(f.storage, "putFile");
const get = vi.spyOn(f.storage, "getObject");
const now = Date.now(), source = await verifyOriginalHarnessVolumeSource({ ...f.witness,
observedAt: new Date(now).toISOString(), expiresAt: new Date(now + 120_000).toISOString() });
expect(source.fingerprint).not.toBe(f.input.source.fingerprint);
const result = await preserveNativeHarnessFilesystemIndependence(db, { ...f.input, source });
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const receipt = run!.runnerProfileJson!.nativeHarnessFilesystemIndependence as Record<string, unknown>;
expect(receipt).toEqual({ ...f.previous, sourceCapabilityFingerprint: source.fingerprint,
factsDigest: expect.any(String), localFactsDigest: expect.any(String), observedAt: expect.any(String) });
if (touched) expect(receipt.localFactsDigest).not.toBe(f.previous.localFactsDigest);
expect(receipt.factsDigest).not.toBe(f.previous.factsDigest);
expect(result.receiptDigest).not.toBe(f.first.receiptDigest);
expect(await hasValidNativeHarnessFilesystemIndependence(db, f.input)).toBe(true);
expect(put).not.toHaveBeenCalled(); expect(get).toHaveBeenCalled();
expect(await db.select().from(nativeHarnessBackups).where(eq(nativeHarnessBackups.companyId, f.input.companyId))).toEqual(backups);
expect(await db.select().from(environmentLeases).where(eq(environmentLeases.companyId, f.input.companyId))).toEqual(leases);
expect(await db.select().from(nativeRunFinalizations).where(eq(nativeRunFinalizations.companyId, f.input.companyId))).toEqual(finalizers);
expect(await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.companyId, f.input.companyId)))
.toEqual(runs.map(row => ({ ...row, runnerProfileJson: { ...row.runnerProfileJson, nativeHarnessFilesystemIndependence: receipt } })));
const audits = await db.select().from(activityLog).where(and(eq(activityLog.companyId, f.input.companyId),
eq(activityLog.action, "native_harness_filesystem_independence_verified")));
expect(audits).toHaveLength(2);
expect(audits.find(row => row.details?.previousReceiptDigest === f.first.receiptDigest)?.details)
.toMatchObject({ previousReceiptDigest: f.first.receiptDigest, previousReceiptState: { factsDigest: f.previous.factsDigest,
localFactsDigest: f.previous.localFactsDigest, sourceCapabilityFingerprint: f.previous.sourceCapabilityFingerprint,
observedAt: f.previous.observedAt }, factsDigest: receipt.factsDigest });
const audit = audits.find(row => row.details?.previousReceiptDigest === f.first.receiptDigest)!.details!;
expect(nativeSha256({ ...receipt, ...audit.previousReceiptState as Record<string, unknown> })).toBe(f.first.receiptDigest);
expect((await preserveNativeHarnessFilesystemIndependence(db, f.input)).receiptDigest).toBe(result.receiptDigest);
expect(put).not.toHaveBeenCalled();
expect(await db.select().from(activityLog).where(and(eq(activityLog.companyId, f.input.companyId),
eq(activityLog.action, "native_harness_filesystem_independence_verified")))).toHaveLength(2);
});
it.each(["touch", "new-inode"])("preserves a valid WEB receipt after metadata-only %s", async control => {
const f = await webFixture(), first = await preserveNativeHarnessFilesystemIndependence(db, f.input);
const before = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const backups = await db.select().from(nativeHarnessBackups).where(eq(nativeHarnessBackups.companyId, f.input.companyId));
const stat = await lstat(f.supervisorPath);
if (control === "touch") await utimes(f.supervisorPath, stat.atime, new Date(stat.mtimeMs + 5_000));
else { await copyFile(f.supervisorPath, `${f.supervisorPath}.copy`); await rename(`${f.supervisorPath}.copy`, f.supervisorPath);
expect((await lstat(f.supervisorPath)).ino).not.toBe(stat.ino); }
expect(await hasValidNativeHarnessFilesystemIndependence(db, f.input)).toBe(true);
const put = vi.spyOn(f.storage, "putFile"), get = vi.spyOn(f.storage, "getObject");
expect((await preserveNativeHarnessFilesystemIndependence(db, f.input)).receiptDigest).toBe(first.receiptDigest);
expect(get).toHaveBeenCalled(); expect(put).not.toHaveBeenCalled();
expect(await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId))).toEqual(before);
expect(await db.select().from(nativeHarnessBackups).where(eq(nativeHarnessBackups.companyId, f.input.companyId))).toEqual(backups);
expect(await db.select().from(activityLog).where(and(eq(activityLog.companyId, f.input.companyId),
eq(activityLog.action, "native_harness_filesystem_independence_verified")))).toHaveLength(1);
});
it("keeps prior WEB proof reconstructable when activity redaction masks a source path", async () => {
const f = await webFixture(), path = `codex-home/${userInfo().username}/notes.txt`;
await mkdir(join(f.sourceRoot, "codex-home", userInfo().username));
await writeFile(join(f.sourceRoot, path), "preserved local notes");
const first = await preserveNativeHarnessFilesystemIndependence(db, f.input);
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const previous = run!.runnerProfileJson!.nativeHarnessFilesystemIndependence as Record<string, unknown>;
await db.update(heartbeatRuns).set({ runnerProfileJson: { ...run!.runnerProfileJson, retainedWarning: "later metadata" } })
.where(eq(heartbeatRuns.id, f.runId));
const [settings] = await db.select().from(instanceSettings);
await db.update(instanceSettings).set({ general: { ...settings!.general, censorUsernameInLogs: true } }).where(eq(instanceSettings.id, settings!.id));
try {
await logActivity(db, { companyId: f.input.companyId, actorType: "system", actorId: "redaction-fixture",
action: "redaction_fixture", entityType: "heartbeat_run", entityId: f.runId, details: { path } });
const [redacted] = await db.select().from(activityLog).where(and(eq(activityLog.companyId, f.input.companyId), eq(activityLog.action, "redaction_fixture")));
expect(redacted!.details!.path).not.toBe(path);
await preserveNativeHarnessFilesystemIndependence(db, f.input);
const [current] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const receipt = current!.runnerProfileJson!.nativeHarnessFilesystemIndependence as Record<string, unknown>;
const audits = await db.select().from(activityLog).where(and(eq(activityLog.companyId, f.input.companyId),
eq(activityLog.action, "native_harness_filesystem_independence_verified")));
const audit = audits.find(row => row.details?.previousReceiptDigest === first.receiptDigest)!.details!;
const reconstructed = audit.previousReceipt ?? { ...receipt, ...audit.previousReceiptState as Record<string, unknown> };
expect(nativeSha256(reconstructed)).toBe(first.receiptDigest);
expect(reconstructed).toEqual(previous);
expect(audit).not.toHaveProperty("previousReceipt");
expect(audit.previousReceiptState).toEqual({ factsDigest: previous.factsDigest,
localFactsDigest: previous.localFactsDigest, sourceCapabilityFingerprint: previous.sourceCapabilityFingerprint, observedAt: previous.observedAt });
expect(await hasValidNativeHarnessFilesystemIndependence(db, f.input)).toBe(true);
await db.update(heartbeatRuns).set({ runnerProfileJson: { ...current!.runnerProfileJson, retainedWarning: "another metadata update" } })
.where(eq(heartbeatRuns.id, f.runId));
await preserveNativeHarnessFilesystemIndependence(db, f.input);
const [latest] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const history = await db.select().from(activityLog).where(and(eq(activityLog.companyId, f.input.companyId),
eq(activityLog.action, "native_harness_filesystem_independence_verified")));
expect(history).toHaveLength(3);
for (const row of history.filter(row => row.details?.previousReceiptState)) {
expect(nativeSha256({ ...latest!.runnerProfileJson!.nativeHarnessFilesystemIndependence as Record<string, unknown>,
...row.details!.previousReceiptState as Record<string, unknown> })).toBe(row.details!.previousReceiptDigest);
}
} finally { await db.update(instanceSettings).set({ general: settings!.general }).where(eq(instanceSettings.id, settings!.id)); }
});
it("refuses a WEB facts refresh that would drop unknown prior receipt fields", async () => {
const f = await staleWebFixture();
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const profile = { ...run!.runnerProfileJson, nativeHarnessFilesystemIndependence: { ...f.previous, unknownEvidence: "retain" } };
await db.update(heartbeatRuns).set({ runnerProfileJson: profile }).where(eq(heartbeatRuns.id, f.runId));
await expect(preserveNativeHarnessFilesystemIndependence(db, f.input)).rejects.toThrow("native_harness_filesystem_independence_unverified");
expect((await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId)))[0]!.runnerProfileJson).toEqual(profile);
expect(await db.select().from(activityLog).where(and(eq(activityLog.companyId, f.input.companyId),
eq(activityLog.action, "native_harness_filesystem_independence_verified")))).toHaveLength(1);
});
it.each(["wrong-key", "unreadable-archive", "active-source"])("reverifies valid WEB coverage before idempotent preservation: %s", async control => {
const f = await webFixture(); await preserveNativeHarnessFilesystemIndependence(db, f.input);
expect(await hasValidNativeHarnessFilesystemIndependence(db, f.input)).toBe(true);
if (control === "wrong-key") vi.stubEnv("PAPERCLIP_SECRETS_MASTER_KEY", "62".repeat(32));
if (control === "unreadable-archive") vi.spyOn(f.storage, "getObject").mockRejectedValue(new Error("archive unavailable"));
if (control === "active-source") {
const get = f.storage.getObject.bind(f.storage); let changed = false;
vi.spyOn(f.storage, "getObject").mockImplementation(async (...args) => {
if (!changed) { changed = true; await db.update(heartbeatRuns).set({ controllerLeaseExpiresAt: new Date(Date.now() + 60_000) })
.where(eq(heartbeatRuns.id, f.runId)); }
return get(...args);
});
}
const before = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
await expect(preserveNativeHarnessFilesystemIndependence(db, f.input)).rejects.toThrow();
const after = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
expect(after[0]!.runnerProfileJson).toEqual(before[0]!.runnerProfileJson);
expect(await db.select().from(activityLog).where(and(eq(activityLog.companyId, f.input.companyId),
eq(activityLog.action, "native_harness_filesystem_independence_verified")))).toHaveLength(1);
});
it.each(["active-source", "changed-source", "changed-mode", "added-file", "wrong-key", "unreadable-archive", "aborted", "replaced-volume"])(
"retains a stale WEB receipt when fresh proof fails: %s", async control => {
const f = await staleWebFixture();
if (control === "active-source") await db.update(heartbeatRuns).set({ controllerLeaseExpiresAt: new Date(Date.now() + 60_000) })
.where(eq(heartbeatRuns.id, f.runId));
if (control === "changed-source") await writeFile(f.supervisorPath, `${f.supervisor} `);
if (control === "changed-mode") { const mode = (await lstat(f.supervisorPath)).mode & 0o777;
await chmod(f.supervisorPath, mode === 0o400 ? 0o600 : 0o400); }
if (control === "added-file") await writeFile(join(f.sourceRoot, "codex-home/uncovered.txt"), "new bytes");
if (control === "wrong-key") vi.stubEnv("PAPERCLIP_SECRETS_MASTER_KEY", "62".repeat(32));
if (control === "unreadable-archive") vi.spyOn(f.storage, "getObject").mockRejectedValue(new Error("archive unavailable"));
if (control === "replaced-volume") vi.stubEnv("RAILWAY_VOLUME_ID", randomUUID());
const before = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
const backups = await db.select().from(nativeHarnessBackups).where(eq(nativeHarnessBackups.companyId, f.input.companyId));
const put = vi.spyOn(f.storage, "putFile");
await expect(preserveNativeHarnessFilesystemIndependence(db, { ...f.input,
...(control === "aborted" ? { signal: AbortSignal.abort() } : {}) })).rejects.toThrow();
expect(await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId))).toEqual(before);
expect(await db.select().from(nativeHarnessBackups).where(eq(nativeHarnessBackups.companyId, f.input.companyId))).toEqual(backups);
expect(put).not.toHaveBeenCalled();
expect(await db.select().from(activityLog).where(and(eq(activityLog.companyId, f.input.companyId),
eq(activityLog.action, "native_harness_filesystem_independence_verified")))).toHaveLength(1);
});
it.each(["matching-winner", "changed-receipt", "active-source", "changed-byte", "touched-source"])(
"guards stale WEB refresh publication after concurrent %s", async control => {
const f = await staleWebFixture();
let release!: () => void, entered!: () => void, delay = true;
const blocked = new Promise<void>(resolve => { release = resolve; }), ready = new Promise<void>(resolve => { entered = resolve; });
const get = f.storage.getObject.bind(f.storage);
vi.spyOn(f.storage, "getObject").mockImplementation(async (...args) => {
if (delay) { delay = false; entered(); await blocked; } return get(...args);
});
const late = preserveNativeHarnessFilesystemIndependence(db, f.input);
try {
await ready;
const winner = control === "matching-winner" ? await preserveNativeHarnessFilesystemIndependence(db, f.input) : null;
if (control === "changed-receipt") {
const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
await db.update(heartbeatRuns).set({ runnerProfileJson: { ...run!.runnerProfileJson,
nativeHarnessFilesystemIndependence: { ...f.previous, observedAt: new Date().toISOString() } } }).where(eq(heartbeatRuns.id, f.runId));
}
if (control === "active-source") await db.update(heartbeatRuns).set({ controllerLeaseExpiresAt: new Date(Date.now() + 60_000) })
.where(eq(heartbeatRuns.id, f.runId));
if (control === "changed-byte") await writeFile(f.supervisorPath, `${f.supervisor} `);
if (control === "touched-source") { const stat = await lstat(f.supervisorPath);
await utimes(f.supervisorPath, stat.atime, new Date(stat.mtimeMs + 5_000)); }
const before = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId));
release();
if (winner) expect((await late).receiptDigest).toBe(winner.receiptDigest);
else await expect(late).rejects.toThrow();
expect(await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.runId))).toEqual(before);
expect(await db.select().from(activityLog).where(and(eq(activityLog.companyId, f.input.companyId),
eq(activityLog.action, "native_harness_filesystem_independence_verified")))).toHaveLength(winner ? 2 : 1);
} finally { release(); await late.catch(() => undefined); }
});
it.each(["active-web", "owned-finalizer", "physical-writer", "foreign-supervisor", "missing-manifest", "symlink", "foreign-controller"])(
"refuses WEB evidence with %s and retains the original recovery state", async control => {
const f = await webFixture(), where = eq(heartbeatRuns.id, f.runId);
@@ -189,10 +189,11 @@ async function assetBindings(db: Db, anchor: Run, sourceRunId: string) {
}
function matchingReceipt(value: unknown, facts: Awaited<ReturnType<typeof databaseFacts>>, input: Scope,
bindings: Awaited<ReturnType<typeof assetBindings>>["bindings"], controller: ControllerBinding, web?: WebBinding): Receipt | null {
bindings: Awaited<ReturnType<typeof assetBindings>>["bindings"], controller: ControllerBinding, web?: WebBinding,
factsDigest = facts.factsDigest): Receipt | null {
const receipt = value as Receipt | undefined;
if (receipt?.schema !== (web ? WEB_SCHEMA : SCHEMA) || receipt.selectedSessionId !== input.selectedSessionId || receipt.scopeDigest !== input.scopeDigest
|| receipt.lastRunId !== facts.anchor.id || receipt.sourceRunId !== facts.source.id || receipt.factsDigest !== facts.factsDigest
|| receipt.lastRunId !== facts.anchor.id || receipt.sourceRunId !== facts.source.id || receipt.factsDigest !== factsDigest
|| !/^[a-f0-9]{64}$/.test(receipt.sourceCapabilityFingerprint) || !/^[a-f0-9]{64}$/.test(receipt.localFactsDigest)
|| !Number.isFinite(Date.parse(receipt.observedAt)) || !Array.isArray(receipt.packages) || !Array.isArray(receipt.versions)) return null;
return digest(bindings) === receipt.assetsDigest && digest(receipt.packages) === digest(bindings.packages)
@@ -384,7 +385,7 @@ async function controllerBinding(db: Db, facts: Awaited<ReturnType<typeof databa
async function preserveControllerEvidence(db: Db, facts: Awaited<ReturnType<typeof databaseFacts>>, input: Scope & {
source: OriginalHarnessVolumeCapability; storage: StorageService; signal: AbortSignal;
}, history?: CapturedHistory, web?: CapturedWeb): Promise<ControllerBinding> {
}, history?: CapturedHistory, web?: CapturedWeb, existing?: ControllerBinding): Promise<ControllerBinding> {
const material = await readLegacyNativeControllerArchiveSource(facts.source);
if (material.paths.root !== scopedRunnerdStateRoot(facts.execution)) fail();
if (material.controller === undefined && !web) {
@@ -411,6 +412,19 @@ async function preserveControllerEvidence(db: Db, facts: Awaited<ReturnType<type
const durable = checkpoint.records.get(key); return durable === undefined || digest(JSON.parse(value)) !== digest(JSON.parse(durable));
})) fail();
}
if (existing) {
if (!existing.archive || existing.archive.controllerDigest !== (material.controller === undefined ? null : sha(material.controller))
|| existing.archive.supervisorDigest !== (material.supervisor === undefined ? null : sha(material.supervisor))) fail();
const current = await controllerBinding(db, facts, existing.archive, false, history?.binding, web?.binding);
if (digest(current) !== digest(existing)) fail();
const descriptor = nativeControllerEvidenceDescriptor({ companyId: input.companyId, issueId: facts.anchor.nativeIssueId!,
runId: facts.source.id, controllerDigest: existing.archive.controllerDigest, supervisorDigest: existing.archive.supervisorDigest,
purpose: "filesystem_independence", history: history?.binding.entries, historyBindingDigest: history ? digest(history.binding) : undefined,
...(web ? { web: { entries: web.binding.entries, bindingDigest: digest(web.binding) } } : {}) });
if (!await verifyDurableNativeHarnessBackup({ db, storage: input.storage, companyId: input.companyId,
scopeDigest: descriptor.scopeDigest, manifestSha256: descriptor.manifestSha256, backupId: existing.archive.id, signal: input.signal })) fail();
return current;
}
const [coordinator] = await db.select().from(nativeRunFinalizations).where(and(eq(nativeRunFinalizations.companyId, input.companyId),
eq(nativeRunFinalizations.runId, facts.source.id)));
if (!coordinator) fail();
@@ -707,18 +721,29 @@ async function localFacts(facts: Awaited<ReturnType<typeof databaseFacts>>, asse
async function preserveWebEvidence(db: Db, input: Scope & {
source: OriginalHarnessVolumeCapability; storage: StorageService; signal: AbortSignal;
}) {
const facts = await databaseFacts(db, input, false, true), history = await captureAuthorityHistory(db, facts, input.signal);
const facts = await databaseFacts(db, input, false, true);
const previous = facts.anchor.runnerProfileJson?.[KEY] as Receipt | undefined;
const previousDigest = previous == null ? null : digest(previous);
if (previous != null && !previous.controller?.archive) fail();
const history = await captureAuthorityHistory(db, facts, input.signal);
const web = await captureWebEvidence(facts, input.signal);
const controller = await preserveControllerEvidence(db, facts, input, history, web), bindings = { packages: [], versions: [] };
const previous = facts.anchor.runnerProfileJson?.[KEY];
const controller = await preserveControllerEvidence(db, facts, input, history, web, previous?.controller), bindings = { packages: [], versions: [] };
if (previous != null) {
if (!await hasValidNativeHarnessFilesystemIndependence(db, input)) fail();
return { scopeDigest: input.scopeDigest, sourceRunId: facts.source.id, receiptDigest: digest(previous) };
if (!/^[a-f0-9]{64}$/.test(previous.factsDigest)
|| !matchingReceipt(previous, facts, input, bindings, controller, web.binding, previous.factsDigest)) fail();
if (matchingReceipt(previous, facts, input, bindings, controller, web.binding)) {
if (!await hasValidNativeHarnessFilesystemIndependence(db, input)) fail();
await assertOriginalHarnessVolumeSource(input.source); input.signal.throwIfAborted();
return { scopeDigest: input.scopeDigest, sourceRunId: facts.source.id, receiptDigest: previousDigest! };
}
}
const receipt: Receipt = { schema: WEB_SCHEMA, selectedSessionId: input.selectedSessionId, scopeDigest: input.scopeDigest,
lastRunId: facts.anchor.id, sourceRunId: facts.source.id, factsDigest: facts.factsDigest,
sourceCapabilityFingerprint: input.source.fingerprint, localFactsDigest: web.localDigest,
assetsDigest: digest(bindings), ...bindings, controller, web: web.binding, observedAt: new Date().toISOString() };
const previousReceiptState = previous ? { factsDigest: previous.factsDigest, localFactsDigest: previous.localFactsDigest,
sourceCapabilityFingerprint: previous.sourceCapabilityFingerprint, observedAt: previous.observedAt } : null;
if (previousReceiptState && digest({ ...receipt, ...previousReceiptState }) !== previousDigest) fail();
let published = receipt;
const publications: ActivityPublication[] = [];
await db.transaction(async transaction => {
@@ -734,14 +759,16 @@ async function preserveWebEvidence(db: Db, input: Scope & {
const existing = current.anchor.runnerProfileJson?.[KEY];
if (existing != null) {
const winner = matchingReceipt(existing, current, input, bindings, currentController, web.binding);
if (!winner) fail(); published = winner; return;
if (winner) { published = winner; return; }
}
if ((existing == null ? null : digest(existing)) !== previousDigest) fail();
await tx.update(heartbeatRuns).set({ runnerProfileJson: { ...current.anchor.runnerProfileJson, [KEY]: receipt } })
.where(and(eq(heartbeatRuns.id, current.anchor.id), eq(heartbeatRuns.companyId, input.companyId)));
await logActivity(tx, { companyId: input.companyId, actorType: "system", actorId: "native-harness-preservation",
action: "native_harness_filesystem_independence_verified", entityType: "heartbeat_run", entityId: current.anchor.id,
agentId: current.anchor.agentId, runId: current.anchor.id, details: { scopeDigest: input.scopeDigest, sourceRunId: current.source.id,
sourceCapabilityFingerprint: receipt.sourceCapabilityFingerprint, factsDigest: receipt.factsDigest, coverage: "web_evidence" } }, publications);
sourceCapabilityFingerprint: receipt.sourceCapabilityFingerprint, factsDigest: receipt.factsDigest, coverage: "web_evidence",
...(previousReceiptState ? { previousReceiptDigest: previousDigest, previousReceiptState } : {}) } }, publications);
});
for (const publication of publications) publishActivity(publication);
return { scopeDigest: input.scopeDigest, sourceRunId: facts.source.id, receiptDigest: digest(published) };