mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-31 06:04:11 +01:00
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>
This commit is contained in:
1 parent
6df9fc2221
commit
54cc522e86
99 files changed
+4275
-192
No files matched your search
@@ -80,6 +80,7 @@ test("the script's path list and key inputs cover what the release build of the
|
||||
assert.deepEqual(HASHED_INPUTS, [
|
||||
`${RUNNER_PACKAGE}/rust-toolchain.toml`,
|
||||
`${RUNNER_PACKAGE}/acpx-profiles.json`,
|
||||
`${RUNNER_PACKAGE}/antigravity-distributions.json`,
|
||||
`${RUNNER_PACKAGE}/runner/Cargo.toml`,
|
||||
`${RUNNER_PACKAGE}/runner/Cargo.lock`,
|
||||
`${RUNNER_PACKAGE}/runner/crates/**`,
|
||||
@@ -280,6 +281,51 @@ test("every file the crate includes by path sits under a hashed input, so an edi
|
||||
assert.deepEqual([...forms].sort(), ["concat-manifest-dir", "concat-metavariable", "literal"]);
|
||||
});
|
||||
|
||||
test("isolated Docker Rust stages preserve the layout of embedded schema and identity inputs", () => {
|
||||
const crateRoot = path.join(repoRoot, RUNNER_PACKAGE, "runner/crates");
|
||||
const sources = [];
|
||||
(function walk(dir) {
|
||||
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) walk(full);
|
||||
else if (entry.name.endsWith(".rs")) sources.push(full);
|
||||
}
|
||||
})(crateRoot);
|
||||
for (const [dockerfile, stage] of [["Dockerfile", "runner-build"], ["docker/daytona-runner/Dockerfile", "runnerd-build"]]) {
|
||||
const docker = readFileSync(path.join(repoRoot, dockerfile), "utf8");
|
||||
const body = docker.split(/^FROM .+ AS /m).find(part => part.startsWith(`${stage}\n`));
|
||||
assert.ok(body, `${dockerfile}: missing ${stage}`);
|
||||
const workdir = /^WORKDIR (.+)$/m.exec(body)?.[1];
|
||||
assert.ok(workdir?.startsWith("/"), `${dockerfile}: an absolute stage working directory is required`);
|
||||
const copies = [...body.matchAll(/^COPY (?!-)(.+)$/gm)].flatMap(([, instruction]) => {
|
||||
const args = instruction.trim().split(/\s+/);
|
||||
const destination = path.posix.resolve(workdir, args.pop());
|
||||
return args.map(source => {
|
||||
const absolute = path.join(repoRoot, source);
|
||||
return { source: absolute, destination: statSync(absolute).isDirectory() ? destination : path.posix.join(destination, path.basename(source)) };
|
||||
});
|
||||
});
|
||||
const copiedPath = source => {
|
||||
const copy = copies.find(copy => source === copy.source || source.startsWith(`${copy.source}${path.sep}`));
|
||||
return copy ? path.posix.join(copy.destination, path.relative(copy.source, source)) : null;
|
||||
};
|
||||
let checked = 0;
|
||||
for (const source of sources) {
|
||||
for (const { resolved } of crateIncludes(source, readFileSync(source, "utf8"))) {
|
||||
// Cargo test-only fixture trees are intentionally absent from production
|
||||
// Docker stages. Their cache coverage is checked by the preceding test.
|
||||
if (/\/(?:test-fixtures|test\/fixtures|tests\/runner-e2e\/fixtures)(?:\/|$)/.test(resolved)) continue;
|
||||
const stagedSource = copiedPath(source);
|
||||
assert.ok(stagedSource, `${dockerfile}: crate source was not copied: ${path.relative(repoRoot, source)}`);
|
||||
const expected = path.posix.resolve(path.posix.dirname(stagedSource), path.relative(path.dirname(source), resolved));
|
||||
assert.equal(copiedPath(resolved), expected, `${dockerfile}: include input ${path.relative(repoRoot, resolved)} must retain its crate-relative path`);
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
assert.ok(checked > 0, `${dockerfile}: the guard must see real Rust include paths`);
|
||||
}
|
||||
});
|
||||
|
||||
test("every PR reader restores the crate after the dependency cache and marks it before building", () => {
|
||||
for (const [name, buildCommand] of READERS) {
|
||||
const body = job(prWorkflow, name);
|
||||
|
||||
@@ -74,6 +74,9 @@ export function paperclipRunnerBinaryNeedsBuild({
|
||||
|
||||
const runnerRoot = path.join(packageRoot, "runner");
|
||||
const buildInputs = [
|
||||
path.join(packageRoot, "antigravity-distributions.json"),
|
||||
path.join(packageRoot, "rust-toolchain.toml"),
|
||||
path.join(packageRoot, "protocol"),
|
||||
path.join(runnerRoot, "Cargo.toml"),
|
||||
path.join(runnerRoot, "Cargo.lock"),
|
||||
path.join(runnerRoot, ".cargo"),
|
||||
|
||||
@@ -52,8 +52,8 @@ assert.equal(JSON.parse(await readFile(join(server, 'package.json'), 'utf8')).na
|
||||
const { resolvePaperclipRunnerBinary } = await import(pathToFileURL(join(server, 'dist/services/native-runtime/native-codex-runner.js')));
|
||||
const { createCapabilityRunnerdCodexTransport } = await import(pathToFileURL(join(server, 'dist/vendor/paperclip-runner/live/runnerd-codex-transport.js')));
|
||||
const { QUALIFIED_ACPX_PROFILES } = await import(pathToFileURL(join(server, 'dist/vendor/paperclip-runner/drivers/acpx/qualified-profiles.js')));
|
||||
assert.equal(QUALIFIED_ACPX_PROFILES.pi.agentProfileVersion, 34);
|
||||
assert.equal(QUALIFIED_ACPX_PROFILES.pi.commandDigest, 'sha256:ed4b32cc8fbf36309d38b3bd378f41f8e7b9e96a2405d275cc55e89042d94c7f');
|
||||
assert.equal(QUALIFIED_ACPX_PROFILES.pi.agentProfileVersion, 35);
|
||||
assert.equal(QUALIFIED_ACPX_PROFILES.pi.commandDigest, 'sha256:9473de6ef4baf2bd82fe8b6757239bdc160960b0a78ae6f47fa9e0996544226c');
|
||||
assert.equal(Object.hasOwn(QUALIFIED_ACPX_PROFILES.pi, 'qualificationModel'), false);
|
||||
assert.equal(Object.hasOwn(QUALIFIED_ACPX_PROFILES.pi, 'reportedModelId'), false);
|
||||
const daemon = resolvePaperclipRunnerBinary();
|
||||
|
||||
@@ -54,6 +54,7 @@ export const CACHED_PATHS = [
|
||||
export const HASHED_INPUTS = [
|
||||
`${RUNNER_PACKAGE}/rust-toolchain.toml`,
|
||||
`${RUNNER_PACKAGE}/acpx-profiles.json`,
|
||||
`${RUNNER_PACKAGE}/antigravity-distributions.json`,
|
||||
`${RUNNER_PACKAGE}/runner/Cargo.toml`,
|
||||
`${RUNNER_PACKAGE}/runner/Cargo.lock`,
|
||||
`${RUNNER_PACKAGE}/runner/crates/**`,
|
||||
|
||||
Reference in new issue
Block a user