mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 01:41:42 +02:00
fix: carry server-owned workspace continuations through runner bridges
Co-Authored-By: Paperclip <paperclip@paperclip.ing>
This commit is contained in:
1 parent
941467168c
commit
b852cdec59
18 files changed
+270
-28
No files matched your search
@@ -32,12 +32,12 @@
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"copilot": {
|
||||
"agentProfileVersion": 54,
|
||||
"agentProfileVersion": 55,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:171d865b9b6ff2eda3926dc957caf2993cd7e1fa1b4359e8b60b83f589bd67fa",
|
||||
"commandDigest": "sha256:2c9e7f0fee36d2f7f0b253a86f3587b7bf2d9406f774b3b91e13678881ee7d48",
|
||||
"requiresProviderPolicy": true
|
||||
},
|
||||
"claude": {
|
||||
|
||||
@@ -659,6 +659,11 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let expected_canonical_task_context_file =
|
||||
argument(&args, "--expected-canonical-task-context-file");
|
||||
let emit_tool_call = args.iter().any(|value| value == "--emit-tool-call");
|
||||
let terminal_result = argument(&args, "--terminal-result-file")
|
||||
.map(|path| -> Result<Value, Box<dyn std::error::Error>> {
|
||||
Ok(serde_json::from_str(&fs::read_to_string(path)?)?)
|
||||
})
|
||||
.transpose()?;
|
||||
let replay_completed_tool_call = args
|
||||
.iter()
|
||||
.any(|value| value == "--replay-completed-tool-call");
|
||||
@@ -976,6 +981,12 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if message.get("method").is_none() && message.get("id") == Some(&json!("terminal-result-request")) {
|
||||
let success = message.pointer("/result/success") == Some(&json!(true));
|
||||
log_call(call_log.as_deref(), if success { "terminal-result:accepted" } else { "terminal-result:rejected" })?;
|
||||
finish_turn(&state_path, &mut state, if success { "completed" } else { "failed" })?;
|
||||
continue;
|
||||
}
|
||||
if message.get("method").is_none() && message.get("id") == Some(&json!("tool-request-1")) {
|
||||
if message.pointer("/result/success") == Some(&json!(false)) {
|
||||
log_call(call_log.as_deref(), "tool-response:failure")?;
|
||||
@@ -1706,6 +1717,12 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
"arguments":{"query":{"PRIVATE_QUERY":"private search"}}
|
||||
}}),
|
||||
)?;
|
||||
} else if let Some(result) = &terminal_result {
|
||||
send(json!({
|
||||
"id": "terminal-result-request", "method": "item/tool/call",
|
||||
"params": { "threadId": state.thread_id, "turnId": provider_turn_id,
|
||||
"callId": "terminal-result-call", "tool": "paperclip_finish", "arguments": result }
|
||||
}))?;
|
||||
} else if emit_tool_call {
|
||||
send(json!({
|
||||
"id": "tool-request-1",
|
||||
|
||||
@@ -47,7 +47,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
|
||||
agent_runtime_package: None,
|
||||
agent_runtime_version: None,
|
||||
command_digest:
|
||||
"sha256:171d865b9b6ff2eda3926dc957caf2993cd7e1fa1b4359e8b60b83f589bd67fa",
|
||||
"sha256:2c9e7f0fee36d2f7f0b253a86f3587b7bf2d9406f774b3b91e13678881ee7d48",
|
||||
requires_provider_policy: true,
|
||||
},
|
||||
"claude" => AcpxReleaseProfile {
|
||||
|
||||
@@ -557,13 +557,14 @@ fn admit_terminal_tool_authority(
|
||||
"paperclip_finish" => {
|
||||
matches!(disposition.as_str(), "done" | "needs_review")
|
||||
|| (disposition == "yielded"
|
||||
&& matches!(
|
||||
input
|
||||
.get("continuation")
|
||||
.and_then(|continuation| continuation.get("kind"))
|
||||
.and_then(Value::as_str),
|
||||
Some("response_wake" | "monitor" | "case_handoff")
|
||||
))
|
||||
&& (input.get("continuation").map_or(true, Value::is_null)
|
||||
|| matches!(
|
||||
input
|
||||
.get("continuation")
|
||||
.and_then(|continuation| continuation.get("kind"))
|
||||
.and_then(Value::as_str),
|
||||
Some("response_wake" | "monitor" | "case_handoff" | "same_agent")
|
||||
)))
|
||||
}
|
||||
"paperclip_block" => disposition == "blocked",
|
||||
_ => false,
|
||||
@@ -5311,7 +5312,27 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_tool_authority_rejects_an_unbound_yield() {
|
||||
fn terminal_tool_authority_preserves_controller_resolved_workspace_yields() {
|
||||
for continuation in [None, Some(Value::Null), Some(json!({
|
||||
"kind": "same_agent",
|
||||
"summary": "Continue the prepared repository.",
|
||||
"idempotencyKey": "repository:task:receipt:run"
|
||||
}))] {
|
||||
let mut state = opencode_result_state();
|
||||
let mut result = valid_opencode_result();
|
||||
result["reportedWorkDisposition"] = json!("yielded");
|
||||
if let Some(value) = continuation {
|
||||
result["continuation"] = value;
|
||||
}
|
||||
admit_terminal_tool_authority(&mut state, "paperclip_finish", &result, false).unwrap();
|
||||
assert_eq!(state.active_provider_result_disposition.as_deref(), Some("yielded"));
|
||||
assert_eq!(state.active_provider_result_fingerprint.as_deref(), Some(semantic_value_digest(&result).as_str()));
|
||||
assert!(state.validate().is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_tool_authority_does_not_record_a_controller_rejected_yield() {
|
||||
let mut state = opencode_result_state();
|
||||
let mut result = valid_opencode_result();
|
||||
result["reportedWorkDisposition"] = json!("yielded");
|
||||
@@ -5321,9 +5342,10 @@ mod tests {
|
||||
"idempotencyKey": "same-agent-1"
|
||||
});
|
||||
|
||||
assert!(
|
||||
admit_terminal_tool_authority(&mut state, "paperclip_finish", &result, false,).is_err()
|
||||
);
|
||||
admit_terminal_tool_authority(&mut state, "paperclip_finish", &result, true).unwrap();
|
||||
assert!(state.active_provider_result_fingerprint.is_none());
|
||||
assert!(state.active_provider_result_disposition.is_none());
|
||||
assert!(state.validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -7,9 +7,9 @@ import { fileURLToPath } from "node:url";
|
||||
import { resolve, dirname } from "node:path";
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const fixture = resolve(root, "test/fixtures/copilot-profile-v54-identity.json");
|
||||
const fixture = resolve(root, "test/fixtures/copilot-profile-v55-identity.json");
|
||||
const historical = JSON.parse(readFileSync(resolve(root, "test/fixtures/copilot-profile-v12-identity.json"), "utf8"));
|
||||
const declaration = { ...historical.declaration, agentProfileVersion: 54 };
|
||||
const declaration = { ...historical.declaration, agentProfileVersion: 55 };
|
||||
delete declaration.permissionIdentitySourceSha256;
|
||||
const sources = {
|
||||
displayEventSourceSha256: "src/drivers/acpx/copilot-events.ts",
|
||||
|
||||
@@ -317,10 +317,10 @@ describe("qualified ACPX runtime sidecar", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it.each([true, false])("resolves an omitted continuation before the production sidecar validator only with a saved monitor: %s", async eligible => {
|
||||
it.each([["monitor", true], ["monitor", false], ["same_agent", true], ["same_agent", false]] as const)("resolves an omitted continuation before the production sidecar validator with saved %s authority=%s", async (kind, eligible) => {
|
||||
const raw = { reportedWorkDisposition: "yielded", summary: "The review fix is published.", evidence: [], verification: [],
|
||||
completionClaim: { contractRevision: "1", objectiveSatisfied: false, criteria: [], remainingWork: [{ description: "Wait for checks.", blocksCompletion: true }] } };
|
||||
const continuation = { kind: "monitor", summary: "Wait for the saved task monitor.", idempotencyKey: "task-monitor:task:deadline" };
|
||||
const continuation = { kind, summary: "Continue from saved task state.", idempotencyKey: "saved:task:receipt" };
|
||||
const tools = new Map<string, any>(), emitted: any[] = [];
|
||||
const handler = loadWaitForTool({ tools, emitted, validate: validatePrpStructuredRunResult });
|
||||
const commands = loadActualToolCommands(tools);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
PRP_BLOCK_RESULT_OUTPUT_SCHEMA,
|
||||
PRP_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA,
|
||||
PRP_BLOCK_TOOL_DESCRIPTION,
|
||||
PRP_COMPLETION_RESULT_BRIDGE_INPUT_SCHEMA,
|
||||
PRP_COMPLETION_RESULT_OUTPUT_SCHEMA,
|
||||
PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA,
|
||||
PRP_COMPLETION_TOOL_DESCRIPTION,
|
||||
@@ -59,6 +60,21 @@ describe("provider-neutral completion result schema", () => {
|
||||
expect(validate(report)).toBe(false);
|
||||
});
|
||||
|
||||
it("carries a normalized workspace continuation only on the internal bridge", () => {
|
||||
const bridge = new Ajv2020({ strict: false }).compile(PRP_COMPLETION_RESULT_BRIDGE_INPUT_SCHEMA);
|
||||
const provider = new Ajv2020({ strict: false }).compile(PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA);
|
||||
const report = { ...baseResult, reportedWorkDisposition: "yielded", continuation: {
|
||||
kind: "same_agent", summary: "Use the staged task checkout.", idempotencyKey: "repository:task:receipt:run" } };
|
||||
expect(bridge(report)).toBe(true);
|
||||
expect(provider(report)).toBe(false);
|
||||
for (const invalid of [
|
||||
{ ...report, reportedWorkDisposition: "done" },
|
||||
{ ...report, continuation: { ...report.continuation, kind: "retry" } },
|
||||
{ ...report, continuation: { ...report.continuation, idempotencyKey: "" } },
|
||||
]) expect(bridge(invalid)).toBe(false);
|
||||
expect(bridge(baseResult)).toBe(true);
|
||||
});
|
||||
|
||||
it("allows done with no verification and no actionable attention", () => {
|
||||
expect(validate(structuredClone(baseResult))).toBe(true);
|
||||
});
|
||||
|
||||
@@ -384,6 +384,20 @@ export const PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA = {
|
||||
else: { properties: { continuation: { type: "null" } } },
|
||||
} as const;
|
||||
|
||||
export const PRP_COMPLETION_RESULT_BRIDGE_INPUT_SCHEMA = {
|
||||
...PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA,
|
||||
properties: {
|
||||
...PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA.properties,
|
||||
continuation: {
|
||||
...PRP_COMPLETION_RESULT_PROVIDER_INPUT_SCHEMA.properties.continuation,
|
||||
properties: {
|
||||
...waitContinuationSchema.properties,
|
||||
kind: { type: "string", enum: [...waitContinuationSchema.properties.kind.enum, "same_agent"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const PRP_BLOCK_RESULT_PROVIDER_INPUT_SCHEMA = {
|
||||
type: "object",
|
||||
additionalProperties: true,
|
||||
|
||||
@@ -87,9 +87,21 @@ vi.mock("./installation-integrity.js", () => ({ verifyNativeAcpxInstallation: vi
|
||||
|
||||
describe("Copilot build-owned installation", () => {
|
||||
it("admits the current profile with environment launch preparation", () => {
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion).toBe(54);
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion).toBe(55);
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBeUndefined();
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe("sha256:171d865b9b6ff2eda3926dc957caf2993cd7e1fa1b4359e8b60b83f589bd67fa");
|
||||
expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe("sha256:2c9e7f0fee36d2f7f0b253a86f3587b7bf2d9406f774b3b91e13678881ee7d48");
|
||||
});
|
||||
|
||||
it("preserves v54 while binding current workspace continuation revalidation in v55", () => {
|
||||
const prior = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v54-identity.json", import.meta.url), "utf8"));
|
||||
const current = JSON.parse(readFileSync(new URL("../../../test/fixtures/copilot-profile-v55-identity.json", import.meta.url), "utf8"));
|
||||
expect(prior.commandDigest).toBe("sha256:171d865b9b6ff2eda3926dc957caf2993cd7e1fa1b4359e8b60b83f589bd67fa");
|
||||
expect(Object.keys(current.declaration).filter(key => current.declaration[key] !== prior.declaration[key]).sort()).toEqual([
|
||||
"agentProfileVersion", "profileCompatibilitySourceSha256", "runtimeRequestBridgeSourceSha256",
|
||||
"semanticCompletionContractSourceSha256", "semanticContinuationSourceSha256",
|
||||
]);
|
||||
expect(isSupportedAcpxProfileVersion("copilot", 54)).toBe(true);
|
||||
expect(current.commandDigest).not.toBe(prior.commandDigest);
|
||||
});
|
||||
|
||||
it("preserves v53 while binding repository setup completion guidance in v54", () => {
|
||||
|
||||
@@ -48,12 +48,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
|
||||
"protocolVersion": 1,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 54,
|
||||
"agentProfileVersion": 55,
|
||||
"agentServerPackage": "@github/copilot",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"agentRuntimePackage": null,
|
||||
"agentRuntimeVersion": null,
|
||||
"commandDigest": "sha256:171d865b9b6ff2eda3926dc957caf2993cd7e1fa1b4359e8b60b83f589bd67fa",
|
||||
"commandDigest": "sha256:2c9e7f0fee36d2f7f0b253a86f3587b7bf2d9406f774b3b91e13678881ee7d48",
|
||||
"permissionPolicy": "interactive"
|
||||
},
|
||||
"claude": {
|
||||
|
||||
@@ -2,13 +2,13 @@ import { QUALIFIED_ACPX_PROFILE_DATA } from "./generated-profiles.js";
|
||||
|
||||
/** Decodable historical profile revisions, not permission to launch them.
|
||||
* Exact current profile/package/digest admission is checked separately. */
|
||||
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53;
|
||||
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54;
|
||||
type CurrentAcpxProfileVersion = typeof QUALIFIED_ACPX_PROFILE_DATA[keyof typeof QUALIFIED_ACPX_PROFILE_DATA]["agentProfileVersion"];
|
||||
export type AcpxProfileVersion = HistoricalAcpxProfileVersion | CurrentAcpxProfileVersion;
|
||||
|
||||
const historicalVersions: Readonly<Record<string, readonly HistoricalAcpxProfileVersion[]>> = {
|
||||
pi: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5],
|
||||
grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53],
|
||||
grok: [1, 2, 3, 4, 5], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54],
|
||||
cursor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
||||
};
|
||||
|
||||
|
||||
+2
-2
@@ -47,7 +47,7 @@ import { RUNNERD_CANONICAL_ITEM } from "./codex-driver-values.js";
|
||||
|
||||
describe("Codex app-server Codex driver", () => {
|
||||
it.each([
|
||||
["monitor", false], ["monitor", true], ["same_agent", false], ["same_agent", true],
|
||||
["monitor", false], ["monitor", true], ["same_agent", false], ["same_agent", true], ["same_agent", "explicit"],
|
||||
] as const)("uses saved %s continuation with nullable input=%s", async (kind, nullable) => {
|
||||
const transport = new FakeCodexTransport();
|
||||
const continuation = { kind, summary: "Continue from saved task state", idempotencyKey: "saved:task:receipt" };
|
||||
@@ -59,7 +59,7 @@ describe("Codex app-server Codex driver", () => {
|
||||
});
|
||||
try {
|
||||
await session.startTurn({ message: { role: "user", text: "Check the published PR later." } });
|
||||
const report = { ...structuredClone(result), reportedWorkDisposition: "yielded", ...(nullable ? { continuation: null } : {}) };
|
||||
const report = { ...structuredClone(result), reportedWorkDisposition: "yielded", ...(nullable === "explicit" ? { continuation } : nullable ? { continuation: null } : {}) };
|
||||
const response = await transport.invoke({ id: "finish-monitor", method: "item/tool/call", params: {
|
||||
threadId: "thread-1", turnId: "turn-1", callId: "finish-monitor", tool: "paperclip_finish", arguments: report,
|
||||
} });
|
||||
|
||||
@@ -25,6 +25,20 @@ describe("controller-owned completion continuation", () => {
|
||||
.toEqual({ ...yielded, continuation: preparation });
|
||||
});
|
||||
|
||||
it("rechecks a sidecar-resolved workspace continuation against current server authority", async () => {
|
||||
const preparation = { kind: "same_agent", summary: "Continue with the prepared repository.", idempotencyKey: "repository:task:receipt:run" };
|
||||
const report = { ...yielded, continuation: preparation };
|
||||
const read = vi.fn().mockResolvedValue({ ...context, completionContinuation: preparation });
|
||||
const resolved = await resolveCompletionContinuation(report, "run", read);
|
||||
expect(resolved).toEqual(report);
|
||||
expect(read).toHaveBeenCalledExactlyOnceWith();
|
||||
for (const response of [null, { ...context, completionContinuation: { ...preparation, idempotencyKey: "foreign" } },
|
||||
{ ...context, completionContinuation: { ...preparation, summary: "changed" } },
|
||||
{ ...context, completionContinuation: preparation, run: { id: "foreign" } }]) {
|
||||
await expect(resolveCompletionContinuation(report, "run", async () => response)).rejects.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
null, { ...context, run: { id: "other" } }, { ...context, activeTask: { executionRunId: "other" } },
|
||||
{ ...context, completionContinuation: null }, { ...context, completionContinuation: { kind: "response_wake" } },
|
||||
|
||||
@@ -5,7 +5,8 @@ export async function resolveCompletionContinuation(
|
||||
readTaskContext?: () => Promise<unknown>,
|
||||
): Promise<unknown> {
|
||||
const report = object(value);
|
||||
if (report.reportedWorkDisposition !== "yielded" || report.continuation != null) return value;
|
||||
const supplied = object(report.continuation);
|
||||
if (report.reportedWorkDisposition !== "yielded" || (report.continuation != null && supplied.kind !== "same_agent")) return value;
|
||||
const response = readTaskContext ? await readTaskContext() : null;
|
||||
const text = object(response).type === "inputText" ? object(response).text : response;
|
||||
let context: Record<string, unknown>;
|
||||
@@ -19,6 +20,10 @@ export async function resolveCompletionContinuation(
|
||||
typeof continuation.idempotencyKey !== "string" || !continuation.idempotencyKey.trim()) {
|
||||
throw new Error("Cannot yield without a continuation: no eligible saved monitor or workspace continuation exists on this task. Save a monitor for a deferred check, provide an explicit response_wake for a real response wait, or continue the unfinished work. Do not repeat completed actions.");
|
||||
}
|
||||
if (report.continuation != null && (continuation.kind !== "same_agent" || supplied.summary !== continuation.summary ||
|
||||
supplied.idempotencyKey !== continuation.idempotencyKey)) {
|
||||
throw new Error("The supplied workspace continuation does not match current server-owned task state. Read the current task context and preserve its authority.");
|
||||
}
|
||||
return { ...report, continuation: { kind: continuation.kind, summary: continuation.summary, idempotencyKey: continuation.idempotencyKey } };
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PRP_COMPLETION_TOOL_NAME, PRP_COMPLETION_RESULT_BRIDGE_INPUT_SCHEMA } from "../contracts/completion-result.js";
|
||||
import { RunnerLaunchPreparationStore } from "./runner-launch-preparation-store.js";
|
||||
import type { PreparedRunnerProviderLaunch, RunnerProviderLaunchRequirements } from "./runner-provider-launch.js";
|
||||
import { validatePreparedRunnerLaunch, runnerProviderLaunchRequirements, createRunnerProviderLaunch, createRunnerdCodexAppServerArgs, trustedRuntimeReadOnlyRoots } from "./runner-provider-launch.js";
|
||||
@@ -3211,7 +3212,7 @@ function authorizedToolSet(
|
||||
operationId: String(tool.name ?? ""),
|
||||
version: 1,
|
||||
description: String(tool.description ?? ""),
|
||||
inputSchema: record(tool.inputSchema),
|
||||
inputSchema: tool.name === PRP_COMPLETION_TOOL_NAME ? PRP_COMPLETION_RESULT_BRIDGE_INPUT_SCHEMA : record(tool.inputSchema),
|
||||
responseSchema: {},
|
||||
}))
|
||||
.sort((left, right) =>
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { CodexAppServerDriver } from "../drivers/codex/codex-app-server-driver.js";
|
||||
import { createCodexTaskEnvelope } from "../contracts/codex.js";
|
||||
import { createCapabilityRunnerdCodexTransport, defaultCapabilityRunnerdBinary } from "./runnerd-codex-transport.js";
|
||||
import { collectUntilTerminal } from "../drivers/codex/codex-app-server-driver.test-support.js";
|
||||
|
||||
it.each(["omitted", "null", "resolved"] as const)("retains a server-owned workspace yield through the actual Rust bridge (%s)", async form => {
|
||||
const root = await mkdtemp(join(tmpdir(), "runnerd-saved-continuation-")), runId = "run-saved-workspace";
|
||||
const continuation = { kind: "same_agent", summary: "Continue with the prepared task repository.", idempotencyKey: "repository:task:receipt:run" };
|
||||
const report = { schema: "paperclip.run_result.v1", reportedWorkDisposition: "yielded", summary: "The repository is staged; the requested verification continues.",
|
||||
completionClaim: { contractRevision: "1", objectiveSatisfied: false, criteria: [{ criterionId: "objective", status: "not_satisfied", evidenceRefs: [] }],
|
||||
remainingWork: [{ description: "Read the prepared repository and deliver its result.", blocksCompletion: true }] },
|
||||
evidence: [], verification: [], attentionRequests: [], artifacts: [],
|
||||
...(form === "resolved" ? { continuation } : form === "null" ? { continuation: null } : {}) };
|
||||
const reportFile = join(root, "provider-report.json"), callLog = join(root, "provider-calls.txt");
|
||||
await writeFile(reportFile, JSON.stringify(report));
|
||||
const workspace = join(root, "workspace");
|
||||
await Promise.all([mkdir(join(root, "home")), mkdir(workspace)]);
|
||||
const environment = { PATH: process.env.PATH, HOME: join(root, "home"), CODEX_HOME: join(root, "codex-home"), LANG: "C.UTF-8" };
|
||||
const bundle = createCapabilityRunnerdCodexTransport({
|
||||
runnerBinary: process.env.PAPERCLIP_ATTACH_TRANSITION_RUNNER ?? defaultCapabilityRunnerdBinary(),
|
||||
codexCommand: resolve(import.meta.dirname, "../../runner/target/debug/fake-codex-app-server"),
|
||||
codexArgs: ["--state-file", join(root, "provider-state.json"), "--call-log", callLog, "--terminal-result-file", reportFile],
|
||||
stateDirectory: root, environment,
|
||||
prpIdentity: { runnerInstanceId: "runner-saved-workspace", environmentLeaseId: "lease-saved-workspace", runId, normalizedSessionId: "session-saved-workspace", turnId: "turn-saved-workspace", itemId: "item-saved-workspace" },
|
||||
});
|
||||
const dynamicToolHandler = vi.fn(async () => ({ run: { id: runId }, activeTask: { executionRunId: runId }, completionContinuation: continuation }));
|
||||
const completionFeedback = vi.fn(async () => "The saved repository continuation is accepted.");
|
||||
const driver = new CodexAppServerDriver({ approvalPolicy: "never", includeCollaborationModeInstructions: false, environment,
|
||||
taskEnvelope: createCodexTaskEnvelope({ objective: "Verify the prepared repository.", contractRevision: "1", criteria: [{ id: "objective", requirement: "Deliver the result." }], constraints: [] }),
|
||||
dynamicTools: [{ name: "get_task_context", description: "Read current task authority.", inputSchema: { type: "object", properties: {}, additionalProperties: false } }],
|
||||
dynamicToolHandler, completionFeedback, transportFactory: () => bundle.transport, requireProviderSessionIdentity: true });
|
||||
let session: Awaited<ReturnType<typeof driver.openSession>> | undefined;
|
||||
try {
|
||||
session = await driver.openSession({ runId, normalizedSessionId: "session-saved-workspace", workingDirectory: workspace });
|
||||
await session.startTurn({ message: { role: "user", text: "Continue with the prepared checkout." } });
|
||||
const events = await collectUntilTerminal(session.events());
|
||||
expect(completionFeedback).toHaveBeenCalledExactlyOnceWith({ ...report, continuation });
|
||||
expect(dynamicToolHandler).toHaveBeenCalledOnce();
|
||||
expect(events.filter(event => event.eventType === "run.result.proposed")).toHaveLength(1);
|
||||
expect(events.at(-1)?.eventType).toBe("turn.completed");
|
||||
expect((await session.snapshot()).semanticResult?.result).toEqual({ ...report, continuation });
|
||||
expect(await readFile(callLog, "utf8")).toContain("terminal-result:accepted");
|
||||
const journal = JSON.parse(await readFile(join(root, "control-plane/control-plane-state.json"), "utf8"));
|
||||
const terminals = journal.committedEvents.filter((event: { eventType: string }) => event.eventType === "run.terminal");
|
||||
expect(terminals).toHaveLength(1);
|
||||
expect(terminals[0].envelope.payload).toMatchObject({ runId, eventType: "run.terminal", payload: { runTerminalState: "succeeded", reportedWorkDisposition: "yielded" } });
|
||||
} finally {
|
||||
await session?.close();
|
||||
await bundle.transport.close();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
}, 30_000);
|
||||
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"commandDigest": "sha256:2c9e7f0fee36d2f7f0b253a86f3587b7bf2d9406f774b3b91e13678881ee7d48",
|
||||
"declaration": {
|
||||
"schema": "paperclip.acpx_profile_declaration.v1",
|
||||
"agent": "copilot",
|
||||
"agentProfileVersion": 55,
|
||||
"acpxVersion": "0.13.1",
|
||||
"agentServerVersion": "1.0.88",
|
||||
"protocolPolicyRevision": "copilot-agent-manual-v1",
|
||||
"acpxPatchSha256": "00d6af17216cb0cad902b48633eb19bf0ec6b05837a984cb24661fca593f09c1",
|
||||
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
|
||||
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
|
||||
"agentFilesBinding": "registered-runtime-context-v1",
|
||||
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
|
||||
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
|
||||
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
|
||||
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
|
||||
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
|
||||
"messageIdentityContract": "copilot-native-message-id-v1",
|
||||
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
|
||||
"permissionContextContract": "copilot-edit-permission-context-v1",
|
||||
"permissionContextSourceSha256": "de2d38d702b3c574161af0c0c04790fb6f6347916eb3916ce7a131a8c229121b",
|
||||
"permissionAdapterSourceSha256": "ee5929ed2ee647ad0369465dee9d51a00b2dd28e1ff735b43e969ddc9c164668",
|
||||
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
|
||||
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
|
||||
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
|
||||
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
|
||||
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
|
||||
"semanticBridgeSourceSha256": "89e3dde25ff35bfd4d7f520fd9d14af3cb9027f182a2d51431d298f1015be580",
|
||||
"toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706",
|
||||
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
|
||||
"semanticSidecarSourceSha256": "cc90b59bdfe6a82f7fa3e819e8671f71f7b14047e68942e8086618ffbaacf59f",
|
||||
"semanticDirectDriverSourceSha256": "ff572638f2cb5d9f8249ff8e2f412311c1e3af209ef0bf7e5ea9f10a21d91103",
|
||||
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
|
||||
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
|
||||
"semanticCompletionContractSourceSha256": "3343df6362252d44d12b079ff78c8f6539d1544f3ce5ec36ff36db70ab0fac31",
|
||||
"semanticValidatorsSourceSha256": "f22d9caf2035b600abc1a6625ae23ca97f1409d7e29ed5b7264bb8f9784c7dc9",
|
||||
"semanticSchemaBundleSourceSha256": "016391a087cc217aafed4ba1f4efafc6e4881a7525952dce824b7f4d24530603",
|
||||
"semanticSidecarProtocolSourceSha256": "03c0c95b900f9dc18f29f6be27b8c79bc0bcab29780190d46a74e95328081236",
|
||||
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
|
||||
"displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877",
|
||||
"displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e",
|
||||
"configuredEnvironmentSourceSha256": "5c9008d121979a3a8987e6b8a5752172eafdc4a2b5d725a73f3bbb8fb83be8ff",
|
||||
"configuredEnvironmentRustSourceSha256": "85c87fae62b8a9377e4f0a4c00302c7519fae4397b1c48253f8f2b7ba7faf994",
|
||||
"profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282",
|
||||
"profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571",
|
||||
"profileInstallationSourceSha256": "e4aacb08015fa9104dce2b0c7d150cc432231c8a3e07d2cec2a325b388f7f382",
|
||||
"attachmentPolicySourceSha256": "4f7638b7cc64e18f6a4ba33fd46de2b3d49225e32aeab41f5c695b268fa4f8e2",
|
||||
"singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f",
|
||||
"nativeSessionRuntimeSourceSha256": "e8a3bee66a1ff1c6f251b5ae57453ffb908336da9c4a6d98f41f94f7ee94a8e8",
|
||||
"nativeRuntimeContextSourceSha256": "df93bc8bcfa9dfdcf26ad6a69ea0bd3e56ef186c5e962b243fb2288f7074584b",
|
||||
"nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90",
|
||||
"harnessSessionContractSourceSha256": "03f4cd7f76f34f6d82e009b66aa51f18f6e23dc235b660778060e35b6ea07412",
|
||||
"harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249",
|
||||
"harnessSessionSourceSha256": "81361733e86b56dd5a66415aefc6f72e69be24acc8d3417370154ebb7fef6e94",
|
||||
"transportContractSourceSha256": "4fa9593226ff3280bc12140bfcbc2c0986e4cc8d4733381c334138dd03b08f46",
|
||||
"nativeBackendSourceSha256": "2406483fd35a564971c22e10fc2bad26bae16ff5a3fb6c65690fcf156522ab6d",
|
||||
"nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44",
|
||||
"nativeInstructionsSourceSha256": "ee8df5d85b263d5d2d0dc9ede8602cb7daf6388687108fe05e2371f5f56c3492",
|
||||
"providerLaunchSourceSha256": "cba71368f82ae8d300123efa09f23744e19e82142788fa9ad238338fefec9624",
|
||||
"launchPreparationStoreSourceSha256": "ff6025568bbc0aa6ce545772d73dfca2d3da226740bd3a7b386b069e76cff296",
|
||||
"runtimeRequestBridgeSourceSha256": "fb2e91e9a7f4b10ae0d936e1e1c74ac9095733ef67fe816491be45791048efa1",
|
||||
"runtimeRequestFacadeSourceSha256": "d0ed28e0c5ddad34db8f437e704c8a755eb0b47f27e08e44185f66d1d2205295",
|
||||
"runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211",
|
||||
"guardedInstallationSourceSha256": "f6d0c33de38259efefa0f4e0f9737cd30cfd3996a8e875e3fbdb8c72dc133dca",
|
||||
"nativeExecutionSourceSha256": "fc7c6568feb0d8d709e9baa51839420c9b90336b4865a9b8405aba06e4d1e9c4",
|
||||
"profileCompatibilitySourceSha256": "d4edec781031a44f7e96a1e4ef398af4675529b7ec0f9c56c4915d9ec81a20cf",
|
||||
"installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d",
|
||||
"assetOwnershipSourceSha256": "1785e3f31e5dad5a13b53e25d8cf9482a9bd89a27b6c127830aed6b81acc605c",
|
||||
"assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f",
|
||||
"credentialBindingSourceSha256": "54fd09ad430dac04df98d829061f6701e73b5bf2a0f4de836d2ac36ee511009f",
|
||||
"sidecarErrorSourceSha256": "b09af4577f8bf42eee7036b2ad9842202cfc763e72b53c3287f82c6090cdad80",
|
||||
"profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456",
|
||||
"metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727",
|
||||
"permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
|
||||
"semanticContinuationSourceSha256": "d248e47a0fe7f2536d1e64cea0b0b410f34104800cd643d995b4473104ecc8e0",
|
||||
"runtimeHostSourceSha256": "60e5454f030e87f2afa8b1fb120b26adb126b6b17d0e5e82f49c77f76f466cbb",
|
||||
"runtimeAdapterSourceSha256": "e4c764c2c3c7dd655a965b39472bd1ad8d577d1e7258b25594b171c9429752b8",
|
||||
"runtimeSandboxSourceSha256": "92dfa53f5a3ed0062913ae417eeccec243b430d444495efde2a8e9c4c02e78ea",
|
||||
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
|
||||
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
|
||||
"nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b"
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@ export const DAYTONA_IMAGE_INPUT_PATHS = [
|
||||
"packages/paperclip-runner/test/fixtures/copilot-profile-v48-identity.json",
|
||||
"packages/paperclip-runner/test/fixtures/copilot-profile-v49-identity.json",
|
||||
"packages/paperclip-runner/test/fixtures/copilot-profile-v54-identity.json",
|
||||
"packages/paperclip-runner/test/fixtures/copilot-profile-v55-identity.json",
|
||||
"packages/paperclip-runner/scripts/materialize-cursor-distribution.mjs",
|
||||
"packages/paperclip-runner/scripts/cursor-runtime-patch.mjs",
|
||||
"packages/paperclip-runner/scripts/build-verified-provider-entrypoints.mjs",
|
||||
|
||||
Reference in new issue
Block a user