mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-22 00:30:19 +02:00
Add company-scoped Muse native control, OAuth, wake receiver and recovery. Recheck current authority after waits and before effects or receipt replay. Preserve task-read scope, assignment expiry, independent holds, nonblocking reassignment and truthful cleanup provenance. Retain source trust on progress comments and verify current Muse V8 authority through real signed-JWT HTTP requests. Preserve the upstream trimmed ordinary authentication lookup. Refs #16448. Preserve captured canonical queue recovery and concurrent environment preparation while retaining the scoped Muse V8 exclusion. Normal migration 0354 and existing Muse SQL remain unchanged. Fresh current-head Greptile review is 5/5 with the project-write finding addressed. Required CI and independent technical review gate the merge; maintainer override is reserved for formal approval rules. Recheck current Muse project-write authority after all receipt/task/run/admission waits, before creation or replay. Real PostgreSQL regressions reproduce four late effects/replays on the old route; the repaired three-file treatment passes 354 cases with no skips, and the scoped server typecheck passes. Ordinary/Dot behavior is preserved. Co-Authored-By: Paperclip <noreply@paperclip.ing>
343 lines
27 KiB
TypeScript
343 lines
27 KiB
TypeScript
/** Opt-in deterministic operator smoke against an isolated loopback server.
|
|
* This scripts a synthetic external worker through production HTTP/native Runner
|
|
* surfaces. It is not a registered Product E2E campaign or live Muse qualification.
|
|
* pnpm --filter @paperclipai/server exec tsx scripts/muse-runner-smoke.ts --run --base http://127.0.0.1:3311 \
|
|
* --auth-file /private/path/operator.json --company <uuid> --agent <uuid> \
|
|
* --instance <instance-id> --server-source-sha <40-char-sha> --evidence-dir /private/path/evidence
|
|
*/
|
|
import assert from "node:assert/strict";
|
|
import { execFileSync } from "node:child_process";
|
|
import { createHash, randomUUID } from "node:crypto";
|
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
import { join, resolve } from "node:path";
|
|
import { setTimeout as delay } from "node:timers/promises";
|
|
import { fileURLToPath } from "node:url";
|
|
import { digestPaperclipSemanticContent } from "../src/vendor/paperclip-runner/index.js";
|
|
|
|
type Json = Record<string, unknown>;
|
|
function object(value: unknown): Json {
|
|
assert.ok(value && typeof value === "object" && !Array.isArray(value), "Expected an object");
|
|
return value as Json;
|
|
}
|
|
function string(value: unknown): string { assert.equal(typeof value, "string"); return value as string; }
|
|
function rows(value: unknown): Json[] { assert.ok(Array.isArray(value)); return value.map(object); }
|
|
function option(name: string): string {
|
|
const index = process.argv.indexOf(name);
|
|
assert.ok(index >= 0 && process.argv[index + 1], `Missing ${name}`);
|
|
return process.argv[index + 1];
|
|
}
|
|
if (process.argv.includes("--help")) {
|
|
console.log("Synthetic Muse smoke: --run --base <loopback-url> --auth-file <private-json> --company <uuid> --agent <uuid> --instance <instance-id> --server-source-sha <sha> --evidence-dir <private-dir>");
|
|
process.exit(0);
|
|
}
|
|
assert.ok(process.argv.includes("--run"), "Pass --run for this bounded synthetic smoke");
|
|
const base = new URL(option("--base"));
|
|
assert.ok(["127.0.0.1", "localhost", "[::1]"].includes(base.hostname), "Use an isolated loopback server");
|
|
assert.ok(["http:", "https:"].includes(base.protocol) && !base.username && !base.password && base.pathname === "/");
|
|
const authFile = option("--auth-file"), companyId = option("--company"), agentId = option("--agent");
|
|
const serverSourceSha = option("--server-source-sha"), instanceId = option("--instance");
|
|
assert.match(serverSourceSha, /^[a-f0-9]{40}$/);
|
|
assert.match(companyId, /^[a-f0-9-]{36}$/); assert.match(agentId, /^[a-f0-9-]{36}$/);
|
|
const evidenceDirectory = resolve(option("--evidence-dir"));
|
|
await mkdir(evidenceDirectory, { recursive: true, mode: 0o700 });
|
|
const operator = object(JSON.parse(await readFile(authFile, "utf8")));
|
|
const cookie = string(operator.cookie);
|
|
assert.equal(operator.base, base.origin, "The private operator session must belong to this target");
|
|
assert.equal(operator.companyId, companyId, "Use the authorized isolated company");
|
|
const startedAt = new Date().toISOString(), deadline = Date.now() + 5 * 60_000;
|
|
const marker = `synthetic-muse-${randomUUID()}`;
|
|
const bindingPath = `/api/companies/${companyId}/agents/${agentId}/muse-binding`;
|
|
const evidence: Json = {
|
|
schema: "paperclip.synthetic-muse-smoke.v1", synthetic: true,
|
|
registeredCampaign: false, liveMuseQualification: false, browserVerified: false,
|
|
sourceSha: execFileSync("git", ["rev-parse", "HEAD"], { encoding: "utf8" }).trim(), serverSourceSha,
|
|
scriptSha256: createHash("sha256").update(await readFile(fileURLToPath(import.meta.url))).digest("hex"),
|
|
target: base.origin, companyId, agentId, marker, startedAt,
|
|
worker: "scripted HTTP peer; no real Muse, model, or hooks installation",
|
|
profile: { provider: "muse", driver: "muse_external", bridgeRevision: "muse-v1", expectedTurns: 2 },
|
|
accounting: { providerCalls: 0, usage: null, cost: null, coverage: "unavailable" },
|
|
assertions: [], steps: [], operations: [], cleanup: {},
|
|
};
|
|
let stage = "preflight", credentials: Json | undefined, detectorTimer: NodeJS.Timeout | undefined;
|
|
let detectorRequests = 0, detectorError = false, issueId: string | undefined, createdBindingId: string | undefined;
|
|
let detectorFlight: Promise<void> | undefined, cleanupDeadline: number | undefined;
|
|
const runIds: string[] = [], operationEvidence: Json[] = [];
|
|
function step(name: string, details: Json = {}) {
|
|
stage = name; (evidence.steps as Json[]).push({ stage: name, at: new Date().toISOString(), ...details });
|
|
console.log(JSON.stringify({ synthetic: true, stage: name, ...details }));
|
|
}
|
|
async function http(path: string, method = "GET", body?: Json, token?: string, board = false, headers: Record<string, string> = {}): Promise<unknown> {
|
|
const requestDeadline = cleanupDeadline ?? deadline;
|
|
assert.ok(Date.now() < requestDeadline, "Smoke deadline exceeded");
|
|
const response = await fetch(new URL(path, base), {
|
|
method, headers: { ...(board ? { Cookie: cookie, Origin: base.origin } : {}), ...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
...(body ? { "Content-Type": "application/json" } : {}), ...headers },
|
|
...(body ? { body: JSON.stringify(body) } : {}), signal: AbortSignal.timeout(Math.max(1, Math.min(15000, requestDeadline - Date.now()))), redirect: "error",
|
|
});
|
|
// Never print transport bodies: pairing responses contain private credentials.
|
|
assert.ok(response.ok, `HTTP ${response.status} at ${method} ${path}`);
|
|
return response.status === 204 ? null : response.json();
|
|
}
|
|
const board = (path: string, method = "GET", body?: Json) => http(path, method, body, undefined, true);
|
|
const query = async (body: Json) => object(await http("/api/muse/v1/queries", "POST", { version: 1, ...body }, string(credentials?.accessToken)));
|
|
async function command(body: Json): Promise<Json> {
|
|
const requestId = typeof body.requestId === "string" ? body.requestId : randomUUID();
|
|
let outcome = object(await http("/api/muse/v1/commands", "POST", { version: 1, ...body, requestId }, string(credentials?.accessToken)));
|
|
while (["pending", "reserved", "dispatched"].includes(String(outcome.status))
|
|
&& !(body.command === "request_user_input" && outcome.requestId === body.nativeRequestId)) {
|
|
assert.equal(typeof body.assignmentId, "string", "Only assigned operations may return pending");
|
|
await delay(150);
|
|
outcome = await query({ query: "operation.receipt", assignmentId: body.assignmentId, requestId });
|
|
}
|
|
assert.ok(!["unknown", "rejected"].includes(String(outcome.status)), "An uncertain/rejected operation cannot be retried with a new ID");
|
|
assert.notEqual(outcome.isError, true, `Native ${String(body.name ?? body.command)} failed`);
|
|
operationEvidence.push({ requestId, command: body.command, ...(body.name ? { tool: body.name } : {}), status: outcome.status });
|
|
evidence.operations = operationEvidence;
|
|
return outcome;
|
|
}
|
|
async function until<T>(name: string, read: () => Promise<T>, predicate: (value: T) => boolean): Promise<T> {
|
|
while (Date.now() < deadline) {
|
|
const value = await read(); if (predicate(value)) return value;
|
|
await delay(250);
|
|
}
|
|
throw new Error(`Deadline while waiting for ${name}`);
|
|
}
|
|
async function signal() {
|
|
await http("/api/muse/v1/signal", "GET", undefined, undefined, false,
|
|
{ "X-Paperclip-Muse-Binding": string(credentials?.bindingId) });
|
|
detectorRequests++;
|
|
}
|
|
async function assignment(): Promise<Json> {
|
|
return until<Json>("an offered assignment", async () => {
|
|
const runs = rows(await board(`/api/companies/${companyId}/heartbeat-runs?agentId=${agentId}&limit=100`));
|
|
const terminal = runs.find(run => !runIds.includes(string(run.id))
|
|
&& (run.issueId === issueId || run.nativeIssueId === issueId || object(run.contextSnapshot ?? {}).issueId === issueId)
|
|
&& ["failed", "cancelled", "timed_out"].includes(String(run.status)));
|
|
if (terminal) {
|
|
const run = object(await board(`/api/heartbeat-runs/${string(terminal.id)}`));
|
|
evidence.nativeStartupFailure = { runId: run.id, status: run.status, runtimeMode: run.runtimeMode,
|
|
driverKind: run.driverKind, nativePhase: run.nativePhase, errorCode: run.errorCode, error: run.error,
|
|
createdAt: run.createdAt, startedAt: run.startedAt, finishedAt: run.finishedAt };
|
|
step("native-startup-failed", { runId: run.id, status: run.status });
|
|
throw new Error(`Native run ${string(run.id)} ended before an offered assignment; inspect nativeStartupFailure`);
|
|
}
|
|
const mailbox = await query({ query: "mailbox", after: 0 });
|
|
const item = rows(mailbox.items).find(row => row.kind === "assignment" && !runIds.includes(string(object(row.references).runId)));
|
|
return item ? query({ query: "assignment.read", assignmentId: object(item.references).assignmentId }) : {};
|
|
}, value => typeof value.assignmentId === "string" && value.status === "offered");
|
|
}
|
|
async function accept(work: Json): Promise<void> {
|
|
const ref = object(work.binding), runId = string(ref.runId);
|
|
runIds.push(runId); assert.ok(runIds.length <= 2, "Only two native turns are authorized");
|
|
await command({ command: "accept", assignmentId: work.assignmentId });
|
|
const run = object(await board(`/api/heartbeat-runs/${runId}`));
|
|
assert.equal(run.runtimeMode, "native"); assert.equal(run.driverKind, "muse_external");
|
|
assert.equal(run.status, "running");
|
|
const profile = object(object(run.runnerProfileJson).nativeExecutionInput);
|
|
assert.equal(profile.schema, "paperclip.native-execution-input.v8");
|
|
assert.equal(object(profile.workspace).access, "none");
|
|
step("native-accepted", { turn: runIds.length, runId });
|
|
}
|
|
async function finish(work: Json, summary: string): Promise<void> {
|
|
const contract = object(work.completionContract);
|
|
const result = { reportedWorkDisposition: "completed", summary,
|
|
completionClaim: { contractRevision: string(contract.revision), objectiveSatisfied: true,
|
|
criteria: (contract.criterionIds as unknown[]).map(id => ({ criterionId: string(id), status: "passed", evidenceRefs: [] })), remainingWork: [] },
|
|
evidence: [], verification: [{ commandOrCheck: "Independent board document readback", status: "pass" }] };
|
|
const receipt = await command({ command: "tool", assignmentId: work.assignmentId, name: "paperclip_finish", arguments: result });
|
|
const canonical = object(object(receipt.result).completionReport);
|
|
assert.equal(canonical.schema, "paperclip.run_result.v1"); assert.equal(canonical.reportedWorkDisposition, "done");
|
|
await command({ command: "finish", assignmentId: work.assignmentId, result: canonical });
|
|
const runId = string(object(work.binding).runId);
|
|
const run = await until("server native finalization", async () => object(await board(`/api/heartbeat-runs/${runId}`)), value =>
|
|
value.status === "succeeded" && object(value.resultJson).finalizationPhase === "committed");
|
|
const issue = object(await board(`/api/issues/${issueId}`)); assert.equal(issue.status, "done"); assert.equal(issue.executionRunId, null);
|
|
await until("released Muse assignment", async () => object(object(await board(bindingPath)).binding), value =>
|
|
value.liveAssignments === 0 && value.uncertainOperations === 0 && object(value.stop).status === "none");
|
|
(evidence.assertions as Json[]).push({ assertion: "native-server-finalization", runId, status: run.status,
|
|
finishedAt: run.finishedAt, nativePhase: run.nativePhase, finalizationPhase: object(run.resultJson).finalizationPhase,
|
|
issueStatus: issue.status, canonicalReportSchema: canonical.schema });
|
|
step("server-finalized", { turn: runIds.length, runId });
|
|
}
|
|
try {
|
|
const health = object(await board("/api/health")); assert.equal(health.status, "ok"); assert.equal(health.authReady, true);
|
|
assert.equal(health.commit, serverSourceSha, "Use the stable isolated checkout SHA for this smoke");
|
|
if (health.devServer && typeof object(health.devServer).restartRequired === "boolean") assert.equal(object(health.devServer).restartRequired, false, "Restart the isolated dev server before this smoke");
|
|
const settings = object(await board("/api/instance/settings/experimental"));
|
|
assert.equal(settings.enableMuse, true); assert.equal(settings.enableNativeRunner, true);
|
|
assert.equal(settings.enableWorktreeRunExecution, true, "Activate isolated worktree execution before creating this issue");
|
|
assert.equal(settings.worktreeRunExecutionActivationInstanceId, instanceId, "Activation must match the isolated instance");
|
|
const cutoff = string(settings.worktreeRunExecutionActivatedAt);
|
|
assert.ok(Number.isFinite(Date.parse(cutoff)) && Date.parse(cutoff) <= Date.now(), "Execution cutoff must be armed");
|
|
const initial = object(await board(bindingPath)); assert.equal(initial.enabled, true);
|
|
const publicOrigin = new URL(string(initial.publicOrigin));
|
|
assert.equal(publicOrigin.protocol, "https:", "Configure the synthetic public setup origin before pairing");
|
|
assert.ok(!publicOrigin.username && !publicOrigin.password && publicOrigin.pathname === "/" && !publicOrigin.search && !publicOrigin.hash);
|
|
evidence.preflight = { instanceId, armedWorktreeExecution: true, executionCutoff: cutoff, publicOrigin: publicOrigin.origin,
|
|
observedCurrentCheckoutSha: health.commit, processStartedAt: object(health.serverInfo).processStartedAt, normalBoardOrigin: base.origin };
|
|
if (initial.binding) {
|
|
const existing = object(initial.binding);
|
|
assert.equal(existing.liveAssignments, 0, "Do not replace active work");
|
|
assert.equal(existing.uncertainOperations, 0, "Resolve previous native effects before this smoke");
|
|
assert.equal(object(existing.stop).status, "none", "Resolve previous worker uncertainty before this smoke");
|
|
}
|
|
step("pairing");
|
|
const previous = initial.binding ? object(initial.binding) : undefined;
|
|
const pairing = object(await board(bindingPath, "POST", previous && previous.status !== "revoked"
|
|
? { replaceBindingId: previous.id, expectedRevision: previous.revision } : {}));
|
|
createdBindingId = string(pairing.bindingId);
|
|
// Synthetic peer exercises the production public-client OAuth flow. Tokens
|
|
// stay in this process; actual Muse uses Secure Vault/authd instead.
|
|
const redirectUri = "https://synthetic-muse.example/oauth/callback";
|
|
const client = object(await http("/api/muse/v1/oauth/register", "POST", {
|
|
client_name: "Bounded synthetic Muse smoke", redirect_uris: [redirectUri],
|
|
token_endpoint_auth_method: "none", grant_types: ["authorization_code", "refresh_token"], response_types: ["code"],
|
|
}));
|
|
const verifier = randomUUID() + randomUUID(), state = randomUUID();
|
|
const authorization = new URL("/api/muse/v1/oauth/authorize", base);
|
|
authorization.search = new URLSearchParams({ client_id: string(client.client_id), redirect_uri: redirectUri,
|
|
response_type: "code", scope: "paperclip:agent offline_access", state,
|
|
code_challenge: createHash("sha256").update(verifier).digest("base64url"), code_challenge_method: "S256",
|
|
pairing_ticket: string(pairing.ticket) }).toString();
|
|
const authorized = await fetch(authorization, { redirect: "manual", signal: AbortSignal.timeout(15000) });
|
|
assert.equal(authorized.status, 303, "Muse pairing must use the dedicated PKCE authorization flow");
|
|
const callback = new URL(string(authorized.headers.get("location")));
|
|
assert.equal(callback.origin + callback.pathname, redirectUri);
|
|
assert.equal(callback.searchParams.get("state"), state);
|
|
const tokens = object(await http("/api/muse/v1/oauth/token", "POST", {
|
|
grant_type: "authorization_code", client_id: client.client_id, redirect_uri: redirectUri,
|
|
code: string(callback.searchParams.get("code")), code_verifier: verifier,
|
|
}));
|
|
assert.equal(tokens.expires_in, 900);
|
|
credentials = { bindingId: pairing.bindingId, generation: pairing.generation, accessToken: string(tokens.access_token) };
|
|
const identified = await query({ query: "identify" });
|
|
assert.equal(identified.companyId, companyId); assert.equal(identified.agentId, agentId);
|
|
evidence.authentication = "synthetic DCR + single-use pairing ticket + S256 PKCE; tokens only in memory";
|
|
evidence.signalMode = "public_binding_header";
|
|
evidence.liveMuseDetectorCompatible = evidence.signalMode === "public_binding_header";
|
|
await signal(); detectorTimer = setInterval(() => {
|
|
if (detectorFlight) return;
|
|
detectorFlight = signal().catch(() => { detectorError = true; }).finally(() => { detectorFlight = undefined; });
|
|
}, 5000);
|
|
const challenge = await until("persisted detector contact and challenge", async () => {
|
|
const mailbox = await query({ query: "mailbox", after: 0 });
|
|
return rows(mailbox.items).find(row => row.kind === "readiness_challenge") ?? {};
|
|
}, value => typeof object(value.references ?? {}).nonce === "string");
|
|
await command({ command: "challenge.confirm", nonce: object(challenge.references).nonce });
|
|
const ready = object(object(await board(bindingPath)).binding);
|
|
assert.equal(ready.status, "ready"); assert.equal(ready.receiverDetected, true); assert.equal(ready.backgroundReplyVerified, true);
|
|
step("synthetic-readiness-verified");
|
|
const created = object(await board(`/api/companies/${companyId}/issues`, "POST", {
|
|
title: `Synthetic Muse native smoke ${marker}`, description: "A deterministic two-turn operator smoke. Write the synthetic report, ask its native question, finish, then apply the synthetic follow-up.",
|
|
status: "todo", assigneeAgentId: agentId, responsibleUserId: operator.userId, reviewPolicy: null, priority: "low",
|
|
}));
|
|
issueId = string(created.id); evidence.issueId = issueId;
|
|
const first = await assignment(); await accept(first);
|
|
const documentBody = `# Synthetic Muse report\n\nMarker: ${marker}\n\n17 + 25 = 42.\n`;
|
|
await command({ command: "tool", assignmentId: first.assignmentId, name: "write_document", arguments: {
|
|
key: "report", title: "Synthetic Muse report", body: documentBody, baseRevisionId: null, idempotencyKey: randomUUID() } });
|
|
const document = object(await board(`/api/issues/${issueId}/documents/report`)); assert.equal(document.body, documentBody.trim());
|
|
step("native-document-written", { runId: runIds[0], canonicalBody: true });
|
|
const nativeRequestId = `smoke-question-${randomUUID()}`;
|
|
await command({ command: "request_user_input", assignmentId: first.assignmentId, nativeRequestId,
|
|
questionSet: { schema: "paperclip.question_set.v1", questions: [{ id: "environment", prompt: "Which synthetic environment?", required: true,
|
|
answerMode: "single_select", options: [{ id: "test", label: "Test" }, { id: "production", label: "Production" }] }] } });
|
|
const interaction = await until("actual pending native question", async () =>
|
|
rows(await board(`/api/issues/${issueId}/interactions`)).find(row => row.kind === "ask_user_questions" && object(row.payload).runtimeRequestId === nativeRequestId) ?? {}, value => value.status === "pending");
|
|
assert.equal(interaction.sourceRunId, runIds[0]);
|
|
const pendingEvent = await until("durable runtime_request.created v2", async () =>
|
|
rows(await board(`/api/heartbeat-runs/${runIds[0]}/events?limit=1000`)).find(row => {
|
|
if (row.eventType !== "runtime_request.created") return false;
|
|
const event = object(object(row.payload).prpEvent ?? {});
|
|
return event.eventType === "runtime_request.created" && object(object(event.payload).request).requestId === nativeRequestId;
|
|
}) ?? {}, value => typeof value.id === "number");
|
|
const nativeRequest = object(object(object(object(pendingEvent.payload).prpEvent).payload).request);
|
|
assert.equal(nativeRequest.schema, "paperclip.runtime_request.v2"); assert.equal(nativeRequest.status, "pending");
|
|
step("native-question-pending", { runId: runIds[0], interactionId: interaction.id, nativeRequestId });
|
|
const answered = object(await board(`/api/issues/${issueId}/interactions/${interaction.id}/respond`, "POST", {
|
|
answers: [{ questionId: "environment", optionIds: ["test"] }] })); assert.equal(answered.status, "answered");
|
|
const input = await until("durable answer input", () => query({ query: "input.pending", assignmentId: first.assignmentId, nativeRequestId }), value => typeof value.inputDigest === "string");
|
|
assert.equal(input.requestId, nativeRequestId); assert.equal(input.turnId, object(first.binding).turnId);
|
|
assert.equal(input.inputDigest, digestPaperclipSemanticContent({ requestId: nativeRequestId, turnId: input.turnId, response: input.response }));
|
|
const resolvedEvent = await until("durable native answer resolution", async () =>
|
|
rows(await board(`/api/heartbeat-runs/${runIds[0]}/events?limit=1000`)).find(row => {
|
|
if (row.eventType !== "runtime_request.resolved") return false;
|
|
const event = object(object(row.payload).prpEvent ?? {});
|
|
return event.eventType === "runtime_request.resolved" && object(event.payload).requestId === nativeRequestId;
|
|
}) ?? {}, value => typeof value.id === "number");
|
|
const resolvedPayload = object(object(object(resolvedEvent.payload).prpEvent).payload);
|
|
assert.equal(resolvedPayload.turnId, input.turnId); assert.equal(resolvedPayload.action, "submit");
|
|
const continuationReceiptId = randomUUID(), continuationFile = join(evidenceDirectory, `continuation-${continuationReceiptId}.json`);
|
|
await writeFile(continuationFile, JSON.stringify({ synthetic: true, continuationReceiptId, requestId: nativeRequestId,
|
|
turnId: input.turnId, inputDigest: input.inputDigest, response: input.response }), { mode: 0o600, flag: "wx" });
|
|
assert.equal(object(JSON.parse(await readFile(continuationFile, "utf8"))).inputDigest, input.inputDigest);
|
|
await command({ command: "consume_input", assignmentId: first.assignmentId, nativeRequestId, inputDigest: input.inputDigest,
|
|
continuationReceiptId, continuationPersisted: true });
|
|
const consumed = await query({ query: "input.pending", assignmentId: first.assignmentId, nativeRequestId }); assert.equal(consumed.consumed, true);
|
|
evidence.question = { interactionId: interaction.id, nativeRequestId, questionCommandId: `question_${interaction.id}`,
|
|
pendingNative: true, nativeEventId: pendingEvent.id, resolvedNativeEventId: resolvedEvent.id, resolvedNativeRequestId: resolvedPayload.requestId, nativeRequestSchema: nativeRequest.schema, answeredStatus: answered.status, inputDigest: input.inputDigest, continuationReceiptId, continuationPersisted: true, consumed: true };
|
|
step("native-question-consumed");
|
|
await finish(first, "Saved the synthetic report and consumed the selected test environment.");
|
|
const reopen = object(await board(`/api/issues/${issueId}/comments`, "POST", { body: `${marker}: revise the report after completed work.`, reopen: true, clientRequestId: randomUUID() }));
|
|
const second = await assignment(); await accept(second);
|
|
const followUp = object(await board(`/api/issues/${issueId}/comments`, "POST", { body: `${marker}: add follow-up acknowledged.`, clientRequestId: randomUUID() }));
|
|
const mailbox = await until("active native follow-up reference", () => query({ query: "mailbox", after: 0 }), value =>
|
|
rows(value.items).some(row => row.kind === "follow_up" && object(row.references).commentId === followUp.id));
|
|
assert.ok(rows(mailbox.items).some(row => object(row.references).assignmentId === second.assignmentId && object(row.references).commentId === followUp.id));
|
|
const history = await command({ command: "tool", assignmentId: second.assignmentId, name: "get_task_history", arguments: {} });
|
|
assert.ok(rows(object(history.result).comments).some(comment => comment.id === followUp.id));
|
|
const latest = object(await board(`/api/issues/${issueId}/documents/report`));
|
|
const revised = `${documentBody}\nFollow-up acknowledged.\n`;
|
|
await command({ command: "tool", assignmentId: second.assignmentId, name: "write_document", arguments: {
|
|
key: "report", title: "Synthetic Muse report", body: revised, baseRevisionId: latest.latestRevisionId, idempotencyKey: randomUUID() } });
|
|
assert.equal(object(await board(`/api/issues/${issueId}/documents/report`)).body, revised.trim());
|
|
evidence.followUp = { reopenCommentId: reopen.id, activeCommentId: followUp.id, mailboxReference: true, successfulHistoryReceipt: true, reportRevised: true };
|
|
await finish(second, "Applied the synthetic follow-up to the report.");
|
|
await delay(1500);
|
|
const runs = rows(await board(`/api/companies/${companyId}/heartbeat-runs?agentId=${agentId}&limit=100`)).filter(run =>
|
|
run.issueId === issueId || run.nativeIssueId === issueId || object(run.contextSnapshot ?? {}).issueId === issueId);
|
|
assert.equal(runs.length, 2, "Unread follow-up must not create a third run"); assert.ok(runs.every(run => run.status === "succeeded"));
|
|
const completedRuns = await Promise.all(runs.map(async run => object(await board(`/api/heartbeat-runs/${string(run.id)}`))));
|
|
evidence.runs = completedRuns.map(run => ({ id: run.id, status: run.status, runtimeMode: run.runtimeMode, driverKind: run.driverKind,
|
|
createdAt: run.createdAt, startedAt: run.startedAt, finishedAt: run.finishedAt,
|
|
nativeInputSchema: object(object(run.runnerProfileJson).nativeExecutionInput).schema,
|
|
finalizationPhase: object(run.resultJson ?? {}).finalizationPhase }));
|
|
assert.equal(detectorError, false); evidence.detector = { syntheticContacts: detectorRequests, cadenceMs: 5000,
|
|
signalMode: evidence.signalMode, secretUsed: false,
|
|
realHooksInstalled: false, liveMuseCompatible: evidence.liveMuseDetectorCompatible };
|
|
evidence.passed = true; step("passed", { nativeTurns: 2 });
|
|
} catch (error) {
|
|
evidence.passed = false; evidence.failure = { stage, observed: error instanceof Error ? error.message : "Unknown failure", classification: "requires diagnosis" };
|
|
process.exitCode = 1;
|
|
} finally {
|
|
if (detectorTimer) clearInterval(detectorTimer);
|
|
await detectorFlight;
|
|
cleanupDeadline = Date.now() + 30_000;
|
|
const cleanup: Json = {};
|
|
try {
|
|
for (const runId of runIds) {
|
|
const run = object(await board(`/api/heartbeat-runs/${runId}`));
|
|
if (["queued", "running"].includes(String(run.status))) await board(`/api/heartbeat-runs/${runId}/cancel`, "POST", { cancellationRequestId: randomUUID() });
|
|
}
|
|
if (createdBindingId) {
|
|
const connection = object(await board(bindingPath)), binding = connection.binding ? object(connection.binding) : undefined;
|
|
assert.equal(binding?.id, createdBindingId, "Never revoke an unrelated connection during cleanup");
|
|
if (binding && binding.status !== "revoked") await board(`${bindingPath}/revoke`, "POST", { bindingId: binding.id, generation: binding.generation, expectedRevision: binding.revision });
|
|
} else cleanup.noAuthorityCreated = true;
|
|
// Revocation deliberately removes all OAuth authority. The synthetic
|
|
// detector is stopped locally; no post-revocation acknowledgement is forged.
|
|
cleanup.authenticatedRemoteCleanup = "unconfirmed after strict revocation";
|
|
cleanup.normalAuthorityRevoked = true; cleanup.syntheticDetectorStopped = true;
|
|
if (createdBindingId) {
|
|
const after = object(object(await board(bindingPath)).binding);
|
|
assert.equal(after.id, createdBindingId);
|
|
const stop = object(after.stop);
|
|
cleanup.stopStatus = stop.status; cleanup.nativeEffectsUnknown = stop.nativeEffectsUnknown;
|
|
if (stop.status !== "none" || after.uncertainOperations !== 0) cleanup.requiresOperatorReview = true;
|
|
}
|
|
} catch { cleanup.completed = false; cleanup.requiresOperatorReview = true; }
|
|
evidence.cleanup = cleanup; evidence.completedAt = new Date().toISOString();
|
|
await writeFile(join(evidenceDirectory, "evidence.json"), JSON.stringify(evidence, null, 2), { mode: 0o600 });
|
|
console.log(JSON.stringify({ synthetic: true, passed: evidence.passed === true, stage, evidence: join(evidenceDirectory, "evidence.json"), cleanup }));
|
|
}
|