Files
PaperClipAI/server/src/services/native-runtime/provider-profile.ts
T
DottaandPaperclip 61bd4ac691 feat(agents): set up Antigravity with saved Google subscriptions (#18189)
## Thinking Path

> - Paperclip manages agents and their work.
> - Users choose providers and managed accounts while adding agents.
> - Antigravity was missing from the ordinary additional-agent picker
and its saved Google path did not complete native readiness.
> - A user needs a successful Test, an enabled task action, and a stable
account binding.
> - This pull request connects the ordinary setup flow to the native
candidate.
> - It also preserves retired credential refreshes across database
failures and server restarts.

## Linked Issues or Issue Description

Built on merged #18188 and #18187. Refs: #12503, #14938 for related
legacy CLI proposals.

**Agent or provider**

Google Antigravity ACP 1.4.3.

**Why this adapter is useful**

Users need to set up an Antigravity agent and run real work through
their managed Google subscription.

**How the agent is invoked**

Native Paperclip Runner uses the pinned Antigravity ACP runtime. Exact
operator admission remains required; the local candidate result does not
enable general release.

## What Changed

- Show Google Antigravity in the ordinary adapter grid and preserve the
draft choice when setup resumes.
- Reuse the same user’s saved personal default Google subscription and
discover the exact low model through the native account.
- Run a prompt-free native Test and wait for authoritative readiness
before enabling task assignment.
- Carry the managed account through native runs and recover retired
refreshes through the existing encrypted, fsynced journal with exact
generation checks.
- Retain the latest rotated credential through verification, storage,
and partial home-removal failures. Retry the exact account’s retained
cleanup before another provider use.
- Reconcile cleanup before taking the credential mutation lock, fence
owners arriving during lock acquisition, and support nested lock holders
without a join cycle.
- Bound sign-in retirement outside database transactions and preserve
claims for the existing cleanup owner; one pending sign-in does not
block unrelated accounts.
- Cover Gemini and Pi API-key recovery, keep API-key bulk manual-model
editing usable, and keep adapter discovery available after candidate
expiry.
- Skip taskless native timer work before run or credential admission.
Preserve current master monitor and deadline scheduling.

## Verification

- The registered local saved-Google-subscription journey passed all 16
matchers in 196,520 ms using eval source `12aba` against actual serving
source `4a37ee7c`. Both turns stayed in one conversation, produced exact
downloaded files of 61 and 119 bytes after reload, and used distinct
native primary processes. All observed owners retired, leases released,
and credential fences settled. Independent readback confirmed Done work,
no active owned runs, a connected saved account, and the retained agent
paused for inspection.
- The measured runtime is production-equivalent to pre-restack
`65ef31dc` except its test-only transport fixture correction.
Current-master head `ff7c9249` preserves all 99 product paths: 98 exact
blobs and only eight inherited plugin-panel lines in `agents.ts`. Its
native binary was rebuilt for inherited Codex-only diagnostic changes;
the changed Rust behavior is outside Antigravity's ACPX execution path.
Shared native/UI changes have focused regression coverage. The recorded
live result is not represented as a measured replay of `ff7c9249`.
- Full workspace build and typecheck passed on `ff7c9249`. All 376
focused tests in 14 files passed, covering Google credential/retirement
controls and the inherited native-tool, redaction, route, and UI
recovery changes. Token gates, maps, and diff checks passed. The
required-close fixture was separately demonstrated red, then passed all
317 selected provider-wiring cases.
- [Current-head
CI](https://github.com/paperclipai/paperclip/actions/runs/40959387972)
is green: 55 successful checks, two skipped workflow checks, and an
optional neutral review check. Required `ci / verify` and `ci / e2e`
passed. Greptile and Paperclip Review both report 5/5 on exact
`ff7c9249`, with zero unresolved review threads.
- This qualifies the selected operator-authorized local
saved-subscription candidate. The original exploratory journey remains
historical evidence; the repeatable fixed-driver eval is in the next PR.

## Risks

- Google refresh recovery must preserve the exact grant, account, and
credential generation. An uncertain native owner or failed encrypted
handoff retains the private home and blocks cleanup.
- A successful local saved-subscription candidate does not qualify fresh
OAuth, API keys, Cloud, Linux, other models, or general release.
- Timer filtering changes only taskless native Runner admissions.
Existing task-bound monitors and legacy adapters retain their owners.

## Model Used

OpenAI Codex (GPT-6). The host did not expose the exact model revision
or context limit. The model used tools for code edits, tests, and
review.

## 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
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-29 16:51:50 -05:00

874 lines
37 KiB
TypeScript

import { resolveGrokDisableWebSearch } from "../../vendor/paperclip-runner/index.js";
import { agentHarnessType, normalizeClaudeRunnerOptions } from "@paperclipai/shared";
import { resolveClaudeEffort, resolveGrokEffort, resolveQualifiedAcpxProfile } from "../../vendor/paperclip-runner/index.js";
import {
isPaperclipRunnerProvider,
PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES,
PAPERCLIP_RUNNER_ACPX_PROFILES,
resolvePaperclipRunnerPermissionMode,
resolvePaperclipRunnerCursorMode,
resolvePaperclipRunnerPiThinkingLevel,
type PaperclipRunnerProvider,
} from "@paperclipai/adapter-utils";
import {
codexLocalReasoningEffortsForModel,
isCodexLocalKnownModel,
} from "@paperclipai/adapter-codex-local";
import {
AGENTCORE_QUALIFIED_MODEL,
CLAUDE_MANAGED_QUALIFIED_MODEL,
} from "../provider-profile-qualification.js";
import { resolveAcpxQualification, resolveAntigravityCandidateAdmission, type AcpxQualificationCandidate } from "./acpx-qualification.js";
import { compatibleCodexModel } from "./codex-model-fallback.js";
export const QUALIFIED_OPENCODE_RUNNER_VERSION = "1.18.34" as const;
export const DEFAULT_OPENCODE_RUNNER_MODEL =
"openrouter/deepseek/deepseek-v4-flash-0731" as const;
export const CLAUDE_MANAGED_BETA_VERSION = "managed-agents-2026-04-01" as const;
export const DEFAULT_ACPX_RUNNER_MODELS = {
grok: "grok-4.7",
claude: "claude-opus-5-5",
// These profiles require explicit configuration. Admission belongs to the runner.
codex: null,
cursor: null,
copilot: null,
pi: null,
} as const;
export type QualifiedPaperclipRunnerAcpxAgent =
keyof typeof DEFAULT_ACPX_RUNNER_MODELS;
type AdmittedPaperclipRunnerAcpxAgent = QualifiedPaperclipRunnerAcpxAgent | AcpxQualificationCandidate | "antigravity";
export type PaperclipRunnerProviderProfile =
| { provider: "muse"; backend: "muse_external"; model: null; museBindingId: string }
| { provider: "grok_bot"; backend: "grok_bot_mcp"; model: null; externalBindingId: string }
| { provider: "openai_dot"; backend: "openai_dot_mcp"; model: null; dotBindingId: string }
| {
provider: "codex";
backend: "codex_app_server";
model: string | null;
}
| {
provider: "opencode";
backend: "opencode_server";
model: string;
}
| {
provider: "claude_managed";
backend: "claude_managed_agents_api";
managedProfileId: string;
model: string | null;
maxSessionListCostUsd: number | null;
}
| {
provider: "aws_agentcore";
backend: "aws_agentcore_harness_api";
agentCoreProfileId: string;
model: string | null;
maxEstimatedSessionCostUsd: number | null;
}
| {
provider: "acpx";
backend: "acpx_runtime";
model: string;
acpxAgent: AdmittedPaperclipRunnerAcpxAgent;
};
export type PaperclipRunnerNativeProviderInput =
| { provider: "muse"; model: null; museBinding: import("../../vendor/paperclip-runner/index.js").MuseBindingSnapshot }
| { provider: "grok_bot"; model: null; dotBinding: import("../../vendor/paperclip-runner/index.js").DotBindingSnapshot }
| { provider: "openai_dot"; model: null; dotBinding: import("../../vendor/paperclip-runner/index.js").DotBindingSnapshot }
| {
provider: "codex";
model: string | null;
homeMode?: "isolated" | "preconfigured";
codexApprovalPolicy: "never" | "on-request" | "untrusted";
codexReasoningEffort?: string;
codexWebSearch?: "disabled";
}
| {
provider: "opencode";
model: string;
opencodePermissionMode: "allow" | "ask" | "deny";
}
| {
provider: "claude_managed";
model: string;
managedProfile: {
profileId: string;
anthropicAgentId: string;
agentVersion: string;
environmentId: string;
betaVersion: typeof CLAUDE_MANAGED_BETA_VERSION;
};
maxSessionListCostUsd: number;
}
| {
provider: "aws_agentcore";
model: string;
agentCoreProfile: {
profileId: string;
region: string;
accountId: string;
harnessArn: string;
harnessVersion: string;
endpointArn: string;
endpointQualifier: string;
agentRuntimeArn: string;
memoryArn: string;
memoryId: string;
invocationRoleArn: string;
contextBucket: string;
contextPrefix: string;
contextKmsKeyArn: string;
qualificationRevision: string;
eventExpiryDays: 90;
};
maxEstimatedSessionCostUsd: number;
invocationLimits: {
maxIterations: number;
maxOutputTokens: number;
timeoutSeconds: number;
};
}
| {
provider: "acpx";
model: string;
acpxAgent: AdmittedPaperclipRunnerAcpxAgent;
homeMode?: "isolated" | "preconfigured";
acpxPermissionMode: "approve-all" | "approve-paperclip" | "approve-reads" | "deny-all";
acpxSessionMode?: "agent" | "plan" | "ask";
piThinkingLevel?: "off" | "low" | "high" | "max";
claudeEffort?: "low" | "medium" | "high" | "xhigh" | "max";
grokEffort?: "minimal" | "low" | "medium" | "high" | "xhigh";
grokDisableWebSearch?: boolean;
};
export class PaperclipRunnerProviderProfileError extends Error {
constructor(
readonly code: string,
message: string,
) {
super(message);
this.name = "PaperclipRunnerProviderProfileError";
}
}
function asRecord(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
? value as Record<string, unknown>
: {};
}
function optionalString(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0
? value.trim()
: null;
}
/** Configuration can be saved before pairing; task admission requires a binding. */
export function validatePaperclipRunnerDotConfig(config: Record<string, unknown>, requireBinding = true): string | null {
const bindingId = optionalString(config.dotBindingId);
if ((requireBinding && !bindingId) || (bindingId && !/^[0-9a-f-]{36}$/i.test(bindingId))
|| config.allowUnmeteredProvider !== true || (config.lifecycleMode && config.lifecycleMode !== "per_turn")
|| optionalString(config.model)) {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_dot_config_invalid", "Dot requires per-turn lifecycle and explicit externally billed, unmetered-provider acknowledgement. Pair a binding before assigning work.");
}
return bindingId;
}
/** Closed personal runtime configuration; model and workspace grants are excluded. */
export function validatePaperclipRunnerMuseConfig(config: Record<string, unknown>, requireBinding = true): string | null {
const bindingId = optionalString(config.museBindingId);
if ((requireBinding && !bindingId) || (bindingId && !/^[0-9a-f-]{36}$/i.test(bindingId))
|| config.allowUnmeteredProvider !== true || (config.lifecycleMode && config.lifecycleMode !== "per_turn")
|| optionalString(config.model) || config.dotWorkspaceAccess === true || config.dotAttachmentAccess === true) {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_muse_config_invalid", "Muse requires a paired personal-agent binding, per-turn lifecycle and unavailable-usage acknowledgement.");
}
return bindingId;
}
export function validatePaperclipRunnerGrokConfig(config: Record<string, unknown>, requireBinding = true): string | null {
const bindingId = optionalString(config.externalBindingId);
if ((requireBinding && !bindingId) || (bindingId && !/^[0-9a-f-]{36}$/i.test(bindingId))
|| config.allowUnmeteredProvider !== true || (config.lifecycleMode && config.lifecycleMode !== "per_turn")
|| optionalString(config.model) || config.dotWorkspaceAccess === true || config.dotAttachmentAccess === true
|| optionalString(config.cwd) || optionalString(config.command)) {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_grok_config_invalid", "Grok Bot requires a bound per-turn work session and an explicit acknowledgement of unreported provider spending. Paperclip workspace, shell and third-party app tools are unavailable.");
}
return bindingId;
}
/** A task may change a local Runner model (and Codex effort), but not provider identity. */
export function projectPaperclipRunnerTaskConfig(
backend: "codex_app_server" | "opencode_server" | "acpx_runtime",
agentConfig: unknown,
taskOverrides: unknown,
managedModel?: string,
): Record<string, unknown> {
const base = asRecord(agentConfig);
const task = asRecord(taskOverrides);
const config = { ...base };
// The server resolves a connection's model namespace after task overrides.
// Carry that model into durable input without allowing routing to change the harness.
const model = optionalString(managedModel) ?? optionalString(task.model);
const grok = backend === "acpx_runtime" && agentHarnessType("paperclip_runner", base) === "grok_local";
const supportsEffort = backend === "codex_app_server" || grok || (backend === "acpx_runtime" && agentHarnessType("paperclip_runner", base) === "claude_local");
const effortKey = supportsEffort
? ["modelReasoningEffort", "reasoningEffort", "effort"].find((key) => key in task)
: undefined;
if (supportsEffort && ((model !== null && model !== optionalString(base.model)) || effortKey !== undefined)) {
delete config.modelReasoningEffort;
delete config.reasoningEffort;
delete config.effort;
}
if (model !== null) config.model = model;
if (effortKey !== undefined) config[grok ? "reasoningEffort" : "modelReasoningEffort"] = task[effortKey];
return config;
}
function positiveNumberOrNull(
value: unknown,
code: string,
message: string,
): number | null {
if (value === undefined || value === null || value === "") return null;
const parsed = Number(value);
if (!Number.isFinite(parsed) || parsed <= 0) {
throw new PaperclipRunnerProviderProfileError(code, message);
}
return parsed;
}
function boundedPositiveInteger(
value: unknown,
fallback: number,
maximum: number,
code: string,
label: string,
): number {
if (value === undefined || value === null || value === "") return fallback;
if (
typeof value !== "number"
|| !Number.isSafeInteger(value)
|| value <= 0
|| value > maximum
) {
throw new PaperclipRunnerProviderProfileError(
code,
`${label} must be an integer between 1 and ${maximum}.`,
);
}
return value;
}
function assertPermissionMode(
provider: PaperclipRunnerProvider,
config: Record<string, unknown>,
): void {
const capability = PAPERCLIP_RUNNER_PERMISSION_CAPABILITIES[provider];
if (!capability.configurable) return;
const configured = config[capability.configKey];
if (
configured !== undefined
&& resolvePaperclipRunnerPermissionMode(provider, configured) !== configured
) {
if (provider === "codex") {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_codex_permission_mode_unqualified",
"Paperclip Runner currently supports Codex only with codexPermissionMode set to never. Update the agent configuration before starting a new native run.",
);
}
throw new PaperclipRunnerProviderProfileError(
"runner_permission_mode_invalid",
`${capability.configKey} is not supported by ${provider}.`,
);
}
}
/**
* Rebind a persisted Claude Managed run only to the still-qualified profile
* and the profile's current company secret. The secret itself may rotate, but
* the agent configuration must rotate its binding to the same profile-owned
* secret before recovery can continue.
*/
export function assertManagedProfileRecoveryBinding(input: {
adapterConfig: unknown;
snapshot: {
profileId: string;
anthropicAgentId: string;
agentVersion: string;
environmentId: string;
betaVersion: string;
};
stored: {
id: string;
anthropicAgentId: string;
agentVersion: string;
environmentId: string;
betaVersion: string;
apiKeySecretId: string;
};
}): void {
const { snapshot, stored } = input;
if (
snapshot.profileId !== stored.id
|| snapshot.anthropicAgentId !== stored.anthropicAgentId
|| snapshot.agentVersion !== stored.agentVersion
|| snapshot.environmentId !== stored.environmentId
|| snapshot.betaVersion !== stored.betaVersion
) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_claude_managed_recovery_identity_mismatch",
"The persisted Claude Managed identity no longer matches its qualified profile.",
);
}
const rawBinding = asRecord(asRecord(input.adapterConfig).env).ANTHROPIC_API_KEY;
const boundSecretId = asRecord(rawBinding).secretId;
if (boundSecretId !== stored.apiKeySecretId) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_claude_managed_recovery_secret_mismatch",
"The persisted Claude Managed run is not bound to its profile's current API-key secret.",
);
}
}
/** Revalidate the operator's AWS profile revocation and immutable identity on recovery. */
export function assertAgentCoreProfileRecoveryBinding(input: {
snapshot: {
profileId: string;
region: string;
accountId: string;
harnessArn: string;
harnessVersion: string;
endpointArn: string;
endpointQualifier: string;
agentRuntimeArn: string;
memoryArn: string;
memoryId: string;
invocationRoleArn: string;
contextBucket: string;
contextPrefix: string;
contextKmsKeyArn: string;
qualificationRevision: string;
eventExpiryDays: number;
};
stored: {
id: string;
configuration: Record<string, unknown>;
};
}): void {
const { snapshot, stored } = input;
const configuration = asRecord(stored.configuration);
const fields = [
"region",
"accountId",
"harnessArn",
"harnessVersion",
"endpointArn",
"endpointQualifier",
"agentRuntimeArn",
"memoryArn",
"memoryId",
"invocationRoleArn",
"contextBucket",
"contextPrefix",
"contextKmsKeyArn",
"qualificationRevision",
"eventExpiryDays",
] as const;
if (
snapshot.profileId !== stored.id
|| fields.some((field) => snapshot[field] !== configuration[field])
) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_aws_agentcore_recovery_identity_mismatch",
"The persisted AWS AgentCore identity no longer matches its qualified profile.",
);
}
}
export function claudeRunnerUnsupportedSettings(config: Record<string, unknown>): string[] {
config = normalizeClaudeRunnerOptions(config).config;
const unsupported = ["chrome", "agentCommand", "acpAgentCommand", "stateDir", "acpStateDir", "warmHandleIdleMs", "acpWarmHandleIdleMs", "extraArgs", "args", "filesystemScope", "filesystemExtraPaths", "filesystemSandboxCommand", "networkScope", "networkAllowlist", "maxTurnsPerRun", "permissionMode", "acpPermissionMode"]
.filter(key => config[key] !== undefined && config[key] !== false && config[key] !== "" && config[key] !== 0 && !(Array.isArray(config[key]) && config[key].length === 0));
if (config.command && config.command !== "claude") unsupported.push("command");
if (config.mode && config.mode !== "persistent") unsupported.push("mode");
if (config.acpMode && config.acpMode !== "persistent") unsupported.push("acpMode");
if (config.engine && !["auto", "cli", "acp"].includes(String(config.engine))) unsupported.push("engine");
if (config.nonInteractivePermissions && config.nonInteractivePermissions !== "deny") unsupported.push("nonInteractivePermissions");
if (config.acpNonInteractivePermissions && config.acpNonInteractivePermissions !== "deny") unsupported.push("acpNonInteractivePermissions");
if (config.dangerouslySkipPermissions === false) unsupported.push("dangerouslySkipPermissions");
return unsupported;
}
/**
* Resolve the immutable provider identity used for a fresh Paperclip Runner
* selection. The persisted adapterConfig is the authority; runtimeConfig is
* deliberately not consulted so model-profile or migration metadata cannot
* silently switch the harness selected for a run.
*/
export function resolvePaperclipRunnerProviderProfile(
adapterConfig: unknown,
context: { companyId?: string } = {},
): PaperclipRunnerProviderProfile {
const config = asRecord(adapterConfig);
const candidate = config.provider ?? "codex";
if (!isPaperclipRunnerProvider(candidate)) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_provider_unsupported",
"Paperclip Runner provider must be Codex, OpenCode, Claude Managed, AWS AgentCore, or ACPX.",
);
}
if (candidate === "muse") {
const bindingId = validatePaperclipRunnerMuseConfig(config)!;
return { provider: "muse", backend: "muse_external", model: null, museBindingId: bindingId };
}
if (candidate === "grok_bot") {
return { provider: "grok_bot", backend: "grok_bot_mcp", model: null, externalBindingId: validatePaperclipRunnerGrokConfig(config)! };
}
if (candidate === "openai_dot") {
const bindingId = validatePaperclipRunnerDotConfig(config)!;
return { provider: "openai_dot", backend: "openai_dot_mcp", model: null, dotBindingId: bindingId };
}
if (candidate === "acpx" && agentHarnessType("paperclip_runner", config) === "claude_local") {
const unsupported = claudeRunnerUnsupportedSettings(config);
if (unsupported.length) throw new PaperclipRunnerProviderProfileError("agent_runner_config_incompatible",
`Paperclip Runner cannot honor these settings: ${unsupported.join(", ")}. Remove them or select Legacy runner in Advanced.`);
}
assertPermissionMode(candidate, config);
try {
resolvePaperclipRunnerCursorMode(candidate, config.acpxAgent, config.acpxSessionMode);
} catch (error) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_mode_invalid",
error instanceof Error ? error.message : "Invalid Cursor session mode",
);
}
try { if (candidate === "acpx" && agentHarnessType("paperclip_runner", config) === "claude_local") resolveClaudeEffort("claude", config.modelReasoningEffort ?? config.reasoningEffort ?? config.effort); } catch (error) {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_claude_effort_invalid", error instanceof Error ? error.message : "Invalid Claude effort");
}
try { if (candidate === "acpx" && config.acpxAgent === "grok") resolveGrokEffort("grok", config.modelReasoningEffort ?? config.reasoningEffort ?? config.effort); } catch (error) {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_grok_effort_invalid", error instanceof Error ? error.message : "Invalid Grok effort");
}
try { if (candidate === "acpx" && config.acpxAgent === "grok") resolveGrokDisableWebSearch("grok", config.disableWebSearch); } catch (error) {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_grok_search_invalid", error instanceof Error ? error.message : "Invalid Grok search policy");
}
try { resolvePaperclipRunnerPiThinkingLevel(candidate, config.acpxAgent, config.piThinkingLevel); } catch (error) {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_pi_thinking_invalid", error instanceof Error ? error.message : "Invalid Pi thinking level");
}
const model = optionalString(config.model);
if (candidate === "codex") {
return {
provider: "codex",
backend: "codex_app_server",
model,
};
}
if (candidate === "opencode") {
if (!model || !model.includes("/") || model.endsWith("/")) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_opencode_model_invalid",
"Paperclip Runner OpenCode requires model in provider/model form.",
);
}
return {
provider: "opencode",
backend: "opencode_server",
model,
};
}
if (candidate === "claude_managed") {
const managedProfileId = optionalString(config.managedProfileId);
if (!managedProfileId) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_claude_managed_profile_required",
"Paperclip Runner Claude Managed requires a company managed-agent profile.",
);
}
if (config.managedAgentsRetentionAcknowledged !== true) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_claude_managed_retention_required",
"Paperclip Runner Claude Managed requires acknowledgement of stateful beta retention.",
);
}
if (model !== null && model !== CLAUDE_MANAGED_QUALIFIED_MODEL) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_claude_managed_model_unqualified",
`The Claude Managed profile requires exact model ${CLAUDE_MANAGED_QUALIFIED_MODEL}.`,
);
}
return {
provider: "claude_managed",
backend: "claude_managed_agents_api",
managedProfileId,
model,
maxSessionListCostUsd: positiveNumberOrNull(
config.maxSessionListCostUsd,
"paperclip_runner_claude_managed_spend_cap_invalid",
"Paperclip Runner Claude Managed requires a positive session spend ceiling when overridden.",
),
};
}
if (candidate === "aws_agentcore") {
const agentCoreProfileId = optionalString(config.agentCoreProfileId);
if (!agentCoreProfileId) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_aws_agentcore_profile_required",
"Paperclip Runner AWS AgentCore requires a company remote-agent profile.",
);
}
if (config.agentCoreRetentionAcknowledged !== true) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_aws_agentcore_retention_required",
"Paperclip Runner AWS AgentCore requires acknowledgement of 90-day Memory retention.",
);
}
if (model !== null && model !== AGENTCORE_QUALIFIED_MODEL) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_aws_agentcore_model_unqualified",
`The AWS AgentCore profile requires exact model ${AGENTCORE_QUALIFIED_MODEL}.`,
);
}
return {
provider: "aws_agentcore",
backend: "aws_agentcore_harness_api",
agentCoreProfileId,
model,
maxEstimatedSessionCostUsd: positiveNumberOrNull(
config.maxEstimatedSessionCostUsd,
"paperclip_runner_aws_agentcore_spend_cap_invalid",
"Paperclip Runner AWS AgentCore requires a positive estimated session spend ceiling when overridden.",
),
};
}
const acpxAgent = config.acpxAgent ?? "claude";
if (acpxAgent === "copilot" && (!model || ["auto", "default"].includes(model.toLowerCase()))) {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_acpx_model_required", "GitHub Copilot requires an explicit model ID; there is no default model.");
}
const pendingAcpxProfile = PAPERCLIP_RUNNER_ACPX_PROFILES.find(profile => profile.value === acpxAgent && !profile.qualified);
if (pendingAcpxProfile) {
if (!model) throw new PaperclipRunnerProviderProfileError("paperclip_runner_acpx_model_required", `${pendingAcpxProfile.label} requires an explicit model ID; there is no default model.`);
if (pendingAcpxProfile.value === "antigravity") {
try {
if (resolveAntigravityCandidateAdmission({ companyId: context.companyId, model }, process.env)) {
return { provider: "acpx", backend: "acpx_runtime", model, acpxAgent: "antigravity" };
}
} catch {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_acpx_qualification_invalid", "Antigravity candidate admission requires the authorized company, exact model and runtime identity.");
}
}
// Qualification authority belongs to the isolated server operator. Never
// read this allowlist from adapter config, credential refs, or run env.
let qualification: AcpxQualificationCandidate | undefined;
try {
qualification = resolveAcpxQualification({ kind: "acpx", agent: pendingAcpxProfile.value, model }, process.env);
} catch {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_acpx_qualification_invalid", "ACPX qualification requires an exact host-authorized candidate and model.");
}
if (qualification) {
return { provider: "acpx", backend: "acpx_runtime", model, acpxAgent: qualification };
}
throw new PaperclipRunnerProviderProfileError("paperclip_runner_acpx_agent_unavailable", `${pendingAcpxProfile.label} is awaiting native Runner acceptance. Its profile is not enabled for production runs.`);
}
if ((acpxAgent === "cursor" || acpxAgent === "pi") && !model) {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_acpx_model_required", "This ACPX agent requires an explicit model ID; there is no default model.");
}
if (acpxAgent !== "claude" && acpxAgent !== "codex" && acpxAgent !== "grok" && acpxAgent !== "cursor" && acpxAgent !== "copilot" && acpxAgent !== "pi") {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_acpx_agent_unavailable",
"Paperclip Runner ACPX requires a qualified agent profile.",
);
}
const selectedModel = acpxAgent === "grok" && model === "grok-build"
? DEFAULT_ACPX_RUNNER_MODELS.grok
: model || DEFAULT_ACPX_RUNNER_MODELS[acpxAgent] || "";
try {
resolveQualifiedAcpxProfile(acpxAgent, selectedModel);
} catch (error) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_acpx_model_unqualified",
error instanceof Error ? error.message : "Paperclip Runner ACPX requires an admitted model.",
);
}
return {
provider: "acpx",
backend: "acpx_runtime",
model: selectedModel,
acpxAgent,
};
}
/**
* Project the operator-owned adapter configuration and a qualified stored
* profile into the closed native execution descriptor. Editable adapter
* configuration can select a stored profile, but it cannot replace that
* profile's immutable remote resource identity.
*/
export function resolvePaperclipRunnerNativeProviderInput(input: {
companyId?: string;
backend: PaperclipRunnerProviderProfile["backend"];
adapterConfig: unknown;
/** Verified image CLI version for a fresh remote Codex run only. */
codexCliVersion?: string | null;
museBinding?: import("../../vendor/paperclip-runner/index.js").MuseBindingSnapshot;
dotBinding?: import("../../vendor/paperclip-runner/index.js").DotBindingSnapshot;
managedProfile?: {
id: string;
profileKey: string;
anthropicAgentId: string;
agentVersion: string;
environmentId: string;
betaVersion: string;
defaultModel: string;
defaultMaxListCostCents: number;
} | null;
agentCoreProfile?: {
id: string;
profileKey: string;
configuration: Record<string, unknown>;
} | null;
}): PaperclipRunnerNativeProviderInput {
const config = asRecord(input.adapterConfig);
const profile = resolvePaperclipRunnerProviderProfile(config, { companyId: input.companyId });
if (config.homeMode !== undefined && config.homeMode !== "isolated" && config.homeMode !== "preconfigured") {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_home_mode_invalid", "homeMode must be isolated or preconfigured");
}
const preconfigured = config.homeMode === "preconfigured";
if (preconfigured && profile.provider !== "codex" && !(profile.provider === "acpx" && ["claude", "codex"].includes(profile.acpxAgent))) {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_home_mode_unsupported", "Preconfigured home supports native Codex and ACPX Claude only");
}
if (preconfigured && profile.provider === "acpx" && config.acpxPermissionMode !== undefined && config.acpxPermissionMode !== "approve-all") {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_home_permission_mode_unsupported", "Preconfigured home currently requires ACPX approve-all; local permission rules still apply");
}
if (preconfigured && profile.provider === "acpx" && profile.acpxAgent === "codex") {
throw new PaperclipRunnerProviderProfileError("paperclip_runner_home_state_boundary_unsupported", "preconfigured ACPX Codex cannot protect Runner state; use native Codex or isolated mode");
}
if (profile.backend !== input.backend) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_provider_changed",
"Paperclip Runner provider changed after this run selected its native backend.",
);
}
if (profile.provider === "muse") {
if (!input.museBinding || input.museBinding.bindingId !== profile.museBindingId) throw new PaperclipRunnerProviderProfileError("paperclip_runner_muse_binding_unavailable", "Muse binding must be resolved by the server after normal admission.");
return { provider: "muse", model: null, museBinding: input.museBinding };
}
if (profile.provider === "grok_bot") {
if (!input.dotBinding || input.dotBinding.bindingId !== profile.externalBindingId) throw new PaperclipRunnerProviderProfileError("paperclip_runner_grok_binding_unavailable", "The Grok binding must be resolved by the server after normal admission.");
return { provider: "grok_bot", model: null, dotBinding: input.dotBinding };
}
if (profile.provider === "openai_dot") {
if (!input.dotBinding || input.dotBinding.bindingId !== profile.dotBindingId) throw new PaperclipRunnerProviderProfileError("paperclip_runner_dot_binding_unavailable", "Dot binding must be resolved by the server after normal admission.");
return { provider: "openai_dot", model: null, dotBinding: input.dotBinding };
}
if (profile.provider === "opencode") {
return {
provider: "opencode",
model: profile.model,
opencodePermissionMode: resolvePaperclipRunnerPermissionMode(
"opencode",
config.opencodePermissionMode,
) as "allow" | "ask" | "deny",
};
}
if (profile.provider === "acpx") {
return {
provider: "acpx",
model: profile.model,
acpxAgent: profile.acpxAgent,
...(profile.acpxAgent === "claude" ? { claudeEffort: resolveClaudeEffort("claude", config.modelReasoningEffort ?? config.reasoningEffort ?? config.effort) } : {}),
...(profile.acpxAgent === "grok" ? { grokEffort: resolveGrokEffort("grok", config.modelReasoningEffort ?? config.reasoningEffort ?? config.effort), grokDisableWebSearch: resolveGrokDisableWebSearch("grok", config.disableWebSearch) } : {}),
...(profile.acpxAgent === "pi" ? { piThinkingLevel: resolvePaperclipRunnerPiThinkingLevel("acpx", "pi", config.piThinkingLevel) } : {}),
...(profile.acpxAgent === "cursor" ? {
acpxSessionMode: resolvePaperclipRunnerCursorMode("acpx", "cursor", config.acpxSessionMode),
} : {}),
...(preconfigured ? { homeMode: "preconfigured" as const } : {}),
acpxPermissionMode: resolvePaperclipRunnerPermissionMode(
"acpx",
config.acpxPermissionMode,
) as "approve-all" | "approve-paperclip" | "approve-reads" | "deny-all",
};
}
if (profile.provider === "claude_managed") {
const stored = input.managedProfile;
if (
!stored
|| (
profile.managedProfileId !== stored.id
&& profile.managedProfileId !== stored.profileKey
)
) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_claude_managed_profile_mismatch",
"The qualified Claude Managed profile does not match the adapter selection.",
);
}
if (stored.betaVersion !== CLAUDE_MANAGED_BETA_VERSION) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_claude_managed_beta_unqualified",
"The Claude Managed profile beta version is not qualified.",
);
}
const model = profile.model ?? optionalString(stored.defaultModel);
const maxSessionListCostUsd = profile.maxSessionListCostUsd
?? stored.defaultMaxListCostCents / 100;
if (!model) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_claude_managed_model_invalid",
"The Claude Managed profile requires a model.",
);
}
if (model !== CLAUDE_MANAGED_QUALIFIED_MODEL) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_claude_managed_model_unqualified",
`The Claude Managed profile requires exact model ${CLAUDE_MANAGED_QUALIFIED_MODEL}.`,
);
}
if (!Number.isFinite(maxSessionListCostUsd) || maxSessionListCostUsd <= 0) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_claude_managed_spend_cap_invalid",
"The Claude Managed profile requires a positive session spend ceiling.",
);
}
return {
provider: "claude_managed",
model,
managedProfile: {
profileId: stored.id,
anthropicAgentId: stored.anthropicAgentId,
agentVersion: stored.agentVersion,
environmentId: stored.environmentId,
betaVersion: CLAUDE_MANAGED_BETA_VERSION,
},
maxSessionListCostUsd,
};
}
if (profile.provider === "aws_agentcore") {
const stored = input.agentCoreProfile;
if (
!stored
|| (
profile.agentCoreProfileId !== stored.id
&& profile.agentCoreProfileId !== stored.profileKey
)
) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_aws_agentcore_profile_mismatch",
"The qualified AWS AgentCore profile does not match the adapter selection.",
);
}
const remote = asRecord(stored.configuration);
const required = (key: string): string => {
const value = optionalString(remote[key]);
if (!value) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_aws_agentcore_profile_invalid",
`The qualified AWS AgentCore profile is missing ${key}.`,
);
}
return value;
};
if (remote.eventExpiryDays !== 90) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_aws_agentcore_retention_unqualified",
"The qualified AWS AgentCore profile must retain Memory events for exactly 90 days.",
);
}
const maxEstimatedSessionCostUsd = profile.maxEstimatedSessionCostUsd
?? positiveNumberOrNull(
remote.defaultMaxEstimatedSessionCostUsd,
"paperclip_runner_aws_agentcore_spend_cap_invalid",
"The AWS AgentCore profile requires a positive estimated session spend ceiling.",
);
if (maxEstimatedSessionCostUsd === null) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_aws_agentcore_spend_cap_invalid",
"The AWS AgentCore profile requires a positive estimated session spend ceiling.",
);
}
const model = profile.model ?? required("defaultModel");
if (model !== AGENTCORE_QUALIFIED_MODEL) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_aws_agentcore_model_unqualified",
`The AWS AgentCore profile requires exact model ${AGENTCORE_QUALIFIED_MODEL}.`,
);
}
return {
provider: "aws_agentcore",
model,
agentCoreProfile: {
profileId: stored.id,
region: required("region"),
accountId: required("accountId"),
harnessArn: required("harnessArn"),
harnessVersion: required("harnessVersion"),
endpointArn: required("endpointArn"),
endpointQualifier: required("endpointQualifier"),
agentRuntimeArn: required("agentRuntimeArn"),
memoryArn: required("memoryArn"),
memoryId: required("memoryId"),
invocationRoleArn: required("invocationRoleArn"),
contextBucket: required("contextBucket"),
contextPrefix: required("contextPrefix"),
contextKmsKeyArn: required("contextKmsKeyArn"),
qualificationRevision: required("qualificationRevision"),
eventExpiryDays: 90,
},
maxEstimatedSessionCostUsd,
invocationLimits: {
maxIterations: boundedPositiveInteger(
config.maxIterations,
8,
8,
"paperclip_runner_aws_agentcore_max_iterations_invalid",
"AWS AgentCore maxIterations",
),
maxOutputTokens: boundedPositiveInteger(
config.maxOutputTokens,
4_096,
4_096,
"paperclip_runner_aws_agentcore_max_output_tokens_invalid",
"AWS AgentCore maxOutputTokens",
),
timeoutSeconds: boundedPositiveInteger(
config.timeoutSeconds,
300,
300,
"paperclip_runner_aws_agentcore_timeout_invalid",
"AWS AgentCore timeoutSeconds",
),
},
};
}
const effort = config.modelReasoningEffort ?? config.reasoningEffort ?? config.effort;
const allowedEfforts = ["minimal", "low", "medium", "high", "xhigh", "max", "ultra"];
if (effort !== undefined && effort !== null && effort !== "" && (
typeof effort !== "string" || !allowedEfforts.includes(effort)
|| (profile.model && isCodexLocalKnownModel(profile.model)
&& !codexLocalReasoningEffortsForModel(profile.model).includes(effort as "minimal" | "low" | "medium" | "high" | "xhigh" | "max" | "ultra"))
)) {
throw new PaperclipRunnerProviderProfileError(
"paperclip_runner_codex_effort_invalid",
"Paperclip Runner Codex reasoning effort is not supported for this model.",
);
}
return {
provider: "codex",
model: compatibleCodexModel(profile.model, input.codexCliVersion ?? null),
...(preconfigured ? { homeMode: "preconfigured" as const } : {}),
codexApprovalPolicy: resolvePaperclipRunnerPermissionMode(
"codex",
config.codexPermissionMode,
) as "never" | "on-request" | "untrusted",
...(typeof effort === "string" && effort ? { codexReasoningEffort: effort } : {}),
...(config.search === false ? { codexWebSearch: "disabled" as const } : {}),
};
}