mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 14:16:39 +01:00
## Thinking Path > - Paperclip manages agents and their work. > - Native Runner providers need verified executables before they can run. > - Google Antigravity ships a native ACP server and a helper executable. > - An archive hash alone does not verify the installed runtime or its child process cleanup. > - This pull request adds a pinned installer and a small, verified compatibility patch. > - Later pull requests add Runner execution, account setup, and the recorded user journey eval. ## Linked Issues or Issue Description Refs: #12503, #14938. Those pull requests use the legacy CLI adapter. This stack adds native Runner ACP support. **Agent or provider** Google Antigravity ACP 1.4.3. **Why this adapter is useful** Users need a reproducible runtime before they can select Antigravity for a Paperclip agent. **How the agent is invoked** The installer prepares the native ACP server and helper. This pull request does not enable task execution. Default execution remains closed pending qualification. ## What Changed - Pin the complete macOS ARM64 runtime closure. Keep other platforms as inventory only. - Verify archive entries, installed bytes, ownership, and cache layout. - Patch exact upstream code anchors for model admission, instruction loading, native process ownership, and per-call file tool completion. - Package a self-contained installer and expose `paperclipai runtime setup antigravity`. - Record candidate limitations and historical acceptance evidence. ## Verification - Runtime provisioning, packaging, metadata, and patch support tests: 62 passed. The separate exact pinned native patch run passed all 5 tests with no skips and no Google authentication. - CLI runtime setup tests: 5 passed. - Map contract check passed. Staged diff whitespace check passed. - The first test command ran before dependency installation and failed on missing esbuild. The same command passed after installation. - Current master has a stale lockfile. Dependencies were resolved locally with scripts disabled. The generated lockfile is excluded from this pull request. - Final head: required `ci / verify` and `ci / e2e` passed, and Greptile is 5/5 with both review threads resolved. The optional external Paperclip Review remains queued. Two original timing failures are retained; one failed-job rerun passed without source or deadline changes. ## Risks - The patch depends on exact upstream native code. A new upstream release must get a new verified closure. - Only macOS ARM64 has a complete verified runtime. Installation does not qualify authentication, task execution, or other platforms. - The installer downloads official Google assets when the verified cache is absent. ## 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, test execution, 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>
59 lines
4.6 KiB
TypeScript
59 lines
4.6 KiB
TypeScript
import { mkdtemp, mkdir, realpath, rm, symlink, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { pathToFileURL } from "node:url";
|
|
import { Command } from "commander";
|
|
import { afterEach, expect, it } from "vitest";
|
|
import { buildPiSetupEnvironment, registerRuntimeCommands, resolvePiProvisioner, resolveRemoteCompanionImporter } from "../commands/runtime.js";
|
|
const roots: string[] = [];
|
|
afterEach(async () => { await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))); });
|
|
async function fixture() {
|
|
const root = await realpath(await mkdtemp(join(tmpdir(), "paperclip-cli-runtime-"))); roots.push(root);
|
|
const cli = join(root, "dist/vendor/paperclip-runner/cli"); await mkdir(cli, { recursive: true });
|
|
await writeFile(join(root, "package.json"), '{"name":"@paperclipai/server"}');
|
|
await writeFile(join(root, "dist/index.js"), "throw new Error('server must not start during setup resolution')");
|
|
await writeFile(join(cli, "provision-pi.cjs"), "fixture");
|
|
return { root, cli, url: pathToFileURL(join(root, "dist/index.js")).href };
|
|
}
|
|
it("locates the public server's setup entrypoint without importing its API server", async () => {
|
|
const f = await fixture(); expect(await resolvePiProvisioner(f.url)).toBe(join(f.cli, "provision-pi.cjs"));
|
|
});
|
|
it("rejects foreign package identity and a setup entrypoint outside that package", async () => {
|
|
const f = await fixture(); const other = await fixture();
|
|
await writeFile(join(f.root, "package.json"), '{"name":"foreign"}');
|
|
await expect(resolvePiProvisioner(f.url)).rejects.toThrow("identity");
|
|
await writeFile(join(f.root, "package.json"), '{"name":"@paperclipai/server"}');
|
|
await rm(join(f.cli, "provision-pi.cjs")); await symlink(join(other.cli, "provision-pi.cjs"), join(f.cli, "provision-pi.cjs"));
|
|
await expect(resolvePiProvisioner(f.url)).rejects.toThrow("escapes");
|
|
});
|
|
it("rejects unsupported runtime setup providers before resolution", async () => {
|
|
const program = new Command(); registerRuntimeCommands(program);
|
|
await expect(program.parseAsync(["node", "paperclipai", "runtime", "setup", "untrusted"])).rejects.toThrow("Supported runtime setup: paperclipai runtime setup pi, cursor, or antigravity");
|
|
});
|
|
|
|
it("resolves the companion importer only inside the actual public server tar layout", async () => {
|
|
const f = await fixture(); const path = join(f.root, "dist/services/native-runtime/remote-pi-companion.js");
|
|
await mkdir(join(f.root, "dist/services/native-runtime"), { recursive: true });
|
|
await writeFile(path, "throw new Error('resolution must not execute importer')");
|
|
expect(await resolveRemoteCompanionImporter(f.url)).toBe(path);
|
|
const other = await fixture(); await rm(path); await symlink(join(other.cli, "provision-pi.cjs"), path);
|
|
await expect(resolveRemoteCompanionImporter(f.url)).rejects.toThrow("escapes");
|
|
});
|
|
it("requires an explicit digest for the companion import command", async () => {
|
|
const program = new Command(); program.exitOverride().configureOutput({ writeErr() {} }); registerRuntimeCommands(program);
|
|
await expect(program.parseAsync(["node", "paperclipai", "runtime", "import-remote", "/unused"])).rejects.toThrow("sha256");
|
|
});
|
|
|
|
it("passes explicit setup network settings without provider keys or ambient Node/npm configuration", () => {
|
|
const network = { PATH: "/public/bin", LC_ALL: "C.UTF-8", HTTPS_PROXY: "http://proxy.example:8080", HTTP_PROXY: "http://proxy.example:8080", NO_PROXY: "localhost", SSL_CERT_FILE: "/public/ca.pem", SSL_CERT_DIR: "/public/certs", NODE_EXTRA_CA_CERTS: "/public/extra-ca.pem" };
|
|
const environment = buildPiSetupEnvironment({ ...network, LANG: "foreign", HOME: "/private/home", OPENROUTER_API_KEY: "must-not-forward", ANTHROPIC_API_KEY: "must-not-forward", NPM_TOKEN: "must-not-forward", npm_config_registry: "https://foreign.example", NODE_OPTIONS: "--require /foreign.js", NODE_PATH: "/foreign/modules", NODE_TLS_REJECT_UNAUTHORIZED: "0" });
|
|
expect(environment).toEqual({ ...network, LANG: "C.UTF-8" });
|
|
expect(buildPiSetupEnvironment({})).toEqual({ PATH: "/usr/bin:/bin", LANG: "C.UTF-8" });
|
|
});
|
|
|
|
it("preserves lowercase proxy settings and leaves precedence to Node/npm", () => {
|
|
const lower = { http_proxy: "http://lower-proxy.example:8080", https_proxy: "http://lower-proxy.example:8080", no_proxy: "localhost" };
|
|
expect(buildPiSetupEnvironment(lower)).toEqual({ PATH: "/usr/bin:/bin", LANG: "C.UTF-8", ...lower });
|
|
expect(buildPiSetupEnvironment({ ...lower, HTTPS_PROXY: "http://upper-proxy.example:8080", OPENROUTER_API_KEY: "must-not-forward" })).toEqual({ PATH: "/usr/bin:/bin", LANG: "C.UTF-8", ...lower, HTTPS_PROXY: "http://upper-proxy.example:8080" });
|
|
});
|