diff --git a/tests/runner-e2e/catalog.ts b/tests/runner-e2e/catalog.ts index 9c17d42c293..2014a717cf9 100644 --- a/tests/runner-e2e/catalog.ts +++ b/tests/runner-e2e/catalog.ts @@ -1147,21 +1147,21 @@ export const runnerSuites: readonly RunnerSuiteFixture[] = [ description: "Stop while one exact Cursor or Copilot native permission remains unanswered; require cancelled provider settlement, caller-owned acknowledgement, stale-answer refusal and independent retirement/no effects.", groups: ["native"], profiles: extendedHarnessProfiles.filter(profile => ["cursor", "copilot"].includes(profile.qualificationCandidate ?? "")), environments: runnerEnvironments, tasks: nativeActiveStopTasks, expectedMatrixSize: 4, - definitionMetadata: { version: 19, copilotDefinition: { version: 18, titleCreation: "explicit-search-composer", taskBinding: "browser-creation-response-id", processAuthority: "local-native-root-or-attested-remote-root", remoteTerminalDiagnostics: "bounded-incomplete-reasons-with-strict-failure", remoteSetupOrder: "observed-action-published-before-context-approval", remoteBootstrapContext: "already-exposed-dedicated-tool-no-http-substitution", bootstrapPrompt: "already-exposed-dedicated-context-before-native-edit", editOrigin: "exact-permission-target-and-native-call-identity", stopAcknowledgement: "retained-current-mainline-api-response", contextPermission: "context-and-bounded-attested-discovery-sequence", permissionCard: "current-operation-and-decision-control-excludes-history", qualification: "pending", scheduling: "explicit-only", evidence: "paperclip.e2e.native-active-stop-settlement.v3", pendingObservation: "retained-api-before-stop-with-fixture-correlation", normalCompletionAccepted: false, permissionPolicy: "approve-reads", lifecycle: "per_turn", remoteEvidence: "paperclip.e2e.native-active-stop-remote-retirement.v1", remoteObservationCoverage: "continuous-through-owned-process-retirement", filesystemAfterRemoteRetirementObserved: false, localObservationCoverage: "four-phases-through-cleanup", providerDeath: "not-covered" }, qualification: "pending", scheduling: "explicit-only", evidence: "paperclip.e2e.native-active-stop-settlement.v2", pendingObservation: "retained-api-before-caller-uuid-stop", normalCompletionAccepted: false, permissionPolicy: "approve-reads", lifecycle: "per_turn", remoteEvidence: "paperclip.e2e.native-active-stop-remote-retirement.v1", remoteObservationCoverage: "continuous-through-owned-process-retirement", filesystemAfterRemoteRetirementObserved: false, localObservationCoverage: "four-phases-through-cleanup", providerDeath: "not-covered" }, + definitionMetadata: { version: 20, copilotDefinition: { version: 19, titleCreation: "explicit-search-composer", taskBinding: "browser-creation-response-id", processAuthority: "local-native-root-or-attested-remote-root", remoteTerminalDiagnostics: "bounded-incomplete-reasons-with-strict-failure", remoteSetupOrder: "observed-action-published-before-context-approval", remoteBootstrapContext: "already-exposed-dedicated-tool-no-http-substitution", bootstrapPrompt: "already-exposed-dedicated-context-before-native-edit", editOrigin: "exact-permission-target-and-native-call-identity", stopAcknowledgement: "retained-current-mainline-api-response", contextPermission: "context-and-bounded-attested-discovery-sequence-await-origin-rows", permissionCard: "current-operation-and-decision-control-excludes-history", qualification: "pending", scheduling: "explicit-only", evidence: "paperclip.e2e.native-active-stop-settlement.v3", pendingObservation: "retained-api-before-stop-with-fixture-correlation", normalCompletionAccepted: false, permissionPolicy: "approve-reads", lifecycle: "per_turn", remoteEvidence: "paperclip.e2e.native-active-stop-remote-retirement.v1", remoteObservationCoverage: "continuous-through-owned-process-retirement", filesystemAfterRemoteRetirementObserved: false, localObservationCoverage: "four-phases-through-cleanup", providerDeath: "not-covered" }, qualification: "pending", scheduling: "explicit-only", evidence: "paperclip.e2e.native-active-stop-settlement.v2", pendingObservation: "retained-api-before-caller-uuid-stop", normalCompletionAccepted: false, permissionPolicy: "approve-reads", lifecycle: "per_turn", remoteEvidence: "paperclip.e2e.native-active-stop-remote-retirement.v1", remoteObservationCoverage: "continuous-through-owned-process-retirement", filesystemAfterRemoteRetirementObserved: false, localObservationCoverage: "four-phases-through-cleanup", providerDeath: "not-covered" }, }, { id: "copilot-provider-death", label: "Copilot provider death", manualOnly: true, description: "Terminate only the verified owned Copilot child with an unanswered permission; require callback expiry, stale-answer refusal, no replay and independent retirement.", groups: ["native"], profiles: extendedHarnessProfiles.filter(profile => profile.qualificationCandidate === "copilot"), environments: runnerEnvironments, tasks: copilotProviderDeathTasks, expectedMatrixSize: 2, - definitionMetadata: { version: 16, titleCreation: "explicit-search-composer", taskBinding: "browser-creation-response-id", linuxDeathExecutable: "verified-inherited-descriptor-and-live-executable-inode", processAuthority: "local-native-root-or-attested-remote-root", remoteTerminalDiagnostics: "bounded-incomplete-reasons-with-strict-failure", remoteSetupOrder: "observed-action-published-before-context-approval", remoteBootstrapContext: "already-exposed-dedicated-tool-no-http-substitution", finalUiStatus: "blocked-with-current-mainline-attention-label", bootstrapPrompt: "already-exposed-dedicated-context-before-native-edit", editOrigin: "exact-permission-target-and-native-call-identity", contextPermission: "context-and-bounded-attested-discovery-sequence", permissionCard: "current-operation-and-decision-control-excludes-history", qualification: "pending", scheduling: "explicit-only", evidence: "paperclip.e2e.copilot-provider-death.v1", signal: "SIGKILL", target: "owned-native-child-only", permissionPolicy: "approve-reads", lifecycle: "per_turn", automaticRetries: 0 }, + definitionMetadata: { version: 17, titleCreation: "explicit-search-composer", taskBinding: "browser-creation-response-id", linuxDeathExecutable: "verified-inherited-descriptor-and-live-executable-inode", processAuthority: "local-native-root-or-attested-remote-root", remoteTerminalDiagnostics: "bounded-incomplete-reasons-with-strict-failure", remoteSetupOrder: "observed-action-published-before-context-approval", remoteBootstrapContext: "already-exposed-dedicated-tool-no-http-substitution", finalUiStatus: "blocked-with-current-mainline-attention-label", bootstrapPrompt: "already-exposed-dedicated-context-before-native-edit", editOrigin: "exact-permission-target-and-native-call-identity", contextPermission: "context-and-bounded-attested-discovery-sequence-await-origin-rows", permissionCard: "current-operation-and-decision-control-excludes-history", qualification: "pending", scheduling: "explicit-only", evidence: "paperclip.e2e.copilot-provider-death.v1", signal: "SIGKILL", target: "owned-native-child-only", permissionPolicy: "approve-reads", lifecycle: "per_turn", automaticRetries: 0 }, }, { id: "copilot-protection", label: "Copilot native protection", manualOnly: true, description: "Exact native denial with independently correlated provider settlement and acknowledged run Stop, plus attached command settlement with independent process evidence.", groups: ["native"], profiles: extendedHarnessProfiles.filter(profile => profile.qualificationCandidate === "copilot"), environments: runnerEnvironments, tasks: copilotProtectionTasks, expectedMatrixSize: 4, - definitionMetadata: { version: 20, titleCreation: "explicit-search-composer", taskBinding: "browser-creation-response-id", remoteTerminalDiagnostics: "bounded-incomplete-reasons-with-strict-failure", remoteSetupOrder: "observed-action-published-before-context-approval", remoteBootstrapContext: "already-exposed-dedicated-tool-no-http-substitution", denialContext: "already-exposed-dedicated-tool-before-write", stopAcknowledgement: "retained-current-mainline-api-response", contextPermission: "context-and-bounded-attested-discovery-sequence", permissionCard: "current-operation-and-decision-control-excludes-history", semanticCompletionEvidence: "paperclip.e2e.copilot-semantic-completion.v4", qualification: "pending", naturalSettlementObservationMs: 2000, scheduling: "explicit-only", evidence: "copilot_tool_evidence_v1", profileVersion: QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion, denialTerminal: "correlated-provider-settlement-and-audited-run-stop", denialSettlementEvidence: "paperclip.e2e.copilot-denial-settlement.v4", activeTurnCancellation: "not-implied-by-completed-provider-turn", settlement: "attached-finite-command-only", settlementMarker: "private-diagnostic-not-deliverable", remoteEvidence: "owned-lease-sealed-observer" }, + definitionMetadata: { version: 21, titleCreation: "explicit-search-composer", taskBinding: "browser-creation-response-id", remoteTerminalDiagnostics: "bounded-incomplete-reasons-with-strict-failure", remoteSetupOrder: "observed-action-published-before-context-approval", remoteBootstrapContext: "already-exposed-dedicated-tool-no-http-substitution", denialContext: "already-exposed-dedicated-tool-before-write", stopAcknowledgement: "retained-current-mainline-api-response", contextPermission: "context-and-bounded-attested-discovery-sequence-await-origin-rows", permissionCard: "current-operation-and-decision-control-excludes-history", semanticCompletionEvidence: "paperclip.e2e.copilot-semantic-completion.v4", qualification: "pending", naturalSettlementObservationMs: 2000, scheduling: "explicit-only", evidence: "copilot_tool_evidence_v1", profileVersion: QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion, denialTerminal: "correlated-provider-settlement-and-audited-run-stop", denialSettlementEvidence: "paperclip.e2e.copilot-denial-settlement.v4", activeTurnCancellation: "not-implied-by-completed-provider-turn", settlement: "attached-finite-command-only", settlementMarker: "private-diagnostic-not-deliverable", remoteEvidence: "owned-lease-sealed-observer" }, }, { id: "native-provider-loss", label: "Lose a runtime with an unanswered native permission", manualOnly: true, diff --git a/tests/runner-e2e/copilot-context-permission.test.ts b/tests/runner-e2e/copilot-context-permission.test.ts index 342179e1764..fcbbbb94806 100644 --- a/tests/runner-e2e/copilot-context-permission.test.ts +++ b/tests/runner-e2e/copilot-context-permission.test.ts @@ -31,7 +31,7 @@ describe("Copilot initial context permission fixture", () => { const { rows } = JSON.parse(await readFile(new URL("./fixtures/copilot-context-permission-v15.json", import.meta.url), "utf8")); const company = rows[0].companyId, run = rows[0].runId; expect(pendingCopilotContextPermission(rows, company, run)).toMatchObject({ runId: run, requestId: expect.any(String), toolCallId: expect.any(String) }); - for (const kind of ["mutation", "other-prompt", "not-read-only", "foreign-company", "foreign-source", "foreign-bridge", "missing-decline", "duplicate", "unknown-target", "missing-read-origin"]) { + for (const kind of ["mutation", "other-prompt", "not-read-only", "foreign-company", "foreign-source", "foreign-bridge", "missing-decline", "duplicate", "unknown-target"]) { const bad = structuredClone(rows); const payload = (r: any) => r.payload.prpEvent.payload; if (kind === "mutation") payload(bad[2]).operation = "edit"; @@ -43,10 +43,24 @@ describe("Copilot initial context permission fixture", () => { if (kind === "missing-decline") payload(bad[0]).request.choices = [{ key: "accept" }]; if (kind === "duplicate") bad.push(structuredClone(bad[0])); if (kind === "unknown-target") payload(bad[1]).details.push({ name: "target", value: "other.txt" }); - if (kind === "missing-read-origin") bad.splice(2, 1); expect(() => pendingCopilotContextPermission(bad, company, run), kind).toThrow(); } }); + it("waits for permission-first tool rows without approving incomplete evidence", async () => { + const { rows } = JSON.parse(await readFile(new URL("./fixtures/copilot-context-permission-v15.json", import.meta.url), "utf8")); + const company = rows[0].companyId, run = rows[0].runId; + for (const missing of [[1], [2], [1, 2]]) { + const partial = rows.filter((_: unknown, index: number) => !missing.includes(index)); + expect(pendingCopilotContextPermission(partial, company, run)).toBeUndefined(); + } + const foreign = structuredClone(rows).filter((_: unknown, index: number) => index !== 2); + foreign[0].companyId = "foreign"; + expect(() => pendingCopilotContextPermission(foreign, company, run)).toThrow("foreign durable evidence"); + const mutation = structuredClone(rows).filter((_: unknown, index: number) => index !== 1); + mutation.find((row: any) => row.eventType === "tool.execution.started").payload.prpEvent.payload.readOnly = false; + expect(() => pendingCopilotContextPermission(mutation, company, run)).toThrow("cannot approve this card"); + expect(pendingCopilotContextPermission(rows, company, run)).toMatchObject({ operationId: "get_task_context" }); + }); it("waits for the native notice when the durable request arrives first", async () => { const { rows } = JSON.parse(await readFile(new URL("./fixtures/copilot-context-permission-v15.json", import.meta.url), "utf8")); expect(pendingCopilotContextPermission(rows.slice(0, 3), rows[0].companyId, rows[0].runId)).toBeUndefined(); diff --git a/tests/runner-e2e/copilot-context-permission.ts b/tests/runner-e2e/copilot-context-permission.ts index d5336eb8e4b..8b1664b10e9 100644 --- a/tests/runner-e2e/copilot-context-permission.ts +++ b/tests/runner-e2e/copilot-context-permission.ts @@ -24,23 +24,25 @@ export function pendingCopilotContextPermission(events: readonly unknown[], comp if (!permissions.length) return undefined; // Native notice can follow the durable card. if (permissions.length !== 1 || !permissions[0]!.declineOffered) throw new Error("Context fixture permission is ambiguous"); const native = permissions[0]!, group = notices.filter(n => n.toolCallId === native.toolCallId); + const tools = group.filter(n => n.stage === "tool"); const started = group.filter(n => n.stage === "tool" && n.status === "pending" && n.operation === (discovery ? undefined : "read")); const canonical = rows.filter(r => r.eventType === "tool.execution.started" && record(record(record(r.payload).prpEvent).payload).executionId === native.toolCallId); - if (started.length !== 1 || canonical.length !== 1 || group.length !== 2 + if (tools.length > 1 || started.length !== tools.length || canonical.length > 1 || group.length !== permissions.length + tools.length || group.some(n => n.target !== undefined || n.commandSha256 !== undefined || n.shellId !== undefined || n.readTargetSha256 !== undefined)) throw new Error("Context fixture lacks an exact read-only origin"); - for (const r of [row, canonical[0]!, ...group.map(n => rows.find(r => r.seq === n.seq)!)]) { + for (const r of [row, ...canonical, ...group.map(n => rows.find(r => r.seq === n.seq)!)]) { const e = record(record(r?.payload).prpEvent); if (r?.companyId !== companyId || r.runId !== runId || e.schema !== "paperclip.prp.event.v1" || e.sourceKind !== "runner" || e.eventType !== r.eventType || e.runId !== runId || e.turnId !== native.turnId || e.normalizedSessionId !== frame.normalizedSessionId || e.sourceInstanceId !== frame.sourceInstanceId || r.sourceInstanceId !== e.sourceInstanceId || r.sourceSeq !== e.sourceSeq || !Number.isSafeInteger(e.sourceSeq) || !Number.isSafeInteger(r.seq)) throw new Error("Context fixture has foreign durable evidence"); } - const tool = record(record(record(canonical[0]!.payload).prpEvent).payload); - if (tool.name !== `paperclip-${operationId}` || tool.operation !== (discovery ? "search" : "read") || tool.readOnly !== true || tool.target !== null || tool.transport !== "builtin" - || request.schema !== "paperclip.runtime_request.v2" || request.type !== "permission" || request.status !== "pending" + if (request.schema !== "paperclip.runtime_request.v2" || request.type !== "permission" || request.status !== "pending" || request.requestKind !== "permission_approval" || request.turnId !== native.turnId || request.method !== "session/request_permission" || !(hasAcpxNativeOrigin(request.origin, "copilot", "session/request_permission") || hasAcpxNativeOrigin(request.origin, "acpx", "session/request_permission")) || !Array.isArray(request.choices) || !["accept", "decline"].every(key => request.choices.some((c: Row) => c.key === key))) throw new Error("Context fixture cannot approve this card"); + const tool = record(record(record(canonical[0]?.payload).prpEvent).payload); + if (canonical.length && (tool.name !== `paperclip-${operationId}` || tool.operation !== (discovery ? "search" : "read") || tool.readOnly !== true || tool.target !== null || tool.transport !== "builtin")) throw new Error("Context fixture cannot approve this card"); + if (!started.length || !canonical.length) return undefined; // Await matching origin rows within the existing deadline. if (discovery && !isCopilotContextDiscoveryProgress(tool.progress)) throw new Error("Context discovery must search only for the required context tool"); return { operationId, runId, requestId: native.requestId!, turnId: native.turnId, toolCallId: native.toolCallId }; }