mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-01 06:42:06 +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>
211 lines
9.4 KiB
TypeScript
211 lines
9.4 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { paperclipRunnerSupportsPlatform } from "@paperclipai/shared";
|
|
|
|
import {
|
|
paperclipRunnerBinaryNeedsBuild,
|
|
resolveNativeRunnerDevBinary,
|
|
resolveNativeRunnerRequirement,
|
|
} from "../../../scripts/dev-runner-native-binary.mjs";
|
|
|
|
const tempRoots = new Set<string>();
|
|
const initialPlatform = Object.getOwnPropertyDescriptor(process, "platform")!;
|
|
const initialArchitecture = Object.getOwnPropertyDescriptor(process, "arch")!;
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
Object.defineProperty(process, "platform", initialPlatform);
|
|
Object.defineProperty(process, "arch", initialArchitecture);
|
|
for (const root of tempRoots) {
|
|
fs.rmSync(root, { recursive: true, force: true });
|
|
}
|
|
tempRoots.clear();
|
|
});
|
|
|
|
function createRunnerCheckout(): { root: string; source: string; binary: string } {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-dev-runner-binary-"));
|
|
tempRoots.add(root);
|
|
|
|
const runnerRoot = path.join(root, "packages", "paperclip-runner", "runner");
|
|
const source = path.join(runnerRoot, "crates", "runner-core", "src", "main.rs");
|
|
const binary = path.join(
|
|
root,
|
|
"packages",
|
|
"paperclip-runner",
|
|
"dist",
|
|
"bin",
|
|
process.platform === "win32" ? "paperclip-runnerd.exe" : "paperclip-runnerd",
|
|
);
|
|
fs.mkdirSync(path.dirname(source), { recursive: true });
|
|
fs.mkdirSync(path.dirname(binary), { recursive: true });
|
|
fs.writeFileSync(path.join(runnerRoot, "Cargo.toml"), "[workspace]\n", "utf8");
|
|
fs.writeFileSync(path.join(runnerRoot, "Cargo.lock"), "", "utf8");
|
|
fs.writeFileSync(source, "fn main() {}\n", "utf8");
|
|
fs.writeFileSync(binary, nativeHeader(process.platform, process.arch));
|
|
return { root, source, binary };
|
|
}
|
|
|
|
function nativeHeader(platform: string, architecture: string): Buffer {
|
|
const bytes = Buffer.alloc(128);
|
|
if (platform === "darwin") {
|
|
bytes.writeUInt32LE(0xfeedfacf);
|
|
bytes.writeUInt32LE(architecture === "x64" ? 0x01000007 : 0x0100000c, 4);
|
|
} else if (platform === "win32") {
|
|
bytes.set([0x4d, 0x5a]); bytes.writeUInt32LE(64, 60);
|
|
bytes.writeUInt32LE(0x00004550, 64); bytes.writeUInt16LE(architecture === "x64" ? 0x8664 : 0xaa64, 68);
|
|
} else {
|
|
bytes.set([0x7f, 0x45, 0x4c, 0x46, 2, 1]); bytes.writeUInt16LE(architecture === "x64" ? 62 : 183, 18);
|
|
}
|
|
return bytes;
|
|
}
|
|
|
|
describe("paperclip runner native dev prerequisite", () => {
|
|
it.each(["x64", "arm64"].flatMap(architecture => [[undefined, false], ["2.38", false], ["2.39", true], ["3.0", true]].map(([glibcVersionRuntime, native]) => ({ architecture, glibcVersionRuntime, native }))))("uses the measured libc prerequisite on Linux $architecture/$glibcVersionRuntime when no native run or flag requires it", ({ architecture, glibcVersionRuntime, native }) => {
|
|
Object.defineProperty(process, "platform", { ...initialPlatform, value: "linux" });
|
|
Object.defineProperty(process, "arch", { ...initialArchitecture, value: architecture });
|
|
const nativeRunnerRequired = paperclipRunnerSupportsPlatform("codex_local", "linux", architecture, glibcVersionRuntime ? "glibc" : "unknown", glibcVersionRuntime as string | undefined);
|
|
expect(nativeRunnerRequired).toBe(native);
|
|
expect(paperclipRunnerBinaryNeedsBuild({ repoRoot: "/checkout/without/a/staged/binary", nativeRunnerRequired })).toBe(native);
|
|
});
|
|
it("uses an explicit status response and fails safe when status is unknown", () => {
|
|
expect(
|
|
resolveNativeRunnerRequirement({
|
|
exitCode: 0,
|
|
stdout: "pnpm warning\n{\"nativeRunnerRequired\":false}\n",
|
|
}),
|
|
).toEqual({ nativeRunnerRequired: false, valid: true });
|
|
expect(
|
|
resolveNativeRunnerRequirement({
|
|
exitCode: 0,
|
|
stdout: "{\"nativeRunnerRequired\":true}\n",
|
|
}),
|
|
).toEqual({ nativeRunnerRequired: true, valid: true });
|
|
expect(
|
|
resolveNativeRunnerRequirement({
|
|
exitCode: 1,
|
|
stdout: "",
|
|
}),
|
|
).toEqual({ nativeRunnerRequired: true, valid: false });
|
|
expect(
|
|
resolveNativeRunnerRequirement({
|
|
exitCode: 0,
|
|
stdout: "{\"unexpected\":true}\n",
|
|
}),
|
|
).toEqual({ nativeRunnerRequired: true, valid: false });
|
|
});
|
|
|
|
it("builds only when the staged binary is missing or older than Rust inputs", () => {
|
|
const checkout = createRunnerCheckout();
|
|
const now = Date.now();
|
|
const old = new Date(now - 2_000);
|
|
const current = new Date(now + 2_000);
|
|
const next = new Date(now + 4_000);
|
|
|
|
fs.utimesSync(checkout.source, old, old);
|
|
fs.utimesSync(checkout.binary, current, current);
|
|
expect(
|
|
paperclipRunnerBinaryNeedsBuild({
|
|
repoRoot: checkout.root,
|
|
nativeRunnerRequired: true,
|
|
}),
|
|
).toBe(false);
|
|
|
|
fs.utimesSync(checkout.source, next, next);
|
|
expect(
|
|
paperclipRunnerBinaryNeedsBuild({
|
|
repoRoot: checkout.root,
|
|
nativeRunnerRequired: true,
|
|
}),
|
|
).toBe(true);
|
|
|
|
fs.rmSync(checkout.binary);
|
|
expect(
|
|
paperclipRunnerBinaryNeedsBuild({
|
|
repoRoot: checkout.root,
|
|
nativeRunnerRequired: true,
|
|
}),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("keeps default-off legacy development Node-only", () => {
|
|
expect(
|
|
paperclipRunnerBinaryNeedsBuild({
|
|
repoRoot: "/checkout/without/a/staged/binary",
|
|
nativeRunnerRequired: false,
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
"antigravity-distributions.json", "rust-toolchain.toml", "protocol/provider-schemas/acpx-sidecar.schema.json",
|
|
])("rebuilds an existing native binary after changing embedded input %s", input => {
|
|
const checkout = createRunnerCheckout();
|
|
const dependency = path.join(checkout.root, "packages/paperclip-runner", input);
|
|
fs.mkdirSync(path.dirname(dependency), { recursive: true });
|
|
fs.writeFileSync(dependency, "fixture compile input\n");
|
|
const now = Date.now(), old = new Date(now - 2_000), built = new Date(now + 2_000), changed = new Date(now + 4_000);
|
|
fs.utimesSync(dependency, old, old);
|
|
fs.utimesSync(checkout.binary, built, built);
|
|
const options = { repoRoot: checkout.root, nativeRunnerRequired: true };
|
|
expect(paperclipRunnerBinaryNeedsBuild(options)).toBe(false);
|
|
fs.utimesSync(dependency, changed, changed);
|
|
expect(paperclipRunnerBinaryNeedsBuild(options)).toBe(true);
|
|
expect(paperclipRunnerBinaryNeedsBuild({ ...options, nativeRunnerRequired: false })).toBe(false);
|
|
});
|
|
|
|
it.each(["arm64", "x64"])("uses the staged Mac %s target before the flat build output", architecture => {
|
|
const checkout = createRunnerCheckout();
|
|
const target = path.join(path.dirname(checkout.binary), `darwin-${architecture}`, "paperclip-runnerd");
|
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
fs.writeFileSync(target, nativeHeader("darwin", architecture));
|
|
const old = new Date(Date.now() - 2000), current = new Date(Date.now() + 2000);
|
|
fs.utimesSync(checkout.binary, old, old);
|
|
fs.utimesSync(target, current, current);
|
|
const input = { repoRoot: checkout.root, nativeRunnerRequired: true, platform: "darwin", architecture };
|
|
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(false);
|
|
fs.writeFileSync(checkout.binary, nativeHeader("linux", "x64"));
|
|
const wrongHostNewer = new Date(Date.now() + 4000);
|
|
fs.utimesSync(checkout.binary, wrongHostNewer, wrongHostNewer);
|
|
expect(resolveNativeRunnerDevBinary(input)).toEqual({ binary: target, sourceBuild: false });
|
|
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(false);
|
|
fs.rmSync(checkout.binary);
|
|
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(false);
|
|
fs.utimesSync(target, old, old);
|
|
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(true);
|
|
fs.rmSync(target);
|
|
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(true);
|
|
});
|
|
|
|
it.each(["arm64", "x64"])("uses only a newer matching Mac %s source build without changing release selection", architecture => {
|
|
const checkout = createRunnerCheckout();
|
|
const target = path.join(path.dirname(checkout.binary), `darwin-${architecture}`, "paperclip-runnerd");
|
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
fs.writeFileSync(target, nativeHeader("darwin", architecture));
|
|
fs.writeFileSync(checkout.binary, nativeHeader("darwin", architecture));
|
|
const old = new Date(Date.now() - 2000), fresh = new Date(Date.now() + 2000), changed = new Date(Date.now() + 4000);
|
|
fs.utimesSync(target, old, old); fs.utimesSync(checkout.binary, fresh, fresh);
|
|
const input = { repoRoot: checkout.root, nativeRunnerRequired: true, platform: "darwin", architecture };
|
|
expect(resolveNativeRunnerDevBinary(input)).toEqual({ binary: checkout.binary, sourceBuild: true });
|
|
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(false);
|
|
fs.utimesSync(checkout.source, changed, changed);
|
|
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(true);
|
|
fs.writeFileSync(checkout.binary, nativeHeader("darwin", architecture === "x64" ? "arm64" : "x64"));
|
|
expect(resolveNativeRunnerDevBinary(input)).toEqual({ binary: target, sourceBuild: false });
|
|
fs.rmSync(target);
|
|
expect(resolveNativeRunnerDevBinary(input)).toBeNull();
|
|
expect(paperclipRunnerBinaryNeedsBuild(input)).toBe(true);
|
|
});
|
|
|
|
it("does not build a workspace binary when an explicit binary is configured", () => {
|
|
expect(
|
|
paperclipRunnerBinaryNeedsBuild({
|
|
repoRoot: "/checkout/without/a/staged/binary",
|
|
nativeRunnerRequired: true,
|
|
configuredBinary: "/opt/paperclip/paperclip-runnerd",
|
|
}),
|
|
).toBe(false);
|
|
});
|
|
});
|