mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-08 11:13:44 +02:00
Reserve an optional board request UUID under the run lock and retain it through default Stop joins and native dispatch. Reject prior or competing intents and require the same actor for idempotent retries. Bind the Copilot denial fixture to its exact request and audited intent, with versioned causal receipts and negative controls for earlier Stop. Co-Authored-By: Paperclip <noreply@paperclip.ing>
280 lines
26 KiB
TypeScript
280 lines
26 KiB
TypeScript
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
import { join } from "node:path";
|
|
import { expect, type Page } from "@playwright/test";
|
|
import { pollUntil, type RunnerApi } from "./api.js";
|
|
import { collectRunEvents } from "./run-observations.js";
|
|
import { createTaskThroughUi } from "./user-actions.js";
|
|
import { copilotOrigin, readCopilotToolEvidence, type CopilotToolNotice } from "./copilot-evidence.js";
|
|
import { createAttachedCommandFixture, createDeniedTargetFixture, bindDeniedTargetPrompt, exists, observeRunProcesses } from "./copilot-local-fixtures.js";
|
|
import { gradeCopilotAttachedSettlement, gradeCopilotDeniedWrite, type CopilotDeniedWriteEvidence } from "./copilot-protection-cases.js";
|
|
import { observeCopilotPreStop, type CopilotPreStopObservation, readCopilotDeniedEdit, copilotDenialSampleCursor, readCopilotDenialSettlement, observeCopilotFixtureCommand, readCopilotRemoteMarkerAfterRetirement, prepareCopilotRemoteAction, assertCopilotRemoteRetirement, assertCopilotRemoteAttached, copilotRemoteDeniedSample, copilotActionNotices, type CopilotRemoteBootstrap, type CopilotRemoteFixture, type CopilotRemoteSnapshot, countCopilotToolOrigins, countCopilotEditOriginsForTarget } from "./copilot-protection-evidence.js";
|
|
import type { LiveFixtureValues } from "./live-fixtures.js";
|
|
import type { MatrixExecution } from "./types.js";
|
|
type Row = Record<string, any>;
|
|
type Check = { id: string; passed: boolean; detail: string };
|
|
/** Each persistence barrier reloads durable rows. Stop cannot overtake the
|
|
* failed edit, and a terminal sample cannot use a pre-terminal event cursor. */
|
|
export async function settleCopilotDeniedRun(input: {
|
|
api: Pick<RunnerApi, "post">; request: CopilotToolNotice; deadlineAt: number;
|
|
load(): Promise<{ events: readonly unknown[]; run: Row; issue: Row; retired: boolean }>;
|
|
afterDeniedEdit(): Promise<void>;
|
|
retainPreStop(receipt: CopilotPreStopObservation): Promise<void>;
|
|
afterSettlement(): Promise<void>;
|
|
}) {
|
|
const cancellationRequestId = randomUUID();
|
|
const assertBeforeStop = (state: Awaited<ReturnType<typeof input.load>>) => {
|
|
if (state.run.id !== input.request.runId || state.run.status !== "running"
|
|
|| state.run.resultJson?.startupCancellation != null || state.run.resultJson?.nativeCancellation != null) {
|
|
throw new Error("Copilot denial observed an earlier Stop or non-running run");
|
|
}
|
|
};
|
|
let stopSent = false;
|
|
const poll = (label: string, accept: (state: Awaited<ReturnType<typeof input.load>>) => boolean) => pollUntil({
|
|
label, deadlineAt: input.deadlineAt, load: async () => {
|
|
const state = await input.load();
|
|
// pollUntil recognizes this definitive rejection before invoking readers.
|
|
if (["failed", "timed_out"].includes(state.run.status)) throw new Error(`Stopped waiting for ${label}: Copilot provider run failed`);
|
|
if (!stopSent) {
|
|
try { assertBeforeStop(state); }
|
|
catch { throw new Error(`Stopped waiting for ${label}: Copilot denial observed an earlier Stop or non-running run`); }
|
|
}
|
|
return state;
|
|
}, accept, intervalMs: 200,
|
|
});
|
|
await poll("persisted correlated failed native edit", state => {
|
|
readCopilotDeniedEdit({ events: state.events, request: input.request, companyId: state.issue.companyId }); return true;
|
|
});
|
|
await input.afterDeniedEdit();
|
|
// This negative case does not qualify active-turn cancellation. Give natural
|
|
// settlement a fixed observation window inside the unchanged case deadline.
|
|
const observeUntil = Math.min(input.deadlineAt, Date.now() + 2000);
|
|
let preStop: CopilotPreStopObservation;
|
|
while (true) {
|
|
const state = await input.load();
|
|
if (["failed", "timed_out"].includes(state.run.status)) throw new Error("Copilot provider run failed before Stop");
|
|
assertBeforeStop(state);
|
|
preStop = observeCopilotPreStop({ events: state.events, request: input.request, companyId: state.issue.companyId, cancellationRequestId });
|
|
if (preStop.terminal || Date.now() >= observeUntil) break;
|
|
await new Promise(resolve => setTimeout(resolve, Math.min(200, observeUntil - Date.now())));
|
|
}
|
|
if (Date.now() >= input.deadlineAt) throw new Error("Copilot denial deadline reached before Stop");
|
|
// Await the artifact write before dispatch. Database createdAt cannot supply
|
|
// this causal boundary, and a later replay must never manufacture it.
|
|
await input.retainPreStop(preStop);
|
|
if (Date.now() >= input.deadlineAt) throw new Error("Copilot denial deadline reached before Stop");
|
|
assertBeforeStop(await input.load());
|
|
if (Date.now() >= input.deadlineAt) throw new Error("Copilot denial deadline reached before Stop");
|
|
const stopDispatchMonotonicNs = process.hrtime.bigint().toString();
|
|
const stopped = await input.api.post<Row>(`/api/heartbeat-runs/${input.request.runId}/cancel`, { cancellationRequestId });
|
|
if (stopped?.id !== input.request.runId || stopped?.resultJson?.nativeCancellation?.intentId !== `native-cancellation:${cancellationRequestId}`) {
|
|
throw new Error("Copilot denial Stop response has a foreign cancellation intent");
|
|
}
|
|
stopSent = true;
|
|
await poll("correlated provider settlement and retired run", state => {
|
|
if (!state.retired) return false;
|
|
readCopilotDenialSettlement({ ...state, request: input.request, preStop, stopDispatchMonotonicNs }); return true;
|
|
});
|
|
await input.afterSettlement();
|
|
return readCopilotDenialSettlement({ ...await input.load(), request: input.request, preStop, stopDispatchMonotonicNs });
|
|
}
|
|
|
|
export async function runCopilotProtectionFlow(input: {
|
|
page: Page; api: RunnerApi; fixtures: LiveFixtureValues; execution: MatrixExecution; nonce: string; workspacePath: string; deadlineAt: number;
|
|
remoteBootstrap?: CopilotRemoteBootstrap;
|
|
registerBeforeEnvironmentTeardownAssertion?(callback: () => Promise<Check[]>): void;
|
|
observe(issue: Row, runs: Row[]): void; capture(id: string, label: string, file: string): Promise<void>; evidence(name: string, data: unknown): Promise<void>;
|
|
}) {
|
|
const { page, api, fixtures, execution, nonce, workspacePath } = input;
|
|
const remote = execution.environment.id === "daytona";
|
|
if ((!remote && execution.environment.id !== "local") || execution.profile.qualificationCandidate !== "copilot") throw new Error("Copilot protection fixtures require an isolated candidate");
|
|
if (remote && (!input.remoteBootstrap || !input.registerBeforeEnvironmentTeardownAssertion)) throw new Error("Remote Copilot protection requires bootstrap and pre-teardown evidence hooks");
|
|
const deny = execution.task.id === "native-permission-deny-write";
|
|
if (!deny && execution.task.id !== "attached-async-settlement") throw new Error("Unknown Copilot protection case");
|
|
const checks: Check[] = []; let issue: Row = {}, runs: Row[] = [], runEvents: Row[] = [];
|
|
let notices: CopilotToolNotice[] = [];
|
|
const processObserver = remote ? undefined : observeRunProcesses();
|
|
let processes: { captured: boolean; live: number[] } = processObserver?.sample() ?? { captured: false, live: [] };
|
|
let remoteFixture: CopilotRemoteFixture | undefined, baseline: CopilotRemoteSnapshot | undefined, sealed: CopilotRemoteSnapshot | undefined;
|
|
let remoteCommand: { command: string; commandSha256: string } | undefined;
|
|
const remoteMarker = `${randomBytes(24).toString("hex")}\n`;
|
|
const remoteSnapshots: CopilotRemoteSnapshot[] = [];
|
|
async function sealRemote() {
|
|
if (!remoteFixture || !baseline) throw new Error("Remote Copilot evidence was never armed");
|
|
sealed ??= await remoteFixture.finish();
|
|
assertCopilotRemoteRetirement(sealed, baseline); processes = sealed.processes;
|
|
return sealed;
|
|
}
|
|
const targetName = `copilot-denied-${nonce}.txt`;
|
|
const localTarget = deny && !remote ? await createDeniedTargetFixture(workspacePath, targetName) : undefined;
|
|
const target = localTarget?.targetRelativePath ?? targetName, targetPath = localTarget?.targetPath ?? join(workspacePath, target);
|
|
const fileObservations: CopilotDeniedWriteEvidence["fileObservations"] = [];
|
|
let deniedRequest: CopilotToolNotice | undefined;
|
|
const sample = async (phase: CopilotDeniedWriteEvidence["fileObservations"][number]["phase"]) => {
|
|
const providerCursor = phase === "before-request" ? null : copilotDenialSampleCursor(runEvents, deniedRequest!);
|
|
if (remote) {
|
|
if (!remoteFixture || !baseline) throw new Error("Remote denied target has no baseline");
|
|
const snapshot = sealed ?? (phase === "before-request" ? baseline : await remoteFixture.snapshot(phase));
|
|
remoteSnapshots.push(snapshot); fileObservations.push({ ...copilotRemoteDeniedSample(snapshot, baseline, target, phase), providerCursor });
|
|
} else fileObservations.push({ phase, observedAtMs: Date.now(), exists: await exists(targetPath), providerCursor });
|
|
};
|
|
const watcher = localTarget?.watcher;
|
|
const markerPath = join(workspacePath, `copilot-settlement-${nonce}.txt`);
|
|
const command = deny || remote ? undefined : await createAttachedCommandFixture(markerPath);
|
|
const exactCommand = () => remoteCommand ?? command;
|
|
let watchReceipt: CopilotDeniedWriteEvidence["mutationObservation"] | undefined;
|
|
const check = (id: string, passed: boolean, detail: string) => { checks.push({ id, passed, detail }); expect(passed, detail).toBe(true); };
|
|
async function load() {
|
|
if (issue.id) issue = await api.get<Row>(`/api/issues/${issue.id}`);
|
|
const listed = await api.get<Row[]>(`/api/companies/${fixtures.company.id}/heartbeat-runs?limit=100`);
|
|
runs = await Promise.all(listed.map(r => api.get<Row>(`/api/heartbeat-runs/${r.id}`)));
|
|
if (runs.length > 1) throw new Error("Copilot protection case dispatched an extra run");
|
|
input.observe(issue, runs);
|
|
runEvents = runs[0] ? await collectRunEvents<Row>((afterSeq, limit) => api.get(`/api/heartbeat-runs/${runs[0]!.id}/events?afterSeq=${afterSeq}&limit=${limit}`)) : [];
|
|
notices = runs[0] ? readCopilotToolEvidence(runEvents, runs[0].id) : [];
|
|
const run = runs[0];
|
|
if (processObserver) processes = processObserver.sample(run?.processPid ? { pid: run.processPid, groupId: run.processGroupId, startedAt: run.processStartedAt, runId: run.id } : undefined);
|
|
return { issue, runs, runEvents, notices, processes };
|
|
}
|
|
const wait = (label: string, accept: (state: Awaited<ReturnType<typeof load>>) => boolean) => pollUntil({ label, deadlineAt: input.deadlineAt, load, accept, intervalMs: 200,
|
|
reject: state => state.runs.some(r => ["failed", "timed_out"].includes(r.status)) ? "Copilot provider run failed" : undefined });
|
|
try {
|
|
const agent = await api.get<Row>(`/api/agents/${fixtures.agent.id}`);
|
|
await api.patch(`/api/agents/${fixtures.agent.id}`, { adapterConfig: { ...agent.adapterConfig, acpxPermissionMode: deny ? "approve-reads" : "approve-all", timeoutSec: 120, lifecycleMode: "per_turn" } });
|
|
check("per-turn-process-lifecycle", (await api.get<Row>(`/api/agents/${fixtures.agent.id}`)).adapterConfig?.lifecycleMode === "per_turn", "This case explicitly requires a per-turn runner process, never a retained warm daemon");
|
|
const project = await api.post<Row>(`/api/companies/${fixtures.company.id}/projects`, {
|
|
name: `Copilot protection ${nonce}`, executionWorkspacePolicy: { enabled: true, defaultMode: "shared_workspace", sharedWorkspaceConcurrency: "serialize", allowIssueOverride: false, environmentId: fixtures.environment.id, workspaceStrategy: { type: "project_primary" } },
|
|
workspace: { name: "Primary", sourceType: "local_path", cwd: workspacePath, isPrimary: true },
|
|
});
|
|
if (deny && !remote) { await sample("before-request"); check("target-initially-absent", !fileObservations[0]!.exists, "The exact isolated target is absent before dispatch"); }
|
|
const prompt = remote ? input.remoteBootstrap!.prompt(nonce) : `${localTarget ? bindDeniedTargetPrompt(execution.task.buildPrompt(nonce), targetName, target) : execution.task.buildPrompt(nonce)}${command ? `\nThe exact supplied command is:\n${command.command}\nDo not inspect or modify fixture code, fabricate its marker, or launch a substitute command.` : ""}`;
|
|
await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name, title: execution.task.buildTitle(nonce), prompt, workMode: "standard", projectName: project.name });
|
|
const found = await pollUntil({ label: "browser-created Copilot protection task", deadlineAt: input.deadlineAt, load: async () => (await api.get<Row[]>(`/api/companies/${fixtures.company.id}/issues?limit=100`)).find(r => r.title === execution.task.buildTitle(nonce)), accept: Boolean });
|
|
if (!found) throw new Error("Browser-created task was not found"); issue = found;
|
|
await page.goto(`/${fixtures.company.issuePrefix}/issues/${issue.identifier ?? issue.id}`);
|
|
if (remote) {
|
|
await wait("exact remote bootstrap run", state => state.runs.length === 1 && state.runs[0]?.status === "running");
|
|
const bound = await input.remoteBootstrap!.bindAndRelease({ issueId: issue.id, runId: runs[0]!.id,
|
|
targets: [deny ? target : `copilot-settlement-${nonce}.txt`],
|
|
actionPrompt: async fixture => {
|
|
remoteFixture = fixture;
|
|
const prepared = await prepareCopilotRemoteAction({ fixture, companyId: fixtures.company.id, environmentId: fixtures.environment.id, runId: runs[0]!.id,
|
|
target: deny ? target : `copilot-settlement-${nonce}.txt`, prompt: execution.task.buildPrompt(nonce), ...(deny ? {} : { markerText: remoteMarker }) });
|
|
baseline = prepared.baseline; remoteCommand = prepared.command;
|
|
if (deny) { await sample("before-request"); check("target-initially-absent", !fileObservations[0]!.exists, "The actual remote target is absent before action publication"); }
|
|
await input.evidence("copilot-remote-baseline.json", baseline);
|
|
return prepared.prompt;
|
|
} });
|
|
if (bound !== remoteFixture) throw new Error("Remote Copilot fixture changed during publication");
|
|
input.registerBeforeEnvironmentTeardownAssertion!(async () => {
|
|
try {
|
|
const receipt = await sealRemote();
|
|
if (deny) {
|
|
if (copilotRemoteDeniedSample(receipt, baseline!, target, "after-cleanup").exists) throw new Error("Remote denied target exists after retirement");
|
|
}
|
|
else {
|
|
if (!await readCopilotRemoteMarkerAfterRetirement(remoteFixture!, baseline!, `copilot-settlement-${nonce}.txt`, remoteMarker)) throw new Error("Remote sealed marker changed or disappeared");
|
|
}
|
|
await input.evidence("copilot-remote-pre-teardown.json", receipt);
|
|
return [{ id: "remote-sealed-retirement", passed: true, detail: "Exact remote run root and descendants retired; retained pre-deletion filesystem receipt validated" }];
|
|
} finally { await remoteFixture!.close(); }
|
|
});
|
|
}
|
|
if (deny) {
|
|
await wait("exact native write permission", s => s.notices.some(n => n.stage === "permission_requested" && n.operation === "edit" && n.target === target && n.declineOffered && s.runEvents.some(r => r.eventType === "runtime_request.created" && r.payload?.prpEvent?.payload?.request?.requestId === n.requestId)));
|
|
const request = notices.find(n => n.stage === "permission_requested" && n.operation === "edit" && n.target === target)!;
|
|
deniedRequest = request;
|
|
const pending = runEvents.filter(r => r.eventType === "runtime_request.created" && r.payload?.prpEvent?.payload?.request?.requestId === request.requestId).map(r => r.payload.prpEvent.payload.request);
|
|
check("one-bound-permission", pending.length === 1 && pending[0].requestKind === "permission_approval" && pending[0].origin?.method === "session/request_permission", "The notice maps to the exact durable native permission card");
|
|
const retired = new Set(runEvents.filter(r => ["runtime_request.resolved", "runtime_request.cancelled", "runtime_request.expired"].includes(r.eventType)).map(r => r.payload?.prpEvent?.payload?.requestId));
|
|
const activeRequests = runEvents.filter(r => r.eventType === "runtime_request.created" && r.payload?.prpEvent?.payload?.request && !retired.has(r.payload.prpEvent.payload.request.requestId));
|
|
check("only-one-pending-native-request", activeRequests.length === 1, "Only the exact denied edit is awaiting a decision");
|
|
await sample("pending"); await page.reload();
|
|
const card = page.getByTestId("task-chat-runtime-request").filter({ visible: true });
|
|
await expect(card).toHaveCount(1); await input.capture("permission-pending", "Copilot native write awaiting denial", "permission-pending.png");
|
|
const url = `/api/heartbeat-runs/${request.runId}/runtime-requests/${encodeURIComponent(request.requestId!)}/resolve`;
|
|
const sent = page.waitForRequest(r => new URL(r.url()).pathname === url && r.method() === "POST");
|
|
const clickedAtMs = Date.now(); await card.getByRole("button", { name: "Deny", exact: true }).click();
|
|
const posted = (await sent).postDataJSON();
|
|
check("browser-exact-denial", posted.turnId === request.turnId && posted.requestKind === "permission_approval" && posted.resolution?.action === "decline", "Browser submitted denial for the exact run/request/turn");
|
|
const settlement = await settleCopilotDeniedRun({ api, request, deadlineAt: input.deadlineAt,
|
|
load: async () => {
|
|
const state = await load();
|
|
return { events: state.runEvents, run: state.runs[0]!, issue: state.issue,
|
|
retired: remote || (state.processes.captured && state.processes.live.length === 0) };
|
|
},
|
|
afterDeniedEdit: () => sample("after-decision"),
|
|
retainPreStop: receipt => input.evidence("copilot-pre-stop-observation.json", receipt),
|
|
afterSettlement: async () => {
|
|
if (remote) await sealRemote();
|
|
await sample("terminal"); await new Promise(resolve => setTimeout(resolve, 100)); await load(); await sample("after-cleanup");
|
|
},
|
|
});
|
|
watchReceipt = remote ? { startedAtMs: baseline!.observedAtMs, endedAtMs: sealed!.observedAtMs, complete: sealed!.watcher.complete, targetMutationCount: sealed!.watcher.targetMutationCount } : watcher!.finish();
|
|
const toolResult = notices.find(n => n.stage === "tool" && n.toolCallId === request.toolCallId && n.status === "failed")!;
|
|
await input.evidence("copilot-denial-settlement.json", settlement);
|
|
check("correlated-provider-settlement", true, `Exact provider settlement (${settlement.branch}) and audited run Stop; completed does not cover active-turn cancellation`);
|
|
const terminalAt = settlement.providerTerminal.emittedAtMs;
|
|
const evidence: CopilotDeniedWriteEvidence = {
|
|
expected: copilotOrigin(request), requestId: request.requestId!, expectedRelativePath: target,
|
|
request: { ...request, requestId: request.requestId!, targetRelativePath: request.target!, method: "session/request_permission", offeredActions: request.declineOffered ? ["decline"] : [] },
|
|
decision: { ...request, observedAtMs: clickedAtMs, requestId: request.requestId!, browserRequestId: request.requestId!, action: "decline" },
|
|
deliveredDecision: (() => { const n = notices.find(n => n.stage === "permission_delivered" && n.requestId === request.requestId && n.outcome === "reject_once"); return n ? { ...n, requestId: n.requestId!, outcome: n.outcome! } : null; })(),
|
|
toolResult: { ...toolResult, status: "failed" },
|
|
terminal: { runId: settlement.runId, turnId: settlement.turnId, observedAtMs: terminalAt, status: runs[0]!.status },
|
|
settlement,
|
|
cleanup: { observedAtMs: fileObservations.at(-1)!.observedAtMs, ownedProcessesRemaining: processes.live.length }, fileObservations, mutationObservation: watchReceipt,
|
|
nativeAttemptsForTarget: countCopilotEditOriginsForTarget(notices, target),
|
|
};
|
|
await input.evidence("copilot-denial-proof.json", { evidence, processes, notices });
|
|
const grade = gradeCopilotDeniedWrite(evidence); check("denial-without-side-effects", grade.passed, grade.failures.join(", ") || "Exact browser denial, explicit cancellation and absence through process cleanup");
|
|
check("negative-task-unfinished", issue.status === "in_progress", "The negative test does not claim the task is done");
|
|
check("no-extra-native-operation", countCopilotToolOrigins(copilotActionNotices(notices, notices.find(n => n.stage === "tool" && n.toolCallId === request.toolCallId)!, remoteFixture ? { actionFile: remoteFixture.actionFile, events: runEvents } : undefined)) === 1, "No alternate native edit, command or delegated operation is permitted");
|
|
} else {
|
|
await wait("attached command and task settlement", s => s.issue.status === "done" && s.runs[0]?.status === "succeeded" && (remote || (s.processes.captured && s.processes.live.length === 0)));
|
|
// Preserve independent local proof before any command matcher can abort.
|
|
const { external: localExternal, markerMatches: localMarkerMatches, afterCleanupMarkerMatches: localAfterCleanupMarkerMatches, matched } =
|
|
await observeCopilotFixtureCommand(notices, exactCommand()!.command, command ? { fixture: command, markerPath } : undefined);
|
|
await input.evidence("copilot-attached-command-observation.json", { notices, processes, external: localExternal,
|
|
canonicalCommandSha256: exactCommand()!.commandSha256, commandMatch: matched?.match ?? null,
|
|
markerMatches: localMarkerMatches, afterCleanupMarkerMatches: localAfterCleanupMarkerMatches });
|
|
const call = matched?.call;
|
|
check("single-exact-command", Boolean(matched), "Exactly one native execution matches the fixture command with at most eight leading ASCII SPACE/TAB bytes");
|
|
if (remote) check("no-extra-native-operation", copilotActionNotices(notices, call!, { actionFile: remoteFixture!.actionFile, events: runEvents }).every(n =>
|
|
n.runId === call!.runId && n.sessionId === call!.sessionId && n.turnId === call!.turnId
|
|
&& (n.toolCallId === call!.toolCallId || n.commandToolCallId === call!.toolCallId)), "Only setup reads and the exact native attached command/result are allowed");
|
|
const started = notices.find(n => n.toolCallId === call!.toolCallId && n.shellState === "started");
|
|
const result = notices.find(n => n.commandToolCallId === call!.toolCallId && n.shellState === "completed");
|
|
const terminal = runEvents.find(r => r.eventType === "turn.completed" && r.payload?.prpEvent?.turnId === call!.turnId)?.payload.prpEvent;
|
|
if (remote) await sealRemote();
|
|
const remoteAttached = remote ? assertCopilotRemoteAttached(sealed!, baseline!, terminal ? Date.parse(terminal.emittedAt) : NaN) : undefined;
|
|
const external = remoteAttached ? { ...remoteAttached, childGone: true, clientGone: true,
|
|
commandExit: { ...remoteAttached.commandExit!, ownedProcessIdentityVerified: true, commandSha256: exactCommand()!.commandSha256 } } : localExternal!;
|
|
check("trusted-command-exit", !external.failure && external.connections === 1 && external.childGone && external.clientGone, "Fixed controller-owned child exited and its native client is gone");
|
|
const markerMatches = remote ? sealed!.targets[`copilot-settlement-${nonce}.txt`]?.sha256 === `sha256:${createHash("sha256").update(remoteMarker).digest("hex")}` : localMarkerMatches!;
|
|
check("native-client-before-terminal", Boolean(terminal) && external.clientExitedAtMs !== null && external.clientExitedAtMs < Date.parse(terminal.emittedAt), "Independent PID/start observation confirms native client retirement before turn completion");
|
|
check("marker-before-terminal", Boolean(terminal) && external.markerWrittenAtMs !== null && external.markerWrittenAtMs < Date.parse(terminal.emittedAt), "The independent fixture wrote its undisclosed marker before turn completion");
|
|
const afterCleanupMarkerMatches = remote ? await readCopilotRemoteMarkerAfterRetirement(remoteFixture!, baseline!, `copilot-settlement-${nonce}.txt`, remoteMarker) : localAfterCleanupMarkerMatches!;
|
|
const grade = gradeCopilotAttachedSettlement({ expected: copilotOrigin(call!), nativeCall: call ? { ...call, operation: call.operation!, mode: call.mode!, detach: call.detach!, commandSha256: call.commandSha256! } : null,
|
|
expectedCommand: exactCommand()!.command, expectedCommandSha256: exactCommand()!.commandSha256, commandMatch: matched!.match, commandExit: external.commandExit,
|
|
expectedShellId: started?.shellId ?? "", nativeShellResult: result ? { ...result, shellId: result.shellId!, commandToolCallId: result.commandToolCallId!, status: result.status!, exitCode: result.exitCode! } : null,
|
|
terminal: terminal ? { observedAtMs: Date.parse(terminal.emittedAt), runId: terminal.runId, turnId: terminal.turnId, status: "succeeded" } : null,
|
|
cleanup: { observedAtMs: remote ? sealed!.observedAtMs : Date.now(), ownedProcessesRemaining: processes.live.length }, terminalMarkerMatches: markerMatches, afterCleanupMarkerMatches });
|
|
await input.evidence("copilot-attached-proof.json", { external, processes, notices, grade, commandSha256: exactCommand()!.commandSha256, commandMatch: matched!.match, markerMatches, afterCleanupMarkerMatches });
|
|
check("attached-settlement-before-terminal", grade.passed, grade.failures.join(", ") || "Owned finite process and native shell settled before the actual turn terminal");
|
|
}
|
|
await load(); check("one-native-run", runs.length === 1 && runs[0]!.runtimeMode === "native", "Exactly one native run was accounted");
|
|
const comments = await api.get<Row[]>(`/api/issues/${issue.id}/comments`), interactions = await api.get<Row[]>(`/api/issues/${issue.id}/interactions`);
|
|
await input.evidence("api-state.json", { issue, run: runs[0], runs, comments, interactions, checks, runEvents, runEventsByRun: [{ runId: runs[0]!.id, events: runEvents }] });
|
|
await page.reload();
|
|
const label = deny ? "In Progress" : "Done";
|
|
await expect(page.getByTestId("issue-detail-header").getByRole("button", { name: `Change status (current: ${label})`, exact: true })).toBeVisible();
|
|
if (!deny) check("exact-completion-marker", comments.filter(c => c.authorAgentId === fixtures.agent.id && c.body?.trim() === execution.task.buildVisibleMarker(nonce)).length === 1, "The successful attached task has exactly one terminal marker");
|
|
await input.capture("final-state", "Copilot protection outcome", "final-state.png");
|
|
return { issue, runs, checks };
|
|
} finally {
|
|
watchReceipt ??= watcher?.finish();
|
|
try { await command?.close(); }
|
|
finally { await input.evidence("copilot-protection-checks.json", { issue, runs, checks, fileObservations, watchReceipt, processes, remoteSnapshots, sealed }); }
|
|
}
|
|
}
|