openclaw/scripts/mantis/finalize-request-proof.mts
Martin Cleary a960844a9c
feat(qa): run isolated behavioral proof for inline reviews (#138953)
* feat(telegram): add isolated Test Server proof workflow

Add maintainer-only exact-head admission, durable at-most-once QA lease consumption, isolated candidate execution, and normalized trusted Telegram Test Server observations.

Co-authored-by: brokemac79 <255583030+brokemac79@users.noreply.github.com>

* feat(proof): bind named Web UI and canonical Telegram QA evidence

Reuse the existing formatting QA recipe, preserve isolated exact-candidate execution and produce the consumer request-bound receipt. Keep named smoke scenarios distinct and protect stationary harness ancestry.

Co-authored-by: brokemac79 <255583030+brokemac79@users.noreply.github.com>

* fix(proof): complete isolated QA execution and bounded failure capture

Reuse canonical ephemeral device pairing and QA RPC scopes, preserve strict startup probes and recorder locks, and retain bounded wrong-text attempts without Telegram delivery.

Co-authored-by: brokemac79 <255583030+brokemac79@users.noreply.github.com>

* fix(mantis): revoke proof forwarding and enforce lease roles

* fix(mantis): close proof producer CI gates

* test(mantis): consolidate related proof suites within CI budget

* test(mantis): preserve fast QA ownership when grouping integration cases

* fix(mantis): accept exact branch-qualified workflow paths

* fix(mantis): align live admission workflow path checks

* feat(mantis): collect selected proof inside the originating review

* fix(mantis): bound proof storage and preserve failure evidence

Reuse verified storage across request-bound candidates, reserve backing capacity, retain sanitized rejection evidence, and repair cleanup and observation finalization. Scoped checks and dirty review pass; full Gateway and sandboxed browser runtime proof remain required before publication.

* fix(mantis): retain bridge identity before startup

* fix(mantis): keep candidate config readable under private umask

* fix(qa): use verified rootless networking and join candidate shutdown

* fix(qa): repair proof tooling checks and deterministic recorder fixture

---------

Co-authored-by: brokemac79 <255583030+brokemac79@users.noreply.github.com>
2026-09-07 14:09:37 +01:00

246 lines
8.4 KiB
TypeScript

#!/usr/bin/env node
// Fresh trusted job only; never import or execute anything from the evidence ZIP.
import { execFileSync } from "node:child_process";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import path from "node:path";
import { z } from "zod";
import {
createRequestReceipt,
requestEvidenceDigest,
requestIdentitySchema,
requestProofDefinitions,
type RequestEvidence,
type RequestExecution,
} from "./request-proof.ts";
const scenario = z
.enum(["web-ui-chat-proof", "telegram-bot-e2e-proof", "telegram-markdown-parser-fidelity"])
.parse(process.argv[2] ?? "web-ui-chat-proof");
const definition = requestProofDefinitions[scenario];
const identity = requestIdentitySchema.parse({
request_id: process.env.REQUEST_ID,
...(process.env.PLAN_SHA256 ? { plan_sha256: process.env.PLAN_SHA256 } : {}),
repository: { id: process.env.GITHUB_REPOSITORY_ID, full_name: process.env.GITHUB_REPOSITORY },
pull_request: Number(process.env.TARGET_PR),
candidate_sha: process.env.CANDIDATE_SHA,
scenario,
workflow: {
path: definition.workflow,
sha: process.env.GITHUB_WORKFLOW_SHA,
},
harness: { sha: process.env.GITHUB_WORKFLOW_SHA },
run: { id: process.env.GITHUB_RUN_ID, attempt: Number(process.env.GITHUB_RUN_ATTEMPT) },
});
const token = process.env.GH_TOKEN;
if (!token) {
throw new Error("Finalizer GitHub authentication is unavailable");
}
const headers = {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
};
const repo = `/repos/${identity.repository.full_name}`;
async function bytes(response: Response, maximum: number) {
if (!response.ok || !response.body) {
throw new Error("GitHub evidence request failed");
}
const chunks: Uint8Array[] = [];
let size = 0;
for await (const chunk of response.body) {
size += chunk.byteLength;
if (size > maximum) {
throw new Error("GitHub response exceeded evidence limit");
}
chunks.push(chunk);
}
return Buffer.concat(chunks);
}
async function api(endpoint: string): Promise<unknown> {
const response = await fetch(`https://api.github.com${endpoint}`, {
headers,
redirect: "error",
signal: AbortSignal.timeout(30_000),
});
return JSON.parse((await bytes(response, 2 * 1024 * 1024)).toString("utf8"));
}
const apiId = z.number().int().positive().max(Number.MAX_SAFE_INTEGER);
const attemptPath = `${repo}/actions/runs/${identity.run.id}/attempts/${identity.run.attempt}`;
const workflowBranch = process.env.GITHUB_REF?.startsWith("refs/heads/")
? process.env.GITHUB_REF.slice("refs/heads/".length)
: undefined;
const run = z
.object({
id: apiId,
run_attempt: z.number().int(),
event: z.string(),
head_sha: z.string(),
path: z.string(),
display_title: z.string(),
repository: z.object({ id: apiId }),
head_repository: z.object({ id: apiId }),
})
.parse(await api(attemptPath));
if (
String(run.id) !== identity.run.id ||
run.run_attempt !== identity.run.attempt ||
run.event !== "workflow_dispatch" ||
run.head_sha !== identity.workflow.sha ||
// Only the current trusted workflow branch may qualify GitHub's run path.
(run.path !== identity.workflow.path &&
(!workflowBranch || run.path !== `${identity.workflow.path}@${workflowBranch}`)) ||
run.display_title !== `${definition.runName} [${identity.request_id}]` ||
String(run.repository.id) !== identity.repository.id ||
String(run.head_repository.id) !== identity.repository.id
) {
throw new Error("Workflow identity mismatch");
}
const jobs = z
.object({
total_count: z.number(),
jobs: z.array(
z.object({
name: z.string(),
status: z.string(),
conclusion: z.string().nullable(),
run_id: apiId,
head_sha: z.string(),
}),
),
})
.parse(await api(`${attemptPath}/jobs?per_page=100`));
const selected = jobs.jobs.filter((job) => job.name === definition.job);
const observerJob = selected[0];
if (
jobs.total_count > 100 ||
jobs.total_count !== jobs.jobs.length ||
selected.length !== 1 ||
!observerJob ||
observerJob.status !== "completed" ||
String(observerJob.run_id) !== identity.run.id ||
observerJob.head_sha !== identity.workflow.sha
) {
throw new Error("Observer job identity is missing or ambiguous");
}
const outcomes: Record<string, RequestExecution> = {
success: "completed",
failure: "failed",
cancelled: "cancelled",
timed_out: "timed_out",
skipped: "skipped",
};
const execution = outcomes[observerJob.conclusion ?? ""] ?? "failed";
let reason: string | undefined;
const pr = z
.object({
state: z.string(),
head: z.object({ sha: z.string(), repo: z.object({ id: apiId }).nullable() }),
})
.parse(await api(`${repo}/pulls/${identity.pull_request}`));
if (
pr.state !== "open" ||
pr.head.sha !== identity.candidate_sha ||
String(pr.head.repo?.id) !== identity.repository.id
) {
reason = "PR is no longer open at the exact same-repository candidate head.";
}
const root = path.resolve(".artifacts/mantis-request-finalizer");
mkdirSync(root, { recursive: true });
let evidence: RequestEvidence | null = null;
let files: unknown;
try {
const name = `${definition.artifact}-${identity.run.id}-${identity.run.attempt}`;
const artifactSchema = z.object({
id: apiId,
name: z.string(),
expired: z.boolean(),
size_in_bytes: z.number().int().positive(),
digest: z.string().regex(/^sha256:[0-9a-f]{64}$/),
workflow_run: z.object({
id: apiId,
repository_id: apiId,
head_repository_id: apiId,
head_sha: z.string(),
}),
});
const list = z
.object({ total_count: z.number(), artifacts: z.array(artifactSchema) })
.parse(
await api(
`${repo}/actions/runs/${identity.run.id}/artifacts?per_page=100&name=${encodeURIComponent(name)}`,
),
);
if (list.total_count !== 1 || list.artifacts.length !== 1) {
throw new Error("Missing or ambiguous artifact");
}
const artifact = list.artifacts[0];
if (
!artifact ||
artifact.name !== name ||
artifact.expired ||
artifact.size_in_bytes > 16 * 1024 * 1024 ||
String(artifact.workflow_run.id) !== identity.run.id ||
String(artifact.workflow_run.repository_id) !== identity.repository.id ||
String(artifact.workflow_run.head_repository_id) !== identity.repository.id ||
artifact.workflow_run.head_sha !== identity.workflow.sha
) {
throw new Error("Artifact identity mismatch");
}
let response = await fetch(`https://api.github.com${repo}/actions/artifacts/${artifact.id}/zip`, {
headers,
redirect: "manual",
signal: AbortSignal.timeout(30_000),
});
if (response.status === 302) {
const location = new URL(response.headers.get("location") ?? "");
if (
location.protocol !== "https:" ||
location.username ||
location.password ||
!(
location.hostname.endsWith(".blob.core.windows.net") ||
location.hostname.endsWith(".actions.githubusercontent.com")
)
) {
throw new Error("Unexpected artifact storage");
}
// Never forward GitHub authorization to artifact storage or redirects.
response = await fetch(location, {
redirect: "error",
credentials: "omit",
signal: AbortSignal.timeout(60_000),
});
}
const archive = await bytes(response, 16 * 1024 * 1024);
const digest = requestEvidenceDigest(archive);
if (artifact.size_in_bytes !== archive.length || artifact.digest !== `sha256:${digest}`) {
throw new Error("Archive digest mismatch");
}
evidence = { artifact_id: String(artifact.id), artifact_name: name, sha256: digest };
const archivePath = path.join(root, "evidence.zip");
const filesPath = path.join(root, "files.json");
writeFileSync(archivePath, archive, { flag: "wx" });
execFileSync(
"python3",
[
"-I",
"-S",
"scripts/mantis/read-request-archive.py",
definition.archive,
archivePath,
filesPath,
],
{ timeout: 30_000, stdio: "pipe" },
);
files = JSON.parse(readFileSync(filesPath, "utf8"));
} catch {
reason ??=
"Evidence archive is unavailable, ambiguous, expired, malformed, unsafe, partial, or does not match its authoritative identity/digest.";
}
const receipt = createRequestReceipt(identity, execution, evidence, files, reason);
writeFileSync(path.join(root, "receipt.json"), `${JSON.stringify(receipt, null, 2)}\n`, {
flag: "wx",
});
console.log(
`Mantis request assertion: ${receipt.assertion_outcome}; execution: ${receipt.execution_outcome}`,
);