mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
## Thinking Path > - Paperclip manages agents and their work. > - Native Runner must bind each provider turn to the selected task, account, model, and instructions. > - Antigravity needs native ACP admission and physical process ownership across continuation. > - A saved conversation must not reuse an old process owner or silently switch its effective model. > - This pull request adds the native candidate behind exact operator admission. > - The default release gate remains closed while qualification is pending. ## Linked Issues or Issue Description Builds on merged #18187. Product account setup and the candidate producer are in dependent #18189. The repeatable browser journey is in #18190. Refs: #12503, #14938; those proposals use legacy CLI adapters. **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 is required while qualification remains pending. ## What Changed - Add native Antigravity ACP model and instruction admission, selected-account homes, and fenced refresh ownership. - Carry exact candidate authority from the control plane through Runner, Rust, the sidecar, and each current run. Clamp daemon lifetime to the absolute permission expiry, including replacement owners. - Preserve a rotated OAuth refresh token before later identity verification can fail, including failed or cancelled startup admission. Make private lease-close retries safe. - Bound native probe retirement and retry the exact original owner; do not delete credential homes before physical proof. - Preserve Gemini API-key recovery when shared Google subscription capability is added. - Resume the same tagged native conversation with fresh per-turn process ownership and physical native retirement. - Include native manifests in isolated Rust image stages, release cache keys, and dev rebuild detection. The workflow changes only extend existing cache input lists. - Keep current master provider behavior and append Grok 13, Pi 35, and Copilot 84 input identities. Preserve prior identities, including saved Copilot 82 and 83 runs. ## Verification - Runner TypeScript main checks, profile generators, Rust formatting, and offline workspace checks pass. The native backend integration target passes 28 tests. - The complete transport and cleanup group passes 387 tests without unhandled rejections. These checks use local scripted children and actual process and port ownership. - Saved-profile decoding, native-execution parsing, installation, and selected-credential controls pass 208 focused tests. All 109 pre-existing profile identity files remain byte-for-byte unchanged. Copilot, Grok, and shared TypeScript/Rust generation checks pass. - The final recovery fixture passes all seven cases. After the master rebase, the combined recovery and owned-Node-runtime group passes all 10 cases. Each expiry case sets one 45-second absolute grant before startup. It includes the deliberate six-second cold delay, four existing eight-second command budgets, and active-turn headroom. The test proves an active turn before expiry and then checks the daemon, owned process group, child processes, and all three native port fences. The 60-second test deadline does not renew provider authority. - The consolidated model-discovery and pending-release group passes 22 tests. Google credential compatibility and auth recovery pass 25 tests, including existing Gemini API-key recovery. The full product settings test group remains intact in #18189. - Earlier failures remain in their original CI runs and local records. They exposed the Copilot 82 decoder gap, an expired five-second fixture bootstrap ticket, and a fixture grant consumed before activation on loaded CI. Each repair has a separate result. Production ticket limits and admission behavior are unchanged by the final test commits. - An unrelated decision-expiry check failed in [the earlier CI run](https://github.com/paperclipai/paperclip/actions/runs/40941986698/job/123688668997). Its owning code is unchanged from the base. The unchanged suite passes 20 tests on isolated PostgreSQL. A controlled database rollback reproduces the earlier wake: clean startup schedules the 120-second decision expiry, while rollback reconciliation leaves a 30-second continuation-coalescing wake. The decision stays open, with no wake delivery or error. The fresh final-head server shard passes; the failed result is preserved. - Final native commit `ec6bc7e4d77f5ae0cc4946dbba6c662e95a87f5b` passed `ci / verify` and `ci / e2e`. Fresh Greptile and Paperclip reviews both score 5/5, with no open findings. Antigravity retains the recorded native `.5` closure and exact low-model profile. This PR does not claim global or cross-platform qualification. ## Risks - The new transport carries private managed credentials. Exact ownership, account generation, model, instructions, and native retirement are fail-closed. - Only local macOS ARM64 has a verified complete Antigravity runtime. Default Antigravity task execution remains disabled without current trusted operator admission. The Google server sign-in lane and production candidate producer ship in #18189; this native slice does not expose the subscription catalog choice. - Shared launch inputs change provider identities. New identities are append-only; existing recorded identities remain available. ## 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>
162 lines
5.5 KiB
TypeScript
162 lines
5.5 KiB
TypeScript
import { z } from "zod";
|
|
|
|
/** Value-free routing stored on the connection, never supplied by an agent binding. */
|
|
export const aiProviderRoutingSchema = z
|
|
.object({
|
|
kind: z.enum(["openrouter", "bedrock", "gateway", "local"]),
|
|
protocol: z.enum(["responses", "messages", "chat", "bedrock"]),
|
|
baseUrl: z.string().trim().max(2048).optional(),
|
|
auth: z
|
|
.enum(["bearer", "api_key", "none"])
|
|
.default("bearer"),
|
|
region: z
|
|
.string()
|
|
.regex(/^[a-z]{2}(?:-[a-z]+)+-\d$/)
|
|
.optional(),
|
|
models: z
|
|
.array(
|
|
z
|
|
.object({
|
|
id: z.string().trim().min(1).max(256),
|
|
label: z.string().trim().max(160).optional(),
|
|
})
|
|
.strict(),
|
|
)
|
|
.max(200)
|
|
.default([]),
|
|
})
|
|
.strict()
|
|
.superRefine((route, ctx) => {
|
|
const invalid = (message: string) =>
|
|
ctx.addIssue({ code: "custom", message });
|
|
if (route.kind === "bedrock") {
|
|
if (
|
|
route.protocol !== "bedrock" ||
|
|
!route.region ||
|
|
route.auth !== "bearer" ||
|
|
route.baseUrl
|
|
)
|
|
invalid(
|
|
"Bedrock requires a region and a Bedrock API key.",
|
|
);
|
|
} else if (
|
|
route.protocol === "bedrock" ||
|
|
route.region
|
|
)
|
|
invalid("AWS settings require Bedrock.");
|
|
if (
|
|
route.kind === "openrouter" &&
|
|
(route.baseUrl || route.auth !== "bearer")
|
|
)
|
|
invalid("OpenRouter uses its official endpoint and an API key.");
|
|
if (route.auth === "api_key" && route.protocol !== "messages")
|
|
invalid("API-key header authentication requires Anthropic Messages.");
|
|
if (route.kind === "gateway" || route.kind === "local") {
|
|
try {
|
|
const url = new URL(route.baseUrl ?? "");
|
|
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(
|
|
url.hostname,
|
|
);
|
|
if (
|
|
url.username ||
|
|
url.password ||
|
|
url.search ||
|
|
url.hash ||
|
|
(url.protocol !== "https:" && !(url.protocol === "http:" && loopback))
|
|
)
|
|
throw new Error();
|
|
if (route.kind === "local" && !loopback)
|
|
invalid("A local endpoint must use localhost or a loopback address.");
|
|
} catch {
|
|
invalid(
|
|
"Enter an HTTPS base URL without credentials, query parameters, or a fragment. Local endpoints may use HTTP.",
|
|
);
|
|
}
|
|
}
|
|
});
|
|
export type AiProviderRouting = z.infer<typeof aiProviderRoutingSchema>;
|
|
export function aiRoutingHarness(
|
|
adapter: string,
|
|
provider?: unknown,
|
|
acpxAgent?: unknown,
|
|
): string {
|
|
if (adapter !== "paperclip_runner") return adapter;
|
|
if (provider === "acpx" && acpxAgent === "copilot") return "copilot_runtime";
|
|
if (provider === "acpx" && acpxAgent === "antigravity") return "antigravity_runtime";
|
|
const runner =
|
|
provider === "acpx" && ["claude", "grok", "pi"].includes(String(acpxAgent))
|
|
? acpxAgent
|
|
: provider;
|
|
return (
|
|
(
|
|
{
|
|
claude: "claude_local",
|
|
codex: "codex_local",
|
|
opencode: "opencode_local",
|
|
grok: "grok_local",
|
|
pi: "pi_local",
|
|
} as Record<string, string>
|
|
)[String(runner)] ?? "unsupported"
|
|
);
|
|
}
|
|
export function isAiRoutingCompatible(
|
|
route: AiProviderRouting,
|
|
harness: string,
|
|
): boolean {
|
|
if (route.kind === "bedrock") return harness === "claude_local";
|
|
if (route.kind === "openrouter")
|
|
return [
|
|
"claude_local",
|
|
"codex_local",
|
|
"opencode_local",
|
|
"pi_local",
|
|
"hermes_local",
|
|
].includes(harness);
|
|
if (route.protocol === "responses") return harness === "codex_local";
|
|
if (route.protocol === "messages") return harness === "claude_local";
|
|
return ["opencode_local", "hermes_local"].includes(harness);
|
|
}
|
|
export function aiRoutingBaseUrl(
|
|
route: AiProviderRouting,
|
|
harness: string,
|
|
): string {
|
|
return route.kind === "openrouter"
|
|
? `https://openrouter.ai/api${harness === "claude_local" ? "" : "/v1"}`
|
|
: (route.baseUrl ?? "").replace(/\/+$/, "");
|
|
}
|
|
|
|
export function aiRoutingModel(
|
|
route: AiProviderRouting,
|
|
harness: string,
|
|
model: string,
|
|
): string {
|
|
if (!model || (harness !== "opencode_local" && !(harness === "pi_local" && route.kind === "openrouter"))) return model;
|
|
const prefix = route.kind === "openrouter" ? "openrouter" : "paperclip";
|
|
return model.startsWith(`${prefix}/`) ? model : `${prefix}/${model}`;
|
|
}
|
|
|
|
/** Catalog identity is derived from routing, including accounts saved before these rows existed. */
|
|
export function aiConnectionCatalogSlug(provider: string, routing?: Pick<AiProviderRouting, "kind" | "protocol">): string {
|
|
if (routing?.kind === "gateway") {
|
|
return routing.protocol === "messages" ? "messages-api"
|
|
: routing.protocol === "chat" ? "chat-completions-api" : "responses-api";
|
|
}
|
|
return routing?.kind ?? provider;
|
|
}
|
|
|
|
/** Direct catalog setup presets; legacy gateway links still open their existing setup. */
|
|
export function aiProviderSetupPreset(source: string | null): {
|
|
provider: "openrouter" | "bedrock" | "gateway" | "local" | "google";
|
|
protocol?: AiProviderRouting["protocol"];
|
|
label?: string;
|
|
} | undefined {
|
|
switch (source) {
|
|
case "responses-api": return { provider: "gateway", protocol: "responses", label: "Responses API" };
|
|
case "messages-api": return { provider: "gateway", protocol: "messages", label: "Messages API" };
|
|
case "chat-completions-api": return { provider: "gateway", protocol: "chat", label: "Chat Completions API" };
|
|
case "google-ai": return { provider: "google" };
|
|
case "google": case "openrouter": case "bedrock": case "gateway": case "local": return { provider: source };
|
|
default: return undefined;
|
|
}
|
|
}
|