mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-13 15:55:46 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The chat-feature evals measure whether agents complete real chat requests through the product > - The chat catalog eval harness and its repairs live on the `chat-eval/master-candidate` branch, not on master > - Today's work made 139 of 242 chat evals pass in verified single-case live runs. Each product fix landed on master as its own pull request > - The eval-side changes (setups, graders, fault seams, evidence capture) still exist only on the candidate > - This pull request lands the harness on master. Master stays the source of truth for product behavior > - The benefit is one tree: evals run against master commits, and product fixes reach the evals as soon as they merge ## Linked Issues or Issue Description Refs #19441, Refs #19599, Refs #20270, Refs #20334, Refs #20342, Refs #20344, Refs #20354, Refs #20360, Refs #20373, Refs #20382, Refs #20409 **What existing behavior does this improve?** The chat catalog eval harness and the product it evaluates live on a long-running branch outside master. Eval fixes do not reach master, so master-based eval runs use old setups and graders. **Subsystem affected** `tests/runner-e2e` chat catalog harness, plus a small set of product changes that exist only on the candidate. **Proposed change** Land the harness on master. Every product fix from this eval work already merged to master as its own pull request. Conflicts keep master's product behavior. ## What Changed - Most of the diff (808 of 851 files) is under `tests/runner-e2e/`: chat-catalog domains, scenarios, descriptors, the TestOps manifest, private evidence tooling, and their tests. - Graders now check saved state, setup literals and rendered or clicked links. Many keyword and regex checks on free-form replies are removed. Each changed grader has a bumped `oracleVersion` and calibration tests (a valid reply, a reworded valid reply, and broken variants that must fail). - The shared link helper `hasLink` no longer treats a list link (for example `/COM/issues?view=mine`) as a link to every task without a URL key. - Setup fixes build real state through supported product routes: routine history, agent grants through the board grant route, budget holds with a real cap, and a storage-layer archive fault for `inbox-search.archive-partial-failure`. - Browser boundary journeys (sign-in, chat-disabled, onboarding) now write the standard pass evidence files. - Retargeted cases, approved by the maintainer: `agent-chat.chat-disabled` (Conference Room gate), `agents.restore-instructions` (no saved instruction history), `access.manage-membership` (member permission edits stay board-only). - The private evidence trusted files (`manual-private-handoff.ts`, `manual-private-retention.ts`, `manual-private-report-assets.ts`) are byte-identical to master. - Product files outside `tests/` (43 files, +1,328 −49) carry candidate-only changes that master does not yet have. Most are tests. The main code changes are in `request-human-input.ts`, `heartbeat-run-summary.ts`, `native-completion-feedback.ts`, `native-run-finalizer.ts`, `openapi.ts`, `secrets.ts` and `IssueChatLongThreadPerf.tsx`. ## Verification - `pnpm vitest run chat-catalog-domains chat-catalog-contract.test.ts chat-catalog-consistency.test.ts unit-launcher.test.mjs --testTimeout=180000` in `tests/runner-e2e`: 269 files pass, 3,550 tests pass. - `node --test chat-full-catalog-testops-manifest.test.mjs`: 10 pass. - `node tests/runner-e2e/source-sdk-public-inputs.mjs <HEAD> --check`: no change. - `tsc` for `server` and `tests/runner-e2e`: clean. - Live single-case verification: 139 of 242 cases passed on paid runs, and a person read the saved evidence for each. These are single-case runs, not one full-cohort run. ## Risks - Large diff. Most of it is test-only, but the 43 non-test files change product code paths. Review those files first. - Some graders are now less strict on reply wording. A person must read replies for meaning on those cases. - 42 cases need disposable outside accounts (Notion, Slack, GitHub, Daytona and others) and stay blocked. `access.invite-teammate` stays failing until local agent isolation lands, because the runner's durable state keeps a live invite token. - The paid workflow compares the trusted private-evidence files with master. They are unchanged, so this risk is low. ## Model Used Anthropic Claude Opus 5.5 (`claude-opus-5-5`), with tool use and code execution, in Claude Code. Subagents used the same model. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Paperclip <noreply@paperclip.ing>
528 lines
24 KiB
TypeScript
528 lines
24 KiB
TypeScript
import { CHAT_SETUP_HEALTH_SUITE } from "./chat-setup-health.js";
|
|
import { isEvalRemoteOrigin } from "./eval-remote-gateway.js";
|
|
import { neutralFixtureSuffix } from "./fixture-names.js";
|
|
import { isEverydayReliabilityCase } from "./everyday-reliability-cases.js";
|
|
import type { AiConnectionBinding } from "../../packages/shared/src/ai-connections.js";
|
|
import { verifyInstalledDaytonaPlugin } from "./installed-daytona-plugin.js";
|
|
import { NATIVE_COMPLETION_BUDGET_CENTS } from "./native-completion-defaults.js";
|
|
import path from "node:path";
|
|
import { installedReleaseDaytonaPlugin } from "./installed-release.js";
|
|
import { isManagedHiringCase } from "./chat-cases.js";
|
|
import { FixtureRegistry } from "./fixture-registry.js";
|
|
import { isImmutableDaytonaImage } from "./catalog.js";
|
|
import { TASK_TITLE_BUDGET_CENTS } from "./task-titles.js";
|
|
import { CHAT_FEATURE_SUITE, CHAT_FEATURE_BUDGET_CENTS } from "./chat-feature-cases.js";
|
|
import { codexSubscriptionAgentConfig, stageCodexSubscriptionFixture } from "./codex-subscription-fixture.js";
|
|
import { caseReceiptTask, CASE_RECEIPT_BUDGET_CENTS, CASE_RECEIPT_RUN_TIMEOUT_MS } from "./case-receipt-cases.js";
|
|
import { stageGrokSubscriptionFixture } from "./grok-subscription-fixture.js";
|
|
import { pollUntil, RunnerApiHttpError, type RunnerApi } from "./api.js";
|
|
import { assertCursorCloudQualificationBudgetOptIn, configureCursorCloudQualificationBudget, type CursorCloudQualificationBudget } from "./cursor-cloud-budget.js";
|
|
import type {
|
|
CredentialName,
|
|
MatrixExecution,
|
|
SecretReference,
|
|
SecretReferenceMap,
|
|
RunnerE2ERuntimeUsage,
|
|
} from "./types.js";
|
|
|
|
interface CompanyRecord {
|
|
id: string;
|
|
issuePrefix?: string | null;
|
|
name: string;
|
|
}
|
|
|
|
interface SecretRecord {
|
|
id: string;
|
|
}
|
|
interface PluginRecord {
|
|
id: string;
|
|
pluginKey: string;
|
|
status: string;
|
|
}
|
|
interface EnvironmentRecord {
|
|
id: string;
|
|
driver: string;
|
|
config?: Record<string, unknown>;
|
|
}
|
|
interface AgentRecord {
|
|
id: string;
|
|
name: string;
|
|
companyId: string;
|
|
}
|
|
interface ManagedAccountFixture {
|
|
connectionId: string;
|
|
binding: AiConnectionBinding;
|
|
}
|
|
|
|
interface ProjectRecord {
|
|
id: string;
|
|
name: string;
|
|
primaryWorkspace?: {
|
|
id: string;
|
|
cwd?: string | null;
|
|
} | null;
|
|
}
|
|
|
|
export interface LiveFixtureValues {
|
|
company: CompanyRecord;
|
|
secretRefs: SecretReferenceMap;
|
|
environment: EnvironmentRecord;
|
|
agent: AgentRecord;
|
|
project?: ProjectRecord;
|
|
aiConnection?: ManagedAccountFixture;
|
|
qualificationBudget?: CursorCloudQualificationBudget;
|
|
setupProbe?: { leaseId: string; durationMs: number; defaultRunner?: string };
|
|
setupLeases?: Array<{ leaseId: string; phase: "platform" | "adapter"; durationMs: number }>;
|
|
setupLeaseAccountingIncomplete?: boolean;
|
|
setupQualification?: { recorded: boolean; adapterType: string | null; platformResponses: number; adapterResponses: number };
|
|
|
|
onboardingRuntime?: {
|
|
mode: "production-wizard" | "api-configured-legacy-setup" | "post-onboarding-runtime-switch";
|
|
originalAdapterType: string;
|
|
originalModel: string | null;
|
|
testedAdapterType: string;
|
|
};
|
|
teardown(): Promise<void>;
|
|
}
|
|
|
|
function value<T>(resolved: ReadonlyMap<string, unknown>, id: string): T {
|
|
const result = resolved.get(id);
|
|
if (!result) throw new Error(`Missing resolved fixture ${id}`);
|
|
return result as T;
|
|
}
|
|
|
|
export function selectFixtureRuntimeLeases<T extends {
|
|
id: string; provider?: string | null; heartbeatRunId?: string | null; issueId?: string | null;
|
|
}>(input: { leases: T[]; runIds: string[]; issueId?: string; setupProbeLeaseId?: string; setupLeaseIds?: readonly string[] }): T[] {
|
|
const selectedRunIds = new Set(input.runIds);
|
|
const setupIds = new Set([...(input.setupLeaseIds ?? []), ...(input.setupProbeLeaseId ? [input.setupProbeLeaseId] : [])]);
|
|
const relevant = input.leases.filter(lease => lease.provider === "daytona" && (
|
|
setupIds.has(lease.id) || (input.setupLeaseIds === undefined && selectedRunIds.size === 0)
|
|
|| (lease.heartbeatRunId && selectedRunIds.has(lease.heartbeatRunId))
|
|
|| (input.issueId && lease.issueId === input.issueId)
|
|
));
|
|
return input.setupLeaseIds !== undefined || relevant.length > 0 ? relevant : input.leases;
|
|
}
|
|
|
|
export function fixtureRuntimeCostCoverage(usage: RunnerE2ERuntimeUsage, fixtures: Pick<LiveFixtureValues, "setupLeaseAccountingIncomplete">): RunnerE2ERuntimeUsage {
|
|
if (!fixtures.setupLeaseAccountingIncomplete) return usage;
|
|
const { estimatedListCostUsd: _estimate, ...known } = usage;
|
|
return { ...known, costStatus: "unavailable", costSource: "provider_cost_unavailable" };
|
|
}
|
|
|
|
export async function deleteDaytonaEnvironment(api: Pick<RunnerApi, "delete">, environmentId: string, cleanupDeadlineAt?: number) {
|
|
const deadlineAt = cleanupDeadlineAt ?? Date.now() + 120_000;
|
|
if (!Number.isFinite(deadlineAt)) throw new Error("Daytona cleanup deadline must be finite");
|
|
let lastError: unknown;
|
|
while (Date.now() < deadlineAt) {
|
|
const remaining = deadlineAt - Date.now();
|
|
if (remaining <= 0) break;
|
|
try {
|
|
await api.delete(
|
|
`/api/environments/${environmentId}?destroyReusableSandboxLeases=true`,
|
|
{
|
|
allowNotFound: true,
|
|
timeout: Math.min(30_000, remaining),
|
|
},
|
|
);
|
|
if (Date.now() >= deadlineAt) throw new Error("Daytona cleanup deadline expired");
|
|
return;
|
|
} catch (error) {
|
|
lastError = error;
|
|
const remaining = deadlineAt - Date.now();
|
|
if (remaining <= 0) break;
|
|
await new Promise((resolve) => setTimeout(resolve, Math.min(3_000, remaining)));
|
|
}
|
|
}
|
|
throw new Error(
|
|
`Daytona lease cleanup failed: ${lastError instanceof Error ? lastError.message : String(lastError)}`,
|
|
);
|
|
}
|
|
|
|
export async function installFixtureDaytonaProvider(api: Pick<RunnerApi, "post">, execution: MatrixExecution) {
|
|
const installed = await verifyInstalledDaytonaPlugin(process.env, [execution]);
|
|
return api.post<PluginRecord>("/api/plugins/install", {
|
|
packageName: installed?.packageRoot ?? path.resolve(import.meta.dirname, "../../packages/plugins/sandbox-providers/daytona"),
|
|
isLocalPath: true,
|
|
}, { timeout: 120_000 });
|
|
}
|
|
|
|
/** HTTP liveness after a restart does not imply the provider worker is ready. */
|
|
export async function waitForFixtureDaytonaProvider(
|
|
api: Pick<RunnerApi, "get">,
|
|
input: { companyId: string; deadlineAt: number },
|
|
): Promise<void> {
|
|
const pluginKey = "paperclip.daytona-sandbox-provider";
|
|
const timeout = () => {
|
|
const remaining = input.deadlineAt - Date.now();
|
|
if (remaining <= 0) throw new Error("Daytona fixture readiness deadline expired");
|
|
return Math.min(5_000, remaining);
|
|
};
|
|
await pollUntil({
|
|
label: "Daytona fixture provider readiness",
|
|
deadlineAt: input.deadlineAt,
|
|
intervalMs: 250,
|
|
retryOnError: error => error instanceof RunnerApiHttpError && [404, 502, 503, 504].includes(error.status),
|
|
load: async () => {
|
|
const dashboard = await api.get<{ pluginId: string; worker: { status: string } | null }>(
|
|
`/api/plugins/${pluginKey}/dashboard`, { timeout: timeout() },
|
|
);
|
|
const capabilities = await api.get<{ sandboxProviders?: Record<string, {
|
|
status?: string; source?: string; pluginKey?: string; pluginId?: string; supportsRunExecution?: boolean;
|
|
}> }>(`/api/companies/${input.companyId}/environments/capabilities`, { timeout: timeout() });
|
|
return { dashboard, provider: capabilities.sandboxProviders?.daytona };
|
|
},
|
|
accept: ({ dashboard, provider }) => dashboard.worker?.status === "running"
|
|
&& typeof dashboard.pluginId === "string" && dashboard.pluginId.length > 0
|
|
&& provider?.status === "supported" && provider.source === "plugin"
|
|
&& provider.pluginKey === pluginKey && provider.pluginId === dashboard.pluginId
|
|
&& provider.supportsRunExecution === true,
|
|
reject: ({ dashboard, provider }) => provider?.status === "supported"
|
|
&& (provider.source !== "plugin" || provider.pluginKey !== pluginKey || provider.pluginId !== dashboard.pluginId)
|
|
? "advertised plugin identity does not match the running provider" : undefined,
|
|
});
|
|
}
|
|
|
|
export async function setupLiveFixtures(input: {
|
|
api: RunnerApi;
|
|
execution: MatrixExecution;
|
|
executionNonce: string;
|
|
workspacePath: string;
|
|
credentials: Partial<Record<CredentialName, string>>;
|
|
daytonaImage?: string;
|
|
cleanupDeadlineAt?: number;
|
|
onCompanyCreated?: (companyId: string) => Promise<void>;
|
|
}): Promise<LiveFixtureValues> {
|
|
const { api, execution } = input;
|
|
const cursorCloudQualification = execution.suite.id === "cursor-cloud" && execution.profile.provider === "cursor_cloud";
|
|
if (cursorCloudQualification) assertCursorCloudQualificationBudgetOptIn();
|
|
let qualificationBudget: CursorCloudQualificationBudget | undefined;
|
|
const receiptSmoke = execution.suite.id === "case-handoffs" && execution.task.id === caseReceiptTask.id;
|
|
const registry = new FixtureRegistry();
|
|
let setupProbe: LiveFixtureValues["setupProbe"];
|
|
|
|
if (execution.environment.id === "daytona") {
|
|
registry.register<PluginRecord>({
|
|
id: "sandbox-provider",
|
|
async setup() {
|
|
return installFixtureDaytonaProvider(api, execution);
|
|
},
|
|
async teardown() {
|
|
// The plugin is installed only in the isolated instance/database. The
|
|
// launcher removes that complete instance after the environment lease
|
|
// has been destroyed, so no global uninstall mutation is necessary.
|
|
},
|
|
});
|
|
}
|
|
|
|
registry.register<CompanyRecord>({
|
|
id: "company",
|
|
async setup() {
|
|
const company = await api.post<CompanyRecord>("/api/companies", {
|
|
name: `Company ${neutralFixtureSuffix(execution.id, input.executionNonce)}`,
|
|
description: "Operations company",
|
|
budgetMonthlyCents: [CHAT_FEATURE_SUITE, CHAT_SETUP_HEALTH_SUITE].includes(execution.suite.id) ? CHAT_FEATURE_BUDGET_CENTS
|
|
: receiptSmoke ? CASE_RECEIPT_BUDGET_CENTS
|
|
: ["native-completion", "native-instruction-consolidation", "native-connection-guidance"].includes(execution.suite.id)
|
|
|| (execution.suite.id === "everyday-workflows" && (["hire-reuse", "delegate-feedback"].includes(execution.task.id)
|
|
|| isEverydayReliabilityCase(execution.task.id))) ? NATIVE_COMPLETION_BUDGET_CENTS
|
|
: execution.suite.id === "task-titles" ? TASK_TITLE_BUDGET_CENTS
|
|
: ["cursor-cloud", "stock-harness", "task-workspaces", "case-handoffs", "feedback-persistence", "daytona-restart-continuity", "remote-workspace-storage", "rolling-web-deployment"].includes(execution.suite.id) ? 1_000 : 0,
|
|
});
|
|
await input.onCompanyCreated?.(company.id);
|
|
return company;
|
|
},
|
|
async teardown() {
|
|
// The isolated instance/database is removed by the launcher. Do not call
|
|
// company deletion here: metered runs intentionally retain cost-event
|
|
// references until that instance-wide teardown.
|
|
},
|
|
});
|
|
|
|
registry.register<SecretReferenceMap>({
|
|
id: "secrets",
|
|
dependencies: ["company"],
|
|
async setup(resolved) {
|
|
const company = value<CompanyRecord>(resolved, "company");
|
|
const refs: SecretReferenceMap = {};
|
|
for (const credentialName of execution.suite.id === "chat-features"
|
|
? [...new Set([execution.profile.credential, execution.environment.credential].filter((name): name is CredentialName => !!name))]
|
|
: execution.requiredCredentials) {
|
|
if (["GROK_AUTH_JSON", "CODEX_AUTH_JSON"].includes(credentialName)) continue;
|
|
const rawValue = input.credentials[credentialName];
|
|
if (!rawValue) throw new Error(`Missing credential ${credentialName}`);
|
|
const secret = await api.postSensitive<SecretRecord>(
|
|
`/api/companies/${company.id}/secrets`,
|
|
{
|
|
name: `${credentialName} ${neutralFixtureSuffix(input.executionNonce)}`,
|
|
key: credentialName,
|
|
value: rawValue,
|
|
description: "Model provider credential",
|
|
},
|
|
);
|
|
refs[credentialName] = {
|
|
type: "secret_ref",
|
|
secretId: secret.id,
|
|
version: "latest",
|
|
} satisfies SecretReference;
|
|
}
|
|
return refs;
|
|
},
|
|
});
|
|
|
|
const grokSubscription = execution.profile.credential === "GROK_AUTH_JSON";
|
|
const codexSubscription = execution.profile.credential === "CODEX_AUTH_JSON";
|
|
if (grokSubscription || codexSubscription) {
|
|
registry.register<() => Promise<void>>({
|
|
id: "subscription-login",
|
|
dependencies: ["company"],
|
|
async setup(resolved) {
|
|
const raw = input.credentials[execution.profile.credential];
|
|
if (!raw) throw new Error("Missing explicit subscription login credential");
|
|
return (codexSubscription ? stageCodexSubscriptionFixture : stageGrokSubscriptionFixture)({
|
|
raw, companyId: value<CompanyRecord>(resolved, "company").id,
|
|
environment: process.env,
|
|
});
|
|
},
|
|
async teardown(remove) { await remove(); },
|
|
});
|
|
}
|
|
|
|
registry.register<EnvironmentRecord>({
|
|
id: "environment",
|
|
dependencies: [
|
|
"company",
|
|
"secrets",
|
|
...((grokSubscription || codexSubscription) ? ["subscription-login"] : []),
|
|
...(execution.environment.id === "daytona" ? ["sandbox-provider"] : []),
|
|
],
|
|
async setup(resolved) {
|
|
const company = value<CompanyRecord>(resolved, "company");
|
|
const secretRefs = value<SecretReferenceMap>(resolved, "secrets");
|
|
if (execution.environment.id === "local") {
|
|
// Paperclip has one instance-managed local environment. The company
|
|
// creation API ensures it exists; creating a second local environment
|
|
// is intentionally rejected by the public API.
|
|
const environments = await api.get<EnvironmentRecord[]>(
|
|
`/api/companies/${company.id}/environments?driver=local`,
|
|
);
|
|
const local = environments.find(
|
|
(candidate) => candidate.driver === "local",
|
|
);
|
|
if (!local)
|
|
throw new Error(
|
|
"Isolated Paperclip instance did not create its managed local environment",
|
|
);
|
|
return local;
|
|
}
|
|
return api.post<EnvironmentRecord>(
|
|
`/api/companies/${company.id}/environments`,
|
|
execution.environment.buildEnvironment({
|
|
secretRefs,
|
|
daytonaImage: input.daytonaImage,
|
|
executionId: input.executionNonce,
|
|
}),
|
|
);
|
|
},
|
|
async teardown(environment) {
|
|
if (execution.environment.id === "daytona") {
|
|
await deleteDaytonaEnvironment(api, environment.id, input.cleanupDeadlineAt);
|
|
}
|
|
},
|
|
});
|
|
|
|
const managedHiring = isManagedHiringCase(execution.suite.id, execution.task.id);
|
|
const savedCopilot = execution.profile.qualificationCandidate === "copilot";
|
|
const gatewayUrl = process.env.PAPERCLIP_E2E_GATEWAY_BASE_URL;
|
|
const gatewayToken = process.env.PAPERCLIP_E2E_GATEWAY_TOKEN;
|
|
const budgetedGateway = Boolean(gatewayUrl || gatewayToken);
|
|
if (budgetedGateway && (!gatewayUrl || !gatewayToken || !(execution.environment.id === "local" ? /^http:\/\/127\.0\.0\.1:[0-9]+$/.test(gatewayUrl)
|
|
: isEvalRemoteOrigin(gatewayUrl) && gatewayUrl === process.env.PAPERCLIP_E2E_REMOTE_GATEWAY_ORIGIN)
|
|
|| !["local", "daytona"].includes(execution.environment.id) || !["core-compatibility", "feedback-persistence"].includes(execution.suite.id))) throw new Error("Unqualified fixture gateway route");
|
|
if (managedHiring || savedCopilot || budgetedGateway) {
|
|
registry.register<ManagedAccountFixture>({
|
|
id: "ai-connection",
|
|
dependencies: ["company", ...(savedCopilot ? ["environment"] : [])],
|
|
async setup(resolved) {
|
|
const company = value<CompanyRecord>(resolved, "company");
|
|
const key = savedCopilot ? "COPILOT_GITHUB_TOKEN" : execution.profile.credential;
|
|
const provider = savedCopilot ? "github" : key === "ANTHROPIC_API_KEY" ? "anthropic"
|
|
: key === "OPENROUTER_API_KEY" ? "openrouter"
|
|
: key === "OPENAI_API_KEY" ? "openai" : null;
|
|
if (!provider) throw new Error(`Unsupported managed hiring credential ${key}`);
|
|
const apiKey = input.credentials[key];
|
|
if (!apiKey) throw new Error(`Missing credential ${key}`);
|
|
const account = await api.postSensitive<{ connectionId: string; grantId: string }>(
|
|
`/api/companies/${company.id}/ai-connections`,
|
|
{
|
|
provider,
|
|
method: "api_key",
|
|
name: `Account ${neutralFixtureSuffix(input.executionNonce)}`,
|
|
ownership: budgetedGateway ? "shared" : "personal",
|
|
...(budgetedGateway ? { routing: { kind: "gateway", protocol: provider === "anthropic" ? "messages" : "responses",
|
|
baseUrl: provider === "anthropic" ? gatewayUrl : `${gatewayUrl}/v1`, auth: "bearer", models: [{ id: execution.profile.model }] } } : {}),
|
|
...(savedCopilot ? { environmentId: value<EnvironmentRecord>(resolved, "environment").id } : {}),
|
|
apiKey: budgetedGateway ? gatewayToken : apiKey,
|
|
agentIds: [],
|
|
allAgents: budgetedGateway,
|
|
},
|
|
);
|
|
return {
|
|
connectionId: account.connectionId,
|
|
binding: budgetedGateway
|
|
? { provider, method: "api_key", mode: "shared", connectionId: account.connectionId, grantId: account.grantId }
|
|
: { provider, method: "api_key", mode: "responsible_user" },
|
|
};
|
|
},
|
|
});
|
|
}
|
|
|
|
registry.register<AgentRecord>({
|
|
id: "agent",
|
|
dependencies: [
|
|
"company",
|
|
"secrets",
|
|
"environment",
|
|
...((grokSubscription || codexSubscription) ? ["subscription-login"] : []),
|
|
...(managedHiring || savedCopilot || budgetedGateway ? ["ai-connection"] : []),
|
|
],
|
|
async setup(resolved) {
|
|
const company = value<CompanyRecord>(resolved, "company");
|
|
const environment = value<EnvironmentRecord>(resolved, "environment");
|
|
const secretRefs = value<SecretReferenceMap>(resolved, "secrets");
|
|
const agent = execution.profile.buildAgent({
|
|
environmentId: environment.id,
|
|
environmentFixtureId: execution.environment.id,
|
|
workspacePath: input.workspacePath,
|
|
secretRefs,
|
|
executionId: input.executionNonce,
|
|
});
|
|
const autoCodex = agent.adapterType === "codex_local" && (agent.runner === undefined || agent.runner === "auto");
|
|
const nativeCodex = agent.adapterType === "paperclip_runner" && (agent.adapterConfig as Record<string, unknown>).provider === "codex";
|
|
if (environment.driver === "sandbox" && execution.profile.generation === "native"
|
|
&& execution.profile.provider === "codex" && agent.runner !== "legacy" && (autoCodex || nativeCodex)) {
|
|
if (!isImmutableDaytonaImage(String(environment.config?.image ?? "")) || environment.config?.snapshot) {
|
|
throw new Error("Native Codex qualification requires this fixture's immutable image-backed environment.");
|
|
}
|
|
const startedAt = Date.now();
|
|
const probe = await api.post<{ ok: boolean; details?: { platformQualificationRecorded?: boolean; leaseId?: string } }>(
|
|
`/api/environments/${environment.id}/probe?companyId=${encodeURIComponent(company.id)}`,
|
|
{}, { timeout: 330_000 },
|
|
);
|
|
if (probe.ok !== true || probe.details?.platformQualificationRecorded !== true || !probe.details.leaseId) {
|
|
throw new Error("Native Codex image qualification was not recorded; agent creation was not attempted.");
|
|
}
|
|
setupProbe = { leaseId: probe.details.leaseId, durationMs: Date.now() - startedAt };
|
|
if (autoCodex) {
|
|
const adapters = await api.get<Array<{ type: string; defaultRunner?: string; supportedRunners?: string[] }>>(
|
|
`/api/adapters?companyId=${encodeURIComponent(company.id)}&environmentId=${encodeURIComponent(environment.id)}`,
|
|
);
|
|
const selected = adapters.find(adapter => adapter.type === "codex_local");
|
|
if (selected?.defaultRunner !== "paperclip" || !selected.supportedRunners?.includes("paperclip")) {
|
|
throw new Error("The qualified image does not advertise native Codex as its server-owned default.");
|
|
}
|
|
setupProbe.defaultRunner = selected.defaultRunner;
|
|
}
|
|
}
|
|
if (["stock-harness", "native-connection-guidance", "feedback-persistence", "daytona-restart-continuity", "remote-workspace-storage", "rolling-web-deployment"].includes(execution.suite.id)
|
|
|| (execution.suite.id === "everyday-workflows" && (["hire-reuse", "delegate-feedback"].includes(execution.task.id)
|
|
|| isEverydayReliabilityCase(execution.task.id)))) {
|
|
agent.budgetMonthlyCents = 1_000;
|
|
}
|
|
if (receiptSmoke) {
|
|
agent.budgetMonthlyCents = CASE_RECEIPT_BUDGET_CENTS;
|
|
agent.adapterConfig = { ...(agent.adapterConfig as Record<string, unknown>),
|
|
timeoutSec: CASE_RECEIPT_RUN_TIMEOUT_MS / 1_000, modelReasoningEffort: "low" };
|
|
const runtimeConfig = (agent.runtimeConfig ?? {}) as Record<string, unknown>;
|
|
agent.runtimeConfig = { ...runtimeConfig,
|
|
heartbeat: { ...((runtimeConfig.heartbeat ?? {}) as Record<string, unknown>),
|
|
enabled: false, maxConcurrentRuns: 1, maxDailyRuns: 1, maxDailyCostCents: CASE_RECEIPT_BUDGET_CENTS } };
|
|
}
|
|
if (managedHiring || savedCopilot || budgetedGateway) {
|
|
const account = value<ManagedAccountFixture>(resolved, "ai-connection");
|
|
const config = agent.adapterConfig as Record<string, unknown>;
|
|
if (budgetedGateway && execution.environment.id === "daytona") config.helloProbeTimeoutSec = 300;
|
|
delete config.env;
|
|
agent.runtimeConfig = {
|
|
...(agent.runtimeConfig as Record<string, unknown>),
|
|
aiConnection: account.binding,
|
|
};
|
|
}
|
|
const createdAgent = await api.post<AgentRecord>(
|
|
`/api/companies/${company.id}/agents`,
|
|
codexSubscription ? codexSubscriptionAgentConfig(agent, process.env, company.id) : agent,
|
|
);
|
|
if (cursorCloudQualification) {
|
|
qualificationBudget = await configureCursorCloudQualificationBudget({ api, companyId: company.id, agentId: createdAgent.id });
|
|
}
|
|
return createdAgent;
|
|
},
|
|
async teardown() {
|
|
// Agent state is instance-local. Daytona environment teardown below is
|
|
// the only fixture cleanup that must reach an external provider.
|
|
},
|
|
});
|
|
|
|
if (execution.task.flow === "repository_verification"
|
|
|| execution.task.flow === "finalization_recovery"
|
|
|| execution.task.flow === "followup_stop_resume"
|
|
|| execution.task.flow === "warm_three_turn"
|
|
|| (["extended-harnesses", "kimi-qualification"].includes(execution.suite.id) && execution.task.id === "file-edit-validate")) {
|
|
registry.register<ProjectRecord>({
|
|
id: "project",
|
|
dependencies: ["company", "environment"],
|
|
async setup(resolved) {
|
|
const company = value<CompanyRecord>(resolved, "company");
|
|
const environment = value<EnvironmentRecord>(resolved, "environment");
|
|
return api.post<ProjectRecord>(
|
|
`/api/companies/${company.id}/projects`,
|
|
{
|
|
name: `Workspace project ${neutralFixtureSuffix(input.executionNonce)}`,
|
|
description:
|
|
"Project for the execution workspace and file copy-back",
|
|
executionWorkspacePolicy: {
|
|
enabled: true,
|
|
defaultMode: "shared_workspace",
|
|
sharedWorkspaceConcurrency: "serialize",
|
|
allowIssueOverride: false,
|
|
environmentId: environment.id,
|
|
workspaceStrategy: { type: "project_primary" },
|
|
},
|
|
workspace: {
|
|
name: "Primary",
|
|
sourceType: "local_path",
|
|
cwd: input.workspacePath,
|
|
isPrimary: true,
|
|
},
|
|
},
|
|
);
|
|
},
|
|
async teardown() {
|
|
// The isolated instance is deleted after provider resources are gone.
|
|
},
|
|
});
|
|
}
|
|
|
|
const setup = await registry.setupAll();
|
|
return {
|
|
company: value<CompanyRecord>(setup.values, "company"),
|
|
secretRefs: value<SecretReferenceMap>(setup.values, "secrets"),
|
|
environment: value<EnvironmentRecord>(setup.values, "environment"),
|
|
agent: value<AgentRecord>(setup.values, "agent"),
|
|
...(qualificationBudget ? { qualificationBudget } : {}),
|
|
...(setupProbe ? { setupProbe } : {}),
|
|
...(setup.values.has("project")
|
|
? { project: value<ProjectRecord>(setup.values, "project") }
|
|
: {}),
|
|
...(managedHiring
|
|
? {
|
|
aiConnection: value<ManagedAccountFixture>(
|
|
setup.values,
|
|
"ai-connection",
|
|
),
|
|
}
|
|
: {}),
|
|
teardown: setup.teardown,
|
|
};
|
|
}
|