feat(runner): provision pinned Google Antigravity native runtime (#18187)

## 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>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-10-29 13:25:54 -05:00
1 parent 5d79d88d68
commit 09c05c5505
22 files changed
+3477 -6

No files matched your search

+15 -3
View File
@@ -3,17 +3,17 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { afterEach, expect, it } from "vitest";
import { resolveCursorProvisioner } from "../commands/runtime.js";
import { resolveAntigravityProvisioner, resolveCursorProvisioner } 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() {
async function fixture(provider = "cursor") {
const root = await realpath(await mkdtemp(join(tmpdir(), "cursor-public-setup-")));
roots.push(root);
await mkdir(join(root, "dist/vendor/paperclip-runner/cli"), { recursive: true });
await writeFile(join(root, "package.json"), JSON.stringify({ name: "@paperclipai/server" }));
await writeFile(join(root, "dist/index.js"), "throw new Error('must not import server');");
const provisioner = join(root, "dist/vendor/paperclip-runner/cli/provision-cursor.cjs");
const provisioner = join(root, `dist/vendor/paperclip-runner/cli/provision-${provider}.cjs`);
await writeFile(provisioner, "fixture");
return { root, provisioner, url: pathToFileURL(join(root, "dist/index.js")).href };
}
@@ -36,3 +36,15 @@ it("rejects a provisioner linked outside the installed package", async () => {
await rm(f.provisioner); await symlink(other.provisioner, f.provisioner);
await expect(resolveCursorProvisioner(f.url)).rejects.toThrow("escapes");
});
it("resolves Antigravity setup from the public server without importing it", async () => {
const f = await fixture("antigravity");
expect(await resolveAntigravityProvisioner(f.url)).toBe(f.provisioner);
});
it("rejects an Antigravity provisioner outside the installed server", async () => {
const f = await fixture("antigravity"); const other = await fixture("antigravity");
await rm(f.provisioner); await symlink(other.provisioner, f.provisioner);
await expect(resolveAntigravityProvisioner(f.url)).rejects.toThrow("escapes");
await expect(resolveAntigravityProvisioner(`${f.url}?redirect=1`)).rejects.toThrow("installed Paperclip server");
});
+1 -1
View File
@@ -28,7 +28,7 @@ it("rejects foreign package identity and a setup entrypoint outside that package
});
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 or cursor");
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 () => {
+40 -2
View File
@@ -93,6 +93,43 @@ export async function setupCursorRuntime(): Promise<void> {
} finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); }
}
/** Resolve Antigravity setup without importing the server or starting a provider. */
export async function resolveAntigravityProvisioner(serverUrl: string): Promise<string> {
const url = new URL(serverUrl);
if (url.protocol !== "file:" || url.search || url.hash) throw new Error("Antigravity setup requires an installed Paperclip server");
const entry = await realpath(fileURLToPath(url));
if (!entry.endsWith("/dist/index.js")) throw new Error("Antigravity setup requires the published server layout");
const root = resolve(dirname(entry), "..");
const manifest = join(root, "package.json");
const info = await lstat(manifest);
if (!info.isFile() || info.isSymbolicLink() || info.size > 65536 || JSON.parse(await readFile(manifest, "utf8")).name !== "@paperclipai/server") throw new Error("Antigravity setup server package identity is invalid");
const provisioner = join(root, "dist/vendor/paperclip-runner/cli/provision-antigravity.cjs");
if (await realpath(provisioner) !== provisioner || !(await lstat(provisioner)).isFile()) throw new Error("Antigravity setup entrypoint escapes its server package");
return provisioner;
}
export async function setupAntigravityRuntime(): Promise<void> {
const provisioner = await resolveAntigravityProvisioner(import.meta.resolve("@paperclipai/server"));
const child = spawn(process.execPath, ["--use-env-proxy", provisioner], {
// Setup only downloads public assets. Reuse the existing network allowlist;
// runtime credentials and process-loader settings never reach this child.
stdio: "inherit", env: buildPiSetupEnvironment(),
});
const cancel = () => { if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM"); };
process.on("SIGINT", cancel); process.on("SIGTERM", cancel);
try {
await new Promise<void>((accept, reject) => {
let spawnError: Error | undefined;
child.on("error", error => { spawnError = error; });
child.once("close", (code, signal) => {
if (spawnError) reject(spawnError);
else if (code !== 0 || signal) reject(new Error("Antigravity setup did not finish. Review its error; an invalid installation is never replaced automatically."));
else accept();
});
});
} finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); }
}
export function registerRuntimeCommands(program: Command): void {
const runtime = program.command("runtime").description("Manage explicitly installed agent runtimes");
@@ -112,10 +149,11 @@ export function registerRuntimeCommands(program: Command): void {
} finally { process.off("SIGINT", cancel); process.off("SIGTERM", cancel); }
});
runtime.command("setup <provider>")
.description("Install and verify the pinned Pi or Cursor runtime for this host (public downloads; no model calls)")
.description("Install and verify a pinned Pi, Cursor, or Antigravity runtime (public downloads; no model calls)")
.action(async (provider: string) => {
if (provider === "pi") await setupPiRuntime();
else if (provider === "cursor") await setupCursorRuntime();
else throw new Error("Supported runtime setup: paperclipai runtime setup pi or cursor");
else if (provider === "antigravity") await setupAntigravityRuntime();
else throw new Error("Supported runtime setup: paperclipai runtime setup pi, cursor, or antigravity");
});
}
+12
View File
@@ -2130,6 +2130,18 @@ CLI paths that use stdin avoid that separate limit for the wake prompt.
## Adding a native Runner harness
Google Antigravity's pending integration uses the official ACP server 1.4.3 with
a pinned native admission patch. `paperclipai runtime setup antigravity` installs
its verified assets on macOS ARM64 as the OS user running Paperclip. Setup does
not enable global execution: qualification remains pending and admission is
closed by default. An isolated company can use a finite trusted server-startup
candidate authorization bound to the exact model, local platform, profile and
native closure. The candidate's recorded setup and two-turn task reused the same
owner's managed personal-default Google subscription on macOS ARM64 with
`gemini-3.8-flash-low`; fresh consent and API-key live acceptance remain separate
gates. Workspace OAuth is not runtime authentication. See the
[contract, evidence, and remaining gates](plans/2026-10-28-antigravity-integration.md).
To add a harness/provider to the new native Paperclip Runner, use
[create-runner-adapter](../.agents/skills/create-runner-adapter/SKILL.md), the
[Runner integration playbook](RUNNER-INTEGRATION-PLAYBOOK.md) and its
@@ -0,0 +1,474 @@
# Google Antigravity native Runner integration — 2026-10-28
Owner: Codex integration owner, with separate provider-research, distribution,
and product-configuration workers.
Source baseline: `dce004259394486f6361d0c0dd5a33a5272acc0a`.
Requested finish line: a reviewable native Runner integration. Publication,
merge, deployment, and migration of existing agents are outside this request.
## User outcome and release contract
An operator selects Google Antigravity in native Paperclip Runner, binds an
authorized Google runtime account, selects an exact model, and assigns an ordinary
task. The task produces independently verified files/artifacts, settles through
Paperclip, and can be stopped with physical process cleanup.
- Transport: Google's official Antigravity ACP server through the existing ACPX
harness driver. The standalone `agy` print protocol is a separate interface.
- First environment: local macOS ARM64. Other registry platforms are an inventory,
not a live qualification claim. No new hosting environment is promised.
- Runtime pin: ACP server **1.4.3**, including its matching
`localharness_external` executable. The CLI's separately versioned release is
not this runtime's version. Use explicit setup and verified immutable assets.
- Authentication: the user authorized an existing Google account connection if
available. Managed Gemini API-key bindings are eligible only through the
native `gemini-api-key` method. Google Drive/MCP OAuth is not runtime auth.
The user subsequently requested a fresh test environment and a named Google
account. Explicit local personal OAuth is now part of the candidate; it needs
its own connection flow and must not borrow a workstation login.
- Model policy: caller-selected exact model, passed unchanged and acknowledged
before a prompt, including on load. Acceptance model remains unset until an
authorized account can advertise/accept one. No product model allowlist.
- Required: exact instruction admission on create/load; scoped semantic and MCP
tools; streamed text/tool fidelity; accepted task result/artifact; permission
denial; distinct native questions and permissions; Stop and bounded shutdown;
provider-loss expiry; honest usage; ordinary save/reopen/Test and clean install.
- Active steering, native planning decisions, child interactions, inbound media,
remote platforms, and enterprise login remain discovery items. Do not expose
unsupported behavior as supported. The required work above remains required.
- Spending: no paid prompts or paid infrastructure in the discovery phase. Do
credential-free/deterministic work first. A paid acceptance campaign needs an
explicit model/account and bounded spending record before it starts.
- Admission: keep the profile pending consistently until its required native
acknowledgments and product acceptance pass. A test override is not delivery.
## Architecture and ownership
Provider translation, native metadata, question semantics, auth-home settings,
and executable verification belong under the Antigravity provider boundary.
The existing Runner owns supervision/PRP/replay; Paperclip retains company,
credential, permission, budget, result, and task-state authority.
- Integration owner: Runner profile/contracts, sidecar/Rust admission, CLI wiring,
integration tests, and final evidence reconciliation.
- Distribution worker: pinned registry manifest, complete two-binary closure,
explicit setup/cache provisioning, integrity and cleanup tests.
- Product worker: shared harness registration, explicit managed Google routing,
server configuration, and gated product presentation.
- Research worker: official documentation and shipped-runtime behavior only.
- Serialize shared profile generation, manifests, schemas and lockfile changes.
Follow the repository lockfile policy; do not commit generated lock changes.
## Research and capability inventory
Evidence labels describe the actual boundary, not inferred product support.
| Capability | Native evidence | Runner/product qualification | Remaining work |
| --- | --- | --- | --- |
| Distribution | Registry-observed 1.4.3, macOS ARM64 archive checksum verified | Unqualified | Complete closure verification and clean setup |
| Initialization | Credential-free observed: ACP v1, `antigravity-acp` 1.4.3 | Unqualified | Actual installed ACPX path |
| Auth | Observed methods: personal OAuth, business OAuth, Gemini API key, Agent Platform | Unqualified | Bound account, settings isolation and revoke |
| Missing auth | Observed `session/new` rejects with authentication-required error | Unqualified | Product diagnostic and routing |
| Instructions | Advertises `_meta.antigravity.systemPrompt.append` | Unverified | Exact create/load content acknowledgment and live behavior |
| Models | Account-dependent; official issue reports client-dependent catalogs | Unverified | Exact model selection/ack on create/load; no identity spoofing |
| Native questions | Upstream ACPX identifies `interaction_` permission callbacks as questions | Unverified | Preserve option identity and human round trip |
| MCP | Advertises HTTP/SSE support | Unverified | Scoped tool discovery/invocation and result acceptance |
| Media | Advertises image/audio/embedded context | Unverified | End-to-end conversion; no UI promise yet |
| Continuity | Advertises load/list/resume | Unverified | Process/session distinctions, fencing and failure recovery |
| Shutdown | Credential-free observed stdin EOF exits 0 after init/auth rejection | Unverified during work | Stop, native-child loss and bounded descendant cleanup |
| Usage/cost | Historical issue reports missing ACP usage | Unverified | Inspect current receipts; absent is unknown, not zero |
Public references:
- [Official ACP distribution registry](https://github.com/agentclientprotocol/registry/blob/main/antigravity-acp/agent.json)
- [Google's Zed setup/authentication documentation](https://antigravity.google/docs/ide/extensions/zed/)
- [Upstream ACPX integration](https://github.com/openclaw/acpx/blob/main/agents/Antigravity.md)
- [Client model catalog issue](https://github.com/google-antigravity/antigravity-cli/issues/1318)
- [Instruction metadata and workspace confinement issue](https://github.com/google-antigravity/antigravity-cli/issues/1273)
- [Historical terminal/cancellation issue](https://github.com/google-antigravity/antigravity-cli/issues/1041)
## Acceptance ledger
| Gate | Required observation | Current verdict |
| --- | --- | --- |
| Native admission | Installed dependency verifies exact model and instructions before first prompt and after load | Real create passed; loaded session and model behavior still pending |
| Scoped tools | Real semantic/MCP round trip; wrong-company/revoked/stale authority denied | Unattempted |
| Ordinary task | Browser/server/DB/Runner/provider; verified file/artifact, accepted result, durable task state, cleanup | Account/model selected; product execution remains gated pending acceptance |
| Human decisions | Native question/permission identity, correct response, duplicate/stale denial, loss expiry | Unattempted |
| Stop/lifecycle | Startup/work/pending-input Stop, spontaneous child loss, bounded shutdown and no late revival | Unattempted |
| Setup | Connect/select/save/reopen/Test/reconnect/revoke in selected environment | Real personal connect/save/reload/account catalog passed; agent Test/reconnect/revoke pending |
| Distribution | Clean installed package, explicit setup, verified closure and tamper/missing rejection | Final `.4` cold public setup bundle passed on macOS ARM64; other platforms unqualified |
| Compatibility/checks | Focused existing native and legacy regressions; required repo checks | Focused checks passed; full typecheck/build passed; full suite incomplete due host PostgreSQL resources; final impact checks running |
Credential-free discovery is not a passed Product E2E cell. Original failures and
later attempts retain their own candidate identity. No shipping candidate is
frozen or published yet. Discovery sent no prompts; the later bounded native
slice below sent one prompt through the selected subscription.
## Implementation and acceptance progress
The candidate remains pending. One authenticated native prompt passed exact
instruction behavior and scoped file-tool checks; Product E2E remains pending.
The implementation now includes the registered native harness, a two-executable
verified closure and explicit setup, exact-byte/model native admission receipts,
question translation, scoped credential projection, and fail-closed release
checks at catalog, API, TypeScript driver, sidecar, and Rust launch boundaries.
The operator subsequently requested a fresh test environment and named a Google
account. This adds explicit personal Google runtime sign-in to the first release
contract. The existing local connection-attempt lifecycle owns sign-in, vault
promotion, access, reconnect and expiry. Its setup-only native ACP client sends
only `initialize` and `authenticate`; readiness requires the native result,
physical process retirement, and a verified Google email. A token file appearing
before native onboarding finishes is insufficient. Managed task execution must
silently refresh or fail; it cannot open another browser sign-in. Remote OAuth
execution remains rejected pending separate qualification.
### Evidence observed so far
- The official archive and complete vendor closure were verified. The owned
Mach-O/Python patch is signed locally and pinned independently from the vendor
closure. Subsequent patch candidates retain separate hashes and observations.
- Installed ACPX tests with a controlled native peer exercise nonce/session/model/
instruction receipts, setters, question identity, invalid acknowledgments and
stale connections. They do not prove real model behavior.
- Actual hermetic native Python tests execute Google's protobuf builder, server
configuration builder, native SDK strategy, and a real helper subprocess and
local WebSocket. They reject a cached external executable, exclude browser/
subagent/workflow/ambient command-hook configuration, and fence unattended OAuth
against browser fallback. Native conversation acknowledgment remains controlled.
- A public-layout CJS setup bundle installed the first patch candidate from the
real vendor archive into an isolated cache, verified its signed closure and
removed it. The final candidate requires a fresh installed-layout result.
- Focused OAuth/credential checks passed 20 tests using real local subprocesses,
private files and lifetime-fence sockets with controlled provider responses.
Shared and UI connection regressions passed 37 tests. Local Google lifecycle
checks passed 8 tests, including early-token rejection and retirement-before-
removal. These tests do not establish Google entitlement or live inference.
- Full `pnpm -r typecheck` and `pnpm build` passed with the OAuth integration.
The subsequent metadata discovery changes passed Runner/server TypeScript;
final impact checks remain required after freeze.
- Fresh ordinary browser onboarding created one disposable company. Personal
Google sign-in completed for the user-selected account, its verified email
was saved on the encrypted runtime grant, all login processes retired, and
the connection remained connected after a browser reload. This proves setup
authentication, not model entitlement or task execution.
- Eight real PostgreSQL connection integration tests passed without DB/service
mocks, covering encrypted user credentials, selected homes, company/member/
install authority, revoked defaults, and refresh/reconnect compare-and-swap.
- The final metadata candidate is `1.4.3-paperclip.4`, closure
`05ed2a3e39c95fc813abeb664a78a295d1dcbfd4098f28cbc17d30d710a1ea92`.
Its setup-only `_paperclip/models` path invokes the official account catalog
without creating a conversation, native helper, or prompt. Four actual-source
native regressions and 25 auth/catalog process regressions passed. Discovery
retains the credential home and executable lease if process retirement cannot
yet be proved; deletion follows physical retirement.
- The account catalog supplies suggestions. Explicit compatible model IDs remain
eligible even when absent from discovery, for both OAuth and API-key bindings.
The native exact-model receipt decides admission.
### Preserved failures and limits
| Observation | Resolution or remaining limit |
| --- | --- |
| Imported patch CLI top-level await broke CJS setup bundling | Removed the imported CLI entry branch; public-layout bundle subsequently installed the real archive successfully |
| New Rust pending-profile regression initially failed formatting | Formatted the assertion and regenerated affected unpublished identities; full typecheck subsequently passed |
| Native embedded PostgreSQL could not create host semaphore/shared-memory resources | Preserved failed launch. Started a disposable PostgreSQL 18 container with an owned volume and loopback-only port; fresh source onboarding, health, bootstrap and browser rendering passed |
| One credential test could not acquire its deterministic TCP quorum | Failed closed before staging. A later fresh-directory run passed all 20 focused cases. Host observation found 1,781 reservations in the relevant ephemeral range; the exact original colliding reservations were not captured, so causality is not claimed |
| Review found SDK cached-helper override, hidden browser/hook setup and interactive refresh fallback | Corrected the actual native boundaries and added actual-source regressions; original candidate observations are not transferred to replacement hashes |
| Missing source vendor-shim exports caused the first new server OAuth tests to fail | Added the explicit exports; focused rerun is required/recorded separately |
| Full test run repeatedly failed native PostgreSQL initialization on the shared host | Full suite did not complete. All 16 affected native-session DB cases subsequently passed using the supported external test-DB seam and a separate disposable PostgreSQL container; later full-suite groups remain unrun |
| One existing route test exceeded its 15-second deadline during concurrent checks | Other 94 tests passed; the unchanged route test subsequently passed alone with its original timeout |
| First authenticated native session returned exact receipts but installed ACPX rejected its grouped model selector | Native create and model setter both acknowledged `gemini-3.8-flash-low` and the exact 112 instruction bytes. ACPX preferred the separate family selector `gemini-3.8-flash`, producing an effective-model mismatch. Zero prompts were sent; native process, command lease, managed-home writeback and deletion all completed. Preserve this failed attempt while fixing the provider projection |
The local environment is a fresh checkout instance with an isolated Docker
PostgreSQL database, not the seeded `test-drive` CLI (that command requires an
API key and rejects external PostgreSQL). The failed embedded attempt and fresh
Docker-backed attempt have separate evidence directories. No existing company,
workstation CLI login, Google tool grant, or shared database was borrowed.
Ordinary product task output/result, scoped semantic/MCP tools, human decisions,
loaded sessions, Stop, and native-child loss remain unqualified. The selected
account/model, direct native file-tool slice and final installed setup passed;
typecheck/build passed and the complete test suite remains environment-blocked.
### Selected account and first native slice
The fresh instance's saved personal Google subscription returned its account
catalog through the ordinary authorized metadata API: `gemini-3.8-flash-high`,
`gemini-3.8-flash-medium`, and `gemini-3.8-flash-low`. The first slice selects
**`gemini-3.8-flash-low`** unchanged. This is an acceptance choice, not a product
allowlist. The user authorized the named account and fresh local test drive.
First prove native create/model/instruction admission without a prompt. Then
permit at most two short first-slice prompts, each with a 120-second process
deadline, using the existing subscription and disposable local workspace.
Stop at the first admission, tool, output, or cleanup divergence and diagnose
before another attempt. No subscription change, paid infrastructure, or automatic
paid fallback is authorized by this bounded slice. Per-request provider billing
may remain unavailable; request count and duration do not establish dollar cost.
The broader Product E2E campaign needs its own recorded cells and bound.
The first authenticated native attempt (2026-10-28 14:44 UTC) failed before a
prompt at the ACPX model-selector projection, after two correct native receipts.
The full wrapper error SHA-256 was
`8b6480a8eac6d058877054700dbd69654955e6061f961d23c8eddbbefc349818`;
the message was independently reproduced from the installed wrapper's source.
Native closure `.4` remains unchanged. The provider projection fix reproduced
that exact error against installed ACPX, then passed 28 admission/dependency
regressions. The unchanged native runtime subsequently passed authenticated
session admission at 14:49 UTC: the exact low model and 112 instruction bytes
were acknowledged, no model setter or prompt was needed, and native process/
lease plus managed credential writeback and home cleanup completed. The passing
guard JS digest was
`31b9dd005c7452b61e61f3a29c77f74d7f16b20511d1e0f9eff10dfb649dca4b`.
Current slice tally: two native attempts, zero prompt requests, one preserved
failure, one admission pass, physical cleanup passed for both.
At 15:00 UTC, one bounded native prompt passed on the same `.4` closure and
exact model. The unpredictable proof string appeared only in the admitted
650-byte system append. The real model completed native client write/read
tools; an independent filesystem read matched all 40 expected bytes including
one LF. Native tools, stream, terminal result, physical process retirement,
command lease release and managed credential cleanup passed. The provider
reported context usage but no input/output token or monetary receipt, so cost
remains unknown. This is native dependency evidence, not Product E2E.
The [sanitized native evidence](2026-10-28-antigravity-native-evidence.json)
retains all three attempts, source/runtime identities, the original failure,
one actual prompt and its independent byte oracle. Total native attempts: three;
prompt requests: one. Production admission remains pending because Product E2E
and the remaining release-qualification cells are not complete.
### Proposed Product E2E candidate campaign — not implemented
The existing operator qualification authorization lists only Cursor, Copilot
and Pi; it does not admit Antigravity through the canonical driver, installation,
sidecar and Rust pending gates. Do not mark the profile qualified or inject a
private backend to obtain a pass. A candidate path must bind the exact model and
runtime identity across those boundaries, while default admission stays closed.
The proposed campaign is local-only, uses this disposable company's saved
personal Google grant, selects `gemini-3.8-flash-low`, and permits at most twelve
provider admissions in thirty minutes, with no automatic story retries. It
stops at the first divergence. Provider billing is unavailable; these are
admission/time bounds rather than a fabricated dollar-cost guarantee.
Required cells remain ordinary task/file/artifact/result delivery; native
question and permission responses; active and pending-input Stop; spontaneous
native-child loss with durable expiry; and loaded-session instruction/model
continuity. Each needs an independent state/file/cleanup oracle. Reuse the
existing Product E2E catalog and report pipeline with an explicit attached-local
saved-connection fixture. The fresh-connection suite intentionally rejects
usable pre-existing connections, so it cannot be relabeled as this journey.
Candidate authority should extend the existing trusted startup launch profile:
server operator authorization → Runner launch descriptor → Rust session policy
→ sidecar initialize/open → provider host/install checks. It must bind company,
exact model, profile digest and native closure. Do not place this authority in
agent configuration, provider environment, runtime context, or a saved provider
snapshot. Existing `acpxCandidateProfile` stops at TypeScript transport gates;
it is insufficient on its own. This design is proposed, not implemented or
activated, and does not change any current qualification verdict.
### October 29 additional-agent user walkthrough
The user requested a Codex bootstrap with the authorized local OpenAI key, then
Google Antigravity in the ordinary additional-agent picker. The Codex agent and
saved personal OpenAI connection were created through the UI. A subsequent
strict user-control recording found that the pending Antigravity draft lost its
provider when reopened. That 17-step failed recording is preserved.
The UI now retains a non-secret pending provider choice on the held draft,
labels it Google Antigravity in the roster and overview, and resumes Google
setup through Finish setup. It keeps the physical draft paused and leaves native
qualification and execution configuration unchanged. Change adapter can reuse
the same draft. All 193 affected UI tests, UI typecheck, token gates and map
validation passed.
A fresh 14-step browser-only recording created Antigravity Assistant from New
agent, found Google Antigravity in the main grid with Advanced collapsed, opened
Google setup, returned to Agents, and reopened the correct provider through
Finish setup. Its report and structured log are in the Codex artifact directory
`add-antigravity-agent-verified-2026-10-29-1793285314053`, with 14 actual viewport
screenshots. The earlier failure is in
`add-antigravity-agent-user-journey-2026-10-29-1793284634382`.
This proves additional-agent discovery, draft identity, labeling and resumption.
The pending banner still prevents runnable completion. This walkthrough did not
complete Google OAuth, an Antigravity task, a follow-up turn or Stop acceptance.
The Codex overview also exposed taskless scheduled-heartbeat failures, which
remain outside this additional-agent change. The local test drive was moved to
a stable temporary source mirror while retaining the same isolated database and
saved connection. No provider credentials were copied between instances.
### October 29 finish line: setup and real work
The user clarified that picker/draft success is insufficient. Continue the
recorded user-test → diagnosis/fix → focused checks → fresh user-test loop until
Google Antigravity connects, finishes setup, and completes a real task. Retain
each failed attempt. Include a follow-up turn and independent output evidence;
the original request expects useful results over multiple turns.
| Required outcome | Current evidence | Remaining gap | Next action / owner |
| --- | --- | --- | --- |
| Ordinary Google setup and activation | Fresh `.5` agent passed ordinary picker, saved account reuse, native Test, actual readiness and clean task handoff | No gap in the declared local setup path | Preserve the 38-screen walkthrough and exact candidate identity |
| Real native task and follow-up | `.5` first task and follow-up delivered exact 61/119-byte files, retained the native conversation and persisted Done | No gap in the declared local two-turn task path | Preserve independent bytes, decision lineage and native retirement evidence |
| Local release qualification | Exact native model/instruction receipt and physical native cleanup | Product authority, interactions, cancellation and installed path cells | Prove the local contract with exact candidate identity before changing release flags |
The initial acceptance target remains the local darwin-arm64 test drive and the
selected Google subscription connection. Candidate authorization is bound to
that company, exact model, provider profile, native closure and expiry, with
fresh actual run/session binding. It is supplied only by the server operator;
agent config and provider environment cannot create it. Global qualification
remains pending while gathering real evidence. Diagnose the first divergence
before another paid attempt; provider billing remains unknown rather than zero.
The next recorded user attempt (`antigravity-connect-and-run-2026-10-29-1793286152790`)
completed the selected Google account sign-in, saved the personal subscription,
and discovered the real account model catalog. Selecting Gemini 3.8 Flash Low
still left Test and Finish disabled, so the full user goal failed. Its 15
screenshots and original report are retained. No task prompt was sent.
During implementation the active Codex checkout was externally removed. Snapshot
`7eb51ffb10f4a2fd0937f54060a9d10fd4f8c630` preserved the source; it was recovered
into a stable temporary Git checkout with independent objects and an additional
tracked-source archive. The separate serving mirror, instance database, saved
Google connection and original evidence were unchanged. The remaining native
continuation and taskless timer corrections are being tested there before the
next recorded setup/task attempt.
The second full setup attempt
(`antigravity-setup-task-iteration-2-1793289638292`) reused the saved Google
subscription, reached Configure with Gemini 3.8 Flash Low, and exposed enabled
Test and Finish controls. The real prompt-free runtime Test failed and disabled
Finish. Its expanded details offered only a generic account/model verification
failure. This remains failed acceptance: no Antigravity task ran. The ten-step
recording and all ten screenshots are retained. Diagnose the original native
error and improve safe recovery details before a fresh browser-only attempt;
do not count an enabled picker or passing unit suite as the user outcome.
Independent verification after that failure found no serving-source or runtime
drift: the frozen Runner, selected ACPX sidecar and profile/closure manifests
match the final build ledger, and the original PostgreSQL container remains
healthy with its saved volume. The scoped candidate path is implemented across
server, Runner, Rust and sidecar; the earlier proposed-design section records
the state before implementation, not a present claim that authority is absent.
The next diagnosis reproduced the original `session.open` rejection with zero
model prompts and confirmed physical retirement. Its first failing stage is
installation. The controller selected the JavaScript workspace dependency root
for Antigravity while supplying the native Runner package manifest; the native
asset guard correctly rejected that owner/manifest mismatch. A credential-free
check reproduces the old failure and verifies the real pinned native closure
after selecting the actual Runner package owner. The correction also covers the
vendored source-server layout while preserving existing Copilot behavior.
The rebuilt candidate's third browser-only attempt completed the real native
Test and Finish setup, then ran ANT-2 in its isolated task workspace. Antigravity
created and read back the requested file, registered its attachment, corrected
an initial completion-citation rejection, and reached Done. Done and the actual
file persisted after Reload. Independent download verification then preserved
the first result: 55 exact UTF-8 bytes, SHA-256
`5964636e0fd46950f9296593d33bd864e47e35f79997287cc4498735f839e733`.
The 26-step recording includes 26 screenshots. This is the first complete
setup/task success, not a release-qualification verdict. The follow-up native
conversation and completion/cleanup joins remain to be checked. Usability gaps
observed in this run are a stale paused warning in the post-setup task composer
and misleading failed labels on earlier activity rows after a finish error.
The same task then completed a second ordinary UI turn. Send reopened ANT-2;
Antigravity read the existing file, preserved its first line, appended the new
requested line, registered a second attachment and returned to Done. The updated
file remained readable after Reload. The nine-step follow-up recording has nine
screenshots. Independent download verification preserved all 113 expected bytes
with SHA-256
`cc8160e66e60e98aa1013668760691ee46379606aeaf1e89ab5ff2b3c604d15a`.
The first accepted native result was joined through committed finalization and
an applied status decision to Done; the follow-up native identity and physical
retirement audit are being completed separately from the strict browser runs.
Both independent oracles now pass. The follow-up retained every tagged native
conversation identity field and exact requested/effective low model. It used a
fresh run-bound startup/state root and different physical Runner/sidecar PIDs.
Both native results have accepted rows, committed finalizations and applied
assessments/status decisions to Done. All four recorded Runner/sidecar PIDs and
both process groups are absent, both provider states are closed without an
unconfirmed-exit marker, and the exact retained native-home kernel quorum was
acquired and released. This proves native lifetime retirement beyond Runner
exit alone. Setup readiness and activity-label feedback remain under correction
before the final fresh-agent walkthrough; global qualification is unchanged.
The activity-label diagnosis found two coupled upstream SDK behaviors: local
client file callbacks emitted ACTIVE without a terminal result, and any later
ERROR step failed every open activity. Native patch `.5` closes file callbacks
from their actual `ToolResult` after matching call/step identity and successful
result delivery. It removes the broad error broadcast, retaining the existing
per-call terminal mapping and turn cleanup. No successful outcome is fabricated;
genuine concurrent and scoped errors remain errors. The real hermetic SDK
regression first failed on the missing terminal, then passed actual create/read/
edit bytes, genuine file failures, concurrent calls and scoped/unscoped errors.
All five native regressions and independent semantic review pass. Admission
Python remains byte-identical. The `.4` binaries, cache and real-task evidence
remain historical; `.5` has a new immutable native closure and needs fresh
candidate authority and live acceptance.
The post-Finish UI now waits for the actual lifecycle-ready detail before
showing readiness or enabling task assignment, then refreshes the roster entry
used by the composer. Genuine manual/import pauses and approval/Dot flows stay
intact. All 120 setup tests, UI typecheck and token checks pass. The final user
walkthrough will create a fresh agent and exercise this corrected handoff.
The final `.5` fresh-agent walkthrough passed all 38 recorded steps with 38
screenshots. Google Antigravity was selected from the ordinary additional-agent
grid with Advanced collapsed. The saved Google subscription passed native Test.
The UI visibly waited in Finishing agent setup before enabling assignment at
actual readiness; the composer had no paused warning. A unique marker was then
saved through the ordinary AGENTS.md instruction editor, preserving the default
sentence. ANT-3 requested that configured marker without including its value.
The delivered file contained that marker, was readable and downloadable, and
Done persisted after Reload. Independent download preserved all 61 expected
bytes with SHA-256
`e229f07be3a7c6835f91f9bd07f7daf4bd19f839c91f2fdd17161867750d4483`.
Native event evidence confirms completed edit/read activities stayed completed
when the first finish citation failed; only the actual failed finish was marked
failed, and its corrected retry completed.
A separate nine-step `.5` follow-up also passed with nine screenshots. The same
task reopened through Send, read and updated its existing file, preserved the
instruction marker and appended the requested line. It returned to Done and the
actual updated attachment survived Reload. Independent download preserved all
119 expected bytes with SHA-256
`b1bca87da838f17535613f48932a40043b9e31a820a43e4c9740d619e3dfac05`.
The second run retained the full native conversation identity and exact low
model under the `.5` profile. Both physical Runner/sidecar process groups and
their four recorded PIDs are absent, provider states are closed with confirmed
exit, and the exact native-home kernel quorum was acquired and released. Both
accepted results have committed finalization and applied Done decision lineage.
The first coordinator's current pointer subsequently changed to a superseding
assessment during the user's follow-up reopening, preserving in-progress work;
its original applied Done decision and captured first-run result remain intact.
The oracle retains the initial strict-current-pointer failure and this explicit
lineage explanation instead of rewriting the original grade. Raw native
instruction wire receipts remain unarchived;
the instruction-only marker provenance and actual bytes establish the observed
behavior without claiming an independent wire archive.
### Final repository verification and local handoff
Repository-wide `pnpm -r typecheck` and `pnpm build` pass against the frozen
`.5` source. `pnpm test:run` remains incomplete: its first general-server group
reached the declared 20-minute command bound and reported a 15-second timeout in
the Gmail/Cloud environment-identity case. The exact case passed in isolation
with its unchanged 15-second test budget. The cause of the broad-run timeout is
not established; later full-suite groups were not reached. Original logs and
grades are retained, and owned verification processes were retired. This is not
a claim of a green complete test suite or a production-qualified release.
The externally removed chat workspace was restored from the independent source
checkout on `codex/google-antigravity`. All 106 changed/new source paths, the
native binary, selected sidecar and Git object connectivity were verified after
restoration. The test drive continues to serve from its separate frozen mirror
with the original isolated database and saved account, and reports ready/healthy.
The final `.5` oracle and both browser journeys establish the requested local
setup, task and follow-up outcome; broader release qualification remains pending.
@@ -0,0 +1,203 @@
{
"schema": "paperclip.antigravity.native-evidence.v1",
"date": "2026-10-28",
"sourceBaseline": "dce004259394486f6361d0c0dd5a33a5272acc0a",
"runnerBuildInputsDigest": "c62057227b92",
"platform": "darwin-arm64",
"patchVersion": "1.4.3-paperclip.4",
"acpxVersion": "0.13.1",
"qualification": "pending",
"productE2E": "not_run",
"accountAuthentication": "user-selected saved personal Google runtime connection",
"catalogModels": [
"gemini-3.8-flash-high",
"gemini-3.8-flash-medium",
"gemini-3.8-flash-low"
],
"promptRequestsSent": 1,
"providerMonetaryCostUsd": null,
"billingCoverage": "unavailable; context usage is not an input/output token or price receipt",
"attempts": [
{
"mode": "admission-only",
"selectedModel": "gemini-3.8-flash-low",
"startedAt": "2026-10-28T14:44:03.803Z",
"finishedAt": "2026-10-28T14:44:10.327Z",
"requests": {
"initialize": 1,
"new": 1,
"load": 0,
"modelSetter": 1,
"prompt": 0,
"cancel": 0,
"close": 0
},
"instructionBinding": {
"digest": "sha256:c8104d8f48056bab41a5a64aff3c16891538177127c28c1afd2c438c143b27b3",
"byteLength": 112
},
"admissionGuardDigest": "sha256:d6f427afae003db83e2942706fc4926c54abcedba1d421f1f41de292564eb557",
"runtimeClosure": "sha256:05ed2a3e39c95fc813abeb664a78a295d1dcbfd4098f28cbc17d30d710a1ea92",
"nativeSessionStarted": false,
"promptSent": false,
"admissionVerified": false,
"physicalRetirementVerified": true,
"leaseReleased": true,
"failure": {
"name": "Error",
"messageDigest": "sha256:8b6480a8eac6d058877054700dbd69654955e6061f961d23c8eddbbefc349818"
},
"eventCounts": {
"usage_update": 1,
"available_commands_update": 1
},
"status": "failed",
"classification": "product_defect_model_selector_projection",
"managedCredentialCleanup": true,
"databaseClientsClosed": true
},
{
"mode": "admission-only",
"selectedModel": "gemini-3.8-flash-low",
"startedAt": "2026-10-28T14:49:40.091Z",
"finishedAt": "2026-10-28T14:49:47.924Z",
"requests": {
"initialize": 1,
"new": 1,
"load": 0,
"modelSetter": 0,
"prompt": 0,
"cancel": 0,
"close": 0
},
"instructionBinding": {
"digest": "sha256:90be316d38e1158284c4e9d88d3624e9d7e3fca30d03c993da7a7bfa9e080704",
"byteLength": 112
},
"admissionGuardDigest": "sha256:31b9dd005c7452b61e61f3a29c77f74d7f16b20511d1e0f9eff10dfb649dca4b",
"runtimeClosure": "sha256:05ed2a3e39c95fc813abeb664a78a295d1dcbfd4098f28cbc17d30d710a1ea92",
"nativeSessionStarted": true,
"promptSent": false,
"admissionVerified": true,
"physicalRetirementVerified": true,
"leaseReleased": true,
"eventCounts": {
"usage_update": 1,
"available_commands_update": 1
},
"status": "passed",
"classification": "native_admission_only",
"managedCredentialCleanup": true,
"databaseClientsClosed": true
},
{
"mode": "prompt",
"selectedModel": "gemini-3.8-flash-low",
"startedAt": "2026-10-28T15:00:07.367Z",
"finishedAt": "2026-10-28T15:00:34.155Z",
"requests": {
"initialize": 1,
"new": 1,
"load": 0,
"modelSetter": 0,
"prompt": 1,
"cancel": 0,
"close": 0
},
"instructionBinding": {
"digest": "sha256:9cfa6fda4f2135354bf87b2e68a781afa4348ac86a6c59c542345c71081a0794",
"byteLength": 650
},
"admissionGuardDigest": "sha256:31b9dd005c7452b61e61f3a29c77f74d7f16b20511d1e0f9eff10dfb649dca4b",
"runtimeClosure": "sha256:05ed2a3e39c95fc813abeb664a78a295d1dcbfd4098f28cbc17d30d710a1ea92",
"nativeSessionStarted": true,
"promptSent": true,
"admissionVerified": true,
"physicalRetirementVerified": true,
"leaseReleased": true,
"eventCounts": {
"usage_update": 4,
"available_commands_update": 1,
"tool_call": 2,
"tool_call_update": 3,
"agent_message_chunk": 5
},
"toolEvents": [
{
"tag": "tool_call",
"idHash": "sha256:6274c2f18aca82ccf069e6905297f7a6ae84aab405642af9536f2ca1c3420cd7",
"status": "pending",
"kind": "edit"
},
{
"tag": "tool_call_update",
"idHash": "sha256:6274c2f18aca82ccf069e6905297f7a6ae84aab405642af9536f2ca1c3420cd7",
"status": "in_progress",
"kind": "edit"
},
{
"tag": "tool_call",
"idHash": "sha256:9d9bcb0687bfffd32d7e3c008f6ab55a6ef828d58cc7cb8b6a703054c6182a85",
"status": "in_progress",
"kind": "read"
},
{
"tag": "tool_call_update",
"idHash": "sha256:6274c2f18aca82ccf069e6905297f7a6ae84aab405642af9536f2ca1c3420cd7",
"status": "completed"
},
{
"tag": "tool_call_update",
"idHash": "sha256:9d9bcb0687bfffd32d7e3c008f6ab55a6ef828d58cc7cb8b6a703054c6182a85",
"status": "completed"
}
],
"clientOperations": {
"fs/write_text_file:running": 1,
"fs/write_text_file:completed": 1,
"fs/read_text_file:running": 1,
"fs/read_text_file:completed": 1
},
"permissions": {
"allowed": 1,
"denied": 0
},
"builtinToolAllowlist": [
"finish"
],
"stream": {
"events": 19,
"outputChunks": 5,
"outputBytes": 253,
"thoughtChunks": 0,
"outputSha256": "sha256:f34b143b56dbb6da47c6ca4361e46c21144316cc8f9ff98494c75416ac7e5fde"
},
"usage": {
"used": 5866,
"size": 1048576
},
"turnResult": {
"status": "completed",
"stopReason": "end_turn"
},
"independentOutput": {
"byteLength": 40,
"sha256": "sha256:79c8d4304612d981c7d8946b523e29af4bbc58b18cca8bc128d284e9a50d651e",
"expectedSha256": "sha256:79c8d4304612d981c7d8946b523e29af4bbc58b18cca8bc128d284e9a50d651e",
"equal": true
},
"status": "passed",
"classification": "native_file_tools_and_instruction_behavior",
"managedCredentialCleanup": true,
"databaseClientsClosed": true
}
],
"remainingRequired": [
"ordinary_product_task_and_artifact",
"scoped_semantic_MCP_tools",
"native_question_and_permission_round_trips",
"stop_and_spontaneous_child_loss",
"loaded_session_instruction_behavior",
"normal_agent_setup_Test_reconnect_revoke"
]
}
@@ -0,0 +1,45 @@
# Antigravity candidate identity refresh — 2026-10-29
The scoped Antigravity admission changes shared Runner sources. Grok 9, Pi 32,
and Copilot 73 capture that first source snapshot; the later native conversation
continuation correction changes only Copilot's bound inputs, so its final active
identity advances to 74. The subsequent native package authority correction
changes that same bound transport source, advancing the active identity to 75.
All earlier versioned fixtures remain immutable and
decodable, including those interim revisions.
Grok uses `scripts/grok-profile-identity.mjs`; Copilot uses
`scripts/copilot-profile-identity.mjs`. Both source-bound identities pass their
maintained checks. Pi has no standalone identity generator: Pi 32 derives from
the existing Pi 31 declaration, advances `agentProfileVersion`, rehashes its
declared source fields, and hashes recursively sorted canonical JSON with array
order preserved. All declared source hashes remain unchanged; only its profile
version advances in this coordinated snapshot. Its native wrapper, helper,
extension, platform closure, and other declared contracts remain unchanged.
The installation tests verify those source hashes,
canonical declaration digest, immutable Pi 31 bytes, and historical decoding.
The Antigravity distribution declaration, command digest, and native patch
`1.4.3-paperclip.4` remain unchanged. This identity refresh supplies build
provenance; it does not transfer prior live qualification to changed sources or
mark the pending Antigravity profile qualified.
## Subsequent native .5 candidate
The paragraph above describes the earlier `.4` identity-refresh snapshot and
remains its historical record. The later `1.4.3-paperclip.5` native patch closes
each client file-operation result by its exact call/step identity after delivery.
A genuine finish failure no longer broadcasts failure onto earlier completed
edit/read activities. Its maintained profile command digest is
`sha256:acf5f574b6b9ec4388ff9d081890caa330e66bf1ae3fe3b08e7f0fb0e851b9ce`
and its macOS ARM64 native closure is
`sha256:044792d793e203bc8b4524a418f1816c4d34db67bb41fa4bae5947a050c49b4a`.
Earlier `.4` declarations, artifacts and attempt grades remain immutable.
The [recorded setup and follow-up evidence](2026-10-28-antigravity-integration.md)
retains the fresh `.5` additional-agent setup, instruction-only file proof and
same-task second turn. Independent actual bytes, exact low model, native
conversation identity, accepted/committed Done lineage and physical retirement
were checked for that local saved-subscription attempt. This evidence does not
transfer prior qualifications across source identities or qualify fresh OAuth,
API-key operation, another platform, remote execution or a global release.
+15
View File
@@ -214,6 +214,21 @@ are unavailable. Accepting a native plan ends the planning run successfully whil
the task waits for the next instruction. See the
[Cursor release report](../../doc/plans/2026-10-03-cursor-production-readiness.md).
Google Antigravity is an **unqualified, pending** native ACPX integration. The
explicit `paperclipai runtime setup antigravity` command installs the verified
Antigravity ACP 1.4.3 distribution and its owned admission patch on macOS ARM64.
It verifies both the official vendor closure and the separately pinned patched
closure; npm installation downloads no Antigravity runtime. This setup installs
assets only. Global release qualification remains pending and normal execution
admission stays closed. An isolated operator can admit one company, exact model,
local platform and pinned native closure/profile until a fixed expiry through
trusted server startup authority. The candidate supports a managed saved Google
subscription; the recorded acceptance reused the same owner's personal default
grant and `gemini-3.8-flash-low` on macOS ARM64. This does not qualify fresh Google
consent or the separate API-key route. Google Workspace OAuth does not substitute
for a runtime credential. See the
[integration contract and evidence](../../doc/plans/2026-10-28-antigravity-integration.md).
A release includes all four platform daemons and the Linux provider-pack identity
from its matching Daytona image. Run `stage:release-binaries` with a manifest that
binds each daemon path and SHA-256, plus `remoteProviderPacks: {"linux-x64": {path, sha256}, "linux-arm64": {path, sha256}}` for
@@ -0,0 +1,70 @@
{
"schema": "paperclip.antigravity_distribution.v1",
"version": "1.4.3",
"platforms": {
"darwin-arm64": {
"url": "https://dl.google.com/agy-extensions/releases/macos/agy-acp-server-1.4.3-darwin-arm64.zip",
"archiveSha256": "c57db0a9b8b712daa9c3c17cdb990a61842ea569f279e8bd79d7aa85a303b20b",
"closureSha256": "044792d793e203bc8b4524a418f1816c4d34db67bb41fa4bae5947a050c49b4a",
"provisioning": "verified-closure",
"executable": "agy_acp_server.par",
"args": [],
"entries": [
{
"path": "agy_acp_server.par",
"sha256": "c5231c13eeb6a9fd554567e5ce6411d95fe8c2fe952302bbd52d7b81a4e63f15",
"size": 280804016,
"executable": true
},
{
"path": "localharness_external",
"sha256": "9e8fdc6e20a3262ca0a771850ca4a8414ca839a642cc964ec5c547cc2c33c200",
"size": 122957936,
"executable": true
}
],
"vendorEntries": [
{
"path": "agy_acp_server.par",
"sha256": "1251927db3e21f3d693fd042d6890d82f65eb00587e97f1f3d8f0d798e7f0954",
"size": 282446160,
"executable": true
},
{
"path": "localharness_external",
"sha256": "9e8fdc6e20a3262ca0a771850ca4a8414ca839a642cc964ec5c547cc2c33c200",
"size": 122957936,
"executable": true
}
],
"vendorClosureSha256": "023881d9f3b460adb22634c625725657358679e08c085156cc6de35e5f87b3c2",
"patchVersion": "1.4.3-paperclip.5",
"admissionSourceSha256": "d660759a424ddc27ac382ce4512e85ca4eb87e5ca446ea111e093d3ab257f7da"
},
"darwin-x64": {
"url": "https://dl.google.com/agy-extensions/releases/macos/agy-acp-server-1.4.3-darwin-x86_64.zip",
"archiveSha256": "9392dbbdad7f0663dbfb5a0bf641297b0cf80d2ee9cd2d49a616bbbc7378731d",
"provisioning": "inventory-only",
"executable": "agy_acp_server.par",
"args": []
},
"linux-x64": {
"url": "https://dl.google.com/agy-extensions/releases/linux/agy-acp-server-1.4.3-linux-x86_64.zip",
"archiveSha256": "0634ce5306ba92c932945455e7a83fda78e2d99c33fa8aa6ee0566206a382714",
"provisioning": "inventory-only",
"executable": "agy_acp_server.par",
"args": [
"--uid="
]
},
"linux-arm64": {
"url": "https://dl.google.com/agy-extensions/releases/linux/agy-acp-server-1.4.3-linux-arm64.zip",
"archiveSha256": "05c153443b0b1175151c760eeddea538950bc9843e317d54786a1f03670182bc",
"provisioning": "inventory-only",
"executable": "agy_acp_server.par",
"args": [
"--uid="
]
}
}
}
@@ -0,0 +1,22 @@
import { homedir } from "node:os";
import { dirname, isAbsolute, resolve } from "node:path";
import { fileURLToPath } from "node:url";
export function antigravityRuntimeCachePath(closureSha256, platform = process.platform, architecture = process.arch, home = homedir()) {
if (!/^[a-f0-9]{64}$/.test(closureSha256) || !["darwin", "linux"].includes(platform) || !["arm64", "x64"].includes(architecture) || !isAbsolute(home)) throw new Error("Invalid Antigravity runtime cache identity");
return resolve(home, ".paperclip/runtimes/antigravity", `${platform}-${architecture}`, closureSha256);
}
export function antigravityProvisionerPackageRoot(moduleUrl) {
const url = new URL(moduleUrl);
if (url.protocol !== "file:" || url.search || url.hash) throw new Error("Antigravity setup requires a published provisioner");
const path = fileURLToPath(url);
if (/\/dist\/vendor\/paperclip-runner\/cli\/provision-antigravity\.(?:cjs|js)$/.test(path)) return resolve(dirname(path), "..");
if (/\/dist\/cli\/provision-antigravity\.(?:cjs|js)$/.test(path)) return resolve(dirname(path), "../..");
throw new Error("Antigravity setup requires a published provisioner");
}
export function antigravityProvisionerDestination(moduleUrl, distribution, home) {
antigravityProvisionerPackageRoot(moduleUrl);
return antigravityRuntimeCachePath(distribution.closureSha256, distribution.platform, distribution.architecture, home);
}
@@ -0,0 +1,19 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { antigravityProvisionerDestination, antigravityProvisionerPackageRoot, antigravityRuntimeCachePath } from "./antigravity-provisioner-layout.mjs";
test("both published setup layouts share the OS-user cache", () => {
const distribution = { platform: "darwin", architecture: "arm64", closureSha256: "a".repeat(64) };
for (const root of ["/usr/local/lib/node_modules", "/Users/operator/project/node_modules"]) {
for (const suffix of ["@paperclipai/server/dist/vendor/paperclip-runner/cli/provision-antigravity.cjs", "@paperclipai/paperclip-runner/dist/cli/provision-antigravity.cjs"]) {
const url = `file://${root}/${suffix}`;
assert.equal(antigravityProvisionerDestination(url, distribution, "/Users/operator"), `/Users/operator/.paperclip/runtimes/antigravity/darwin-arm64/${distribution.closureSha256}`);
assert.match(antigravityProvisionerPackageRoot(url), /paperclip-runner$/);
}
}
});
test("foreign layouts and cache identities cannot redirect setup", () => {
for (const url of ["file:///repo/scripts/provision-antigravity.mjs", "file:///tmp/provision-antigravity.cjs", "https://example.com/dist/cli/provision-antigravity.js", "file:///consumer/dist/cli/provision-antigravity.js?redirect=1"]) assert.throws(() => antigravityProvisionerPackageRoot(url), /published provisioner/);
for (const values of [["../escape", "darwin", "arm64", "/Users/operator"], ["a".repeat(64), "../escape", "arm64", "/Users/operator"], ["a".repeat(64), "darwin", "arm64", "relative-home"]]) assert.throws(() => antigravityRuntimeCachePath(...values), /cache identity/);
});
@@ -8,6 +8,12 @@ import { build } from "esbuild";
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
export const verifiedProviderEntrypoints = Object.freeze([
Object.freeze({
name: "provision-antigravity",
source: resolve(packageRoot, "scripts/provision-antigravity.mjs"),
output: resolve(packageRoot, "dist/cli/provision-antigravity.js"),
verifiedOutput: resolve(packageRoot, "dist/cli/provision-antigravity.cjs"),
}),
Object.freeze({
name: "provision-cursor",
source: resolve(packageRoot, "scripts/provision-cursor.mjs"),
@@ -60,6 +66,9 @@ export async function bundleVerifiedProviderEntrypoints({ write = true } = {}) {
await mkdir(resolve(packageRoot, "dist/providers"), { recursive: true });
await cp(resolve(packageRoot, "src/providers"), resolve(packageRoot, "dist/providers"), { recursive: true });
await copyFile(resolve(packageRoot, "cursor-distributions.json"), resolve(packageRoot, "dist/cursor-distributions.json"));
await copyFile(resolve(packageRoot, "antigravity-distributions.json"), resolve(packageRoot, "dist/antigravity-distributions.json"));
await mkdir(resolve(packageRoot, "dist/cli"), { recursive: true });
await copyFile(resolve(packageRoot, "scripts/patch-antigravity-runtime.admission.py"), resolve(packageRoot, "dist/cli/patch-antigravity-runtime.admission.py"));
}
const results = [];
for (const entrypoint of verifiedProviderEntrypoints) {
@@ -41,3 +41,7 @@ test("written provider entrypoints satisfy qualified launch permissions", async
// Package-layout regression runs in the existing verified-entrypoint test lane.
import "./provision-pi-package.test.mjs";
import "./antigravity-provisioner-layout.test.mjs";
import "./materialize-antigravity-distribution.test.mjs";
import "./probe-antigravity-metadata.test.mjs";
import "./patch-antigravity-runtime.test.mjs";
@@ -0,0 +1,277 @@
import { createHash } from "node:crypto";
import { constants } from "node:fs";
import { chmod, lstat, mkdir, mkdtemp, open, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
import { basename, dirname, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { isDeepStrictEqual } from "node:util";
import { inflateRawSync } from "node:zlib";
import { ANTIGRAVITY_PATCH_VERSION, patchAntigravityRuntime } from "./patch-antigravity-runtime.mjs";
const MANIFEST_PATH = fileURLToPath(new URL("../antigravity-distributions.json", import.meta.url));
const ADMISSION_SOURCE_PATH = fileURLToPath(new URL("./patch-antigravity-runtime.admission.py", import.meta.url));
const MAX_ARCHIVE_BYTES = 256 * 1024 * 1024;
const MAX_TREE_BYTES = 1024 * 1024 * 1024;
export const ANTIGRAVITY_TREE_MANIFEST = ".paperclip-antigravity-closure.json";
const RUNTIME_FILES = ["agy_acp_server.par", "localharness_external"];
const SHA256 = /^[a-f0-9]{64}$/;
const digest = bytes => createHash("sha256").update(bytes).digest("hex");
function assertTrustedOwner(stat) {
const uid = process.getuid?.();
if (!Number.isInteger(uid) || !Number.isInteger(stat.uid) || (stat.uid !== uid && stat.uid !== 0)) {
throw new Error("Antigravity assets have an untrusted filesystem owner");
}
}
/** Resolve parent aliases once, then use only the checked canonical path.
* A root-owned sticky temporary directory protects each owned child entry. */
async function ownedDirectory(directory, { privateRoot = false, parentAlias = false } = {}) {
const original = await lstat(directory);
if ((!original.isDirectory() || original.isSymbolicLink()) && !(parentAlias && original.isSymbolicLink())) {
throw new Error("Antigravity closure must be a real directory with private write access");
}
const canonical = await realpath(directory);
let current = canonical;
let root;
while (true) {
const stat = await lstat(current);
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("Antigravity assets have an unsafe ancestor directory");
assertTrustedOwner(stat);
if (!root) root = stat;
const trustedSticky = stat.uid === 0 && (stat.mode & 0o1000) !== 0;
if ((stat.mode & 0o022) && !(trustedSticky && !(privateRoot && current === canonical))) {
throw new Error("Antigravity assets have an unsafe ancestor with shared write access; closures require private write access");
}
const parent = dirname(current);
if (parent === current) break;
current = parent;
}
if (!original.isSymbolicLink() && (root.dev !== original.dev || root.ino !== original.ino)) {
throw new Error("Antigravity asset directory changed during ownership verification");
}
return { directory: canonical, stat: root };
}
function assertOwnedFile(stat, maximum) {
if (!stat.isFile() || stat.nlink !== 1) throw new Error("Antigravity closure contains links or special files");
assertTrustedOwner(stat);
if (stat.mode & 0o7022) throw new Error("Antigravity closure contains unsafe file permissions");
if (stat.size > maximum) throw new Error("Antigravity asset exceeds its file size limit");
}
async function openOwnedFile(path, maximum) {
const parent = await ownedDirectory(dirname(path), { parentAlias: true });
const canonical = join(parent.directory, basename(path));
const before = await lstat(canonical);
assertOwnedFile(before, maximum);
const file = await open(canonical, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
try {
const stat = await file.stat();
assertOwnedFile(stat, maximum);
if (stat.dev !== before.dev || stat.ino !== before.ino) throw new Error("Antigravity asset changed while opening");
return { file, path: canonical, before: stat };
} catch (error) { await file.close(); throw error; }
}
async function readOwnedFile(path, maximum) {
const { file, before } = await openOwnedFile(path, maximum);
try {
const bytes = await file.readFile();
const after = await file.stat();
assertOwnedFile(after, maximum);
if (bytes.length > maximum || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) {
throw new Error("Antigravity asset changed while reading");
}
return bytes;
} finally { await file.close(); }
}
/** Reject an unsafe existing ancestor before creating even a staging directory. */
async function ownedDestination(destination) {
const missing = [];
let ancestor = dirname(destination);
while (true) {
try { await lstat(ancestor); break; }
catch (error) {
if (error.code !== "ENOENT") throw error;
missing.push(basename(ancestor));
ancestor = dirname(ancestor);
}
}
let { directory } = await ownedDirectory(ancestor, { parentAlias: true });
for (const name of missing.reverse()) {
directory = join(directory, name);
try { await mkdir(directory, { mode: 0o700 }); }
catch (error) { if (error.code !== "EEXIST") throw error; }
directory = (await ownedDirectory(directory)).directory;
}
return join(directory, basename(destination));
}
/** Only the checked-in declaration selects downloads and runtime contents. */
export async function antigravityDistribution(platform = process.platform, architecture = process.arch) {
const manifest = JSON.parse((await readOwnedFile(MANIFEST_PATH, 64 * 1024)).toString("utf8"));
const item = manifest.platforms?.[`${platform}-${architecture}`];
if (manifest.schema !== "paperclip.antigravity_distribution.v1" || !/^\d+\.\d+\.\d+$/.test(manifest.version)) throw new Error("Invalid Antigravity distribution declaration");
if (!item) throw new Error(`Antigravity has no pinned distribution for ${platform}/${architecture}`);
if (item.provisioning !== "verified-closure") throw new Error(`Antigravity ${platform}/${architecture} is inventory-only; its complete runtime closure is not verified`);
const url = new URL(item.url);
if (url.protocol !== "https:" || url.hostname !== "dl.google.com" || url.username || url.password || url.search || url.hash || !url.pathname.startsWith("/agy-extensions/releases/")) throw new Error("Antigravity requires an official pinned distribution URL");
if (!SHA256.test(item.archiveSha256) || !SHA256.test(item.closureSha256) || item.executable !== RUNTIME_FILES[0] || !Array.isArray(item.args) || item.args.some(arg => typeof arg !== "string")) throw new Error("Invalid Antigravity distribution pins");
for (const [entries, sha256] of [[item.entries, item.closureSha256], [item.vendorEntries, item.vendorClosureSha256]]) {
if (!Array.isArray(entries) || entries.length !== RUNTIME_FILES.length || entries.some((entry, index) => entry.path !== RUNTIME_FILES[index] || !SHA256.test(entry.sha256) || !Number.isSafeInteger(entry.size) || entry.size < 1 || entry.executable !== true) || digest(JSON.stringify(entries)) !== sha256) throw new Error("Invalid Antigravity complete execution closure pins");
}
if (item.patchVersion !== ANTIGRAVITY_PATCH_VERSION || !SHA256.test(item.admissionSourceSha256)) throw new Error("Antigravity distribution must pin its owned native admission patch");
if (digest(await readOwnedFile(ADMISSION_SOURCE_PATH, 64 * 1024)) !== item.admissionSourceSha256) throw new Error("Antigravity native admission patch source digest mismatch");
return { ...item, version: manifest.version, platform, architecture };
}
/** Hash both native binaries; a cached local manifest never establishes trust. */
export async function antigravityDistributionClosure(directory) {
const root = await ownedDirectory(directory, { privateRoot: true });
directory = root.directory;
const names = (await readdir(directory)).sort();
const files = names.filter(name => name !== ANTIGRAVITY_TREE_MANIFEST);
if (JSON.stringify(files) !== JSON.stringify(RUNTIME_FILES)) throw new Error("Antigravity closure must contain exactly both native binaries");
let bytes = 0;
const entries = [];
for (const name of names) {
const path = join(directory, name);
const maximum = name === ANTIGRAVITY_TREE_MANIFEST ? 64 * 1024 : MAX_TREE_BYTES;
const { file, before } = await openOwnedFile(path, maximum);
try {
if (name === ANTIGRAVITY_TREE_MANIFEST) continue;
bytes += before.size;
if (bytes > MAX_TREE_BYTES) throw new Error("Antigravity closure exceeds size limit");
const hash = createHash("sha256");
let length = 0;
for await (const chunk of file.createReadStream({ autoClose: false })) { hash.update(chunk); length += chunk.length; }
const after = await file.stat();
const current = await lstat(path);
assertOwnedFile(after, maximum);
if (before.ino !== current.ino || before.dev !== current.dev || before.size !== after.size || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || length !== after.size) throw new Error("Antigravity closure changed while being hashed");
entries.push({ path: name, sha256: hash.digest("hex"), size: length, executable: Boolean(after.mode & 0o111) });
} finally { await file.close(); }
}
const current = await ownedDirectory(directory, { privateRoot: true });
if (current.stat.dev !== root.stat.dev || current.stat.ino !== root.stat.ino) throw new Error("Antigravity closure directory changed while being hashed");
return { entries, sha256: digest(JSON.stringify(entries)) };
}
export async function verifyAntigravityDistribution(directory, distribution) {
directory = (await ownedDirectory(directory, { privateRoot: true })).directory;
const pinned = await antigravityDistribution(distribution?.platform, distribution?.architecture);
if (distribution && !isDeepStrictEqual(distribution, pinned)) throw new Error("Antigravity runtime verification requires the checked-in distribution pins");
const closure = await antigravityDistributionClosure(directory);
if (closure.sha256 !== pinned.closureSha256) throw new Error("Antigravity full execution closure digest mismatch");
const record = JSON.parse((await readOwnedFile(join(directory, ANTIGRAVITY_TREE_MANIFEST), 64 * 1024)).toString("utf8"));
if (!isDeepStrictEqual(record, closureRecord(pinned, closure))) throw new Error("Antigravity cached closure record does not match the checked-in pins");
return closure;
}
function closureRecord(distribution, closure) {
return { schema: "paperclip.antigravity_closure.v1", version: distribution.version, patchVersion: distribution.patchVersion, admissionSourceSha256: distribution.admissionSourceSha256,
archiveSha256: distribution.archiveSha256, vendorClosureSha256: distribution.vendorClosureSha256, closureSha256: distribution.closureSha256, entries: closure.entries };
}
/** Strictly inspect the known two-file ZIP before writing any vendor content. */
export function antigravityArchiveEntries(archive) {
const invalid = () => { throw new Error("Antigravity archive contains unsafe or unsupported ZIP entries"); };
if (!Buffer.isBuffer(archive) || archive.length < 22 || archive.length > MAX_ARCHIVE_BYTES) invalid();
// The pinned vendor archive has no comment, ZIP64, spanning, or data descriptors.
const end = archive.length - 22;
if (archive.readUInt32LE(end) !== 0x06054b50 || archive.readUInt16LE(end + 4) || archive.readUInt16LE(end + 6) || archive.readUInt16LE(end + 20)) invalid();
const count = archive.readUInt16LE(end + 10);
const centralSize = archive.readUInt32LE(end + 12);
const centralOffset = archive.readUInt32LE(end + 16);
if (count !== 2 || archive.readUInt16LE(end + 8) !== count || centralOffset + centralSize !== end) invalid();
let cursor = centralOffset;
const entries = [];
const spans = [];
let bytes = 0;
for (let index = 0; index < count; index++) {
if (cursor + 46 > end || archive.readUInt32LE(cursor) !== 0x02014b50) invalid();
const flags = archive.readUInt16LE(cursor + 8);
const method = archive.readUInt16LE(cursor + 10);
const compressedSize = archive.readUInt32LE(cursor + 20);
const size = archive.readUInt32LE(cursor + 24);
const nameLength = archive.readUInt16LE(cursor + 28);
const extraLength = archive.readUInt16LE(cursor + 30);
const commentLength = archive.readUInt16LE(cursor + 32);
const mode = archive.readUInt32LE(cursor + 38) >>> 16;
const local = archive.readUInt32LE(cursor + 42);
const next = cursor + 46 + nameLength + extraLength + commentLength;
if (next > end || (flags & ~0x800) || ![0, 8].includes(method) || archive.readUInt16LE(cursor + 34) || (mode & 0o170000) !== 0o100000 || !(mode & 0o111)) invalid();
const nameBytes = archive.subarray(cursor + 46, cursor + 46 + nameLength);
const path = nameBytes.toString("utf8");
if (!RUNTIME_FILES.includes(path) || !Buffer.from(path).equals(nameBytes) || entries.some(entry => entry.path === path)) invalid();
if (local + 30 > centralOffset || archive.readUInt32LE(local) !== 0x04034b50 || archive.readUInt16LE(local + 6) !== flags || archive.readUInt16LE(local + 8) !== method || archive.readUInt32LE(local + 14) !== archive.readUInt32LE(cursor + 16) || archive.readUInt32LE(local + 18) !== compressedSize || archive.readUInt32LE(local + 22) !== size) invalid();
const localNameLength = archive.readUInt16LE(local + 26);
const start = local + 30 + localNameLength + archive.readUInt16LE(local + 28);
const finish = start + compressedSize;
if (finish > centralOffset || !archive.subarray(local + 30, local + 30 + localNameLength).equals(nameBytes) || spans.some(([a, b]) => local < b && finish > a)) invalid();
bytes += size;
if (!size || bytes > MAX_TREE_BYTES || (method === 0 && size !== compressedSize)) invalid();
entries.push({ path, method, size, start, finish });
spans.push([local, finish]);
cursor = next;
}
if (cursor !== end || RUNTIME_FILES.some(path => !entries.some(entry => entry.path === path))) invalid();
return entries;
}
/** Explicit setup only: no vendor installer, ambient binary, or npm lifecycle. */
export async function materializePinnedAntigravityDistribution(options = {}) {
const distribution = await antigravityDistribution(options.platform, options.architecture);
if (!options.destination) throw new Error("Antigravity materialization requires an explicit destination");
const destination = await ownedDestination(resolve(options.destination));
const parent = await ownedDirectory(dirname(destination));
try { await lstat(destination); throw new Error("Antigravity destination already exists; refusing to overwrite it"); }
catch (error) { if (error.code !== "ENOENT") throw error; }
const temporary = await mkdtemp(join(dirname(destination), ".antigravity-install-"));
try {
let archive;
if (options.archivePath) {
archive = await readOwnedFile(options.archivePath, MAX_ARCHIVE_BYTES);
} else {
const response = await fetch(distribution.url, { redirect: "error", signal: AbortSignal.timeout(180_000) });
if (!response.ok || !response.body) throw new Error(`Antigravity archive download failed: ${response.status}`);
const chunks = [];
let length = 0;
for await (const chunk of response.body) {
length += chunk.length;
if (length > MAX_ARCHIVE_BYTES) throw new Error("Antigravity archive download exceeds size limit");
chunks.push(chunk);
}
archive = Buffer.concat(chunks);
}
if (digest(archive) !== distribution.archiveSha256) throw new Error("Antigravity archive digest mismatch");
const entries = antigravityArchiveEntries(archive);
const unpacked = join(temporary, "vendor");
await mkdir(unpacked, { mode: 0o700 });
for (const entry of entries) {
const compressed = archive.subarray(entry.start, entry.finish);
const content = entry.method === 0 ? compressed : inflateRawSync(compressed, { maxOutputLength: entry.size });
if (content.length !== entry.size) throw new Error("Antigravity archive entry size mismatch");
await writeFile(join(unpacked, entry.path), content, { mode: 0o500, flag: "wx" });
}
const vendorClosure = await antigravityDistributionClosure(unpacked);
if (vendorClosure.sha256 !== distribution.vendorClosureSha256) throw new Error("Antigravity vendor execution closure digest mismatch");
const patched = join(temporary, "runtime");
const patch = await patchAntigravityRuntime({ source: unpacked, destination: patched });
if (patch.version !== distribution.patchVersion || patch.admissionSourceSha256 !== distribution.admissionSourceSha256) throw new Error("Antigravity native admission patch identity mismatch");
const closure = await antigravityDistributionClosure(patched);
if (closure.sha256 !== distribution.closureSha256) throw new Error("Antigravity full execution closure digest mismatch");
await writeFile(join(patched, ANTIGRAVITY_TREE_MANIFEST), `${JSON.stringify(closureRecord(distribution, closure))}\n`, { mode: 0o400, flag: "wx" });
for (const entry of closure.entries) await chmod(join(patched, entry.path), 0o500);
const currentParent = await ownedDirectory(dirname(destination));
if (currentParent.stat.dev !== parent.stat.dev || currentParent.stat.ino !== parent.stat.ino) throw new Error("Antigravity cache parent changed before publication");
await rename(patched, destination);
return { ...distribution, destination, executable: join(destination, distribution.executable) };
} finally { await rm(temporary, { recursive: true, force: true }); }
}
if (process.argv[1]?.endsWith("/materialize-antigravity-distribution.mjs") && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
const [destination, platform, architecture, archivePath] = process.argv.slice(2);
materializePinnedAntigravityDistribution({ destination, platform, architecture, archivePath }).then(result => process.stdout.write(`${JSON.stringify(result)}\n`)).catch(error => { console.error(error.message); process.exitCode = 1; });
}
@@ -0,0 +1,324 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { chmod, copyFile, link, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";
import { pathToFileURL } from "node:url";
import { antigravityArchiveEntries, antigravityDistribution } from "./materialize-antigravity-distribution.mjs";
const hash = bytes => createHash("sha256").update(bytes).digest("hex");
const files = [
{ path: "agy_acp_server.par", content: Buffer.from("native ACP server fixture\n") },
{ path: "localharness_external", content: Buffer.from("owned native child fixture\n") },
];
// A minimal stored ZIP fixture exercises the real inspector and filesystem.
// Integrity comes from the checked-in archive and complete-file SHA-256 pins.
function zip(entries = files) {
const locals = [];
const centrals = [];
let offset = 0;
for (const entry of entries) {
const name = Buffer.from(entry.path);
const content = entry.content ?? Buffer.from("fixture");
const local = Buffer.alloc(30);
local.writeUInt32LE(0x04034b50);
local.writeUInt16LE(20, 4);
local.writeUInt32LE(content.length, 18);
local.writeUInt32LE(content.length, 22);
local.writeUInt16LE(name.length, 26);
const central = Buffer.alloc(46);
central.writeUInt32LE(0x02014b50);
central.writeUInt16LE(0x314, 4);
central.writeUInt16LE(20, 6);
central.writeUInt32LE(content.length, 20);
central.writeUInt32LE(content.length, 24);
central.writeUInt16LE(name.length, 28);
central.writeUInt32LE(((entry.mode ?? 0o100755) << 16) >>> 0, 38);
central.writeUInt32LE(offset, 42);
locals.push(local, name, content);
centrals.push(central, name);
offset += local.length + name.length + content.length;
}
const central = Buffer.concat(centrals);
const end = Buffer.alloc(22);
end.writeUInt32LE(0x06054b50);
end.writeUInt16LE(entries.length, 8);
end.writeUInt16LE(entries.length, 10);
end.writeUInt32LE(central.length, 12);
end.writeUInt32LE(offset, 16);
return Buffer.concat([...locals, central, end]);
}
async function fixture(t, { expectedFiles = files, expectedVendorFiles = files, archive = zip(), patchSuffix = "", baseDirectory = tmpdir(), changeParentDuringPatch = false } = {}) {
const root = await realpath(await mkdtemp(join(baseDirectory, "paperclip-antigravity-provision-test-")));
t.after(() => rm(root, { recursive: true, force: true }));
const scripts = join(root, "scripts");
await mkdir(scripts);
for (const name of ["materialize-antigravity-distribution.mjs", "antigravity-provisioner-layout.mjs", "provision-antigravity.mjs"]) await copyFile(new URL(name, import.meta.url), join(scripts, name));
const admissionSource = "# trusted test admission source\n";
const admissionSourceSha256 = hash(admissionSource);
await writeFile(join(scripts, "patch-antigravity-runtime.admission.py"), admissionSource);
await writeFile(join(scripts, "patch-antigravity-runtime.mjs"), `
import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
export const ANTIGRAVITY_PATCH_VERSION='1.4.3-paperclip.1';
export async function patchAntigravityRuntime({source,destination}) {
await writeFile(${JSON.stringify(join(root, "patch.called"))},'called');
await mkdir(destination,{mode:0o700});
for(const name of ['agy_acp_server.par','localharness_external']) {
const content=await readFile(join(source,name));
await writeFile(join(destination,name),name==='agy_acp_server.par'?Buffer.concat([content,Buffer.from(${JSON.stringify(patchSuffix)})]):content,{mode:0o500});
}
${changeParentDuringPatch ? `await chmod(${JSON.stringify(join(root, "home/.paperclip/runtimes/antigravity/darwin-arm64"))},0o777);` : ""}
return {version:ANTIGRAVITY_PATCH_VERSION,admissionSourceSha256:${JSON.stringify(admissionSourceSha256)}};
}
`);
const entries = expectedFiles.map(entry => ({ path: entry.path, sha256: hash(entry.content), size: entry.content.length, executable: true }));
const closureSha256 = hash(JSON.stringify(entries));
const vendorEntries = expectedVendorFiles.map(entry => ({ path: entry.path, sha256: hash(entry.content), size: entry.content.length, executable: true }));
// The code is copied into an isolated package so its normal fixed manifest
// read uses a tiny trusted fixture. Production has no manifest-path override.
await writeFile(join(root, "antigravity-distributions.json"), JSON.stringify({
schema: "paperclip.antigravity_distribution.v1", version: "1.4.3",
platforms: { "darwin-arm64": {
url: "https://dl.google.com/agy-extensions/releases/macos/fixture.zip",
archiveSha256: hash(archive), closureSha256, vendorEntries, vendorClosureSha256: hash(JSON.stringify(vendorEntries)), patchVersion: "1.4.3-paperclip.1", admissionSourceSha256, provisioning: "verified-closure",
executable: "agy_acp_server.par", args: [], entries,
} },
}));
const archivePath = join(root, "fixture.zip");
await writeFile(archivePath, archive);
const api = await import(pathToFileURL(join(scripts, "materialize-antigravity-distribution.mjs")));
const provisioner = await import(pathToFileURL(join(scripts, "provision-antigravity.mjs")));
const home = join(root, "home");
const destination = join(home, ".paperclip/runtimes/antigravity/darwin-arm64", closureSha256);
const options = { platform: "darwin", architecture: "arm64", archivePath, destination };
const provision = () => provisioner.provisionAntigravityRuntime({ ...options, home, moduleUrl: "file:///consumer/node_modules/@paperclipai/paperclip-runner/dist/cli/provision-antigravity.cjs" });
return { root, destination, options, api, provision, distribution: await api.antigravityDistribution("darwin", "arm64") };
}
test("the inspected native mac ARM closure is pinned; other platforms remain inventory-only", async () => {
const pin = await antigravityDistribution("darwin", "arm64");
assert.equal(pin.version, "1.4.3");
assert.deepEqual(pin.entries.map(entry => entry.path), files.map(entry => entry.path));
assert.equal(hash(JSON.stringify(pin.entries)), pin.closureSha256);
for (const [platform, architecture] of [["darwin", "x64"], ["linux", "x64"], ["linux", "arm64"]]) await assert.rejects(antigravityDistribution(platform, architecture), /inventory-only/);
await assert.rejects(antigravityDistribution("win32", "x64"), /no pinned distribution/);
});
test("ZIP inspection rejects traversal, absolute names, duplicates, links and partial closure", () => {
assert.deepEqual(antigravityArchiveEntries(zip()).map(entry => entry.path), files.map(entry => entry.path));
for (const entries of [
[{ ...files[0], path: "../agy_acp_server.par" }, files[1]],
[{ ...files[0], path: "/agy_acp_server.par" }, files[1]],
[{ ...files[0], path: "dir\\agy_acp_server.par" }, files[1]],
[files[0], files[0]],
[{ ...files[0], mode: 0o120755 }, files[1]],
[{ ...files[0], mode: 0o060755 }, files[1]],
[files[0]],
]) assert.throws(() => antigravityArchiveEntries(zip(entries)), /unsafe or unsupported ZIP/);
});
test("ZIP inspection rejects disagreeing headers and overlapping payloads", () => {
const disagreeing = zip();
disagreeing.writeUInt16LE(8, 8);
assert.throws(() => antigravityArchiveEntries(disagreeing), /unsafe or unsupported ZIP/);
const overlapping = zip();
const end = overlapping.length - 22;
const central = overlapping.readUInt32LE(end + 16);
const secondCentral = central + 46 + Buffer.byteLength(files[0].path);
overlapping.writeUInt32LE(0, secondCentral + 42);
assert.throws(() => antigravityArchiveEntries(overlapping), /unsafe or unsupported ZIP/);
});
test("a corrupted archive never installs and its temporary directory is removed", async t => {
const { api, options, destination } = await fixture(t);
await writeFile(options.archivePath, "corrupt archive");
await assert.rejects(api.materializePinnedAntigravityDistribution(options), /archive digest mismatch/);
await assert.rejects(lstat(destination), { code: "ENOENT" });
assert.deepEqual(await readdir(join(destination, "..")), []);
});
test("installation preserves and verifies the complete two-binary closure", async t => {
const { api, options, destination, distribution } = await fixture(t);
const result = await api.materializePinnedAntigravityDistribution(options);
assert.equal(result.executable, join(destination, files[0].path));
assert.equal(result.closureSha256, distribution.closureSha256);
for (const file of files) {
assert.deepEqual(await readFile(join(destination, file.path)), file.content);
assert.equal((await lstat(join(destination, file.path))).mode & 0o777, 0o500);
}
assert.equal((await api.verifyAntigravityDistribution(destination, distribution)).sha256, distribution.closureSha256);
assert.deepEqual(await readdir(join(destination, "..")), [distribution.closureSha256]);
await assert.rejects(api.materializePinnedAntigravityDistribution(options), /already exists/);
});
test("a failed full-closure check leaves no partial installation", async t => {
const expectedFiles = files.map(file => ({ ...file, content: Buffer.from(`wrong expected ${file.path}`) }));
const { api, options, destination } = await fixture(t, { expectedFiles });
await assert.rejects(api.materializePinnedAntigravityDistribution(options), /full execution closure digest mismatch/);
await assert.rejects(lstat(destination), { code: "ENOENT" });
assert.deepEqual(await readdir(join(destination, "..")), []);
});
test("verified vendor bytes are patched and only the separately pinned final closure is installed", async t => {
const patchSuffix = "\nowned native admission patch\n";
const expectedFiles = files.map((file, index) => ({ ...file, content: index === 0 ? Buffer.concat([file.content, Buffer.from(patchSuffix)]) : file.content }));
const { api, options, destination, root, distribution } = await fixture(t, { expectedFiles, patchSuffix });
await api.materializePinnedAntigravityDistribution(options);
assert.equal(await readFile(join(root, "patch.called"), "utf8"), "called");
assert.notEqual(distribution.vendorClosureSha256, distribution.closureSha256);
assert.deepEqual(await readFile(join(destination, files[0].path)), expectedFiles[0].content);
assert.deepEqual(await readFile(join(destination, files[1].path)), files[1].content);
await api.verifyAntigravityDistribution(destination, distribution);
});
test("vendor closure mismatch stops before the owned patch executes", async t => {
const expectedVendorFiles = files.map(file => ({ ...file, content: Buffer.from(`wrong vendor ${file.path}`) }));
const { api, options, root, destination } = await fixture(t, { expectedVendorFiles });
await assert.rejects(api.materializePinnedAntigravityDistribution(options), /vendor execution closure digest mismatch/);
await assert.rejects(readFile(join(root, "patch.called")), { code: "ENOENT" });
await assert.rejects(lstat(destination), { code: "ENOENT" });
assert.deepEqual(await readdir(join(destination, "..")), []);
});
test("changed admission patch source is rejected before extraction or native execution", async t => {
const { api, options, root, destination } = await fixture(t);
await writeFile(join(root, "scripts/patch-antigravity-runtime.admission.py"), "# changed admission source\n");
await assert.rejects(api.materializePinnedAntigravityDistribution(options), /patch source digest mismatch/);
await assert.rejects(lstat(destination), { code: "ENOENT" });
await assert.rejects(readFile(join(root, "patch.called")), { code: "ENOENT" });
});
test("repeated setup verifies installed bytes and refuses corruption without download or replacement", async t => {
const { provision, destination, options } = await fixture(t);
await provision();
await rm(options.archivePath);
await provision();
const binary = join(destination, files[1].path);
await chmod(binary, 0o700);
await writeFile(binary, "corrupted native child");
await assert.rejects(provision(), /full execution closure digest mismatch/);
assert.equal(await readFile(binary, "utf8"), "corrupted native child");
});
test("rewriting the cached record or passing altered pins cannot authorize changed bytes", async t => {
const { provision, api, destination, distribution } = await fixture(t);
await provision();
const recordPath = join(destination, api.ANTIGRAVITY_TREE_MANIFEST);
await chmod(recordPath, 0o600);
await writeFile(recordPath, JSON.stringify({ ...distribution, closureSha256: "a".repeat(64) }));
await assert.rejects(provision(), /cached closure record/);
await assert.rejects(api.verifyAntigravityDistribution(destination, { ...distribution, closureSha256: "a".repeat(64) }), /checked-in distribution pins/);
});
test("cached links, hardlinks, extra files and missing children fail closed", async t => {
for (const kind of ["symlink", "hardlink", "extra", "missing", "record-link"]) {
const current = await fixture(t);
await current.provision();
const child = join(current.destination, files[1].path);
if (kind === "extra") await writeFile(join(current.destination, "unexpected-native-child"), "unexpected");
else if (kind === "record-link") {
const record = join(current.destination, current.api.ANTIGRAVITY_TREE_MANIFEST);
const target = join(current.root, "record-copy.json");
await copyFile(record, target);
await rm(record);
await symlink(target, record);
} else {
const target = join(current.root, "native-child-copy");
await copyFile(child, target);
await rm(child);
if (kind === "symlink") await symlink(target, child);
if (kind === "hardlink") await link(target, child);
}
await assert.rejects(current.provision(), /both native binaries|links or special files/);
}
});
test("cached assets with shared write access and linked runtime roots fail closed", async t => {
const { provision, api, destination, distribution, root } = await fixture(t);
await provision();
const binary = join(destination, files[1].path);
await chmod(binary, 0o777);
await assert.rejects(provision(), /unsafe file permissions/);
await chmod(binary, 0o500);
const linked = join(root, "linked-runtime");
await symlink(destination, linked);
await assert.rejects(api.verifyAntigravityDistribution(linked, distribution), /real directory/);
await chmod(destination, 0o777);
await assert.rejects(provision(), /private write access/);
await chmod(destination, 0o700);
});
test("a private cached closure is rejected when another user could replace its parent entry", async t => {
const { provision, destination } = await fixture(t);
await provision();
const parent = join(destination, "..");
await chmod(parent, 0o777);
try { await assert.rejects(provision(), /ancestor.*write|unsafe.*ancestor/); }
finally { await chmod(parent, 0o700); }
});
test("publication refuses a replaceable cache parent before patching or creating a temporary installation", async t => {
const { api, options, destination, root } = await fixture(t);
const parent = join(destination, "..");
await mkdir(parent, { recursive: true, mode: 0o700 });
await chmod(parent, 0o777);
try {
await assert.rejects(api.materializePinnedAntigravityDistribution(options), /ancestor.*write|unsafe.*ancestor/);
await assert.rejects(readFile(join(root, "patch.called")), { code: "ENOENT" });
assert.deepEqual(await readdir(parent), []);
} finally { await chmod(parent, 0o700); }
});
test("package pin sources beneath a replaceable ancestor cannot authorize installation", async t => {
const { api, options, root } = await fixture(t);
await chmod(root, 0o777);
try { await assert.rejects(api.materializePinnedAntigravityDistribution(options), /ancestor.*write|unsafe.*ancestor/); }
finally { await chmod(root, 0o700); }
});
test("canonical parent aliases keep a private owned installation valid", async t => {
const { api, options, distribution, root } = await fixture(t);
const home = join(root, "private-home");
await mkdir(home, { mode: 0o700 });
const alias = join(root, "home-alias");
await symlink(home, alias);
const destination = join(alias, "cache", distribution.closureSha256);
await api.materializePinnedAntigravityDistribution({ ...options, destination });
assert.equal((await api.verifyAntigravityDistribution(destination, distribution)).sha256, distribution.closureSha256);
});
test("a root-owned sticky temporary ancestor protects the private installation", async t => {
const temporary = await realpath("/tmp");
const stat = await lstat(temporary);
if (stat.uid !== 0 || !(stat.mode & 0o1000)) return t.skip("requires the host's root-owned sticky temporary directory");
const { provision, api, destination, distribution } = await fixture(t, { baseDirectory: "/tmp" });
await provision();
assert.equal((await api.verifyAntigravityDistribution(destination, distribution)).sha256, distribution.closureSha256);
});
test("source and cached ownership are checked for the current execution principal", async t => {
const { api, provision, destination } = await fixture(t);
await provision();
const getuid = process.getuid;
if (!getuid || getuid() === 0) return t.skip("requires a non-root principal to model foreign ownership without changing filesystem privileges");
// Keep the real kernel metadata; model a different verifier principal rather
// than chowning files or running another user's process during the test.
process.getuid = () => getuid() + 1;
try {
await assert.rejects(api.antigravityDistributionClosure(destination), /untrusted filesystem owner/);
await assert.rejects(api.antigravityDistribution("darwin", "arm64"), /untrusted filesystem owner/);
} finally { process.getuid = getuid; }
});
test("a parent made replaceable while patching cannot receive a published runtime", async t => {
const { api, options, destination } = await fixture(t, { changeParentDuringPatch: true });
try {
await assert.rejects(api.materializePinnedAntigravityDistribution(options), /ancestor.*write|unsafe.*ancestor/);
await assert.rejects(lstat(destination), { code: "ENOENT" });
assert.deepEqual(await readdir(join(destination, "..")), []);
} finally { await chmod(join(destination, ".."), 0o700); }
});
@@ -0,0 +1,442 @@
"""Paperclip-owned admission for the pinned Antigravity 1.4.3 native runtime.
Loaded inside Google's hermetic distribution. No credentials or prompt content
are persisted in receipts. Native creation must acknowledge the exact config
before the ACP client receives authority to send a managed prompt.
"""
import asyncio
import contextvars
import ctypes
import dataclasses
import functools
import hashlib
import os
import stat
import re
import uuid
from acp import exceptions
from google3.cloud.developer_experience.antigravity_extensions.acp_server import request_meta
from google3.cloud.developer_experience.antigravity_extensions.acp_server import system_prompt
from google3.cloud.developer_experience.antigravity_extensions.acp_server import hooks as acp_hooks
from google3.cloud.developer_experience.antigravity_extensions.acp_server import tool_filter
from google3.cloud.developer_experience.antigravity_extensions.acp_server.oauth.credential_manager import OAuthCredentialManager
SCHEMA = "paperclip.antigravity.admission.v1"
MODELS_SCHEMA = "paperclip.antigravity.models.v1"
_CONTEXT = contextvars.ContextVar("paperclip_antigravity_admission", default=None)
def _fail(message):
raise exceptions.RequestError(code=-32602, message="Paperclip admission: " + message)
def _read_request(kwargs):
meta = request_meta.antigravity_meta_from_request_kwargs(kwargs)
value = meta.get("paperclipAdmission")
if value is None:
return None
if not isinstance(value, dict) or set(value) != {
"schema", "nonce", "model", "digest", "byteLength"
}:
_fail("invalid request fields")
if value["schema"] != SCHEMA:
_fail("unsupported schema")
for key in ("nonce", "model"):
if not isinstance(value[key], str) or not 1 <= len(value[key]) <= 512:
_fail("invalid " + key)
if not re.fullmatch(r"sha256:[0-9a-f]{64}", str(value["digest"])):
_fail("invalid instruction digest")
prompt = meta.get("systemPrompt")
if not isinstance(prompt, dict) or set(prompt) != {"append"} or not isinstance(prompt["append"], str):
_fail("exact systemPrompt.append is required, including empty content")
content = prompt["append"].encode("utf-8")
if type(value["byteLength"]) is not int or value["byteLength"] != len(content):
_fail("instruction byte length mismatch")
if value["digest"] != "sha256:" + hashlib.sha256(content).hexdigest():
_fail("instruction digest mismatch")
return {**value, "content": prompt["append"]}
def prepare_native_config(config, strategy):
"""Inspect the protobuf that will actually be sent to this owned harness."""
admission = _CONTEXT.get()
if admission is None:
return None
executable_path = ctypes.create_string_buffer(4096)
path_size = ctypes.c_uint32(len(executable_path))
get_executable_path = ctypes.CDLL("/usr/lib/libSystem.B.dylib")._NSGetExecutablePath
get_executable_path.argtypes = [ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint32)]
get_executable_path.restype = ctypes.c_int
if get_executable_path(executable_path, ctypes.byref(path_size)) != 0:
_fail("cannot resolve the owned native executable path")
helper = os.path.join(os.path.dirname(os.path.realpath(os.fsdecode(executable_path.value))), "localharness_external")
helper_stat = os.lstat(helper)
if not stat.S_ISREG(helper_stat.st_mode) or helper_stat.st_nlink != 1:
_fail("native helper must be the immutable sibling executable")
with open(helper, "rb") as helper_file:
helper_digest = hashlib.file_digest(helper_file, "sha256").hexdigest()
if helper_digest != "9e8fdc6e20a3262ca0a771850ca4a8414ca839a642cc964ec5c547cc2c33c200":
_fail("native sibling helper digest differs")
# The SDK resolves this attribute in its constructor. Updating the global
# environment alone cannot replace that already-cached executable path.
strategy._binary_path = helper
if strategy._env is not None:
strategy._env = {**strategy._env, "ANTIGRAVITY_HARNESS_PATH": helper}
os.environ["ANTIGRAVITY_HARNESS_PATH"] = helper
targets = [model for model in config.models if 1 in model.types]
if len(targets) != 1 or targets[0].name != admission["model"]:
_fail("native text model differs from the exact requested model")
instructions = config.system_instructions
expected = admission["content"]
if expected:
if instructions.WhichOneof("type") != "appended":
_fail("native instruction layering differs")
sections = instructions.appended.appended_sections
if len(sections) != 1 or sections[0].content != expected:
_fail("native instruction content differs")
elif config.HasField("system_instructions"):
# Empty content means no appended section, with the normal native defaults.
if instructions.WhichOneof("type") != "appended" or any(section.content for section in instructions.appended.appended_sections):
_fail("native empty instruction content differs")
if not config.cascade_id:
_fail("native session identity is missing")
if config.harness_side_tools.subagents.enabled or config.harness_side_tools.run_workflow.enabled or config.custom_subagents:
_fail("managed subagents, workflows, and browser agents are unavailable")
if config.harness_side_tools.schedule.enabled:
_fail("managed timers and recurring schedules belong to Paperclip")
return {
**admission,
"sessionId": config.cascade_id,
"connectionId": uuid.uuid4().hex,
"configBytes": config.SerializeToString(deterministic=True),
}
def acknowledge_native_config(prepared, event, config):
"""A receipt exists only after the same native connection acknowledges init."""
if prepared is None:
return None
if not event.HasField("initialize_conversation_response"):
_fail("native initialization acknowledgment is missing")
if event.initialize_conversation_response.cascade_id != prepared["sessionId"]:
_fail("native initialization session differs")
if config.SerializeToString(deterministic=True) != prepared["configBytes"]:
_fail("native initialization config changed")
return {key: prepared[key] for key in (
"schema", "nonce", "sessionId", "connectionId", "model", "digest", "byteLength"
)}
def _receipt(adapter, session_id, admission):
session = adapter._sessions.get(session_id)
if session is None:
_fail("native session is missing")
strategy = getattr(session.agent, "_strategy", None)
receipt = getattr(strategy, "_paperclip_native_admission", None)
if not isinstance(receipt, dict) or receipt.get("sessionId") != session_id:
_fail("native content acknowledgment is missing")
for key in ("schema", "nonce", "model", "digest", "byteLength"):
if receipt.get(key) != admission[key]:
_fail("native content acknowledgment differs: " + key)
if session.mode_id != "default" or session.system_prompt != admission["content"]:
_fail("managed mode or instruction state changed")
if session.current_model_id != admission["model"] or session.agent_model_id != admission["model"]:
_fail("selected model differs from initialized model")
return dict(receipt)
def _add_receipt(result, receipt):
result.field_meta = {**(result.field_meta or {}), "paperclipAntigravityAdmission": receipt}
return result
def install(adapter_type):
if adapter_type.__dict__.get("_paperclip_admission_installed", False):
return
original_initialize = adapter_type.initialize
original_extract = system_prompt.extract_system_prompt
original_list_models = adapter_type._list_available_models
original_create_config = adapter_type._create_agent_config
original_rebuild = adapter_type._rebuild_session_agent
original_replace = adapter_type._maybe_replace_unavailable_model
original_prompt = adapter_type.prompt
original_cleanup = adapter_type.cleanup
original_ext = getattr(adapter_type, "ext_method", None)
original_oauth_mgr = getattr(adapter_type, "_ensure_oauth_mgr", None)
original_hooks_config = acp_hooks.load_hooks_config
original_hooks_config_async = acp_hooks.load_hooks_config_async
original_has_workspace_hooks = acp_hooks.has_workspace_hooks
# Bind this to the manager, because later token use occurs in background
# threads without the request ContextVar. Explicit standalone authenticate
# uses a separate, unflagged manager and retains its interactive login.
if not getattr(OAuthCredentialManager._interactive_login_and_store, "_paperclip_managed_guard", False):
original_interactive_login = OAuthCredentialManager._interactive_login_and_store
@functools.wraps(original_interactive_login)
def interactive_login(manager):
if getattr(manager, "_paperclip_noninteractive", False):
raise ValueError("Google sign-in must be renewed through Paperclip before this managed task can continue")
return original_interactive_login(manager)
interactive_login._paperclip_managed_guard = True
OAuthCredentialManager._interactive_login_and_store = interactive_login
def ensure_oauth_mgr(self):
manager = original_oauth_mgr(self)
if _CONTEXT.get() is not None or getattr(self, "_paperclip_managed_sessions", {}):
manager._paperclip_noninteractive = True
return manager
def hooks_config(*args, **kwargs):
return {} if _CONTEXT.get() is not None else original_hooks_config(*args, **kwargs)
async def hooks_config_async(*args, **kwargs):
return {} if _CONTEXT.get() is not None else await original_hooks_config_async(*args, **kwargs)
def has_workspace_hooks(*args, **kwargs):
return False if _CONTEXT.get() is not None else original_has_workspace_hooks(*args, **kwargs)
@functools.wraps(original_initialize)
async def initialize(self, *args, **kwargs):
if getattr(self, "_paperclip_managed_sessions", {}):
_fail("cannot renegotiate an initialized managed connection")
result = await original_initialize(self, *args, **kwargs)
meta = result.agent_capabilities.field_meta or {}
meta.setdefault("antigravity", {})["paperclipAdmission"] = 1
meta["antigravity"]["paperclipModels"] = 1
result.agent_capabilities.field_meta = meta
self._paperclip_managed_sessions = {}
return result
def extract(kwargs):
admission = _CONTEXT.get()
return admission["content"] if admission is not None else original_extract(kwargs)
def list_models(self, current_model_id):
admission = _CONTEXT.get()
selected = admission["model"] if admission is not None else current_model_id
result = original_list_models(self, selected)
if admission is not None and result.current_model_id != selected:
_fail("provider rejected the exact requested model")
return result
@functools.wraps(original_create_config)
async def create_config(self, *args, **kwargs):
admission = _CONTEXT.get()
if admission is not None and (
kwargs.get("model_id") != admission["model"]
or kwargs.get("system_instructions") != admission["content"]
):
_fail("native model or instruction input differs")
if admission is not None:
requested_filter = kwargs.get("tool_filter", tool_filter.ToolFilterConfig())
if not isinstance(requested_filter, tool_filter.ToolFilterConfig):
_fail("invalid managed native tool filter")
kwargs["tool_filter"] = dataclasses.replace(requested_filter, disabled_tools=tuple(dict.fromkeys((*(requested_filter.disabled_tools or ()), "invoke_subagent", "run_workflow", "schedule"))))
result = await original_create_config(self, *args, **kwargs)
if admission is not None:
config = result[0] if isinstance(result, tuple) else result
external_hooks = (acp_hooks.ExternalPreInvocationHook, acp_hooks.ExternalPostInvocationHook, acp_hooks.ExternalPreToolCallDecideHook, acp_hooks.ExternalPostToolCallHook, acp_hooks.ExternalOnSessionEndHook)
if any(isinstance(hook, external_hooks) for hook in config.hooks):
_fail("managed workspace command hooks are unavailable")
return result
def wrap_open(original, is_new):
@functools.wraps(original)
async def open_session(self, *args, **kwargs):
admission = _read_request(kwargs)
session_id = None if is_new else kwargs.get("session_id", args[0] if args else None)
managed = getattr(self, "_paperclip_managed_sessions", {})
if admission is None:
if session_id in managed:
_fail("managed reload requires a fresh exact content acknowledgment")
return await original(self, *args, **kwargs)
if any(item["nonce"] == admission["nonce"] for item in managed.values()):
_fail("request nonce was already used")
token = _CONTEXT.set(admission)
try:
result = await original(self, *args, **kwargs)
session_id = result.session_id if is_new else session_id
receipt = _receipt(self, session_id, admission)
managed[session_id] = admission
self._paperclip_managed_sessions = managed
return _add_receipt(result, receipt)
finally:
_CONTEXT.reset(token)
return open_session
@functools.wraps(original_rebuild)
async def rebuild(self, session_id):
admission = getattr(self, "_paperclip_managed_sessions", {}).get(session_id) or _CONTEXT.get()
if admission is None:
return await original_rebuild(self, session_id)
token = _CONTEXT.set(admission)
try:
result = await original_rebuild(self, session_id)
_receipt(self, session_id, admission)
return result
finally:
_CONTEXT.reset(token)
@functools.wraps(original_replace)
async def replace(self, session_id):
if session_id in getattr(self, "_paperclip_managed_sessions", {}):
# Never let the native enterprise poller substitute a model on this path.
return
return await original_replace(self, session_id)
@functools.wraps(original_prompt)
async def prompt(self, *args, **kwargs):
session_id = kwargs.get("session_id", args[1] if len(args) > 1 else None)
admission = getattr(self, "_paperclip_managed_sessions", {}).get(session_id)
if admission is not None:
_receipt(self, session_id, admission)
return await original_prompt(self, *args, **kwargs)
def wrap_setting(original):
@functools.wraps(original)
async def setting(self, *args, **kwargs):
session_id = kwargs.get("session_id", args[1] if len(args) > 1 else None)
managed = getattr(self, "_paperclip_managed_sessions", {})
if session_id not in managed:
return await original(self, *args, **kwargs)
admission = _read_request(kwargs)
if admission is None or admission["nonce"] == managed[session_id]["nonce"]:
_fail("managed configuration change requires a fresh admission request")
session = self._sessions[session_id]
if not session.agent.conversation.is_idle:
_fail("cannot change managed configuration during an active turn")
token = _CONTEXT.set(admission)
try:
result = await original(self, *args, **kwargs)
session = self._sessions[session_id]
if session.current_model_id != admission["model"]:
_fail("configuration selected a different model")
session.system_prompt = admission["content"]
managed[session_id] = admission
await rebuild(self, session_id)
return _add_receipt(result, _receipt(self, session_id, admission))
finally:
_CONTEXT.reset(token)
return setting
@functools.wraps(original_cleanup)
async def cleanup(self, session_id=None):
try:
return await original_cleanup(self, session_id)
finally:
managed = getattr(self, "_paperclip_managed_sessions", {})
if session_id is None:
managed.clear()
else:
managed.pop(session_id, None)
async def models(self, params):
if (
not isinstance(params, dict)
or set(params) != {"schema", "nonce", "authMethod"}
or params["schema"] != MODELS_SCHEMA
or params["authMethod"] != "oauth-personal"
or not isinstance(params["nonce"], str)
or not 1 <= len(params["nonce"]) <= 512
):
_fail("invalid model catalog request")
def session_free():
if (
getattr(self, "_sessions", {})
or getattr(self, "_paperclip_managed_sessions", {})
or _CONTEXT.get() is not None
):
_fail("model catalog requires a session-free connection")
def require_personal():
state = self._ensure_auth_state()
if state is None or state.method_id != "oauth-personal":
raise exceptions.RequestError(
code=-32000,
message="Google personal sign-in is required before model discovery",
)
session_free()
try:
require_personal()
# Set the persistent instance policy BEFORE the native auth assertion:
# both assertion and catalog token reads may otherwise open a browser.
manager = self._ensure_oauth_mgr()
manager._paperclip_noninteractive = True
await self._assert_authenticated()
require_personal()
except Exception:
raise exceptions.RequestError(
code=-32000,
message="Google personal sign-in must be renewed through Paperclip before model discovery",
) from None
session_free()
try:
state = await asyncio.to_thread(self._list_available_models, "")
require_personal()
values = [
{"id": model.model_id, "label": model.name}
for model in state.available_models
]
ids = [model["id"] for model in values]
if (
not values
or any(not isinstance(model["id"], str) or not model["id"]
or not isinstance(model["label"], str) or not model["label"]
for model in values)
or len(set(ids)) != len(ids)
or state.current_model_id not in ids
):
raise ValueError("invalid native model catalog")
except Exception:
raise exceptions.RequestError(
code=-32000,
message="Google Antigravity could not list models for this connection",
) from None
session_free()
return {
"schema": MODELS_SCHEMA,
"nonce": params["nonce"],
"authMethod": "oauth-personal",
"models": values,
"defaultModelId": state.current_model_id,
}
async def ext_method(self, method, params):
if method.lstrip("_") == "paperclip/models":
return await models(self, params)
if method.lstrip("_") != "paperclip/admission":
if original_ext is None:
_fail("unknown extension method")
return await original_ext(self, method, params)
if not isinstance(params, dict) or set(params) != {"sessionId", "nonce"}:
_fail("invalid admission status request")
admission = getattr(self, "_paperclip_managed_sessions", {}).get(params["sessionId"])
if admission is None or admission["nonce"] != params["nonce"]:
_fail("unknown admission session or nonce")
return {"paperclipAntigravityAdmission": _receipt(self, params["sessionId"], admission)}
adapter_type.initialize = initialize
if original_oauth_mgr is not None:
adapter_type._ensure_oauth_mgr = ensure_oauth_mgr
acp_hooks.load_hooks_config = hooks_config
acp_hooks.load_hooks_config_async = hooks_config_async
acp_hooks.has_workspace_hooks = has_workspace_hooks
system_prompt.extract_system_prompt = extract
adapter_type._list_available_models = list_models
adapter_type._create_agent_config = create_config
adapter_type.new_session = wrap_open(adapter_type.new_session, True)
adapter_type.load_session = wrap_open(adapter_type.load_session, False)
adapter_type.resume_session = wrap_open(adapter_type.resume_session, False)
adapter_type._rebuild_session_agent = rebuild
adapter_type._maybe_replace_unavailable_model = replace
adapter_type.prompt = prompt
adapter_type.cleanup = cleanup
adapter_type.set_session_model = wrap_setting(adapter_type.set_session_model)
adapter_type.set_config_option = wrap_setting(adapter_type.set_config_option)
adapter_type.ext_method = ext_method
adapter_type._paperclip_admission_installed = True
@@ -0,0 +1,289 @@
import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { chmod, lstat, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { inflateRawSync } from "node:zlib";
export const ANTIGRAVITY_PATCH_SCHEMA = "paperclip.antigravity.native_patch.v1";
export const ANTIGRAVITY_PATCH_VERSION = "1.4.3-paperclip.5";
const SERVER_HASH = "1251927db3e21f3d693fd042d6890d82f65eb00587e97f1f3d8f0d798e7f0954";
const HARNESS_HASH = "9e8fdc6e20a3262ca0a771850ca4a8414ca839a642cc964ec5c547cc2c33c200";
const ACP_ROOT = "google3/cloud/developer_experience/antigravity_extensions/acp_server/";
const SDK_ROOT = "google3/third_party/py/google/antigravity/connections/local/";
const SERVER_SOURCE = ACP_ROOT + "server.py";
const SDK_SOURCE = SDK_ROOT + "local_connection.py";
const EVENT_PROCESSOR_SOURCE = SDK_ROOT + "event_processor.py";
const ADMISSION_SOURCE = ACP_ROOT + "paperclip_admission.py";
const REMOVED_BYTECODE = new Set([
ACP_ROOT + "__pycache__/server.cpython-314.pyc",
SDK_ROOT + "__pycache__/local_connection.cpython-314.pyc",
SDK_ROOT + "__pycache__/event_processor.cpython-314.pyc",
]);
const digest = value => createHash("sha256").update(value).digest("hex");
const fail = message => { throw new Error(`Antigravity native patch: ${message}`); };
export function antigravityParSection(bytes) {
if (!Buffer.isBuffer(bytes) || bytes.length < 32 || bytes.readUInt32LE(0) !== 0xfeedfacf || bytes.readUInt32LE(4) !== 0x0100000c) fail("expected the pinned ARM64 Mach-O executable");
const commandCount = bytes.readUInt32LE(16);
let cursor = 32;
const sections = [];
for (let index = 0; index < commandCount; index++) {
if (cursor + 8 > bytes.length) fail("truncated Mach-O command");
const command = bytes.readUInt32LE(cursor);
const size = bytes.readUInt32LE(cursor + 4);
if (size < 8 || cursor + size > bytes.length) fail("invalid Mach-O command bounds");
if (command === 0x19) {
if (size < 72) fail("truncated Mach-O segment");
const count = bytes.readUInt32LE(cursor + 64);
if (72 + count * 80 > size) fail("invalid Mach-O sections");
for (let section = 0; section < count; section++) {
const offset = cursor + 72 + section * 80;
if (bytes.subarray(offset, offset + 16).toString("utf8").split("\0")[0] !== ".par_data") continue;
const length = Number(bytes.readBigUInt64LE(offset + 40));
const start = bytes.readUInt32LE(offset + 48);
if (!Number.isSafeInteger(length) || length < 22 || start + length > bytes.length) fail("invalid embedded Python archive bounds");
sections.push({ start, length });
}
}
cursor += size;
}
if (sections.length !== 1) fail("expected exactly one embedded Python archive");
return sections[0];
}
const crcTable = Array.from({ length: 256 }, (_, index) => {
let value = index;
for (let bit = 0; bit < 8; bit++) value = (value >>> 1) ^ ((value & 1) ? 0xedb88320 : 0);
return value >>> 0;
});
function crc32(bytes) {
let value = 0xffffffff;
for (const byte of bytes) value = (value >>> 8) ^ crcTable[(value ^ byte) & 0xff];
return (value ^ 0xffffffff) >>> 0;
}
/** Operates only after the complete vendor executable's SHA-256 is verified. */
export function antigravityEmbeddedZip(bytes) {
const end = bytes.length - 22;
if (end < 0 || bytes.readUInt32LE(end) !== 0x06054b50 || bytes.readUInt16LE(end + 4) || bytes.readUInt16LE(end + 6) || bytes.readUInt16LE(end + 20)) fail("unsupported embedded ZIP ending");
const count = bytes.readUInt16LE(end + 10);
const centralOffset = bytes.readUInt32LE(end + 16);
if (bytes.readUInt16LE(end + 8) !== count || centralOffset + bytes.readUInt32LE(end + 12) !== end) fail("invalid embedded ZIP index");
const entries = [];
const names = new Set();
let cursor = centralOffset;
for (let index = 0; index < count; index++) {
if (cursor + 46 > end || bytes.readUInt32LE(cursor) !== 0x02014b50) fail("invalid embedded ZIP entry");
const flags = bytes.readUInt16LE(cursor + 8);
const method = bytes.readUInt16LE(cursor + 10);
const compressedLength = bytes.readUInt32LE(cursor + 20);
const length = bytes.readUInt32LE(cursor + 24);
const nameLength = bytes.readUInt16LE(cursor + 28);
const extraLength = bytes.readUInt16LE(cursor + 30);
const commentLength = bytes.readUInt16LE(cursor + 32);
const local = bytes.readUInt32LE(cursor + 42);
const next = cursor + 46 + nameLength + extraLength + commentLength;
if (next > end || (flags & ~0x800) || ![0, 8].includes(method) || local + 30 > centralOffset || bytes.readUInt32LE(local) !== 0x04034b50) fail("unsupported embedded ZIP entry");
const name = bytes.subarray(cursor + 46, cursor + 46 + nameLength).toString("utf8");
if (names.has(name) || !Buffer.from(name).equals(bytes.subarray(cursor + 46, cursor + 46 + nameLength))) fail("duplicate or invalid embedded ZIP name");
names.add(name);
const dataStart = local + 30 + bytes.readUInt16LE(local + 26) + bytes.readUInt16LE(local + 28);
if (dataStart + compressedLength > centralOffset) fail("invalid embedded ZIP payload bounds");
entries.push({ name, method, length, checksum: bytes.readUInt32LE(cursor + 16), localHeader: bytes.subarray(local, dataStart), payload: bytes.subarray(dataStart, dataStart + compressedLength), central: bytes.subarray(cursor, next) });
cursor = next;
}
if (cursor !== end) fail("invalid embedded ZIP index length");
return entries;
}
function content(entry) {
const value = entry.method === 8 ? inflateRawSync(entry.payload, { maxOutputLength: entry.length }) : entry.payload;
if (value.length !== entry.length || crc32(value) !== entry.checksum) fail("embedded source integrity mismatch");
return value;
}
function storedEntry(name, value) {
const filename = Buffer.from(name);
const checksum = crc32(value);
const localHeader = Buffer.alloc(30 + filename.length);
localHeader.writeUInt32LE(0x04034b50);
localHeader.writeUInt16LE(20, 4);
localHeader.writeUInt16LE(33, 12); // 1980-01-01, stable ZIP date.
localHeader.writeUInt32LE(checksum, 14);
localHeader.writeUInt32LE(value.length, 18);
localHeader.writeUInt32LE(value.length, 22);
localHeader.writeUInt16LE(filename.length, 26);
filename.copy(localHeader, 30);
const central = Buffer.alloc(46 + filename.length);
central.writeUInt32LE(0x02014b50);
central.writeUInt16LE(0x314, 4);
central.writeUInt16LE(20, 6);
central.writeUInt16LE(33, 14);
central.writeUInt32LE(checksum, 16);
central.writeUInt32LE(value.length, 20);
central.writeUInt32LE(value.length, 24);
central.writeUInt16LE(filename.length, 28);
central.writeUInt32LE((0o100644 << 16) >>> 0, 38);
filename.copy(central, 46);
return { name, method: 0, length: value.length, checksum, localHeader, payload: value, central };
}
export function replaceAntigravitySource(source, needle, replacement) {
if (source.split(needle).length !== 2) fail("pinned native source anchor changed");
return source.replace(needle, replacement);
}
export function antigravityPatchedSources(server, sdk, eventProcessor) {
replaceAntigravitySource(server, "class AgyAdapter(Agent):", "class AgyAdapter(Agent):");
server = replaceAntigravitySource(server,
` # Immediately flush open tool calls on error step (validation/execution
# failure).
if step.status == sdk_types_module.StepStatus.ERROR:
if open_tool_calls:
logging.warning(
"Step error encountered; flushing open tool calls: %s. Error: %s",
open_tool_calls,
step.error,
)
for tc_id in list(open_tool_calls):
open_tool_calls.pop(tc_id, None)
await self._client.session_update(
session_id=session_id,
update=schema.ToolCallProgress(
session_update="tool_call_update",
tool_call_id=tc_id,
raw_output=step.error or "Tool execution failed",
status="failed",
),
)
`,
` # A step error belongs only to that step's tool calls. The terminal block
# below resolves their execution/permission identities. Unattributed errors
# remain diagnostics; turn cleanup still settles unfinished work.
if step.status == sdk_types_module.StepStatus.ERROR:
logging.warning(
"Step error encountered for %s. Error: %s", step.id, step.error
)
`,
);
server += "\n\n# Paperclip's owned admission patch, applied to this pinned release only.\nfrom google3.cloud.developer_experience.antigravity_extensions.acp_server import paperclip_admission\npaperclip_admission.install(AgyAdapter)\n";
sdk = replaceAntigravitySource(sdk,
" harness_config = self._build_harness_config()\n process, ws, ws_url = await self._spawn_harness_and_connect_ws()",
" harness_config = self._build_harness_config()\n from google3.cloud.developer_experience.antigravity_extensions.acp_server import paperclip_admission\n paperclip_prepared = paperclip_admission.prepare_native_config(harness_config, self)\n process, ws, ws_url = await self._spawn_harness_and_connect_ws()",
);
sdk = replaceAntigravitySource(sdk,
" init_resp = init_resp_event.initialize_conversation_response\n parsed = event_processor.parse_initialize_response(init_resp)",
" self._paperclip_native_admission = paperclip_admission.acknowledge_native_config(paperclip_prepared, init_resp_event, harness_config)\n init_resp = init_resp_event.initialize_conversation_response\n parsed = event_processor.parse_initialize_response(init_resp)",
);
eventProcessor = replaceAntigravitySource(eventProcessor,
` result = await self._execute_client_tool(tc)
if result is not None:
await self._send_tool_results([result])
`,
` result = await self._execute_client_tool(tc)
if result is not None:
# Native StepUpdate events suppress local custom-tool identities. Close
# ACP file callbacks from their actual result, preserving the same id.
client_file_result = tc.name in {
"client_view_file", "client_create_file", "client_edit_file"
}
if client_file_result and (
result.id != tc.id or result.step_id != tc.step_id
):
raise ValueError("Client file result identity differs from its call")
await self._send_tool_results([result])
if client_file_result:
await self.step_queue.put(tool_call_step.model_copy(update={
"status": types.StepStatus.ERROR
if result.error is not None else types.StepStatus.DONE,
"content": result.result if isinstance(result.result, str) else "",
"error": result.error or "",
"tool_calls": [tc],
}))
`,
);
return { server, sdk, eventProcessor };
}
function rebuildZip(entries, length) {
const paddingName = "paperclip_native_patch_padding";
const used = entries.reduce((total, entry) => total + entry.localHeader.length + entry.payload.length + entry.central.length, 0);
const paddingLength = length - used - 22 - 30 - 46 - Buffer.byteLength(paddingName) * 2;
if (paddingLength < 0 || entries.length + 1 > 0xffff) fail("patched source exceeds the pinned native section");
entries = [...entries, storedEntry(paddingName, Buffer.alloc(paddingLength))];
const locals = [];
const centrals = [];
let offset = 0;
for (const entry of entries) {
const central = Buffer.from(entry.central);
central.writeUInt32LE(offset, 42);
locals.push(entry.localHeader, entry.payload);
centrals.push(central);
offset += entry.localHeader.length + entry.payload.length;
}
const central = Buffer.concat(centrals);
const end = Buffer.alloc(22);
end.writeUInt32LE(0x06054b50);
end.writeUInt16LE(entries.length, 8);
end.writeUInt16LE(entries.length, 10);
end.writeUInt32LE(central.length, 12);
end.writeUInt32LE(offset, 16);
const result = Buffer.concat([...locals, central, end]);
if (result.length !== length) fail("patched section size changed");
return result;
}
export function patchAntigravityServerBytes(server, admissionSource) {
if (digest(server) !== SERVER_HASH) fail("official server SHA-256 mismatch");
const section = antigravityParSection(server);
const entries = antigravityEmbeddedZip(server.subarray(section.start, section.start + section.length));
const byName = new Map(entries.map(entry => [entry.name, entry]));
if (!byName.has(SERVER_SOURCE) || !byName.has(SDK_SOURCE) || !byName.has(EVENT_PROCESSOR_SOURCE) || byName.has(ADMISSION_SOURCE) || [...REMOVED_BYTECODE].some(name => !byName.has(name))) fail("pinned Python runtime layout changed");
const patched = antigravityPatchedSources(content(byName.get(SERVER_SOURCE)).toString("utf8"), content(byName.get(SDK_SOURCE)).toString("utf8"), content(byName.get(EVENT_PROCESSOR_SOURCE)).toString("utf8"));
const replacements = new Map([[SERVER_SOURCE, Buffer.from(patched.server)], [SDK_SOURCE, Buffer.from(patched.sdk)], [EVENT_PROCESSOR_SOURCE, Buffer.from(patched.eventProcessor)]]);
const updated = entries.filter(entry => !REMOVED_BYTECODE.has(entry.name)).map(entry => replacements.has(entry.name) ? storedEntry(entry.name, replacements.get(entry.name)) : entry);
updated.push(storedEntry(ADMISSION_SOURCE, admissionSource));
const archive = rebuildZip(updated, section.length);
const result = Buffer.from(server);
archive.copy(result, section.start);
return result;
}
/** Explicit install step; uses Node built-ins and the macOS signing tool only. */
export async function patchAntigravityRuntime({ source, destination } = {}) {
if (process.platform !== "darwin" || process.arch !== "arm64") fail("only native macOS ARM64 is supported by this patch");
if (!source || !destination) fail("explicit source and destination directories are required");
const original = resolve(source);
const target = resolve(destination);
const serverPath = join(original, "agy_acp_server.par");
const harnessPath = join(original, "localharness_external");
for (const file of [serverPath, harnessPath]) {
const stat = await lstat(file);
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) fail("vendor closure contains links or special files");
}
const [server, harness, admission] = await Promise.all([readFile(serverPath), readFile(harnessPath), readFile(new URL("./patch-antigravity-runtime.admission.py", import.meta.url))]);
if (digest(harness) !== HARNESS_HASH) fail("official helper SHA-256 mismatch");
const patched = patchAntigravityServerBytes(server, admission);
await mkdir(dirname(target), { recursive: true, mode: 0o700 });
try { await lstat(target); fail("destination exists; refusing to replace a runtime"); }
catch (error) { if (error.code !== "ENOENT") throw error; }
const temporary = await mkdtemp(join(dirname(target), ".antigravity-native-patch-"));
try {
const executable = join(temporary, "agy_acp_server.par");
await writeFile(executable, patched, { mode: 0o700 });
execFileSync("/usr/bin/codesign", ["--force", "--sign", "-", "--timestamp=none", executable], { stdio: "pipe", timeout: 30_000 });
execFileSync("/usr/bin/codesign", ["--verify", "--strict", executable], { stdio: "pipe", timeout: 30_000 });
await writeFile(join(temporary, "localharness_external"), harness, { mode: 0o555 });
await chmod(executable, 0o555);
await chmod(join(temporary, "localharness_external"), 0o555);
const signed = await readFile(executable);
const result = { schema: ANTIGRAVITY_PATCH_SCHEMA, version: ANTIGRAVITY_PATCH_VERSION, sourceServerSha256: SERVER_HASH, sourceHarnessSha256: HARNESS_HASH, admissionSourceSha256: digest(admission), unsignedServerSha256: digest(patched), serverSha256: digest(signed), harnessSha256: HARNESS_HASH };
await rename(temporary, target);
return result;
} catch (error) {
await rm(temporary, { recursive: true, force: true });
throw error;
}
}
@@ -0,0 +1,611 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { execFile } from "node:child_process";
import { copyFile, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { test } from "node:test";
import { pathToFileURL } from "node:url";
import { antigravityParSection, patchAntigravityServerBytes, replaceAntigravitySource } from "./patch-antigravity-runtime.mjs";
const hash = value => createHash("sha256").update(value).digest("hex");
const source = process.env.PAPERCLIP_ANTIGRAVITY_NATIVE_TEST_SOURCE;
function runNative(executable, home, requests) {
return new Promise((resolve, reject) => {
const child = execFile(executable, [], {
cwd: dirname(executable),
env: { PATH: process.env.PATH, HOME: home, GEMINI_HOME: join(home, ".gemini"), BROWSER: "/usr/bin/true", AGY_ACP_FORCE_FILE_STORAGE: "1", ANTIGRAVITY_HARNESS_PATH: join(executable, "..", "localharness_external") },
timeout: 30_000, maxBuffer: 2 * 1024 * 1024,
}, (error, stdout, stderr) => error ? reject(Object.assign(error, { stdout, stderr })) : resolve({ events: stdout.trim().split("\n").filter(Boolean).map(line => JSON.parse(line)), stderr }));
child.stdin.end(requests.map(request => JSON.stringify(request)).join("\n") + "\n");
});
}
function initialize() {
return { jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: 1, clientInfo: { name: "paperclip-runner", version: "test" }, clientCapabilities: {} } };
}
function admission(content) {
return { schema: "paperclip.antigravity.admission.v1", nonce: "credential-free-regression", model: "gemini-3.1-flash-lite", digest: "sha256:" + hash(content), byteLength: Buffer.byteLength(content) };
}
// This fixture runs inside the real vendor hermetic Python interpreter. It
// tests native protobuf boundaries without creating a session or using auth.
const nativeBoundaryTests = `
def _paperclip_native_boundary_regressions():
import sys
from types import SimpleNamespace
from google3.learning.genai.api.localharness import localharness_pb2 as pb
def rejected(fn):
try:
fn()
except exceptions.RequestError:
return
raise AssertionError("mismatched native admission was accepted")
for content in (" exact café 🧷\\n", ""):
raw = content.encode("utf-8")
request = {"schema": SCHEMA, "nonce": "native-regression", "model": "literal-model", "digest": "sha256:" + hashlib.sha256(raw).hexdigest(), "byteLength": len(raw)}
metadata = {"antigravity": {"paperclipAdmission": request, "systemPrompt": {"append": content}}}
expected = _read_request(metadata)
assert expected["content"] == content
bad = {"antigravity": {"paperclipAdmission": {**request, "byteLength": len(raw) + 1}, "systemPrompt": {"append": content}}}
rejected(lambda: _read_request(bad))
config = pb.HarnessConfig(cascade_id="native-session", models=[pb.ModelConfig(name="literal-model", types=[1])])
if content:
config.system_instructions.appended.appended_sections.add(content=content)
token = _CONTEXT.set(expected)
try:
strategy = SimpleNamespace(_binary_path="/untrusted/cached/helper", _env={"ANTIGRAVITY_HARNESS_PATH": "/untrusted/env/helper"})
prepared = prepare_native_config(config, strategy)
assert strategy._binary_path == os.environ["ANTIGRAVITY_HARNESS_PATH"]
assert strategy._env["ANTIGRAVITY_HARNESS_PATH"] == strategy._binary_path
native_ack = pb.OutputEvent(initialize_conversation_response=pb.InitializeConversationResponse(cascade_id="native-session"))
receipt = acknowledge_native_config(prepared, native_ack, config)
assert prepare_native_config(config, strategy)["connectionId"] != prepared["connectionId"]
assert receipt["digest"] == request["digest"] and receipt["byteLength"] == len(raw)
assert receipt["sessionId"] == "native-session" and receipt["connectionId"]
rejected(lambda: acknowledge_native_config(prepared, pb.OutputEvent(), config))
wrong = pb.OutputEvent(initialize_conversation_response=pb.InitializeConversationResponse(cascade_id="another-session"))
rejected(lambda: acknowledge_native_config(prepared, wrong, config))
config.models[0].name = "substituted-model"
rejected(lambda: prepare_native_config(config, strategy))
rejected(lambda: acknowledge_native_config(prepared, native_ack, config))
config.models[0].name = "literal-model"
config.system_instructions.appended.appended_sections.add(content="unexpected instruction")
rejected(lambda: prepare_native_config(config, strategy))
del config.system_instructions.appended.appended_sections[:]
if content:
config.system_instructions.appended.appended_sections.add(content=content)
config.harness_side_tools.subagents.enabled = True
rejected(lambda: prepare_native_config(config, strategy))
config.harness_side_tools.subagents.enabled = False
config.harness_side_tools.run_workflow.enabled = True
rejected(lambda: prepare_native_config(config, strategy))
config.harness_side_tools.run_workflow.enabled = False
config.harness_side_tools.schedule.enabled = True
rejected(lambda: prepare_native_config(config, strategy))
config.harness_side_tools.schedule.enabled = False
config.custom_subagents.add(name="unpromised-browser-agent")
rejected(lambda: prepare_native_config(config, strategy))
finally:
_CONTEXT.reset(token)
sys.stderr.write("paperclip-native-boundary-regressions: passed\\n")
_paperclip_native_boundary_regressions()
`;
// Calls the real SDK subprocess/stdio/WebSocket startup. The native helper has
// not received InitializeConversationEvent, so no auth or inference is used.
const nativeHelperIsolationTests = `
def _paperclip_native_helper_isolation_regression():
import asyncio
import subprocess
import sys
from google.antigravity import types as sdk_types
from google.antigravity.tools.tool_runner import ToolRunner
from google.antigravity.connections.local.local_connection_config import LocalAgentConfig
async def run():
home = os.environ["HOME"]
os.makedirs(home, exist_ok=True)
sentinel = os.path.join(home, "untrusted-cached-helper")
marker = os.path.join(home, "untrusted-helper-launched")
with open(sentinel, "w") as output:
output.write("#!/bin/sh\\n: > " + marker + "\\nexit 0\\n")
os.chmod(sentinel, 0o700)
config = LocalAgentConfig(conversation_id="helper-isolation-session-1234567890123456", system_instructions="exact helper isolation", capabilities=sdk_types.CapabilitiesConfig(enable_subagents=False, disabled_tools=[sdk_types.BuiltinTools.SCHEDULE]), models=[sdk_types.ModelTarget(name="literal-model", types=[sdk_types.ModelType.TEXT], endpoint=sdk_types.GeminiAPIEndpoint())], env={"ANTIGRAVITY_HARNESS_PATH": sentinel})
strategy = config.create_strategy(tool_runner=None, hook_runner=None)
assert strategy._binary_path == sentinel, "test must begin with an already-cached override"
raw = b"exact helper isolation"
admission = {"schema": SCHEMA, "nonce": "helper-isolation", "model": "literal-model", "digest": "sha256:" + hashlib.sha256(raw).hexdigest(), "byteLength": len(raw), "content": raw.decode()}
token = _CONTEXT.set(admission)
owned = []
launched_paths = []
ws = None
original_popen = subprocess.Popen
def tracked_popen(*args, **kwargs):
process = original_popen(*args, **kwargs)
owned.append(process)
launched_paths.append(args[0][0])
return process
try:
native = strategy._build_harness_config()
prepare_native_config(native, strategy)
subprocess.Popen = tracked_popen
process, ws, _url = await asyncio.wait_for(strategy._spawn_harness_and_connect_ws(), timeout=10)
assert process.poll() is None, "the actual pinned native helper must be running"
assert len(launched_paths) == 1 and launched_paths[0] == strategy._binary_path
assert launched_paths[0] != sentinel
assert strategy._env["ANTIGRAVITY_HARNESS_PATH"] == launched_paths[0]
assert not os.path.exists(marker), "the cached override must never execute"
finally:
subprocess.Popen = original_popen
_CONTEXT.reset(token)
if ws is not None:
await ws.close()
for process in owned:
if process.poll() is None:
process.kill()
process.wait(timeout=5)
for stream in (process.stdin, process.stdout, process.stderr):
if stream is not None:
stream.close()
assert owned and all(process.poll() is not None for process in owned)
asyncio.run(run())
sys.stderr.write("paperclip-native-helper-isolation-regression: passed\\n")
_paperclip_native_helper_isolation_regression()
`;
const nativeManagedSurfaceTests = `
def _paperclip_managed_surface_regressions():
import asyncio
import json
import sys
from types import SimpleNamespace
from google.antigravity import types as sdk_types
from google.antigravity.tools.tool_runner import ToolRunner
from google3.cloud.developer_experience.antigravity_extensions.acp_server import browser_subagent
from google3.cloud.developer_experience.antigravity_extensions.acp_server.server import AgyAdapter, AuthState
install(AgyAdapter)
patched_create_config = AgyAdapter._create_agent_config
install(AgyAdapter)
assert AgyAdapter._create_agent_config is patched_create_config
class MemoryStore:
def read(self): return None
def write(self, value): pass
def clear(self): pass
class FixtureCredentials(OAuthCredentialManager):
def __init__(self):
super().__init__(client_id="fixture", client_secret="fixture", scopes=[], token_path=os.path.join(os.environ["HOME"], "unused-credentials.json"), keychain_service="fixture", keychain_account="fixture", store=MemoryStore())
self.interactive_calls = 0
self.tier_info = SimpleNamespace(is_empty=True)
def _authenticate_interactively(self):
self.interactive_calls += 1
return SimpleNamespace(valid=True, token="controlled-fixture-token", refresh_token=None)
def _save_credentials(self, credentials): pass
def _resolve_backend(self, token, interactive_login=False): pass
async def run():
workspace = os.path.join(os.environ["HOME"], "managed-surface-workspace")
os.makedirs(workspace, exist_ok=True)
hooks_path = acp_hooks.get_global_hooks_path()
os.makedirs(os.path.dirname(hooks_path), exist_ok=True)
hooks = {"fixture": {"enabled": True, "PreInvocation": [{"hooks": [{"type": "command", "command": "false"}]}]}}
with open(hooks_path, "w") as output: json.dump(hooks, output)
for workspace_hooks_path in acp_hooks.get_workspace_hooks_paths(workspace):
os.makedirs(os.path.dirname(workspace_hooks_path), exist_ok=True)
with open(workspace_hooks_path, "w") as output: json.dump(hooks, output)
assert acp_hooks.has_workspace_hooks(workspace)
assert acp_hooks.load_hooks_config(workspace), "unmanaged source loader must remain functional"
content = " Exact managed source config 🧷\\n"
raw = content.encode()
admission = {"schema": SCHEMA, "nonce": "managed-source-policy", "model": "gemini-3.1-flash-lite", "digest": "sha256:" + hashlib.sha256(raw).hexdigest(), "byteLength": len(raw), "content": content}
adapter = AgyAdapter()
adapter._auth_state = AuthState(method_id="gemini-api-key", api_key="controlled-fixture-key-not-used")
original_preflight = browser_subagent.preflight_launcher
def forbidden_preflight(): raise AssertionError("managed browser launcher must never be probed")
token = _CONTEXT.set(admission)
try:
assert not acp_hooks.has_workspace_hooks(workspace)
assert acp_hooks.load_hooks_config(workspace) == {}
assert await acp_hooks.load_hooks_config_async(workspace) == {}
browser_subagent.preflight_launcher = forbidden_preflight
config, teardown = await adapter._create_agent_config(cwd=workspace, session_id="managed-source-policy-1234567890123456", gemini_home=os.environ["GEMINI_HOME"], model_id=admission["model"], system_instructions=content)
assert teardown is None
assert not config.capabilities.enable_subagents and not config.subagents
assert all(isinstance(hook, acp_hooks.ExternalOnInteractionHook) for hook in config.hooks)
strategy = config.create_strategy(tool_runner=ToolRunner(tools=config._get_all_custom_tools()), hook_runner=None)
native = strategy._build_harness_config()
assert not native.harness_side_tools.subagents.enabled
assert not native.harness_side_tools.run_workflow.enabled and not native.custom_subagents
assert not native.harness_side_tools.schedule.enabled
prepare_native_config(native, strategy)
managed_manager = FixtureCredentials()
adapter._oauth_mgr = managed_manager
assert adapter._ensure_oauth_mgr() is managed_manager
finally:
browser_subagent.preflight_launcher = original_preflight
_CONTEXT.reset(token)
try:
managed_manager.get_access_token()
except ValueError as error:
assert "renewed through Paperclip" in str(error)
else:
raise AssertionError("managed missing/expired credentials launched interactive auth")
assert managed_manager.interactive_calls == 0, "managed policy must survive outside its request context"
standalone = AgyAdapter()
interactive_manager = FixtureCredentials()
standalone._oauth_mgr = interactive_manager
await standalone.authenticate("oauth-personal")
assert interactive_manager.interactive_calls == 1, "explicit standalone authenticate must retain its interactive path"
assert not getattr(interactive_manager, "_paperclip_noninteractive", False)
assert acp_hooks.has_workspace_hooks(workspace)
assert acp_hooks.load_hooks_config(workspace), "unmanaged hooks policy must be unchanged"
asyncio.run(run())
sys.stderr.write("paperclip-managed-surface-regressions: passed\\n")
_paperclip_managed_surface_regressions()
`;
// Exercises the shipped authentication assertion and CCPA model parser. Only
// the metadata response and in-memory credentials are controlled. Session,
// native helper, browser, network, and enterprise-poller factories must not run.
const nativeModelCatalogTests = `
def _paperclip_model_catalog_regressions():
import asyncio
import contextlib
import json
import socket
import subprocess
import sys
import threading
import urllib.request
import webbrowser
from types import SimpleNamespace
from unittest import mock
from google.auth import exceptions as auth_exceptions
from google3.cloud.developer_experience.antigravity_extensions.acp_server import client_info
from google3.cloud.developer_experience.antigravity_extensions.acp_server import server as native_server
from google3.cloud.developer_experience.antigravity_extensions.acp_server import settings_writer
from google3.cloud.developer_experience.antigravity_extensions.acp_server.ccpa_connection import ccpa_client
AgyAdapter = native_server.AgyAdapter
AuthState = native_server.AuthState
install(AgyAdapter)
calls = []
main_thread = threading.get_ident()
response = {
"models": {
"gemini-exact-low-effort": {"displayName": " Exact café Model 🧷 ", "description": "private description must not escape"},
"gemini-exact-high-effort": {"displayName": "Native Default"},
"other-provider": {"displayName": "Filtered for this client"},
"gemini-disabled": {"displayName": "Not entitled", "disabled": True},
"gemini-unlisted": {"displayName": "Not advertised in sorts"},
},
"agentModelSorts": [{"groups": [{"modelIds": ["gemini-exact-low-effort", "other-provider", "gemini-disabled", "gemini-exact-low-effort", "gemini-exact-high-effort"]}]}],
"defaultAgentModelId": "gemini-exact-high-effort",
}
class MemoryStore:
def read(self): return None
def write(self, value): pass
def clear(self): pass
class Credentials(OAuthCredentialManager):
def __init__(self):
super().__init__(client_id="fixture", client_secret="fixture", scopes=[], token_path=os.path.join(os.environ["HOME"], "unused-model-credentials.json"), keychain_service="fixture", keychain_account="fixture", store=MemoryStore())
self._creds = SimpleNamespace(valid=True, token="controlled-catalog-token", refresh_token=None)
self.project_id = "controlled-project"
self.endpoint_url = "https://catalog.invalid"
self.interactive_calls = 0
self.resolutions = 0
def _authenticate_interactively(self):
self.interactive_calls += 1
raise AssertionError("catalog must never authenticate interactively")
def _save_credentials(self, credentials): pass
def _resolve_backend(self, token, interactive_login=False):
assert getattr(self, "_paperclip_noninteractive", False), "noninteractive policy must precede native authentication"
assert not interactive_login
self.resolutions += 1
def forbidden(*args, **kwargs):
raise AssertionError("session-free model discovery reached a forbidden factory")
async def rejected(adapter, params, message=None):
try:
await adapter.ext_method("_paperclip/models", params)
except exceptions.RequestError as error:
text = str(error)
assert "controlled-secret" not in text
assert "private description" not in text
if message is not None: assert message in text, text
return error
raise AssertionError("invalid model catalog operation succeeded")
request = {"schema": MODELS_SCHEMA, "nonce": "catalog-nonce 🧷", "authMethod": "oauth-personal"}
def adapter():
value = AgyAdapter(sdk_agent_factory=forbidden)
value._client_info = client_info.parse(SimpleNamespace(name="paperclip-runner", title="Paperclip Runner", version="test"))
value._oauth_mgr = Credentials()
return value
def fetch(token, endpoint, project, user_agent):
assert threading.get_ident() != main_thread, "catalog must use the bounded caller's worker thread"
assert token == "controlled-catalog-token" and endpoint == "https://catalog.invalid" and project == "controlled-project"
calls.append(user_agent)
return response
async def run():
settings_writer.save_auth_type("oauth-personal")
value = adapter()
assert value._auth_state is None, "saved settings must select the actual native auth method"
result = await value.ext_method("_paperclip/models", request)
assert result == {"schema": MODELS_SCHEMA, "nonce": request["nonce"], "authMethod": "oauth-personal", "models": [{"id": "gemini-exact-low-effort", "label": " Exact café Model 🧷 "}, {"id": "gemini-exact-high-effort", "label": "Native Default"}], "defaultModelId": "gemini-exact-high-effort"}
assert value._auth_state.method_id == "oauth-personal"
assert len(calls) == 1 and value._oauth_mgr.resolutions == 2
assert value._oauth_mgr._paperclip_noninteractive and value._oauth_mgr.interactive_calls == 0
assert value._sessions == {} and value._background_tasks == {}
assert value._proxy_server is None and value._baic_proxy_server is None and value._admin_controls_mgr is None
assert "private description" not in json.dumps(result)
for invalid in (None, {}, {**request, "schema": "other"}, {**request, "authMethod": "oauth-business"}, {**request, "authMethod": "gemini-api-key"}, {**request, "nonce": ""}, {**request, "nonce": 3}, {**request, "nonce": "x" * 513}, {**request, "extra": True}):
await rejected(value, invalid, "invalid model catalog request")
assert len(calls) == 1
for method in ("gemini-api-key", "oauth-business", "agent-platform", "gateway"):
wrong = adapter()
wrong._auth_state = AuthState(method_id=method)
await rejected(wrong, request, "sign-in must be renewed")
assert not getattr(wrong._oauth_mgr, "_paperclip_noninteractive", False)
logged_out = adapter()
logged_out._auth_state = AuthState(method_id="oauth-personal")
logged_out._logged_out = True
await rejected(logged_out, request, "sign-in must be renewed")
assert not getattr(logged_out._oauth_mgr, "_paperclip_noninteractive", False)
settings_writer.clear_auth_type()
await rejected(adapter(), request, "sign-in must be renewed")
settings_writer.save_auth_type("oauth-personal")
active = adapter()
active._sessions["not-created"] = object()
await rejected(active, request, "session-free")
active._sessions.clear()
active._paperclip_managed_sessions = {"not-created": {}}
await rejected(active, request, "session-free")
token = _CONTEXT.set({"model": "must-not-resolve"})
try: await rejected(adapter(), request, "session-free")
finally: _CONTEXT.reset(token)
assert len(calls) == 1
# The persistent manager policy must still hold after this request returns.
manager = value._oauth_mgr
manager._creds = None
try: manager.get_access_token()
except ValueError: pass
else: raise AssertionError("catalog manager lost its noninteractive policy")
await rejected(value, request, "sign-in must be renewed")
assert manager.interactive_calls == 0 and len(calls) == 1
expired = adapter()
class Expired:
valid = False
refresh_token = "controlled-refresh"
def refresh(self, request): raise auth_exceptions.RefreshError("controlled-secret refresh rejected")
expired._oauth_mgr._creds = Expired()
await rejected(expired, request, "sign-in must be renewed")
assert len(calls) == 1
# Provider failures and empty/malformed catalogs must not produce custom or
# hardcoded model fallbacks, or expose backend/credential response text.
with mock.patch.object(ccpa_client, "fetch_available_models", side_effect=RuntimeError("controlled-secret backend detail")):
await rejected(adapter(), request, "could not list models")
for bad in ({}, {"models": {}, "defaultAgentModelId": "unoffered"}, {**response, "models": {**response["models"], "gemini-exact-low-effort": {"displayName": {"controlled-secret": True}}}}):
with mock.patch.object(ccpa_client, "fetch_available_models", return_value=bad):
await rejected(adapter(), request, "could not list models")
with contextlib.ExitStack() as stack:
for owner, name in ((AgyAdapter, "_create_agent_config"), (AgyAdapter, "_ensure_business_auth_mgr"), (AgyAdapter, "_ensure_admin_controls_mgr"), (AgyAdapter, "_init_admin_controls_for_session"), (native_server.session_store, "trajectory_path"), (native_server.browser_subagent, "preflight_launcher"), (native_server.LocalAgentConfig, "create_strategy"), (subprocess, "Popen"), (asyncio, "create_subprocess_exec"), (socket, "create_connection"), (urllib.request, "urlopen"), (webbrowser, "open"), (webbrowser, "open_new_tab")):
stack.enter_context(mock.patch.object(owner, name, side_effect=forbidden))
stack.enter_context(mock.patch.object(ccpa_client, "fetch_available_models", side_effect=fetch))
stack.enter_context(mock.patch.dict(os.environ, {"AGY_ACP_DEFAULT_MODEL": "unoffered-custom-fallback"}))
asyncio.run(run())
sys.stderr.write("paperclip-model-catalog-regressions: passed\\n")
_paperclip_model_catalog_regressions()
`;
// These state tests use the shipped SDK config builder, ACP response classes,
// and SessionInfo. The native acknowledgment is controlled; no agent is entered
// and no authentication or network request occurs.
const nativeStateTests = `
def _paperclip_managed_state_regressions():
import asyncio
import contextlib
import sys
from types import SimpleNamespace
from acp import schema
from google.antigravity import types as sdk_types
from google.antigravity.connections.local.local_connection_config import LocalAgentConfig
from google3.learning.genai.api.localharness import localharness_pb2 as pb
from google3.cloud.developer_experience.antigravity_extensions.acp_server.server import SessionInfo
sid = "native-managed-session-12345678901234567890"
def metadata(content, nonce):
raw = content.encode("utf-8")
return {"antigravity": {"paperclipAdmission": {"schema": SCHEMA, "nonce": nonce, "model": "literal-model", "digest": "sha256:" + hashlib.sha256(raw).hexdigest(), "byteLength": len(raw)}, "systemPrompt": {"append": content}}}
async def rejected(fn):
try:
await fn()
except exceptions.RequestError:
return
raise AssertionError("managed state mismatch was accepted")
class FixtureAdapter:
def __init__(self):
self._sessions = {}
self.prompts = 0
self.replacements = 0
async def initialize(self, **kwargs):
return schema.InitializeResponse(protocol_version=1, agent_capabilities=schema.AgentCapabilities(), auth_methods=[])
def _list_available_models(self, model):
return schema.SessionModelState(current_model_id=model, available_models=[])
async def _create_agent_config(self, **kwargs):
disabled = kwargs["tool_filter"].resolve_disabled_builtins()
assert sdk_types.BuiltinTools.INVOKE_SUBAGENT in disabled and sdk_types.BuiltinTools.RUN_WORKFLOW in disabled
assert sdk_types.BuiltinTools.SCHEDULE in disabled
return LocalAgentConfig(conversation_id=kwargs["session_id"], system_instructions=kwargs["system_instructions"], capabilities=sdk_types.CapabilitiesConfig(enable_subagents=False, disabled_tools=list(disabled)), models=[sdk_types.ModelTarget(name=kwargs["model_id"], types=[sdk_types.ModelType.TEXT], endpoint=sdk_types.GeminiAPIEndpoint())])
async def _initialize_fixture(self, session_id):
expected = _CONTEXT.get()
model = self._list_available_models("native-default").current_model_id
config = await self._create_agent_config(session_id=session_id, model_id=model, system_instructions=expected["content"])
strategy = config.create_strategy(tool_runner=None, hook_runner=None)
native = strategy._build_harness_config()
prepared = prepare_native_config(native, strategy)
ack = pb.OutputEvent(initialize_conversation_response=pb.InitializeConversationResponse(cascade_id=session_id))
strategy._paperclip_native_admission = acknowledge_native_config(prepared, ack, native)
agent = SimpleNamespace(_strategy=strategy, conversation=SimpleNamespace(is_idle=True))
self._sessions[session_id] = SessionInfo(agent=agent, stack=contextlib.AsyncExitStack(), current_model_id=model, system_prompt=expected["content"])
async def new_session(self, cwd, **kwargs):
await self._initialize_fixture(sid)
return schema.NewSessionResponse(session_id=sid)
async def load_session(self, session_id, cwd, **kwargs):
await self._initialize_fixture(session_id)
return schema.LoadSessionResponse()
async def resume_session(self, session_id, cwd, **kwargs):
await self._initialize_fixture(session_id)
return schema.ResumeSessionResponse()
async def _rebuild_session_agent(self, session_id):
await self._initialize_fixture(session_id)
async def _maybe_replace_unavailable_model(self, session_id):
self.replacements += 1
async def prompt(self, prompt, session_id, **kwargs):
self.prompts += 1
return schema.PromptResponse(stop_reason="end_turn")
async def set_session_model(self, model_id, session_id, **kwargs):
self._sessions[session_id].current_model_id = model_id
return schema.SetSessionModelResponse()
async def set_config_option(self, config_id, session_id, value, **kwargs):
self._sessions[session_id].current_model_id = value
return schema.SetSessionConfigOptionResponse(config_options=[])
async def cleanup(self, session_id=None):
if session_id is None:
self._sessions.clear()
else:
self._sessions.pop(session_id, None)
async def run():
adapter = FixtureAdapter()
await adapter.initialize()
first = await adapter.new_session("/fixture", **metadata(" exact café 🧷\\n", "open-nonce"))
receipt = first.field_meta["paperclipAntigravityAdmission"]
assert receipt["sessionId"] == sid and receipt["nonce"] == "open-nonce"
status = await adapter.ext_method("paperclip/admission", {"sessionId": sid, "nonce": "open-nonce"})
assert status["paperclipAntigravityAdmission"] == receipt
session = adapter._sessions[sid]
session.mode_id = "yolo"
await rejected(lambda: adapter.prompt([], sid))
assert adapter.prompts == 0
session.mode_id = "default"
await adapter._maybe_replace_unavailable_model(sid)
assert adapter.replacements == 0
changed = await adapter.set_config_option("model", sid, "literal-model", **metadata("", "setter-nonce"))
changed_receipt = changed.field_meta["paperclipAntigravityAdmission"]
assert changed_receipt["nonce"] == "setter-nonce" and changed_receipt["byteLength"] == 0
assert changed_receipt["connectionId"] != receipt["connectionId"]
await rejected(lambda: adapter.ext_method("paperclip/admission", {"sessionId": sid, "nonce": "open-nonce"}))
await rejected(lambda: adapter.load_session(sid, "/fixture"))
loaded = await adapter.load_session(sid, "/fixture", **metadata(" exact reload ", "load-nonce"))
assert loaded.field_meta["paperclipAntigravityAdmission"]["nonce"] == "load-nonce"
resumed = await adapter.resume_session(sid, "/fixture", **metadata(" exact resume ", "resume-nonce"))
assert resumed.field_meta["paperclipAntigravityAdmission"]["nonce"] == "resume-nonce"
await adapter.cleanup(sid)
assert not adapter._paperclip_managed_sessions
original_extract = system_prompt.extract_system_prompt
try:
install(FixtureAdapter)
asyncio.run(run())
finally:
system_prompt.extract_system_prompt = original_extract
sys.stderr.write("paperclip-managed-state-regressions: passed\\n")
_paperclip_managed_state_regressions()
`;
test("native patch rejects an unpinned executable before decoding", () => {
assert.throws(() => patchAntigravityServerBytes(Buffer.from("ambient executable"), Buffer.from("module")), /SHA-256 mismatch/);
});
test("source changes require a single exact pinned anchor", () => {
assert.throws(() => replaceAntigravitySource("changed", "original", "patch"), /anchor changed/);
assert.throws(() => replaceAntigravitySource("original original", "original", "patch"), /anchor changed/);
});
test("Mach-O section decoding rejects invalid architecture and out-of-bounds native archives", () => {
assert.throws(() => antigravityParSection(Buffer.alloc(64)), /ARM64 Mach-O/);
const bytes = Buffer.alloc(32 + 72 + 80);
bytes.writeUInt32LE(0xfeedfacf);
bytes.writeUInt32LE(0x0100000c, 4);
bytes.writeUInt32LE(1, 16);
bytes.writeUInt32LE(0x19, 32);
bytes.writeUInt32LE(152, 36);
bytes.writeUInt32LE(1, 96);
bytes.write(".par_data", 104);
bytes.writeBigUInt64LE(1000n, 144);
bytes.writeUInt32LE(10, 152);
assert.throws(() => antigravityParSection(bytes), /archive bounds/);
});
test("pinned native closure reproducibly loads exact admission and rejects requests before authentication", {
skip: !source || process.platform !== "darwin" || process.arch !== "arm64",
timeout: 180_000,
}, async t => {
const root = await mkdtemp(join(tmpdir(), "paperclip-antigravity-native-patch-test-"));
t.after(() => rm(root, { recursive: true, force: true }));
const originalHash = hash(await readFile(join(source, "agy_acp_server.par")));
const { patchAntigravityRuntime } = await import("./patch-antigravity-runtime.mjs");
const first = await patchAntigravityRuntime({ source, destination: join(root, "first") });
const second = await patchAntigravityRuntime({ source, destination: join(root, "second") });
assert.deepEqual(second, first, "the complete signed patch must be reproducible");
assert.equal(hash(await readFile(join(source, "agy_acp_server.par"))), originalHash, "vendor source remains immutable");
const content = " exact café 🧷\n";
const valid = admission(content);
const create = (id, value) => ({ jsonrpc: "2.0", id, method: "session/new", params: { cwd: root, mcpServers: [], _meta: { antigravity: { paperclipAdmission: value, systemPrompt: { append: content } } } } });
const models = { schema: "paperclip.antigravity.models.v1", nonce: "native-models-nonce", authMethod: "oauth-personal" };
const result = await runNative(join(root, "first/agy_acp_server.par"), join(root, "home"), [initialize(), create(2, { ...valid, digest: "sha256:" + "0".repeat(64) }), create(3, valid), { jsonrpc: "2.0", id: 4, method: "_paperclip/admission", params: { sessionId: "missing", nonce: valid.nonce } }, { jsonrpc: "2.0", id: 5, method: "_paperclip/models", params: models }, { jsonrpc: "2.0", id: 6, method: "_paperclip/models", params: { ...models, extra: true } }]);
const byId = new Map(result.events.filter(event => event.id !== undefined).map(event => [event.id, event]));
assert.equal(byId.get(1).result.agentInfo.name, "antigravity-acp");
assert.equal(byId.get(1).result.agentCapabilities._meta.antigravity.paperclipAdmission, 1);
assert.equal(byId.get(1).result.agentCapabilities._meta.antigravity.paperclipModels, 1);
assert.match(byId.get(2).error.message, /digest mismatch/);
assert.equal(byId.get(3).error.code, -32000, "valid metadata reaches the native authentication gate");
assert.match(byId.get(4).error.message, /unknown admission session or nonce/, "native status extension fails closed without a session");
assert.equal(byId.get(5).error.code, -32000, "session-free model discovery requires saved personal sign-in");
assert.match(byId.get(5).error.message, /sign-in must be renewed through Paperclip/);
assert.equal(byId.get(6).error.code, -32602, "the native model request contract rejects extra fields");
assert(!result.events.some(event => event.result?._meta?.paperclipAntigravityAdmission), "no native init means no fabricated receipt");
const fixtureScripts = join(root, "fixture-scripts");
const { mkdir } = await import("node:fs/promises");
await mkdir(fixtureScripts);
await copyFile(new URL("./patch-antigravity-runtime.mjs", import.meta.url), join(fixtureScripts, "patch-antigravity-runtime.mjs"));
const module = await readFile(new URL("./patch-antigravity-runtime.admission.py", import.meta.url), "utf8");
await writeFile(join(fixtureScripts, "patch-antigravity-runtime.admission.py"), module + nativeBoundaryTests + nativeHelperIsolationTests + nativeManagedSurfaceTests + nativeModelCatalogTests + nativeStateTests);
const fixture = await import(pathToFileURL(join(fixtureScripts, "patch-antigravity-runtime.mjs")));
await fixture.patchAntigravityRuntime({ source, destination: join(root, "boundary-fixture") });
const tested = await runNative(join(root, "boundary-fixture/agy_acp_server.par"), join(root, "fixture-home"), [initialize()]);
assert.match(tested.stderr, /paperclip-native-boundary-regressions: passed/);
assert.match(tested.stderr, /paperclip-native-helper-isolation-regression: passed/);
assert.match(tested.stderr, /paperclip-managed-surface-regressions: passed/);
assert.match(tested.stderr, /paperclip-model-catalog-regressions: passed/);
assert.match(tested.stderr, /paperclip-managed-state-regressions: passed/);
assert.equal(tested.events[0].result.agentCapabilities._meta.antigravity.paperclipAdmission, 1);
assert.equal(tested.events[0].result.agentCapabilities._meta.antigravity.paperclipModels, 1);
});
test("native client file callbacks settle exact tool identity without broadcasting errors", {
skip: !source || process.platform !== "darwin" || process.arch !== "arm64",
timeout: 90_000,
}, async t => {
const root = await mkdtemp(join(tmpdir(), "paperclip-antigravity-tool-lifecycle-test-"));
t.after(() => rm(root, { recursive: true, force: true }));
const fixtureScripts = join(root, "scripts");
const { mkdir } = await import("node:fs/promises");
await mkdir(fixtureScripts);
await copyFile(new URL("./patch-antigravity-runtime.mjs", import.meta.url), join(fixtureScripts, "patch-antigravity-runtime.mjs"));
const module = await readFile(new URL("./patch-antigravity-runtime.admission.py", import.meta.url), "utf8");
const regression = await readFile(new URL("./patch-antigravity-runtime.tool-lifecycle.test.py", import.meta.url), "utf8");
await writeFile(join(fixtureScripts, "patch-antigravity-runtime.admission.py"), module + "\n" + regression);
const fixture = await import(pathToFileURL(join(fixtureScripts, "patch-antigravity-runtime.mjs")));
await fixture.patchAntigravityRuntime({ source, destination: join(root, "runtime") });
const tested = await runNative(join(root, "runtime/agy_acp_server.par"), join(root, "home"), [initialize()]);
assert.match(tested.stderr, /paperclip-tool-lifecycle-regressions: passed/);
assert.equal(tested.events[0].result.agentCapabilities._meta.antigravity.paperclipAdmission, 1);
});
@@ -0,0 +1,160 @@
"""Credential-free regressions executed in the real pinned hermetic SDK."""
def _paperclip_tool_lifecycle_regressions():
import asyncio
import contextlib
import json
import socket
import subprocess
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest import mock
from acp import schema
from google.antigravity import types as sdk_types
from google.antigravity.connections.local.event_processor import LocalHarnessEventProcessor
from google.antigravity.proto import localharness_pb2 as pb
from google.antigravity.tools.tool_context import ToolContext
from google.antigravity.tools.tool_runner import ToolRunner
from google3.cloud.developer_experience.antigravity_extensions.acp_server import tools
from google3.cloud.developer_experience.antigravity_extensions.acp_server.server import AgyAdapter, SessionInfo
sid = "offline-tool-lifecycle"
workspace = Path(os.environ["HOME"]) / "tool-workspace"
workspace.mkdir(parents=True)
class FileClient:
def __init__(self):
self.updates = []
self.blocked = asyncio.Event()
self.release = asyncio.Event()
async def session_update(self, session_id, update):
assert session_id == sid
self.updates.append(update)
async def write_text_file(self, session_id, path, content):
assert session_id == sid
Path(path).write_bytes(content.encode("utf-8"))
async def read_text_file(self, session_id, path, line=None, limit=None):
assert session_id == sid
if Path(path).name == "blocked.txt":
self.blocked.set()
await self.release.wait()
return schema.ReadTextFileResponse(content=Path(path).read_bytes().decode("utf-8"))
class Adapter(AgyAdapter):
async def _maybe_send_usage_update(self, session_id):
pass
def forbidden(*args, **kwargs):
raise AssertionError("offline lifecycle regression reached network or process launch")
async def run():
client = FileClient()
adapter = Adapter()
adapter._client = client
info = SessionInfo(agent=SimpleNamespace(), stack=contextlib.AsyncExitStack(), cwd=str(workspace), current_model_id="offline")
adapter._sessions[sid] = info
runner = ToolRunner(tools=[tools.make_client_view_file(client, str(workspace)), tools.make_client_create_file(client, str(workspace)), tools.make_client_edit_file(client, str(workspace))])
runner.set_context(ToolContext(SimpleNamespace(conversation_id=sid)))
wire = []
async def send(event):
assert event.HasField("tool_response")
wire.append(event.tool_response)
processor = LocalHarnessEventProcessor(send_input_event_fn=send, tool_runner=runner)
opened = {}
async def drain():
steps = []
while not processor.step_queue.empty():
step = processor.step_queue.get_nowait()
steps.append(step)
await adapter._stream_live_step(sid, step, opened)
return steps
async def call(name, call_id, args, permission_frame=None):
if permission_frame:
info.pending_permission_frames[name] = [permission_frame]
await processor.handle_tool_call(pb.ToolCall(id=call_id, name=name, trajectory_id=sid, arguments_json=json.dumps(args)))
steps = await drain()
assert len(steps) == 2, "actual client ToolResult must yield an ACTIVE and terminal step"
assert [s.tool_calls[0].id for s in steps] == [call_id, call_id]
assert all(s.id == call_id and s.tool_calls[0].step_id is None for s in steps), "preserve the callback protocol's real identity without inventing a step id"
assert wire[-1].id == call_id
frame = permission_frame or call_id
terminals = [u for u in client.updates if u.tool_call_id == frame and u.status in ("completed", "failed")]
assert len(terminals) == 1
assert frame not in opened
return steps[-1], terminals[0], wire[-1]
proof = workspace / "proof.txt"
exact = " exact café 🧷\n"
terminal, update, response = await call("client_create_file", "create-call", {"target_file": str(proof), "code_content": exact}, "create-permission")
assert terminal.status == sdk_types.StepStatus.DONE and terminal.content == ""
assert update.status == "completed" and update.raw_output == ""
assert proof.read_bytes() == exact.encode("utf-8")
assert json.loads(response.response_json) == {"result": None}
terminal, update, response = await call("client_view_file", "read-call", {"absolute_path": str(proof)})
assert terminal.content == exact and update.raw_output == exact
assert json.loads(response.response_json) == {"result": exact}
edited = "replacement\n"
await call("client_edit_file", "edit-call", {"target_file": str(proof), "code_content": edited})
assert proof.read_bytes() == edited.encode("utf-8")
failed_path = workspace / "missing-parent" / "failed-write.txt"
terminal, update, response = await call("client_create_file", "failed-write-call", {"target_file": str(failed_path), "code_content": "must not exist"})
assert terminal.status == sdk_types.StepStatus.ERROR and update.status == "failed"
assert update.raw_output == terminal.error == response.error_message
assert not failed_path.exists()
# A real pending callback overlaps a different callback's actual failure.
(workspace / "blocked.txt").write_bytes(b"still independent\n")
pending = asyncio.create_task(processor.handle_tool_call(pb.ToolCall(id="pending-call", name="client_view_file", trajectory_id=sid, arguments_json=json.dumps({"absolute_path": str(workspace / "blocked.txt")}))))
await client.blocked.wait()
await drain()
assert "pending-call" in opened
terminal, update, response = await call("client_view_file", "missing-call", {"absolute_path": str(workspace / "missing.txt")}, "missing-permission")
assert terminal.status == sdk_types.StepStatus.ERROR and terminal.error
assert update.status == "failed" and update.raw_output == terminal.error
assert response.error_message == terminal.error
assert "pending-call" in opened, "a genuine callback failure must not fail its concurrent sibling"
assert not [u for u in client.updates if u.tool_call_id == "pending-call" and u.status == "failed"]
client.release.set()
await pending
await drain()
assert "pending-call" not in opened
assert [u for u in client.updates if u.tool_call_id == "pending-call" and u.status == "completed"][0].raw_output == "still independent\n"
# Native errors close only the failing call, including permission identity.
def native_step(call_id, status, error="", calls=True):
return sdk_types.Step(id="native-step", trajectory_id=sid, type=sdk_types.StepType.TOOL_CALL, status=status, error=error, tool_calls=[sdk_types.ToolCall(id=call_id, step_id="native-step", name="native_tool")] if calls else [])
info.pending_permission_frames["native_tool"] = ["native-permission"]
await adapter._stream_live_step(sid, native_step("native-execution", sdk_types.StepStatus.ACTIVE), opened)
opened["unrelated"] = "native_other_tool"
await adapter._stream_live_step(sid, native_step("native-execution", sdk_types.StepStatus.ERROR, "exact native failure"), opened)
assert "native-permission" not in opened and "unrelated" in opened
failures = [u for u in client.updates if u.status == "failed"]
assert {u.tool_call_id for u in failures} == {"failed-write-call", "missing-permission", "native-permission"}
assert failures[-1].raw_output == "exact native failure"
before = len(client.updates)
await adapter._stream_live_step(sid, native_step("unattributed", sdk_types.StepStatus.ERROR, "turn-only error", calls=False), opened)
assert len(client.updates) == before and "unrelated" in opened, "an unattributed error has no authority to fail an unrelated tool"
# The existing failed-turn cleanup still settles unresolved work, without
# copying an unrelated step's error or fabricating completed callbacks.
await adapter._close_open_tool_calls(sid, set(opened), "failed")
assert client.updates[-1].tool_call_id == "unrelated" and client.updates[-1].status == "failed"
assert client.updates[-1].raw_output is None
with contextlib.ExitStack() as stack:
for owner, name in ((socket, "create_connection"), (subprocess, "Popen"), (asyncio, "create_subprocess_exec")):
stack.enter_context(mock.patch.object(owner, name, side_effect=forbidden))
asyncio.run(run())
sys.stderr.write("paperclip-tool-lifecycle-regressions: passed\n")
_paperclip_tool_lifecycle_regressions()
@@ -0,0 +1,218 @@
#!/usr/bin/env node
import { spawn, spawnSync } from "node:child_process";
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { StringDecoder } from "node:string_decoder";
import { pathToFileURL } from "node:url";
import { antigravityRuntimeCachePath } from "./antigravity-provisioner-layout.mjs";
import { antigravityDistribution, verifyAntigravityDistribution } from "./materialize-antigravity-distribution.mjs";
const MAX_WIRE_BYTES = 1024 * 1024;
const CLIENT_INFO = Object.freeze({ name: "paperclip-runner", title: "Paperclip Runner", version: "1" });
const AUTH_METHOD_IDS = ["oauth-personal", "oauth-business", "gemini-api-key", "agent-platform"];
const plainObject = value => value !== null && typeof value === "object" && !Array.isArray(value);
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
function probeLimits(options) {
const limits = { startupTimeoutMs: options.startupTimeoutMs ?? 25_000, maxBytes: options.maxBytes ?? MAX_WIRE_BYTES,
eofTimeoutMs: options.eofTimeoutMs ?? 500, terminateTimeoutMs: options.terminateTimeoutMs ?? 500, killTimeoutMs: options.killTimeoutMs ?? 500 };
for (const [name, maximum] of [["startupTimeoutMs", 30_000], ["maxBytes", MAX_WIRE_BYTES], ["eofTimeoutMs", 5_000], ["terminateTimeoutMs", 5_000], ["killTimeoutMs", 5_000]]) {
if (!Number.isSafeInteger(limits[name]) || limits[name] < 1 || limits[name] > maximum) throw new Error("Invalid Antigravity metadata probe limits");
}
return limits;
}
/** No ambient credentials, proxy, hooks, Python, Node, or configuration variables. */
export function antigravityMetadataEnvironment(root, runtimeDirectory) {
return {
PATH: "/usr/bin:/bin", HOME: join(root, "home"), GEMINI_HOME: join(root, "gemini"),
XDG_CONFIG_HOME: join(root, "config"), XDG_DATA_HOME: join(root, "data"), XDG_CACHE_HOME: join(root, "cache"),
TMPDIR: join(root, "tmp"), AGY_ACP_FORCE_FILE_STORAGE: "1", ANTIGRAVITY_HARNESS_PATH: join(runtimeDirectory, "localharness_external"),
NO_COLOR: "1", LANG: "en_US.UTF-8",
};
}
/** One closed protocol exchange. Provider-authored text never enters diagnostics. */
export function createAntigravityMetadataRpc(child, { startupTimeoutMs = 25_000, maxBytes = MAX_WIRE_BYTES } = {}) {
if (!Number.isSafeInteger(startupTimeoutMs) || startupTimeoutMs < 1 || startupTimeoutMs > 30_000 || !Number.isSafeInteger(maxBytes) || maxBytes < 1 || maxBytes > MAX_WIRE_BYTES) throw new Error("Invalid Antigravity metadata probe limits");
const decoder = new StringDecoder("utf8");
const utf8Validator = new TextDecoder("utf-8", { fatal: true });
let buffer = "", bytes = 0, stderrBytes = 0, started = false, settled = false, failure, timer;
let resolveInitialization, rejectInitialization;
const initialized = new Promise((resolve, reject) => { resolveInitialization = resolve; rejectInitialization = reject; });
// Failures before initialize() still have an owned rejection handler.
initialized.catch(() => {});
function fail(code) {
failure ??= new Error(code);
clearTimeout(timer);
if (!settled) { settled = true; rejectInitialization(failure); }
}
function parse(line) {
if (!line.trim()) return;
let message;
try { message = JSON.parse(line); } catch { fail("antigravity_malformed_json"); return; }
if (!plainObject(message) || message.jsonrpc !== "2.0" || message.id !== 1 || "method" in message || settled) { fail("antigravity_unexpected_message"); return; }
if ("error" in message) { fail("antigravity_initialize_rejected"); return; }
if (!plainObject(message.result)) { fail("antigravity_malformed_initialize_result"); return; }
clearTimeout(timer);
settled = true;
resolveInitialization(message.result);
}
child.stdout.on("data", chunk => {
bytes += chunk.length;
if (bytes > maxBytes) { fail("antigravity_stdout_limit"); return; }
try { utf8Validator.decode(chunk, { stream: true }); }
catch { fail("antigravity_malformed_utf8"); return; }
buffer += decoder.write(chunk);
while (!failure && buffer.includes("\n")) {
const end = buffer.indexOf("\n");
const line = buffer.slice(0, end);
buffer = buffer.slice(end + 1);
parse(line);
}
});
child.stdout.once("end", () => {
try { utf8Validator.decode(); }
catch { fail("antigravity_malformed_utf8"); return; }
buffer += decoder.end();
if (buffer.trim()) fail("antigravity_truncated_stream");
});
child.stderr.on("data", chunk => {
stderrBytes += chunk.length;
if (stderrBytes > maxBytes) fail("antigravity_stderr_limit");
});
child.once("error", () => fail("antigravity_spawn_failure"));
child.once("close", () => { if (!settled) fail("antigravity_exited_before_initialize"); });
child.stdin.on("error", () => { if (!settled) fail("antigravity_stdin_failure"); });
return {
initialize() {
if (started) throw new Error("Antigravity metadata initialization can only be sent once");
started = true;
timer = setTimeout(() => fail("antigravity_initialize_timeout"), startupTimeoutMs);
if (!failure) child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: 1, clientInfo: CLIENT_INFO, clientCapabilities: {} } })}\n`);
return initialized;
},
assertHealthy() { if (failure) throw failure; },
close() { clearTimeout(timer); },
};
}
function ownedGroupAlive(child) {
if (!child.pid) return false;
try { process.kill(-child.pid, 0); return true; }
catch (error) {
if (error.code === "ESRCH") return false;
// Darwin can report EPERM for a group containing only an orphaned zombie.
// Inspect numeric state only; never read process commands or environments.
if (error.code === "EPERM" && process.platform === "darwin") {
const snapshot = spawnSync("/bin/ps", ["-axo", "pid=,pgid=,stat="], { encoding: "utf8", timeout: 1_000, maxBuffer: 1024 * 1024, env: { PATH: "/usr/bin:/bin", LANG: "C" } });
if (snapshot.status === 0) {
const rows = snapshot.stdout.trim().split("\n").filter(Boolean).map(line => line.trim().split(/\s+/));
if (rows.every(row => row.length === 3 && /^\d+$/.test(row[0]) && /^\d+$/.test(row[1]) && /^[A-Za-z+<>NLsWX-]+$/.test(row[2]))) return rows.some(row => Number(row[1]) === child.pid && !row[2].startsWith("Z"));
}
}
throw new Error(`antigravity_process_group_check_failed:${error.code}`);
}
}
function signalOwnedGroup(child, signal) {
if (!child.pid) return;
try { process.kill(-child.pid, signal); }
catch (error) { if (error.code !== "ESRCH") throw new Error(`antigravity_process_group_signal_failed:${error.code}`); }
}
/** Finite shutdown stages also retire descendants after their parent exits. */
export async function stopAntigravityMetadataChild(child, { eofTimeoutMs = 500, terminateTimeoutMs = 500, killTimeoutMs = 500 } = {}) {
if (!child) return { method: "not-started", exitCode: null, signal: null };
for (const limit of [eofTimeoutMs, terminateTimeoutMs, killTimeoutMs]) if (!Number.isSafeInteger(limit) || limit < 1 || limit > 5_000) throw new Error("Invalid Antigravity shutdown limits");
const closed = child.exitCode !== null || child.signalCode !== null ? Promise.resolve() : new Promise(resolve => child.once("close", resolve));
let cleanupUncertainty;
const groupMayBeAlive = () => {
try { return ownedGroupAlive(child); }
catch (error) { cleanupUncertainty = error; return true; }
};
child.stdin?.end();
for (const [method, signal, timeout] of [["eof", null, eofTimeoutMs], ["sigterm", "SIGTERM", terminateTimeoutMs], ["sigkill", "SIGKILL", killTimeoutMs]]) {
if (!groupMayBeAlive()) return { method, exitCode: child.exitCode, signal: child.signalCode };
if (signal) {
try { signalOwnedGroup(child, signal); }
catch (error) { cleanupUncertainty = error; }
}
const deadline = Date.now() + timeout;
let timer;
try { await Promise.race([closed, new Promise(resolve => { timer = setTimeout(resolve, timeout); })]); }
finally { clearTimeout(timer); }
if (!groupMayBeAlive()) return { method, exitCode: child.exitCode, signal: child.signalCode };
// A descendant may have closed its inherited pipes. One deadline per stage
// gives that owned process group time to exit without an idle poll loop.
const remaining = deadline - Date.now();
if (remaining > 0) await delay(remaining);
if (!groupMayBeAlive()) return { method, exitCode: child.exitCode, signal: child.signalCode };
}
if (cleanupUncertainty) throw cleanupUncertainty;
throw new Error("antigravity_process_tree_cleanup_unconfirmed");
}
function safeMetadata(result, distribution) {
if (result.protocolVersion !== 1 || !plainObject(result.agentInfo) || result.agentInfo.name !== "antigravity-acp") throw new Error("antigravity_initialize_identity_mismatch");
if (result.agentInfo.version !== distribution.version) throw new Error("antigravity_initialize_version_mismatch");
if (!plainObject(result.agentCapabilities) || !Array.isArray(result.authMethods)) throw new Error("antigravity_malformed_initialize_capabilities");
const capabilities = result.agentCapabilities;
if (distribution.patchVersion && capabilities._meta?.antigravity?.paperclipAdmission !== 1) throw new Error("antigravity_native_admission_patch_not_advertised");
const flag = (object, key) => plainObject(object) && object[key] === true;
const present = (object, key) => plainObject(object) && Object.hasOwn(object, key) && plainObject(object[key]);
return {
schema: "paperclip.antigravity_metadata_probe.v1", version: distribution.version, patchVersion: distribution.patchVersion,
platform: distribution.platform, architecture: distribution.architecture, archiveSha256: distribution.archiveSha256, vendorClosureSha256: distribution.vendorClosureSha256,
closureSha256: distribution.closureSha256, admissionSourceSha256: distribution.admissionSourceSha256,
protocolVersion: 1, agentInfo: { name: "antigravity-acp", title: "Google Antigravity", version: distribution.version },
agentCapabilities: {
loadSession: flag(capabilities, "loadSession"),
promptCapabilities: Object.fromEntries(["image", "audio", "embeddedContext"].map(key => [key, flag(capabilities.promptCapabilities, key)])),
mcpCapabilities: Object.fromEntries(["http", "sse"].map(key => [key, flag(capabilities.mcpCapabilities, key)])),
sessionCapabilities: Object.fromEntries(["list", "resume"].map(key => [key, present(capabilities.sessionCapabilities, key)])),
logout: present(capabilities.auth, "logout"),
antigravity: { tier: flag(capabilities._meta?.antigravity, "tier"), systemPromptAppend: flag(capabilities._meta?.antigravity?.systemPrompt, "append"), paperclipAdmission: 1 },
},
authMethodIds: AUTH_METHOD_IDS.filter(id => result.authMethods.some(method => plainObject(method) && method.id === id)),
requestsSent: ["initialize"], authenticateRequestsSent: 0, sessionRequestsSent: 0, promptRequestsSent: 0,
inferenceVerified: false, qualification: "pending",
};
}
/** Credential-free cache probe. Installation is always a separate explicit action. */
export async function probeAntigravityMetadata(options = {}) {
const limits = probeLimits(options);
const distribution = await antigravityDistribution(options.platform, options.architecture);
const runtimeDirectory = antigravityRuntimeCachePath(distribution.closureSha256, distribution.platform, distribution.architecture, options.home);
await verifyAntigravityDistribution(runtimeDirectory, distribution);
const root = await mkdtemp(join(tmpdir(), "paperclip-antigravity-metadata-"));
let child, rpc, result, shutdown, failure, cleanupFailure;
try {
const env = antigravityMetadataEnvironment(root, runtimeDirectory);
for (const key of ["HOME", "GEMINI_HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME", "TMPDIR"]) await mkdir(env[key], { mode: 0o700 });
child = spawn(join(runtimeDirectory, distribution.executable), distribution.args, { cwd: root, env, stdio: "pipe", detached: true });
rpc = createAntigravityMetadataRpc(child, limits);
result = safeMetadata(await rpc.initialize(), distribution);
} catch (error) {
failure = error;
} finally {
try { shutdown = await stopAntigravityMetadataChild(child, limits); }
catch (error) { cleanupFailure = error; }
finally { rpc?.close(); await rm(root, { recursive: true, force: true }); }
}
if (failure && cleanupFailure) throw new AggregateError([failure, cleanupFailure], `${failure.message}; ${cleanupFailure.message}`);
if (failure) throw failure;
if (cleanupFailure) {
try { rpc?.assertHealthy(); }
catch (error) { throw new AggregateError([error, cleanupFailure], `${error.message}; ${cleanupFailure.message}`); }
throw cleanupFailure;
}
rpc.assertHealthy();
return { ...result, shutdown };
}
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
probeAntigravityMetadata().then(result => console.log(JSON.stringify(result, null, 2))).catch(error => { console.error(error.message); process.exitCode = 1; });
}
@@ -0,0 +1,196 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { EventEmitter } from "node:events";
import { chmod, copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { PassThrough } from "node:stream";
import { test } from "node:test";
import { pathToFileURL } from "node:url";
import { antigravityMetadataEnvironment, createAntigravityMetadataRpc } from "./probe-antigravity-metadata.mjs";
const hash = bytes => createHash("sha256").update(bytes).digest("hex");
const response = {
protocolVersion: 1, agentInfo: { name: "antigravity-acp", title: "private-provider-text 🛰", version: "1.4.3" },
agentCapabilities: { loadSession: true, promptCapabilities: { image: true }, mcpCapabilities: { http: true }, sessionCapabilities: { list: {} }, auth: { logout: {} }, _meta: { antigravity: { tier: true, systemPrompt: { append: true }, paperclipAdmission: 1 } } },
authMethods: [{ id: "oauth-personal", name: "private-provider-text" }, { id: "untrusted-auth-method", name: "private-provider-text" }],
};
async function fixture(t, behavior = "success") {
const root = await mkdtemp(join(tmpdir(), "paperclip-antigravity-probe-test-"));
t.after(() => rm(root, { recursive: true, force: true }));
const scripts = join(root, "scripts");
await mkdir(scripts);
for (const name of ["materialize-antigravity-distribution.mjs", "antigravity-provisioner-layout.mjs", "probe-antigravity-metadata.mjs"]) await copyFile(new URL(name, import.meta.url), join(scripts, name));
const admissionSource = "# trusted test admission source\n";
const admissionSourceSha256 = hash(admissionSource);
await writeFile(join(scripts, "patch-antigravity-runtime.admission.py"), admissionSource);
await writeFile(join(scripts, "patch-antigravity-runtime.mjs"), "export const ANTIGRAVITY_PATCH_VERSION='1.4.3-paperclip.1'; export function patchAntigravityRuntime(){throw new Error('Metadata must never patch or install');}");
const auditPath = join(root, "audit.json");
const script = `#!${process.execPath}
const { writeFileSync } = require('node:fs');
const { spawn } = require('node:child_process');
const behavior=${JSON.stringify(behavior)};
const result=${JSON.stringify(response)};
let buffer='';let descendant;
if(behavior==='ignore-stop') { process.on('SIGTERM',()=>{});setInterval(()=>{},1000); }
process.stdin.on('data',chunk=>{
buffer+=chunk;
if(!buffer.includes('\\n'))return;
const requests=buffer.trim().split('\\n').map(line=>JSON.parse(line));
writeFileSync(${JSON.stringify(auditPath)},JSON.stringify({pid:process.pid,home:process.env.HOME,geminiHome:process.env.GEMINI_HOME,cwd:process.cwd(),env:process.env,requests}));
if(requests.length!==1||requests[0].method!=='initialize')process.exit(41);
if(behavior==='timeout')return;
if(behavior==='malformed')return process.stdout.write('not-json\\n');
if(behavior==='oversized')return process.stdout.write('x'.repeat(8192));
if(behavior==='stderr-limit')return process.stderr.write('sensitive'.repeat(8192));
if(behavior==='unexpected-method')return process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:99,method:'authenticate',params:{token:'sensitive'}})+'\\n');
if(behavior==='wrong-version')result.agentInfo.version='1.4.2';
if(behavior==='wrong-identity')result.agentInfo.name='another-harness';
if(behavior==='wrong-protocol')result.protocolVersion=2;
if(behavior==='bad-capabilities')result.agentCapabilities=[];
if(behavior==='missing-patch')delete result.agentCapabilities._meta.antigravity.paperclipAdmission;
if(behavior==='orphan-child') {
descendant=spawn(process.execPath,['-e',"setInterval(()=>{},1000)"],{stdio:'ignore'});
writeFileSync(${JSON.stringify(join(root, "descendant.json"))},JSON.stringify({pid:descendant.pid}));
}
const payload=JSON.stringify({jsonrpc:'2.0',id:1,result});
process.stderr.write('private-provider-text sensitive-secret\\n');
if(behavior==='truncated') { process.stdout.write(payload);process.exit(0); }
process.stdout.write(payload+'\\n'+(behavior==='extra-message'?'{}\\n':''));
});
process.stdin.on('end',()=>{if(behavior!=='ignore-stop')process.exit(0)});
`;
const nativeFiles = [
{ path: "agy_acp_server.par", content: Buffer.from(script) },
{ path: "localharness_external", content: Buffer.from("#!/bin/sh\nexit 0\n") },
];
const entries = nativeFiles.map(file => ({ path: file.path, sha256: hash(file.content), size: file.content.length, executable: true }));
const closureSha256 = hash(JSON.stringify(entries));
const item = { url: "https://dl.google.com/agy-extensions/releases/macos/fixture.zip", archiveSha256: "a".repeat(64), closureSha256, vendorClosureSha256: closureSha256, vendorEntries: entries, patchVersion: "1.4.3-paperclip.1", admissionSourceSha256, provisioning: "verified-closure", executable: "agy_acp_server.par", args: [], entries };
await writeFile(join(root, "antigravity-distributions.json"), JSON.stringify({ schema: "paperclip.antigravity_distribution.v1", version: "1.4.3", platforms: { "darwin-arm64": item } }));
const home = join(root, "operator-home");
const runtime = join(home, ".paperclip/runtimes/antigravity/darwin-arm64", closureSha256);
await mkdir(runtime, { recursive: true, mode: 0o700 });
for (const file of nativeFiles) await writeFile(join(runtime, file.path), file.content, { mode: 0o500 });
await writeFile(join(runtime, ".paperclip-antigravity-closure.json"), JSON.stringify({ schema: "paperclip.antigravity_closure.v1", version: "1.4.3", patchVersion: item.patchVersion, admissionSourceSha256, archiveSha256: item.archiveSha256, vendorClosureSha256: closureSha256, closureSha256, entries }), { mode: 0o400 });
const api = await import(pathToFileURL(join(scripts, "probe-antigravity-metadata.mjs")));
return {
root, runtime, auditPath,
probe: options => api.probeAntigravityMetadata({ home, platform: "darwin", architecture: "arm64", startupTimeoutMs: 2_000, eofTimeoutMs: 60, terminateTimeoutMs: 60, killTimeoutMs: 250, ...options }),
};
}
test("probe environment has only explicit sterile settings and cannot inherit credentials", () => {
const env = antigravityMetadataEnvironment("/isolated", "/verified/runtime");
assert.equal(env.HOME, "/isolated/home");
assert.equal(env.GEMINI_HOME, "/isolated/gemini");
assert.equal(env.AGY_ACP_FORCE_FILE_STORAGE, "1");
assert.equal(env.ANTIGRAVITY_HARNESS_PATH, "/verified/runtime/localharness_external");
assert.deepEqual(Object.keys(env).sort(), ["AGY_ACP_FORCE_FILE_STORAGE", "ANTIGRAVITY_HARNESS_PATH", "GEMINI_HOME", "HOME", "LANG", "NO_COLOR", "PATH", "TMPDIR", "XDG_CACHE_HOME", "XDG_CONFIG_HOME", "XDG_DATA_HOME"]);
});
test("initialize uses Paperclip identity, preserves split UTF-8 and cannot send another method", async () => {
const child = Object.assign(new EventEmitter(), { stdout: new PassThrough(), stderr: new PassThrough(), stdin: new PassThrough() });
let request = "";
child.stdin.on("data", chunk => { request += chunk; });
const rpc = createAntigravityMetadataRpc(child);
const pending = rpc.initialize();
const payload = Buffer.from(`${JSON.stringify({ jsonrpc: "2.0", id: 1, result: response })}\n`);
const split = payload.indexOf(Buffer.from("🛰")) + 1;
child.stdout.write(payload.subarray(0, split));
child.stdout.write(payload.subarray(split));
assert.equal((await pending).agentInfo.title, response.agentInfo.title);
assert.deepEqual(JSON.parse(request).params.clientInfo, { name: "paperclip-runner", title: "Paperclip Runner", version: "1" });
assert.throws(() => rpc.initialize(), /only be sent once/);
rpc.close();
});
test("invalid UTF-8 is rejected instead of silently replacing wire bytes", async () => {
const child = Object.assign(new EventEmitter(), { stdout: new PassThrough(), stderr: new PassThrough(), stdin: new PassThrough() });
const rpc = createAntigravityMetadataRpc(child);
const pending = rpc.initialize();
child.stdout.write(Buffer.from([0xff, 0x0a]));
await assert.rejects(pending, /malformed_utf8/);
rpc.close();
});
test("real stdio probe sends initialize only, sanitizes metadata and removes its isolated profile", async t => {
const current = await fixture(t);
const output = await current.probe();
const audit = JSON.parse(await readFile(current.auditPath, "utf8"));
assert.deepEqual(audit.requests.map(request => request.method), ["initialize"]);
assert.equal(audit.env.AGY_ACP_FORCE_FILE_STORAGE, "1");
assert.equal(audit.env.GEMINI_API_KEY, undefined);
assert.equal(audit.env.GOOGLE_APPLICATION_CREDENTIALS, undefined);
assert.equal(audit.env.NODE_OPTIONS, undefined);
assert.notEqual(audit.home, process.env.HOME);
assert.notEqual(audit.geminiHome, process.env.GEMINI_HOME);
await assert.rejects(readFile(join(audit.home, "anything")), { code: "ENOENT" });
await assert.rejects(readFile(audit.cwd), { code: "ENOENT" });
assert.deepEqual(output.authMethodIds, ["oauth-personal"]);
assert.equal(output.agentInfo.version, "1.4.3");
assert.equal(output.promptRequestsSent, 0);
assert.equal(output.shutdown.exitCode, 0);
assert.equal(output.shutdown.method, "eof");
assert.doesNotMatch(JSON.stringify(output), /sensitive|private-provider-text|paperclip-antigravity-metadata-/);
});
test("identity, version and protocol mismatches are rejected with sanitized diagnostics", async t => {
for (const [behavior, error] of [["wrong-version", /version_mismatch/], ["wrong-identity", /identity_mismatch/], ["wrong-protocol", /identity_mismatch/], ["bad-capabilities", /malformed_initialize_capabilities/], ["missing-patch", /native_admission_patch_not_advertised/]]) {
const current = await fixture(t, behavior);
await assert.rejects(current.probe(), error);
}
});
test("malformed, oversized, truncated and unexpected protocol streams fail closed", async t => {
for (const [behavior, error] of [["malformed", /malformed_json/], ["oversized", /stdout_limit/], ["stderr-limit", /stderr_limit/], ["unexpected-method", /unexpected_message/], ["truncated", /truncated_stream/], ["extra-message", /unexpected_message/]]) {
const current = await fixture(t, behavior);
await assert.rejects(current.probe({ maxBytes: 4096 }), error);
}
});
test("startup deadline retires the native process and isolated profile", async t => {
const current = await fixture(t, "timeout");
await assert.rejects(current.probe({ startupTimeoutMs: 600 }), /initialize_timeout/);
const audit = JSON.parse(await readFile(current.auditPath, "utf8"));
assert.throws(() => process.kill(audit.pid, 0), { code: "ESRCH" });
await assert.rejects(readFile(audit.cwd), { code: "ENOENT" });
});
test("SIGKILL bounds shutdown when native work ignores EOF and SIGTERM", async t => {
const current = await fixture(t, "ignore-stop");
const output = await current.probe();
const audit = JSON.parse(await readFile(current.auditPath, "utf8"));
assert.equal(output.shutdown.method, "sigkill");
assert.equal(output.shutdown.signal, "SIGKILL");
assert.throws(() => process.kill(audit.pid, 0), { code: "ESRCH" });
});
test("shutdown retires an owned child after its native parent exits", async t => {
const current = await fixture(t, "orphan-child");
try { await current.probe(); }
catch (error) {
// macOS sandboxing can deny probing an already-extinct process group.
// Preserve that fail-closed result and independently check the actual child.
assert.equal(process.platform, "darwin");
assert.match(error.message, /^antigravity_process_group_check_failed:EPERM$/);
}
const descendant = JSON.parse(await readFile(join(current.root, "descendant.json"), "utf8"));
assert.throws(() => process.kill(descendant.pid, 0), { code: "ESRCH" });
});
test("tampered cache fails verification before any native process starts", async t => {
const current = await fixture(t);
const child = join(current.runtime, "localharness_external");
await chmod(child, 0o700);
await writeFile(child, "corrupt");
await assert.rejects(current.probe(), /full execution closure digest mismatch/);
await assert.rejects(readFile(current.auditPath), { code: "ENOENT" });
});
test("invalid startup or shutdown limits fail before any native process starts", async t => {
const current = await fixture(t);
for (const options of [{ startupTimeoutMs: 0 }, { startupTimeoutMs: 30_001 }, { maxBytes: 1024 * 1024 + 1 }, { eofTimeoutMs: -1 }, { killTimeoutMs: 5_001 }]) await assert.rejects(current.probe(options), /Invalid Antigravity metadata probe limits/);
await assert.rejects(readFile(current.auditPath), { code: "ENOENT" });
});
@@ -0,0 +1,31 @@
#!/usr/bin/env node
import { lstat } from "node:fs/promises";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { antigravityProvisionerDestination } from "./antigravity-provisioner-layout.mjs";
import { antigravityDistribution, materializePinnedAntigravityDistribution, verifyAntigravityDistribution } from "./materialize-antigravity-distribution.mjs";
/** Explicit public installation; never invoked by an npm lifecycle hook. */
export async function provisionAntigravityRuntime(options = {}) {
const distribution = await antigravityDistribution(options.platform, options.architecture);
const destination = antigravityProvisionerDestination(options.moduleUrl ?? import.meta.url, distribution, options.home);
const installed = await lstat(destination).catch(error => {
if (error.code !== "ENOENT") throw error;
return null;
});
if (installed) {
if (!installed.isDirectory() || installed.isSymbolicLink()) throw new Error("Antigravity runtime assets must be a real directory");
await verifyAntigravityDistribution(destination, distribution);
} else {
await materializePinnedAntigravityDistribution({ destination, platform: distribution.platform, architecture: distribution.architecture, archivePath: options.archivePath });
}
return { ...distribution, destination, executable: resolve(destination, distribution.executable) };
}
if (process.argv[1] && pathToFileURL(resolve(process.argv[1])).href === import.meta.url) {
provisionAntigravityRuntime().then(result => {
console.log(`Verified Google Antigravity ${result.version} (${result.platform}-${result.architecture}), ${result.patchVersion}`);
console.log(`Runtime: ${result.destination}. Run setup as the same OS user that runs Paperclip.`);
console.log("Native Runner execution remains pending acceptance in this build.");
}).catch(error => { console.error(error.message); process.exitCode = 1; });
}