Merge reviewed Claude setup and prepared session corrections

Co-Authored-By: Paperclip <noreply@paperclip.ing>

* codex/claude-runner-default:
  fix(ui): save the configuration admitted during agent setup
  fix(runner): preserve prepared turns for v7 native input
This commit is contained in:
DottaandPaperclip committed 2026-10-15 07:47:57 -05:00
commit bd74bf5059
5 files changed
+92 -28

No files matched your search

@@ -1,6 +1,6 @@
import { createCodexTaskEnvelope } from "../contracts/codex.js";
import { ACPX_CAPABILITY_PROFILES } from "../drivers/acpx/capability-profiles.js";
import { NATIVE_EXECUTION_INPUT_SCHEMA } from "../contracts/native-execution.js";
import { NATIVE_EXECUTION_INPUT_SCHEMA, NATIVE_EXECUTION_INPUT_SCHEMA_V7 } from "../contracts/native-execution.js";
import type { NativeExecutionInput } from "../contracts/native-execution.js";
import type { PersistedHarnessSession } from "../contracts/harness-driver.js";
import type {
@@ -115,7 +115,8 @@ function createTransportBackedNativeSessionBackend(
}
const driverIdentity = transportDriverIdentity(input);
const isCodex = input.provider.kind === "codex";
const preparedContext = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA;
const preparedContext = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA
|| input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V7;
const supportsCollaborativePlanning =
isCodex ||
input.provider.kind === "opencode" ||
@@ -4,13 +4,13 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { parseNativeExecutionInput, type NativeExecutionInput } from "../contracts/native-execution.js";
import { buildNativeModelEnvelope, parseNativeExecutionInput, type NativeExecutionInput } from "../contracts/native-execution.js";
import { NATIVE_RUNTIME_ASSET_SCHEMA, PAPERCLIP_EXECUTION_PROMPT, PAPERCLIP_EXECUTION_PROMPT_REVISION, nativeRuntimePromptDigest, canonicalNativeRuntimeContextDigest, composeNativeSystemInstructions } from "../contracts/runtime-context.js";
const contextRoots: string[] = [];
afterEach(() => { for (const root of contextRoots.splice(0)) rmSync(root, { recursive: true, force: true }); });
function preparedExecution(base: NativeExecutionInput): NativeExecutionInput {
function preparedExecution(base: NativeExecutionInput, version: "v5" | "v7" = "v5"): NativeExecutionInput {
const root = mkdtempSync(join(tmpdir(), "paperclip-prepared-backend-"));
contextRoots.push(root);
writeFileSync(join(root, "AGENTS.md"), "Keep assigned instructions.");
@@ -21,7 +21,7 @@ function preparedExecution(base: NativeExecutionInput): NativeExecutionInput {
skills: [], mcp: { assignmentSetId: "none", digest, bindingId: null },
};
return parseNativeExecutionInput({
...base, schema: "paperclip.native-execution-input.v5", executionMode: "default", planningContext: null,
...base, schema: `paperclip.native-execution-input.${version}`, executionMode: "default", planningContext: null,
runtimeContext: { ...context, aggregateDigest: canonicalNativeRuntimeContextDigest(context) },
completionContract: { ...base.completionContract, contract: { ...base.completionContract.contract, criteria: [{ id: "objective", requirement: "Complete the task." }] } },
});
@@ -31,7 +31,7 @@ import {
WORKSPACE,
} from "../drivers/codex/codex-app-server-driver.test-support.js";
import { createNativeSessionBackend } from "../index.js";
import { createCodexNativeSessionBackend } from "./codex-native-backend.js";
import { createCodexNativeSessionBackend, createRunnerdNativeSessionBackend } from "./codex-native-backend.js";
import { createOpenCodeNativeSessionBackend } from "./opencode-native-backend.js";
function execution(
@@ -597,8 +597,8 @@ describe("native backend factory", () => {
).toThrow("Codex native backend requires provider kind codex");
});
it("selects prepared transport only for v5 native input and retains prepared constraints", async () => {
const input = preparedExecution(execution());
it.each(["v5", "v7"] as const)("selects prepared transport for %s native input and retains prepared constraints", async version => {
const input = preparedExecution(execution(), version);
const transport = new FakeCodexTransport();
const backend = createCodexNativeSessionBackend(input, {
transportFactory: () => transport,
@@ -626,8 +626,49 @@ describe("native backend factory", () => {
);
});
it("projects v5 OpenCode constraints to the prepared runtime boundary", () => {
const input = preparedExecution(opencodeExecution());
it("allows a completion-only turn after v7 Claude work without wrapping prepared input", async () => {
const { qualificationModel: _model, reportedModelId: _reported, permissionPolicy: _policy, ...profile } = resolveQualifiedAcpxProfile("claude", "claude-opus-5-5");
const input = preparedExecution({ ...acpxExecution("claude"), provider: {
kind: "acpx", agent: "claude", model: "claude-opus-5-5", permissionMode: "approve-all", profile,
} }, "v7");
if (input.schema !== "paperclip.native-execution-input.v7" || input.provider.kind !== "acpx") throw new Error("invalid fixture");
input.provider.claudeEffort = "high";
const transport = new FakeCodexTransport("thread-1", "provider-session-1", {
kind: "acpx", normalizedSessionId: "session", acpxRecordId: "fixture-record",
backendSessionId: "fixture-backend", agentSessionId: "fixture-agent",
profileDigest: `sha256:${"a".repeat(64)}`, workspaceDigest: `sha256:${"b".repeat(64)}`,
requestedModel: input.provider.model, effectiveModel: input.provider.model, permissionMode: "approve-all",
providerLifetimeFenceCandidates: [60001, 60002, 60003],
});
const backend = createRunnerdNativeSessionBackend(input, {
transportFactory: () => transport,
environment: { PAPERCLIP_WORKSPACE_CWD: WORKSPACE },
});
const session = await backend.openSession({
identity: { companyId: "company", agentId: "agent", runId: "run", sessionId: "session" },
workingDirectory: WORKSPACE,
});
const first = JSON.stringify(buildNativeModelEnvelope(input));
const completion = "Call paperclip_finish with the result of the work already completed.";
try {
for (const [index, text] of [first, completion].entries()) {
transport.turnStartResponse = Promise.resolve({ turn: { id: `turn-${index + 1}`, status: "inProgress", items: [] } });
const turn = await session.startTurn({ message: { role: "user", text } });
transport.push("turn/completed", { threadId: "thread-1", turn: { id: turn.turnId, status: "completed", items: [] } });
for await (const event of session.events()) {
if (["turn.completed", "turn.failed", "turn.cancelled"].includes(event.eventType)) break;
}
}
const turns = transport.calls.filter(call => call.method === "turn/start");
expect(turns.map(call => call.params.input)).toEqual([first, completion].map(text => [{ type: "text", text, text_elements: [] }]));
expect(backend.preparedTaskConstraints).toEqual(expect.arrayContaining([expect.stringContaining("Obtain one accepted result")]));
} finally {
await session.close({ reason: "test-completed" });
}
});
it.each(["v5", "v7"] as const)("projects %s OpenCode constraints to the prepared runtime boundary", version => {
const input = preparedExecution(opencodeExecution(), version);
const backend = createOpenCodeNativeSessionBackend(input, {
runtimeDirectory: "/tmp/paperclip-opencode-prepared-factory",
});
@@ -1,5 +1,5 @@
import { createCodexTaskEnvelope } from "../contracts/codex.js";
import { NATIVE_EXECUTION_INPUT_SCHEMA } from "../contracts/native-execution.js";
import { NATIVE_EXECUTION_INPUT_SCHEMA, NATIVE_EXECUTION_INPUT_SCHEMA_V7 } from "../contracts/native-execution.js";
import type { NativeExecutionInput } from "../contracts/native-execution.js";
import type { NativeSessionBackend } from "../contracts/native-session-backend.js";
import { OpenCodeServerDriver, type OpenCodeServerDriverOptions } from "../drivers/opencode/opencode-server-driver.js";
@@ -13,7 +13,8 @@ export function createOpenCodeNativeSessionBackend(
if (input.provider.kind !== "opencode" || !input.provider.model) {
throw new Error("OpenCode native backend requires a persisted OpenCode provider/model selection");
}
const preparedContext = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA;
const preparedContext = input.schema === NATIVE_EXECUTION_INPUT_SCHEMA
|| input.schema === NATIVE_EXECUTION_INPUT_SCHEMA_V7;
const constraints = nativeTaskConstraints(input);
return new HarnessDriverBackend(new OpenCodeServerDriver({
+11 -11
View File
@@ -481,8 +481,8 @@ function Setup({
};
}
async function runTest(nextConnection = connection): Promise<boolean> {
if (!ready) return false;
async function runTest(nextConnection = connection): Promise<Record<string, unknown> | null> {
if (!ready) return null;
const run = ++generation.current;
const setupEnvironment = setupEnvironmentRef.current;
setTestState("running");
@@ -497,7 +497,7 @@ function Setup({
isCurrent: () => run === generation.current && setupEnvironmentRef.current === setupEnvironment,
});
if (run !== generation.current || setupEnvironmentRef.current !== setupEnvironment
|| (needsClaudeSetupQualification(selection) && !qualified)) return false;
|| (needsClaudeSetupQualification(selection) && !qualified)) return null;
if (qualified) {
qualificationTransition.current = { run, companyId, environmentId, runner: qualified.runner };
cache.setQueryData(queryKeys.adapters.availability(companyId, environmentId), qualified.adapters);
@@ -513,7 +513,7 @@ function Setup({
aiConnection: runtimeAiBinding ?? nextConnection?.aiConnection,
environmentId,
});
if (run !== generation.current) return false;
if (run !== generation.current) return null;
setResult(tested);
setTestState(blocking(tested) ? "fail" : tested.status);
return (
@@ -522,7 +522,7 @@ function Setup({
tested.checks.some((check) =>
check.code.endsWith("hello_probe_passed"),
))
);
) ? config : null;
} catch (cause) {
if (run === generation.current) {
setError(
@@ -530,7 +530,7 @@ function Setup({
);
setTestState("fail");
}
return false;
return null;
}
}
async function finish() {
@@ -551,11 +551,11 @@ function Setup({
const staged: Array<{ remove: () => Promise<unknown> }> = [];
let hired = false;
try {
if (connectionAdapter && testState === "idle" && !(await runTest())) return;
const config = preparedConfig();
const credentials = pendingCredentials();
// Untested entered keys must pass a probe before they can be stored.
if (Object.keys(credentials).length && !(await runTest())) return;
const config = (connectionAdapter && testState === "idle") || Object.keys(credentials).length
? await runTest()
: preparedConfig();
if (!config) return;
for (const [key, value] of Object.entries(credentials)) {
const store = connectionAdapter
? storeProviderApiKey
@@ -828,7 +828,7 @@ function Setup({
setRuntimeAiBinding(undefined);
resetTest();
}}
testConnection={runTest}
testConnection={async next => (await runTest(next)) !== null}
testError={
error ??
(
+26 -5
View File
@@ -9,7 +9,10 @@ import {
} from "./NewAgent.test-support";
describe("New agent setup", () => {
it.each(["paperclip", "legacy"] as const)("qualifies new managed Claude on explicit setup and saves measured %s defaults", async measured => {
it.each([
["paperclip", "Connect"], ["legacy", "Connect"],
["paperclip", "Finish setup"], ["legacy", "Finish setup"],
] as const)("qualifies new managed Claude and saves measured %s defaults through %s", async (measured, step) => {
const image = `registry.example.test/runtime@sha256:${"a".repeat(64)}`;
envApi.list.mockResolvedValue([{ id: "sandbox-1", driver: "sandbox", config: { provider: "daytona", image } }]);
settings.get.mockResolvedValue({ defaultEnvironmentId: "sandbox-1" });
@@ -32,12 +35,30 @@ describe("New agent setup", () => {
api.testEnvironment.mockImplementation(async () => { calls.push("adapter"); return pass; });
await render("claude_local");
expect(envApi.probe).not.toHaveBeenCalled();
await click("ClaudeSubscription");
calls.length = 0;
await click("Use saved subscription");
if (step === "Connect") {
await click("ClaudeSubscription");
calls.length = 0;
await click("Use saved subscription");
} else {
managedApi.list.mockResolvedValue({ currentUserId: "user-1", connections: [{
id: "00000000-0000-4000-8000-000000000021", grantId: "00000000-0000-4000-8000-000000000022", companyId: "company-1",
provider: "openrouter", method: "api_key", name: "Company OpenRouter", ownership: "shared", isDefault: false, status: "connected",
routing: { kind: "openrouter", protocol: "messages", auth: "bearer", models: [] },
}] } as any);
await click("AdvancedCustom Gateway");
const picker = container.querySelector('[role="combobox"][aria-label="Connection"]')!;
await act(async () => picker.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true })));
await settle();
const option = [...document.querySelectorAll('[role="option"]')].find(node => node.textContent?.includes("Company OpenRouter")) as HTMLElement;
await act(async () => option.click());
await settle();
await click("Use connection");
expect(api.testEnvironment).not.toHaveBeenCalled();
calls.length = 0;
}
await click("Finish setup");
expect(calls).toEqual(["discovery-before", "platform", "discovery-after", "adapter"]);
expect(envApi.probe).toHaveBeenCalledExactlyOnceWith("sandbox-1", "company-1");
await click("Finish setup");
expect(api.testEnvironment).toHaveBeenCalledTimes(1);
expect(api.hire).toHaveBeenCalledTimes(1);
const tested = api.testEnvironment.mock.calls[0][2].adapterConfig;