Files
PaperClipAI/packages/paperclip-runner/scripts/provision-pi-package.test.mjs
T
DottaandPaperclip 54cc522e86 feat(runner): add gated native Antigravity ACP execution (#18188)
## 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>
2026-10-29 16:36:12 -05:00

82 lines
8.5 KiB
JavaScript

import assert from "node:assert/strict";
import { execFileSync, spawnSync } from "node:child_process";
import { mkdtemp, mkdir, readFile, readdir, realpath, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { createRequire } from "node:module";
import test from "node:test";
import { build } from "esbuild";
import { bundlePiProvisioner } from "./build-verified-provider-entrypoints.mjs";
test("public server tar layout carries a self-contained host provisioner and exact small inputs", async t => {
const root = await realpath(await mkdtemp(join(tmpdir(), "paperclip-pi-public-layout-")));
t.after(() => rm(root, { recursive: true, force: true }));
const pkg = join(root, "package"); const cli = join(pkg, "dist/vendor/paperclip-runner/cli");
await mkdir(cli, { recursive: true });
await writeFile(join(pkg, "package.json"), '{"name":"@paperclipai/server","type":"module"}');
await writeFile(join(pkg, "dist/index.js"), 'throw new Error("do not start the server");');
await writeFile(join(cli, "acpx-runtime-sidecar.cjs"), "// layout fixture only");
await bundlePiProvisioner({ outputRoot: cli });
const inputs = join(cli, "pi-provision-inputs");
assert.deepEqual((await readdir(inputs)).sort(), ["brace-expansion.patch", "package-lock.json", "package.json", "pi-acp-runtime.ts", "pi-acp.patch", "pi-runtime-extension.ts"]);
const packageRoot = resolve(dirname(new URL(import.meta.url).pathname), "..");
for (const [source, destination] of [
["scripts/pi-distribution/package.json", "package.json"], ["scripts/pi-distribution/package-lock.json", "package-lock.json"],
["../../patches/pi-acp@0.0.33.patch", "pi-acp.patch"], ["src/drivers/acpx/pi-acp-runtime.ts", "pi-acp-runtime.ts"],
["src/drivers/acpx/pi-runtime-extension.ts", "pi-runtime-extension.ts"],
]) assert.deepEqual(await readFile(resolve(packageRoot, source)), await readFile(join(inputs, destination)));
// Exercise npm's actual package/ prefix after archive extraction, without
// pretending this small fixture is a complete published Paperclip release.
const archive = join(root, "server.tgz");
execFileSync("tar", ["-czf", archive, "-C", root, "package"], { env: { PATH: "/usr/bin:/bin" }, timeout: 10_000 });
const installed = join(root, "installed"); await mkdir(installed);
execFileSync("tar", ["-xzf", archive, "-C", installed], { env: { PATH: "/usr/bin:/bin" }, timeout: 10_000 });
const installedPackage = join(installed, "package"); const entry = join(installedPackage, "dist/vendor/paperclip-runner/cli/provision-pi.cjs");
const api = createRequire(import.meta.url)(entry);
const layout = await api.provisionPackageRoot(entry);
assert.equal(layout.root, installedPackage);
const installedRunner = join(installedPackage, "dist/vendor/paperclip-runner");
assert.equal(layout.assetRoot, installedRunner);
const network = { PATH: "/usr/bin:/bin", HTTPS_PROXY: "http://proxy.example:8080", HTTP_PROXY: "http://proxy.example:8080", NO_PROXY: "localhost", http_proxy: "http://127.0.0.1:9", https_proxy: "http://127.0.0.1:9", no_proxy: "localhost", SSL_CERT_FILE: "/public/ca.pem", SSL_CERT_DIR: "/public/certs", NODE_EXTRA_CA_CERTS: "/public/extra-ca.pem" };
assert.deepEqual(api.provisionEnvironment({ ...network, HOME: "/private/home", OPENROUTER_API_KEY: "sensitive-canary", NPM_TOKEN: "sensitive-canary", NODE_OPTIONS: "--require /foreign.js", NODE_PATH: "/foreign", NODE_TLS_REJECT_UNAUTHORIZED: "0" }), { ...network, LANG: "C.UTF-8" });
// Real, unmocked installation verifier rejects a corrupt cache before any
// download/process. The positive full-closure proof uses real platform packs.
await mkdir(join(installedRunner, "provider-assets/pi", `${process.platform}-${process.arch}`, "runtime"), { recursive: true });
const deny = join(root, "deny.cjs");
await writeFile(deny, `process.nextTick(()=>{const assert=require('node:assert/strict');assert.equal(process.env.HTTP_PROXY,'http://127.0.0.1:9');assert.equal(process.env.HTTPS_PROXY,'http://127.0.0.1:9');assert.equal(process.env.NO_PROXY,'localhost');assert.equal(process.env.http_proxy,'http://127.0.0.1:9');assert.equal(process.env.https_proxy,'http://127.0.0.1:9');assert.equal(process.env.no_proxy,'localhost');assert.equal(process.env.NODE_EXTRA_CA_CERTS,'/dev/null');for(const key of ['OPENROUTER_API_KEY','NPM_TOKEN','HOME','NODE_OPTIONS','NODE_PATH','NODE_TLS_REJECT_UNAUTHORIZED'])assert.equal(process.env[key],undefined,key);});const fail=()=>{throw Error('UNEXPECTED_NETWORK_OR_CHILD')}; globalThis.fetch=fail; for(const m of ['node:net','node:tls','node:http','node:https']){const x=require(m);for(const k of ['connect','createConnection','request','get'])if(k in x)x[k]=fail;}const c=require('node:child_process');for(const k of ['spawn','execFile','exec'])c[k]=fail;`);
const result = spawnSync(process.execPath, ["--require", deny, entry], { encoding: "utf8", env: { PATH: "/usr/bin:/bin", HTTP_PROXY: "http://127.0.0.1:9", HTTPS_PROXY: "http://127.0.0.1:9", NO_PROXY: "localhost", http_proxy: "http://127.0.0.1:9", https_proxy: "http://127.0.0.1:9", no_proxy: "localhost", NODE_EXTRA_CA_CERTS: "/dev/null", OPENROUTER_API_KEY: "sensitive-canary", NPM_TOKEN: "sensitive-canary", HOME: "/private/home", NODE_OPTIONS: "", NODE_PATH: "/foreign", NODE_TLS_REJECT_UNAUTHORIZED: "0" }, timeout: 10_000 });
assert.ifError(result.error); assert.equal(result.status, 1); assert.match(result.stderr, /Pi setup failed/);
assert.doesNotMatch(result.stderr, /UNEXPECTED_NETWORK_OR_CHILD|sensitive-canary/);
assert.deepEqual(await readdir(join(installedRunner, "provider-assets/pi")), [`${process.platform}-${process.arch}`]);
});
for (const provider of ["pi", "antigravity"]) test(`${provider} CLI setup passes only network settings to its real child and enables Node proxy handling`, async t => {
const root = await mkdtemp(join(tmpdir(), "paperclip-pi-setup-environment-"));
t.after(() => rm(root, { recursive: true, force: true }));
const server = join(root, "node_modules/@paperclipai/server");
const cli = join(server, "dist/vendor/paperclip-runner/cli");
await mkdir(cli, { recursive: true });
await writeFile(join(server, "package.json"), JSON.stringify({ name: "@paperclipai/server", type: "module", exports: "./dist/index.js" }));
await writeFile(join(server, "dist/index.js"), "throw Error('server must not start')");
// A capture child models the public layout only. It cannot download or launch Pi.
await writeFile(join(cli, `provision-${provider}.cjs`), `const assert=require('node:assert/strict');
assert.deepEqual(process.execArgv,['--use-env-proxy']);
assert.equal(process.env.HTTP_PROXY,'http://127.0.0.1:9');
assert.equal(process.env.HTTPS_PROXY,'http://127.0.0.1:9');
assert.equal(process.env.NO_PROXY,'localhost');assert.equal(process.env.http_proxy,'http://127.0.0.1:9');assert.equal(process.env.https_proxy,'http://127.0.0.1:9');assert.equal(process.env.no_proxy,'localhost');
assert.equal(process.env.SSL_CERT_FILE,'/public/ca.pem');
assert.equal(process.env.SSL_CERT_DIR,'/public/certs');
assert.equal(process.env.NODE_EXTRA_CA_CERTS,'/dev/null');
for(const key of ['OPENROUTER_API_KEY','NPM_TOKEN','HOME','NODE_OPTIONS','NODE_PATH','NODE_TLS_REJECT_UNAUTHORIZED']) assert.equal(process.env[key],undefined,key);
console.log('SETUP_NETWORK_BOUNDARY_PASS');`);
const modulePath = join(root, "runtime.mjs");
await build({ entryPoints: [resolve(dirname(new URL(import.meta.url).pathname), "../../../cli/src/commands/runtime.ts")], outfile: modulePath, bundle: true, platform: "node", format: "esm", target: "node24", logLevel: "silent" });
const setupFunction = provider === "pi" ? "setupPiRuntime" : "setupAntigravityRuntime";
const result = spawnSync(process.execPath, ["--input-type=module", "-e", `const runtime=await import(process.argv[1]);await runtime.${setupFunction}();`, modulePath], {
encoding: "utf8", timeout: 10_000, env: { PATH: "/usr/bin:/bin", HTTP_PROXY: "http://127.0.0.1:9", HTTPS_PROXY: "http://127.0.0.1:9", NO_PROXY: "localhost", http_proxy: "http://127.0.0.1:9", https_proxy: "http://127.0.0.1:9", no_proxy: "localhost", SSL_CERT_FILE: "/public/ca.pem", SSL_CERT_DIR: "/public/certs", NODE_EXTRA_CA_CERTS: "/dev/null", OPENROUTER_API_KEY: "sensitive-canary", NPM_TOKEN: "sensitive-canary", HOME: "/private/home", NODE_PATH: "/foreign", NODE_TLS_REJECT_UNAUTHORIZED: "0" },
});
assert.ifError(result.error); assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /SETUP_NETWORK_BOUNDARY_PASS/);
assert.doesNotMatch(result.stdout + result.stderr, /sensitive-canary/);
});