feat(runner): add gated native Antigravity ACP execution (#18188)

## Thinking Path

> - Paperclip manages agents and their work.
> - Native Runner must bind each provider turn to the selected task,
account, model, and instructions.
> - Antigravity needs native ACP admission and physical process
ownership across continuation.
> - A saved conversation must not reuse an old process owner or silently
switch its effective model.
> - This pull request adds the native candidate behind exact operator
admission.
> - The default release gate remains closed while qualification is
pending.

## Linked Issues or Issue Description

Builds on merged #18187. Product account setup and the candidate
producer are in dependent #18189. The repeatable browser journey is in
#18190. Refs: #12503, #14938; those proposals use legacy CLI adapters.

**Agent or provider**

Google Antigravity ACP 1.4.3.

**Why this adapter is useful**

Users need to set up an Antigravity agent and run real work through
their managed Google subscription.

**How the agent is invoked**

Native Paperclip Runner uses the pinned Antigravity ACP runtime. Exact
operator admission is required while qualification remains pending.

## What Changed

- Add native Antigravity ACP model and instruction admission,
selected-account homes, and fenced refresh ownership.
- Carry exact candidate authority from the control plane through Runner,
Rust, the sidecar, and each current run. Clamp daemon lifetime to the
absolute permission expiry, including replacement owners.
- Preserve a rotated OAuth refresh token before later identity
verification can fail, including failed or cancelled startup admission.
Make private lease-close retries safe.
- Bound native probe retirement and retry the exact original owner; do
not delete credential homes before physical proof.
- Preserve Gemini API-key recovery when shared Google subscription
capability is added.
- Resume the same tagged native conversation with fresh per-turn process
ownership and physical native retirement.
- Include native manifests in isolated Rust image stages, release cache
keys, and dev rebuild detection. The workflow changes only extend
existing cache input lists.
- Keep current master provider behavior and append Grok 13, Pi 35, and
Copilot 84 input identities. Preserve prior identities, including saved
Copilot 82 and 83 runs.

## Verification

- Runner TypeScript main checks, profile generators, Rust formatting,
and offline workspace checks pass. The native backend integration target
passes 28 tests.
- The complete transport and cleanup group passes 387 tests without
unhandled rejections. These checks use local scripted children and
actual process and port ownership.
- Saved-profile decoding, native-execution parsing, installation, and
selected-credential controls pass 208 focused tests. All 109
pre-existing profile identity files remain byte-for-byte unchanged.
Copilot, Grok, and shared TypeScript/Rust generation checks pass.
- The final recovery fixture passes all seven cases. After the master
rebase, the combined recovery and owned-Node-runtime group passes all 10
cases. Each expiry case sets one 45-second absolute grant before
startup. It includes the deliberate six-second cold delay, four existing
eight-second command budgets, and active-turn headroom. The test proves
an active turn before expiry and then checks the daemon, owned process
group, child processes, and all three native port fences. The 60-second
test deadline does not renew provider authority.
- The consolidated model-discovery and pending-release group passes 22
tests. Google credential compatibility and auth recovery pass 25 tests,
including existing Gemini API-key recovery. The full product settings
test group remains intact in #18189.
- Earlier failures remain in their original CI runs and local records.
They exposed the Copilot 82 decoder gap, an expired five-second fixture
bootstrap ticket, and a fixture grant consumed before activation on
loaded CI. Each repair has a separate result. Production ticket limits
and admission behavior are unchanged by the final test commits.
- An unrelated decision-expiry check failed in [the earlier CI
run](https://github.com/paperclipai/paperclip/actions/runs/40941986698/job/123688668997).
Its owning code is unchanged from the base. The unchanged suite passes
20 tests on isolated PostgreSQL. A controlled database rollback
reproduces the earlier wake: clean startup schedules the 120-second
decision expiry, while rollback reconciliation leaves a 30-second
continuation-coalescing wake. The decision stays open, with no wake
delivery or error. The fresh final-head server shard passes; the failed
result is preserved.
- Final native commit `ec6bc7e4d77f5ae0cc4946dbba6c662e95a87f5b` passed
`ci / verify` and `ci / e2e`. Fresh Greptile and Paperclip reviews both
score 5/5, with no open findings. Antigravity retains the recorded
native `.5` closure and exact low-model profile. This PR does not claim
global or cross-platform qualification.

## Risks

- The new transport carries private managed credentials. Exact
ownership, account generation, model, instructions, and native
retirement are fail-closed.
- Only local macOS ARM64 has a verified complete Antigravity runtime.
Default Antigravity task execution remains disabled without current
trusted operator admission. The Google server sign-in lane and
production candidate producer ship in #18189; this native slice does not
expose the subscription catalog choice.
- Shared launch inputs change provider identities. New identities are
append-only; existing recorded identities remain available.

## Model Used

OpenAI Codex (GPT-6). The host did not expose the exact model revision
or context limit. The model used tools for code edits, tests, and
review.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-10-29 16:36:12 -05:00
1 parent 6df9fc2221
commit 54cc522e86
99 files changed
+4275 -192

No files matched your search

+4 -4
View File
@@ -330,7 +330,7 @@ jobs:
packages/paperclip-runner/runner/target/release/deps/paperclip_runnerd-*
packages/paperclip-runner/runner/target/release/paperclip-runnerd
packages/paperclip-runner/runner/target/release/paperclip-runnerd.d
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/antigravity-distributions.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
- name: Mark the restored Runner release crate current
if: ${{ steps.runner_release_crate.outputs.cache-hit == 'true' }}
@@ -843,7 +843,7 @@ jobs:
packages/paperclip-runner/runner/target/release/deps/paperclip_runnerd-*
packages/paperclip-runner/runner/target/release/paperclip-runnerd
packages/paperclip-runner/runner/target/release/paperclip-runnerd.d
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/antigravity-distributions.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
- name: Mark the restored Runner release crate current
if: ${{ steps.runner_release_crate.outputs.cache-hit == 'true' }}
@@ -1060,7 +1060,7 @@ jobs:
packages/paperclip-runner/runner/target/release/deps/paperclip_runnerd-*
packages/paperclip-runner/runner/target/release/paperclip-runnerd
packages/paperclip-runner/runner/target/release/paperclip-runnerd.d
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/antigravity-distributions.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
- name: Mark the restored Runner release crate current
if: ${{ steps.runner_release_crate.outputs.cache-hit == 'true' }}
@@ -1418,7 +1418,7 @@ jobs:
packages/paperclip-runner/runner/target/release/deps/paperclip_runnerd-*
packages/paperclip-runner/runner/target/release/paperclip-runnerd
packages/paperclip-runner/runner/target/release/paperclip-runnerd.d
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/antigravity-distributions.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
- name: Mark the restored Runner release crate current
if: ${{ steps.runner_release_crate.outputs.cache-hit == 'true' }}
+2 -2
View File
@@ -471,7 +471,7 @@ jobs:
packages/paperclip-runner/runner/target/release/deps/paperclip_runnerd-*
packages/paperclip-runner/runner/target/release/paperclip-runnerd
packages/paperclip-runner/runner/target/release/paperclip-runnerd.d
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/antigravity-distributions.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
- name: Build the Runner release crate for its cache
if: ${{ matrix.lane == 'rust' && github.repository == 'paperclipai/paperclip' && github.event_name == 'push' && github.ref == 'refs/heads/master' && inputs.ref == github.sha && steps.runner_release_crate.outputs.cache-hit != 'true' }}
@@ -492,7 +492,7 @@ jobs:
packages/paperclip-runner/runner/target/release/deps/paperclip_runnerd-*
packages/paperclip-runner/runner/target/release/paperclip-runnerd
packages/paperclip-runner/runner/target/release/paperclip-runnerd.d
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
key: runner-release-crate-v1-${{ steps.runner_release_crate_key.outputs.scope }}-${{ hashFiles('packages/paperclip-runner/rust-toolchain.toml', 'packages/paperclip-runner/acpx-profiles.json', 'packages/paperclip-runner/antigravity-distributions.json', 'packages/paperclip-runner/runner/Cargo.toml', 'packages/paperclip-runner/runner/Cargo.lock', 'packages/paperclip-runner/runner/crates/**', 'packages/paperclip-runner/protocol/**', 'packages/paperclip-runner/test-fixtures/**', 'packages/paperclip-runner/test/fixtures/**', 'tests/runner-e2e/fixtures/**') }}
build:
if: ${{ !inputs.runner_binaries_only }}
+3 -2
View File
@@ -122,10 +122,11 @@ WORKDIR /app/packages/paperclip-runner
COPY packages/paperclip-runner/rust-toolchain.toml ./
COPY packages/paperclip-runner/runner ./runner
COPY packages/paperclip-runner/protocol ./protocol
COPY packages/paperclip-runner/acpx-profiles.json packages/paperclip-runner/antigravity-distributions.json ./
# Cargo fingerprints source mtimes. Normalize them here and after the full
# source copy below so a fresh checkout cannot invalidate unchanged inputs.
RUN find runner protocol -type f -exec touch -d @0 {} + \
&& touch -d @0 rust-toolchain.toml \
&& touch -d @0 rust-toolchain.toml acpx-profiles.json antigravity-distributions.json \
&& cargo build --release --manifest-path runner/Cargo.toml --locked -p paperclip-runner-core --bin paperclip-runnerd
FROM runner-build AS build
@@ -133,7 +134,7 @@ WORKDIR /app
COPY --from=deps /app /app
COPY . .
RUN find packages/paperclip-runner/runner packages/paperclip-runner/protocol -type f -exec touch -d @0 {} + \
&& touch -d @0 packages/paperclip-runner/rust-toolchain.toml
&& touch -d @0 packages/paperclip-runner/rust-toolchain.toml packages/paperclip-runner/acpx-profiles.json packages/paperclip-runner/antigravity-distributions.json
# Both the browser bundle and server stamp need the source commit. Declare it
# after the stable dependency layers, before either application build.
ARG PAPERCLIP_BUILD_COMMIT=""
+1
View File
@@ -9,6 +9,7 @@ WORKDIR /workspace/packages/paperclip-runner/runner
COPY packages/paperclip-runner/runner/Cargo.toml packages/paperclip-runner/runner/Cargo.lock ./
COPY packages/paperclip-runner/runner/crates ./crates
COPY packages/paperclip-runner/protocol ../protocol
COPY packages/paperclip-runner/acpx-profiles.json packages/paperclip-runner/antigravity-distributions.json ../
RUN cargo build --locked --release -j 2 -p paperclip-runner-core --bin paperclip-runnerd \
&& strip /workspace/packages/paperclip-runner/runner/target/release/paperclip-runnerd
@@ -253,6 +253,7 @@ export const PAPERCLIP_RUNNER_ACPX_PROFILES = Object.freeze([
{ value: "cursor", label: "Cursor", qualified: true, credentialEnvironment: ["CURSOR_API_KEY", "CURSOR_AUTH_TOKEN"] },
{ value: "copilot", label: "GitHub Copilot", qualified: true, credentialEnvironment: ["COPILOT_GITHUB_TOKEN"] },
{ value: "pi", label: "Pi", qualified: true, credentialEnvironment: ["OPENROUTER_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GEMINI_API_KEY"] },
{ value: "antigravity", label: "Google Antigravity", qualified: false, credentialEnvironment: ["GEMINI_API_KEY"] },
] as const);
/** Exact Pi native thinking levels; aliases that silently clamp are not configuration. */
+16 -6
View File
@@ -4,22 +4,32 @@
"driverKind": "acpx_runtime",
"protocolVersion": 1,
"profiles": {
"antigravity": {
"agentProfileVersion": 1,
"agentServerPackage": "native:antigravity-acp",
"agentServerVersion": "1.4.3",
"agentRuntimePackage": "native:antigravity-localharness",
"agentRuntimeVersion": "1.4.3",
"commandDigest": "sha256:acf5f574b6b9ec4388ff9d081890caa330e66bf1ae3fe3b08e7f0fb0e851b9ce",
"requiresProviderPolicy": true,
"qualificationStatus": "pending"
},
"grok": {
"agentProfileVersion": 12,
"agentProfileVersion": 13,
"agentServerPackage": "builtin:grok-acp",
"agentServerVersion": "1",
"agentRuntimePackage": "native:grok",
"agentRuntimeVersion": "1.0.13",
"commandDigest": "sha256:3dc83b2c3493913748cacb09025a8c97b74f3b5bbb1dc204072d1a773fa9af3a",
"commandDigest": "sha256:e311f8111dd12f0c49bf12a80345cfe5f31a1222e0189209adeabf2f9bd0e1b0",
"requiresProviderPolicy": false
},
"pi": {
"agentProfileVersion": 34,
"agentProfileVersion": 35,
"agentServerPackage": "pi-acp",
"agentServerVersion": "0.0.33",
"agentRuntimePackage": "@earendil-works/pi-coding-agent",
"agentRuntimeVersion": "1.0.0",
"commandDigest": "sha256:ed4b32cc8fbf36309d38b3bd378f41f8e7b9e96a2405d275cc55e89042d94c7f",
"commandDigest": "sha256:9473de6ef4baf2bd82fe8b6757239bdc160960b0a78ae6f47fa9e0996544226c",
"requiresProviderPolicy": true
},
"cursor": {
@@ -32,12 +42,12 @@
"requiresProviderPolicy": true
},
"copilot": {
"agentProfileVersion": 81,
"agentProfileVersion": 84,
"agentServerPackage": "@github/copilot",
"agentServerVersion": "1.0.88",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"commandDigest": "sha256:a51bdf77bba778c4d0265bbfd92a0e6c146f9de5debc3b4a2a268c34e5576e0c",
"commandDigest": "sha256:a95f1d8e4d0a18d0c2cbc3bffdf998a2c5b8a582bdf51a72085b01d948d0a04e",
"requiresProviderPolicy": true
},
"claude": {
@@ -53,6 +53,16 @@
},
"additionalProperties": false,
"allOf": [
{
"if": { "properties": { "command": { "enum": ["initialize", "session.open"] } } },
"then": { "properties": { "params": {
"type": "object",
"properties": { "candidateAdmission": { "$ref": "#/$defs/candidateAdmission" } },
"if": { "properties": { "candidateAdmission": {} }, "required": ["candidateAdmission"] },
"then": { "required": ["agent", "model"], "properties": { "agent": { "const": "antigravity" }, "model": { "type": "string" } } }
} } },
"else": { "properties": { "params": { "type": "object", "properties": { "candidateAdmission": false } } } }
},
{
"if": { "properties": { "command": { "const": "turn.steer" } } },
"then": { "properties": { "params": { "$ref": "#/$defs/turnControl" } } }
@@ -63,6 +73,25 @@
}
]
},
"candidateAdmission": {
"description": "The same closed record is accepted only from trusted runner startup CLI and repeated on sidecar initialize/open. Company authority is validated by the server; the runtime additionally checks compiled pins, actual run/session identity, native host, and expiry.",
"type": "object",
"required": ["schema", "companyId", "agent", "model", "profileDigest", "nativeClosureDigest", "platform", "architecture", "expiresAt", "runId", "normalizedSessionId"],
"properties": {
"schema": { "const": "paperclip.acpx-candidate-admission.v1" },
"companyId": { "type": "string", "minLength": 1, "maxLength": 240, "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" },
"agent": { "const": "antigravity" },
"model": { "type": "string", "minLength": 1, "maxLength": 512, "pattern": "^[^\\u0000-\\u001f\\u007f]+$" },
"profileDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
"nativeClosureDigest": { "type": "string", "pattern": "^sha256:[a-f0-9]{64}$" },
"platform": { "const": "darwin" },
"architecture": { "const": "arm64" },
"expiresAt": { "type": "integer", "minimum": 1, "maximum": 9007199254740991 },
"runId": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" },
"normalizedSessionId": { "type": "string", "minLength": 1, "maxLength": 160, "pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]*$" }
},
"additionalProperties": false
},
"piThinkingLevel": { "enum": ["off", "low", "high", "max"] },
"sessionOpenThinkingLevel": {
"type": "object",
@@ -0,0 +1,211 @@
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::generated_acpx_profiles::acpx_release_profile;
use crate::local_runner::LocalRunnerError;
use crate::stable_identity::{is_stable_id, DURABLE_STABLE_ID_CHARS, SHORT_STABLE_ID_CHARS};
const SCHEMA: &str = "paperclip.acpx-candidate-admission.v1";
const MAX_LIFETIME_MS: u64 = 24 * 60 * 60 * 1_000;
const MAX_JSON_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
const DISTRIBUTIONS: &str = include_str!("../../../../antigravity-distributions.json");
/// Operator authority supplied only through trusted process startup. Company
/// membership is authorized by the server; an agent descriptor cannot mint it.
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AcpxCandidateAdmission {
pub schema: String,
pub company_id: String,
pub agent: String,
pub model: String,
pub profile_digest: String,
pub native_closure_digest: String,
pub platform: String,
pub architecture: String,
pub expires_at: u64,
pub run_id: String,
pub normalized_session_id: String,
}
fn invalid() -> LocalRunnerError {
LocalRunnerError::invalid(
"ACPX candidate admission is missing, expired, or does not match the exact authorized execution",
)
}
pub(crate) fn current_time_ms() -> Result<u64, LocalRunnerError> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.ok()
.and_then(|duration| u64::try_from(duration.as_millis()).ok())
.ok_or_else(invalid)
}
impl AcpxCandidateAdmission {
pub fn parse_json(encoded: &str) -> Result<Self, LocalRunnerError> {
if encoded.len() > 4_096 {
return Err(invalid());
}
let admission: Self = serde_json::from_str(encoded).map_err(|_| invalid())?;
admission.validate()?;
Ok(admission)
}
pub fn validate(&self) -> Result<(), LocalRunnerError> {
self.validate_at(
current_time_ms()?,
std::env::consts::OS,
std::env::consts::ARCH,
)
}
fn validate_at(&self, now: u64, os: &str, architecture: &str) -> Result<(), LocalRunnerError> {
let profile = acpx_release_profile("antigravity").ok_or_else(invalid)?;
let manifest: Value = serde_json::from_str(DISTRIBUTIONS).map_err(|_| invalid())?;
let closure = manifest
.pointer("/platforms/darwin-arm64/closureSha256")
.and_then(Value::as_str)
.ok_or_else(invalid)?;
if self.schema != SCHEMA
|| !is_stable_id(&self.company_id, DURABLE_STABLE_ID_CHARS)
|| self.agent != "antigravity"
|| self.model.trim().is_empty()
|| self.model != self.model.trim()
|| self.model.len() > 512
|| self.model.chars().any(char::is_control)
|| matches!(self.model.to_ascii_lowercase().as_str(), "auto" | "default")
|| self.profile_digest != profile.command_digest
|| self.native_closure_digest != format!("sha256:{closure}")
|| self.platform != "darwin"
|| self.architecture != "arm64"
|| os != "macos"
|| architecture != "aarch64"
|| self.expires_at > MAX_JSON_SAFE_INTEGER
|| self.expires_at <= now
|| self.expires_at > now.saturating_add(MAX_LIFETIME_MS)
|| !is_stable_id(&self.run_id, SHORT_STABLE_ID_CHARS)
|| !is_stable_id(&self.normalized_session_id, SHORT_STABLE_ID_CHARS)
{
return Err(invalid());
}
Ok(())
}
pub fn validate_binding(
&self,
agent: &str,
model: &str,
run_id: &str,
normalized_session_id: &str,
) -> Result<(), LocalRunnerError> {
self.validate()?;
if self.agent != agent
|| self.model != model
|| self.run_id != run_id
|| self.normalized_session_id != normalized_session_id
{
return Err(invalid());
}
Ok(())
}
}
#[cfg(test)]
pub(crate) fn test_admission(run_id: &str, session_id: &str) -> AcpxCandidateAdmission {
let manifest: Value = serde_json::from_str(DISTRIBUTIONS).unwrap();
AcpxCandidateAdmission {
schema: SCHEMA.to_owned(),
company_id: "company-1".to_owned(),
agent: "antigravity".to_owned(),
model: "explicit-model".to_owned(),
profile_digest: acpx_release_profile("antigravity")
.unwrap()
.command_digest
.to_owned(),
native_closure_digest: format!(
"sha256:{}",
manifest["platforms"]["darwin-arm64"]["closureSha256"]
.as_str()
.unwrap()
),
platform: "darwin".to_owned(),
architecture: "arm64".to_owned(),
expires_at: current_time_ms().unwrap() + 60_000,
run_id: run_id.to_owned(),
normalized_session_id: session_id.to_owned(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn candidate_record_is_strict_and_pinned_to_native_host_and_time() {
let candidate = test_admission("run-1", "session-1");
let now = candidate.expires_at - 1;
candidate.validate_at(now, "macos", "aarch64").unwrap();
for (os, architecture) in [("linux", "aarch64"), ("macos", "x86_64")] {
assert!(candidate.validate_at(now, os, architecture).is_err());
}
assert!(candidate
.validate_at(candidate.expires_at, "macos", "aarch64")
.is_err());
assert!(candidate
.validate_at(
candidate.expires_at - MAX_LIFETIME_MS - 1,
"macos",
"aarch64"
)
.is_err());
for (field, value) in [
("schema", json!("unknown")),
("companyId", json!("bad company")),
("agent", json!("copilot")),
("model", json!("auto")),
("profileDigest", json!("sha256:wrong")),
("nativeClosureDigest", json!("sha256:wrong")),
("platform", json!("linux")),
("architecture", json!("x64")),
("runId", json!("bad/run")),
("normalizedSessionId", json!("bad/session")),
] {
let mut changed = serde_json::to_value(&candidate).unwrap();
changed[field] = value;
let changed: AcpxCandidateAdmission = serde_json::from_value(changed).unwrap();
assert!(
changed.validate_at(now, "macos", "aarch64").is_err(),
"{field}"
);
}
let mut value = serde_json::to_value(&candidate).unwrap();
value["extra"] = json!(true);
assert!(serde_json::from_value::<AcpxCandidateAdmission>(value).is_err());
assert!(AcpxCandidateAdmission::parse_json(&"x".repeat(4_097)).is_err());
}
#[test]
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn candidate_binds_actual_launch_and_does_not_echo_bad_input() {
let candidate = test_admission("run-1", "session-1");
candidate
.validate_binding("antigravity", "explicit-model", "run-1", "session-1")
.unwrap();
for expected in [
("copilot", "explicit-model", "run-1", "session-1"),
("antigravity", "another-model", "run-1", "session-1"),
("antigravity", "explicit-model", "run-2", "session-1"),
("antigravity", "explicit-model", "run-1", "session-2"),
] {
assert!(candidate
.validate_binding(expected.0, expected.1, expected.2, expected.3)
.is_err());
}
let error = AcpxCandidateAdmission::parse_json("private malformed input").unwrap_err();
assert!(!error.to_string().contains("private"));
}
}
@@ -350,6 +350,18 @@ impl AcpxProviderDescriptor {
expected_identity: Option<AcpxProviderSessionIdentity>,
launch_profile: Option<&AcpxLaunchProfile>,
) -> Result<AcpxProviderSessionConfig, DurableRunnerError> {
let candidate_admission =
launch_profile.and_then(|profile| profile.candidate_admission.clone());
if let Some(admission) = &candidate_admission {
admission
.validate_binding(
&self.agent,
&self.model,
&self.run_id,
&self.normalized_session_id,
)
.map_err(|error| DurableRunnerError::invalid(error.to_string()))?;
}
secure_directory(&self.runtime_directory, "ACPX runtime")?;
let transport = self.verified_transport(launch_profile)?;
// Keep the controller's full authority catalog durable so terminal
@@ -375,6 +387,7 @@ impl AcpxProviderDescriptor {
operations: provider_operations,
};
Ok(AcpxProviderSessionConfig {
candidate_admission,
transport,
agent: self.agent.clone(),
model: self.model.clone(),
@@ -873,6 +886,14 @@ impl AcpxCommandExecutor {
self.state_dir.join(ACPX_PROVIDER_STATE_FILE)
}
fn candidate_admission(
&self,
) -> Option<&crate::acpx_candidate_admission::AcpxCandidateAdmission> {
self.launch_profile
.as_ref()
.and_then(|profile| profile.candidate_admission.as_ref())
}
fn launch_profile_digest(&self) -> Result<String, DurableRunnerError> {
self.launch_profile
.as_ref()
@@ -915,6 +936,11 @@ impl AcpxCommandExecutor {
)))
}
};
if self.candidate_admission().is_some() {
return Err(DurableRunnerError::invalid(
"ACPX candidate cannot adopt a prior durable runner activation",
));
}
let length = file
.metadata()
.map_err(|error| {
@@ -1131,6 +1157,16 @@ impl AcpxCommandExecutor {
DurableRunnerError::invalid(format!("run.prepare ACPX provider is invalid: {error}"))
})?;
descriptor.validate(&self.context)?;
if let Some(admission) = self.candidate_admission() {
admission
.validate_binding(
&descriptor.agent,
&descriptor.model,
&descriptor.run_id,
&descriptor.normalized_session_id,
)
.map_err(|error| DurableRunnerError::invalid(error.to_string()))?;
}
let tool_set = authorized_tool_set(payload)?;
let launch_profile_digest = self.launch_profile_digest()?;
if let Some(state) = self.state.as_ref() {
@@ -1160,6 +1196,11 @@ impl AcpxCommandExecutor {
}
fn attach_run(&mut self, payload: &Value) -> Result<(), DurableRunnerError> {
if self.candidate_admission().is_some() {
return Err(DurableRunnerError::invalid(
"ACPX candidate cannot rotate or adopt a live runner authority",
));
}
let descriptor: AcpxProviderDescriptor = serde_json::from_value(
payload
.get("provider")
@@ -1954,6 +1995,9 @@ impl AcpxCommandExecutor {
}
fn suspend(&mut self) -> Result<CommandExecution, DurableRunnerError> {
if self.candidate_admission().is_some() {
return self.close_session("candidate runner.suspend retires the owned process");
}
if self
.state
.as_ref()
@@ -2189,6 +2233,14 @@ fn goal_terminal_disposition<'a>(
impl CommandExecutor for AcpxCommandExecutor {
fn execute(&mut self, command: &Command) -> Result<CommandExecution, DurableRunnerError> {
self.restore()?;
if self.candidate_admission().is_some()
&& command.command_type == "run.attach"
&& command.payload.get("paperclipNextAuthority").is_some()
{
return Err(DurableRunnerError::invalid(
"ACPX candidate cannot rotate a live runner authority",
));
}
if command.command_type != "run.attach"
&& self
.state
@@ -2788,6 +2840,7 @@ mod tests {
true,
);
let launch_profile = AcpxLaunchProfile {
candidate_admission: None,
authority_digest: format!("sha256:{}", "d".repeat(64)),
command: command_path.clone(),
args: Vec::new(),
@@ -3246,7 +3299,7 @@ mod tests {
#[test]
fn pi_profile_matches_published_identity_and_rejects_prior_profiles() {
let published: Value = serde_json::from_str(include_str!(
"../../../../test-fixtures/pi-acp/profile-v34-identity.json"
"../../../../test-fixtures/pi-acp/profile-v35-identity.json"
))
.unwrap();
let mut value = descriptor("codex");
@@ -3263,6 +3316,7 @@ mod tests {
current.validate(&context()).unwrap();
for prior in [
include_str!("../../../../test-fixtures/pi-acp/profile-v34-identity.json"),
include_str!("../../../../test-fixtures/pi-acp/profile-v33-identity.json"),
include_str!("../../../../test-fixtures/pi-acp/profile-v32-identity.json"),
include_str!("../../../../test-fixtures/pi-acp/profile-v31-identity.json"),
@@ -3314,6 +3368,7 @@ mod tests {
write_artifact(&sidecar, b"qualified sidecar", false);
let args = vec![sidecar.to_string_lossy().into_owned()];
let profile = AcpxLaunchProfile {
candidate_admission: None,
authority_digest: format!("sha256:{}", "d".repeat(64)),
command: command.clone(),
args: args.clone(),
@@ -3344,6 +3399,36 @@ mod tests {
fs::remove_dir_all(directory).unwrap();
}
#[test]
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn candidate_authority_cannot_come_from_descriptor_or_recover_a_prior_runner() {
let directory = temporary_directory("candidate-authority");
let mut admission = crate::acpx_candidate_admission::test_admission("run-1", "session-1");
admission.model = "explicit-test-model".to_owned();
let profile = AcpxLaunchProfile {
candidate_admission: Some(admission.clone()),
authority_digest: format!("sha256:{}", "a".repeat(64)),
command: PathBuf::from("/qualified/node"),
args: vec!["/qualified/acpx-sidecar.js".to_owned()],
artifacts: Vec::new(),
};
let mut injected = descriptor("antigravity");
injected["candidateAdmission"] = json!(admission);
assert!(serde_json::from_value::<AcpxProviderDescriptor>(injected).is_err());
let config = test_config(&directory, Some(profile));
let mut executor = AcpxCommandExecutor::with_runner_config(&directory, &config);
let mut wrong = descriptor("antigravity");
wrong["model"] = json!("another-model");
assert!(executor.prepare(&json!({"provider":wrong})).is_err());
assert!(!executor.state_path().exists());
assert!(executor.attach_run(&json!({})).is_err());
write_artifact(&executor.state_path(), b"{}", false);
let error = executor.restore().unwrap_err().to_string();
assert!(error.contains("cannot adopt a prior durable runner"));
assert!(executor.session.is_none());
fs::remove_dir_all(directory).unwrap();
}
#[cfg(target_os = "macos")]
mod darwin_named_transport_cleanup {
use super::*;
@@ -3363,6 +3448,7 @@ mod tests {
write_artifact(&sidecar, b"qualified sidecar", false);
let args = vec![sidecar.to_string_lossy().into_owned()];
let profile = AcpxLaunchProfile {
candidate_admission: None,
authority_digest: format!("sha256:{}", "d".repeat(64)),
command: command.clone(),
args: args.clone(),
@@ -3484,6 +3570,7 @@ mod tests {
write_artifact(&command, b"qualified node", true);
symlink(&command, &command_link).unwrap();
let profile = AcpxLaunchProfile {
candidate_admission: None,
authority_digest: format!("sha256:{}", "d".repeat(64)),
command: command_link.clone(),
args: Vec::new(),
@@ -3542,6 +3629,7 @@ mod tests {
true,
);
let launch_profile = AcpxLaunchProfile {
candidate_admission: None,
authority_digest: format!("sha256:{}", "d".repeat(64)),
command: command.clone(),
args: Vec::new(),
@@ -3824,6 +3912,7 @@ mod tests {
true,
);
let launch_profile = AcpxLaunchProfile {
candidate_admission: None,
authority_digest: format!("sha256:{}", "d".repeat(64)),
command: command.clone(),
args: Vec::new(),
@@ -4147,6 +4236,7 @@ mod tests {
true,
);
let launch_profile = AcpxLaunchProfile {
candidate_admission: None,
authority_digest: format!("sha256:{}", "d".repeat(64)),
command: command.clone(),
args: Vec::new(),
@@ -4422,6 +4512,7 @@ mod tests {
true,
);
let launch_profile = AcpxLaunchProfile {
candidate_admission: None,
authority_digest: format!("sha256:{}", "d".repeat(64)),
command: command.clone(),
args: Vec::new(),
@@ -4590,6 +4681,7 @@ mod tests {
let sidecar = directory.join("sidecar");
write_artifact(&sidecar, b"qualified sidecar", true);
let launch_profile = AcpxLaunchProfile {
candidate_admission: None,
authority_digest: format!("sha256:{}", "d".repeat(64)),
command: sidecar.clone(),
args: Vec::new(),
@@ -89,6 +89,7 @@ pub struct AcpxProviderRuntimePolicy {
#[derive(Clone, Debug)]
pub struct AcpxProviderSessionConfig {
pub candidate_admission: Option<crate::acpx_candidate_admission::AcpxCandidateAdmission>,
pub transport: AcpxSidecarTransportConfig,
pub agent: String,
pub model: String,
@@ -117,6 +118,18 @@ impl AcpxProviderSessionConfig {
let profile = acpx_release_profile(&self.agent).ok_or_else(|| {
LocalRunnerError::invalid("ACPX agent must name a known immutable profile")
})?;
if let Some(admission) = &self.candidate_admission {
admission.validate_binding(
&self.agent,
&self.model,
&self.run_id,
&self.normalized_session_id,
)?;
} else if profile.qualification_pending {
return Err(LocalRunnerError::invalid(
"ACPX provider qualification is pending; this build cannot launch the provider",
));
}
validate_text(&self.model, MAX_MODEL_CHARS, "ACPX model")?;
if self.claude_effort.as_ref().is_some_and(|effort| {
self.agent != "claude"
@@ -475,6 +488,14 @@ impl AcpxProviderSession {
"ACPX provider session still has unsettled semantic tools",
));
}
if let Some(admission) = &self.config.candidate_admission {
admission.validate_binding(
&self.config.agent,
&self.config.model,
&self.config.run_id,
&self.config.normalized_session_id,
)?;
}
let rotate_turn_identity_ledger = self.state.settled_turn_identity_capacity_reached();
let identity_validation = if rotate_turn_identity_ledger {
self.state
@@ -1367,6 +1388,9 @@ fn session_open_params(config: &AcpxProviderSessionConfig, sidecar_tools: &[Valu
"tools": &sidecar_tools,
"expectedIdentity": config.expected_identity,
});
if let Some(admission) = &config.candidate_admission {
params["candidateAdmission"] = json!(admission);
}
if let Some(effort) = &config.claude_effort {
params["claudeEffort"] = json!(effort);
}
@@ -1385,6 +1409,14 @@ fn session_open_params(config: &AcpxProviderSessionConfig, sidecar_tools: &[Valu
params
}
fn initialize_params(config: &AcpxProviderSessionConfig) -> Value {
let mut params = json!({"agent": config.agent, "model": config.model});
if let Some(admission) = &config.candidate_admission {
params["candidateAdmission"] = json!(admission);
}
params
}
fn bootstrap(
transport: &mut AcpxSidecarTransport,
config: &AcpxProviderSessionConfig,
@@ -1399,7 +1431,7 @@ fn bootstrap(
let sidecar_tools = sidecar_run_tool_operations(&config.tool_set);
let initialized = transport.request(
GeneratedAcpxSidecarCommand::Initialize,
json!({"agent": config.agent, "model": config.model}),
initialize_params(config),
)?;
verify_initialize_response(&initialized, transport.process_id())?;
@@ -1813,6 +1845,7 @@ mod mode_tests {
fn config() -> AcpxProviderSessionConfig {
let operations = Vec::new();
AcpxProviderSessionConfig {
candidate_admission: None,
transport: AcpxSidecarTransportConfig {
command: std::env::current_exe().unwrap(),
args: Vec::new(),
@@ -1866,6 +1899,62 @@ mod mode_tests {
provider_lifetime_fence_candidates: [60_001, 60_002, 60_003],
}
}
#[test]
fn pending_profile_cannot_launch_even_with_explicit_policy() {
let mut config = config();
config.agent = "antigravity".to_owned();
config.pi_thinking_level = None;
assert!(config
.validate()
.unwrap_err()
.to_string()
.contains("qualification is pending"));
}
#[test]
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn candidate_admission_is_bound_and_carried_through_both_bootstrap_requests() {
let mut cfg = config();
assert!(initialize_params(&cfg).get("candidateAdmission").is_none());
assert!(session_open_params(&cfg, &[])
.get("candidateAdmission")
.is_none());
cfg.agent = "antigravity".to_owned();
cfg.mode = None;
let admission = crate::acpx_candidate_admission::test_admission(
&cfg.run_id,
&cfg.normalized_session_id,
);
cfg.candidate_admission = Some(admission.clone());
cfg.validate().unwrap();
assert_eq!(
initialize_params(&cfg)["candidateAdmission"],
json!(admission)
);
assert_eq!(
session_open_params(&cfg, &[])["candidateAdmission"],
json!(admission)
);
for (field, value) in [
("run", "different-run"),
("session", "different-session"),
("model", "different-model"),
("agent", "codex"),
] {
let mut invalid = cfg.clone();
match field {
"run" => invalid.run_id = value.to_owned(),
"session" => invalid.normalized_session_id = value.to_owned(),
"model" => invalid.model = value.to_owned(),
"agent" => invalid.agent = value.to_owned(),
_ => unreachable!(),
}
assert!(invalid.validate().is_err(), "{field}");
}
cfg.candidate_admission.as_mut().unwrap().expires_at = 0;
assert!(cfg.validate().is_err());
}
#[test]
fn model_is_explicit_and_provider_verified_for_every_agent() {
let mut config = config();
@@ -216,6 +216,10 @@ impl AcpxSidecarTransport {
"pi" => &[],
"cursor" => &["CURSOR_API_KEY", "CURSOR_AUTH_TOKEN"],
"copilot" => &["COPILOT_GITHUB_TOKEN"],
"antigravity" => &[
"GEMINI_API_KEY",
"PAPERCLIP_ACPX_ANTIGRAVITY_AUTH_JSON_SECRET",
],
_ => {
return Err(LocalRunnerError::invalid(
"ACPX sidecar credentials require a known agent profile",
@@ -250,7 +254,7 @@ impl AcpxSidecarTransport {
"PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT",
"PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST",
];
if matches!(agent, "pi" | "cursor" | "copilot") {
if matches!(agent, "pi" | "cursor" | "copilot" | "antigravity") {
// Credential values alone are not proof of an explicit task binding.
// The sidecar checks this controller-minted provider/session marker.
keys.push("PAPERCLIP_ACPX_CREDENTIAL_BINDING");
@@ -240,7 +240,8 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
"hasToken": std::env::var("PAPERCLIP_NATIVE_MCP_TOKEN").is_ok(),
"hasUnrelatedSecret": std::env::var("UNRELATED_EVAL_SECRET").is_ok(),
"credentialBinding": std::env::var("PAPERCLIP_ACPX_CREDENTIAL_BINDING").ok(),
"credentialKeys": (["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN", "OPENAI_API_KEY", "CODEX_API_KEY", "OPENROUTER_API_KEY", "GEMINI_API_KEY", "MY_PI_SERVICE_KEY", "LD_API_KEY", "DYLD_API_KEY", "PAPERCLIP_PI_PROVIDERS", "CURSOR_API_KEY", "CURSOR_AUTH_TOKEN", "COPILOT_GITHUB_TOKEN", "GITHUB_TOKEN", "GH_TOKEN"].into_iter().filter(|key| std::env::var(key).is_ok()).collect::<Vec<_>>()),
"credentialKeys": (["ANTHROPIC_API_KEY", "CLAUDE_CODE_OAUTH_TOKEN", "OPENAI_API_KEY", "CODEX_API_KEY", "OPENROUTER_API_KEY", "GEMINI_API_KEY", "PAPERCLIP_ACPX_ANTIGRAVITY_AUTH_JSON_SECRET", "MY_PI_SERVICE_KEY", "LD_API_KEY", "DYLD_API_KEY", "PAPERCLIP_PI_PROVIDERS", "CURSOR_API_KEY", "CURSOR_AUTH_TOKEN", "COPILOT_GITHUB_TOKEN", "GITHUB_TOKEN", "GH_TOKEN"].into_iter().filter(|key| std::env::var(key).is_ok()).collect::<Vec<_>>()),
"ambientAntigravityKeys": (["GEMINI_HOME", "GOOGLE_APPLICATION_CREDENTIALS", "GOOGLE_API_KEY", "GOOGLE_CLOUD_PROJECT", "AGY_ACP_FORCE_FILE_STORAGE", "PYTHONPATH", "PYTHONHOME"].into_iter().filter(|key| std::env::var(key).is_ok()).collect::<Vec<_>>()),
}
}),
)?;
@@ -7,6 +7,7 @@ use std::time::Duration;
#[cfg(unix)]
use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
use paperclip_runner_core::acpx_candidate_admission::AcpxCandidateAdmission;
use paperclip_runner_core::durable::{
capture_bootstrap_ticket, redact_diagnostic_text, run_durable_runner, AcpxLaunchProfile,
DurableRunnerConfig, OpenCodeLaunchProfile, QualifiedLaunchArtifact,
@@ -249,6 +250,9 @@ fn optional_u64(args: &[String], name: &str) -> Result<Option<u64>, LocalRunnerE
}
fn acpx_launch_profile(args: &[String]) -> Result<Option<AcpxLaunchProfile>, LocalRunnerError> {
let candidate_admission = optional_value(args, "--acpx-candidate-admission-json")?
.map(|encoded| AcpxCandidateAdmission::parse_json(&encoded))
.transpose()?;
let authority_digest = optional_value(args, "--acpx-launch-authority-digest")?;
let command = optional_value(args, "--acpx-sidecar-command")?;
let command_sha256 = optional_value(args, "--acpx-sidecar-command-sha256")?;
@@ -261,7 +265,7 @@ fn acpx_launch_profile(args: &[String]) -> Result<Option<AcpxLaunchProfile>, Loc
sidecar,
sidecar_sha256,
) {
(None, None, None, None, None) => Ok(None),
(None, None, None, None, None) if candidate_admission.is_none() => Ok(None),
(
Some(authority_digest),
Some(command),
@@ -272,6 +276,7 @@ fn acpx_launch_profile(args: &[String]) -> Result<Option<AcpxLaunchProfile>, Loc
let command = PathBuf::from(command);
let sidecar = PathBuf::from(sidecar);
Ok(Some(AcpxLaunchProfile {
candidate_admission,
authority_digest,
command: command.clone(),
args: vec![sidecar.to_string_lossy().into_owned()],
@@ -473,6 +478,14 @@ fn run(args: &[String]) -> Result<(), LocalRunnerError> {
{
return run_durable(args);
}
if args
.iter()
.any(|argument| argument == "--acpx-candidate-admission-json")
{
return Err(LocalRunnerError::invalid(
"ACPX candidate admission requires trusted durable runner startup",
));
}
run_local_runner(RunnerConfig {
run_id: value(args, "--run-id")?,
normalized_session_id: value(args, "--session-id")?,
@@ -573,6 +586,42 @@ mod tests {
);
}
#[test]
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn candidate_startup_requires_a_complete_trusted_launch_profile() {
let profiles: serde_json::Value =
serde_json::from_str(include_str!("../../../../../acpx-profiles.json")).unwrap();
let distributions: serde_json::Value = serde_json::from_str(include_str!(
"../../../../../antigravity-distributions.json"
))
.unwrap();
let encoded = json!({
"schema":"paperclip.acpx-candidate-admission.v1", "companyId":"company-1", "agent":"antigravity", "model":"exact-model",
"profileDigest":profiles["profiles"]["antigravity"]["commandDigest"],
"nativeClosureDigest":format!("sha256:{}",distributions["platforms"]["darwin-arm64"]["closureSha256"].as_str().unwrap()),
"platform":"darwin", "architecture":"arm64", "expiresAt":std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_millis() as u64 + 60_000,
"runId":"run-1", "normalizedSessionId":"session-1"
}).to_string();
let mut args = vec!["--acpx-candidate-admission-json".to_owned(), encoded];
assert!(acpx_launch_profile(&args).is_err());
assert!(run(&args).is_err());
args.extend([
"--acpx-launch-authority-digest".to_owned(),
format!("sha256:{}", "a".repeat(64)),
"--acpx-sidecar-command".to_owned(),
"/qualified/node".to_owned(),
"--acpx-sidecar-command-sha256".to_owned(),
format!("sha256:{}", "b".repeat(64)),
"--acpx-sidecar-script".to_owned(),
"/qualified/sidecar.cjs".to_owned(),
"--acpx-sidecar-script-sha256".to_owned(),
format!("sha256:{}", "c".repeat(64)),
]);
let parsed = acpx_launch_profile(&args).unwrap().unwrap();
assert_eq!(parsed.candidate_admission.unwrap().run_id, "run-1");
assert!(acpx_launch_profile(&[]).unwrap().is_none());
}
#[test]
fn pack_inventory_rejects_task_owned_roots_and_writable_aliases() {
let directory =
@@ -118,6 +118,7 @@ pub struct AcpxLaunchProfile {
pub command: PathBuf,
pub args: Vec<String>,
pub artifacts: Vec<QualifiedLaunchArtifact>,
pub candidate_admission: Option<crate::acpx_candidate_admission::AcpxCandidateAdmission>,
}
impl AcpxLaunchProfile {
@@ -163,6 +164,17 @@ impl AcpxLaunchProfile {
);
update(&mut digest, artifact.sha256.as_bytes());
}
if let Some(admission) = &self.candidate_admission {
update(&mut digest, b"paperclip.acpx-candidate-admission.v1");
update(
&mut digest,
&serde_json::to_vec(admission).map_err(|_| {
DurableRunnerError::invalid(
"ACPX candidate admission could not be bound to launch identity",
)
})?,
);
}
Ok(format!("sha256:{:x}", digest.finalize()))
}
}
@@ -257,6 +269,16 @@ impl DurableRunnerConfig {
));
}
if let Some(profile) = self.acpx_launch_profile.as_ref() {
if let Some(admission) = &profile.candidate_admission {
admission
.validate_binding(
&admission.agent,
&admission.model,
&self.run_id,
&self.normalized_session_id,
)
.map_err(|error| DurableRunnerError::invalid(error.to_string()))?;
}
if profile.authority_digest.len() != 71
|| !profile.authority_digest.starts_with("sha256:")
|| !profile.authority_digest[7..]
@@ -349,6 +371,41 @@ mod tests {
}
}
#[test]
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn candidate_launch_digest_binds_authority_and_config_binds_actual_identity() {
let mut profile = AcpxLaunchProfile {
authority_digest: format!("sha256:{}", "a".repeat(64)),
command: PathBuf::from("/qualified/node"),
args: vec!["/qualified/sidecar.cjs".to_owned()],
artifacts: vec![QualifiedLaunchArtifact {
path: PathBuf::from("/qualified/node"),
sha256: format!("sha256:{}", "b".repeat(64)),
}],
candidate_admission: None,
};
let ordinary = profile.canonical_digest().unwrap();
assert_eq!(
ordinary,
"sha256:652c2eb4b49d3a38b028ea5a9803a91188c14a283f1bb478d823681b95be69c6"
);
profile.candidate_admission = Some(crate::acpx_candidate_admission::test_admission(
"run-1",
"session-1",
));
let candidate = profile.canonical_digest().unwrap();
assert_ne!(ordinary, candidate);
let mut config = config();
config.acpx_launch_profile = Some(profile.clone());
config.validate().unwrap();
config.run_id = "wrong-run".to_owned();
assert!(config.validate().is_err());
profile.candidate_admission.as_mut().unwrap().company_id = "company-2".to_owned();
assert_ne!(candidate, profile.canonical_digest().unwrap());
profile.candidate_admission = None;
assert_eq!(ordinary, profile.canonical_digest().unwrap());
}
#[test]
fn validates_durable_event_identity_boundaries() {
let mut boundary = config();
@@ -394,7 +394,25 @@ pub fn run_durable_runner<E: CommandExecutor>(
bootstrap_ticket: BootstrapTicket,
mut executor: E,
) -> Result<(), DurableRunnerError> {
let started = Instant::now();
config.validate()?;
if let Some(admission) = config
.acpx_launch_profile
.as_ref()
.and_then(|profile| profile.candidate_admission.as_ref())
{
let remaining =
Duration::from_millis(admission.expires_at.saturating_sub(current_unix_ms()?));
// Candidate authority is absolute. Include startup/state work in this
// process deadline, and never let a caller's unlimited runtime widen it.
config.max_runtime = if config.max_runtime.is_zero() {
remaining.max(Duration::from_nanos(1))
} else {
config
.max_runtime
.min(remaining.max(Duration::from_nanos(1)))
};
}
let store = DurableStateStore::new(&config.state_dir)?;
let (mut state, recovered) = store.load_or_create(&config)?;
if state.lifecycle == "revoked"
@@ -422,7 +440,6 @@ pub fn run_durable_runner<E: CommandExecutor>(
// reconnects retain the same validated addresses so DNS cannot redirect a
// retry after the trust decision.
let mut endpoint = RunnerTransportEndpoint::new(&config.connect_url, &config.run_id)?;
let started = Instant::now();
let mut bootstrap_ticket = Some(bootstrap_ticket);
let mut lease: Option<LeaseCredential> = None;
let mut authenticated_once = false;
@@ -10,18 +10,30 @@ pub(crate) struct AcpxReleaseProfile {
pub agent_runtime_version: Option<&'static str>,
pub command_digest: &'static str,
pub requires_provider_policy: bool,
pub qualification_pending: bool,
}
pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
Some(match agent {
"antigravity" => AcpxReleaseProfile {
agent_server_package: "native:antigravity-acp",
agent_server_version: "1.4.3",
agent_runtime_package: Some("native:antigravity-localharness"),
agent_runtime_version: Some("1.4.3"),
command_digest:
"sha256:acf5f574b6b9ec4388ff9d081890caa330e66bf1ae3fe3b08e7f0fb0e851b9ce",
requires_provider_policy: true,
qualification_pending: true,
},
"grok" => AcpxReleaseProfile {
agent_server_package: "builtin:grok-acp",
agent_server_version: "1",
agent_runtime_package: Some("native:grok"),
agent_runtime_version: Some("1.0.13"),
command_digest:
"sha256:3dc83b2c3493913748cacb09025a8c97b74f3b5bbb1dc204072d1a773fa9af3a",
"sha256:e311f8111dd12f0c49bf12a80345cfe5f31a1222e0189209adeabf2f9bd0e1b0",
requires_provider_policy: false,
qualification_pending: false,
},
"pi" => AcpxReleaseProfile {
agent_server_package: "pi-acp",
@@ -29,8 +41,9 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
agent_runtime_package: Some("@earendil-works/pi-coding-agent"),
agent_runtime_version: Some("1.0.0"),
command_digest:
"sha256:ed4b32cc8fbf36309d38b3bd378f41f8e7b9e96a2405d275cc55e89042d94c7f",
"sha256:9473de6ef4baf2bd82fe8b6757239bdc160960b0a78ae6f47fa9e0996544226c",
requires_provider_policy: true,
qualification_pending: false,
},
"cursor" => AcpxReleaseProfile {
agent_server_package: "cursor-agent",
@@ -40,6 +53,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
command_digest:
"sha256:759e7415fea8ed2b6b0e6f362b57652b8768835f9d1fdae4a94c1b5eb3669301",
requires_provider_policy: true,
qualification_pending: false,
},
"copilot" => AcpxReleaseProfile {
agent_server_package: "@github/copilot",
@@ -47,8 +61,9 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
agent_runtime_package: None,
agent_runtime_version: None,
command_digest:
"sha256:a51bdf77bba778c4d0265bbfd92a0e6c146f9de5debc3b4a2a268c34e5576e0c",
"sha256:a95f1d8e4d0a18d0c2cbc3bffdf998a2c5b8a582bdf51a72085b01d948d0a04e",
requires_provider_policy: true,
qualification_pending: false,
},
"claude" => AcpxReleaseProfile {
agent_server_package: "@agentclientprotocol/claude-agent-acp",
@@ -58,6 +73,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
command_digest:
"sha256:9d73d1f0f121fb96cc8badb28c22d5bff02d8582eb2e40360a81c189e1b9422a",
requires_provider_policy: false,
qualification_pending: false,
},
"codex" => AcpxReleaseProfile {
agent_server_package: "@agentclientprotocol/codex-acp",
@@ -67,6 +83,7 @@ pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
command_digest:
"sha256:443491f4e814501a5dc47347e4510e84faeec9c9382e2d4e53bfe69386d5aabc",
requires_provider_policy: false,
qualification_pending: false,
},
_ => return None,
})
@@ -1,5 +1,6 @@
#![forbid(unsafe_code)]
pub mod acpx_candidate_admission;
pub mod acpx_event_payload;
pub mod acpx_event_scope;
pub mod acpx_provider_backend;
@@ -133,6 +133,27 @@ impl NativeProviderCommandExecutor {
}
fn select_recovery(&mut self) -> Result<(), DurableRunnerError> {
if !self.recovery_checked
&& self
.config
.acpx_launch_profile
.as_ref()
.is_some_and(|profile| profile.candidate_admission.is_some())
&& [
CODEX_PROVIDER_STATE_FILE,
ACPX_PROVIDER_STATE_FILE,
MANAGED_PROVIDER_STATE_FILE,
DOT_PROVIDER_STATE_FILE,
MUSE_PROVIDER_STATE_FILE,
"credential-observer-state.json",
]
.iter()
.any(|file| self.state_dir.join(file).exists())
{
return Err(DurableRunnerError::invalid(
"ACPX candidate cannot adopt a prior durable runner activation",
));
}
self.credentials.restore()?;
if self.recovery_checked {
return Ok(());
@@ -190,6 +211,32 @@ impl NativeProviderCommandExecutor {
"run.prepare requires a supported provider kind before runner execution",
)
})?;
if let Some(admission) = self
.config
.acpx_launch_profile
.as_ref()
.and_then(|profile| profile.candidate_admission.as_ref())
{
if kind != "acpx" {
return Err(DurableRunnerError::invalid(
"ACPX candidate cannot authorize another provider kind",
));
}
admission
.validate_binding(
payload
.pointer("/provider/agent")
.and_then(Value::as_str)
.unwrap_or(""),
payload
.pointer("/provider/model")
.and_then(Value::as_str)
.unwrap_or(""),
&self.config.run_id,
&self.config.normalized_session_id,
)
.map_err(|error| DurableRunnerError::invalid(error.to_string()))?;
}
self.selected = Some(match kind {
"muse" => SelectedExecutor::Dot(ExternalCommandExecutor::with_codec(
&self.state_dir,
@@ -35,6 +35,7 @@ fn temporary_directory(label: &str) -> PathBuf {
fn config(directory: &std::path::Path) -> AcpxProviderSessionConfig {
let operations = Vec::new();
AcpxProviderSessionConfig {
candidate_admission: None,
transport: AcpxSidecarTransportConfig {
command: PathBuf::from(env!("CARGO_BIN_EXE_fake-acpx-sidecar")),
args: vec!["--mode".to_owned(), "suspend".to_owned()],
@@ -34,6 +34,7 @@ fn tool_set() -> AuthorizedToolSet {
fn config(mode: &str) -> AcpxProviderSessionConfig {
AcpxProviderSessionConfig {
candidate_admission: None,
transport: AcpxSidecarTransportConfig {
command: PathBuf::from(env!("CARGO_BIN_EXE_fake-acpx-sidecar")),
args: vec!["--mode".to_owned(), mode.to_owned()],
@@ -30,6 +30,7 @@ fn tool_set() -> AuthorizedToolSet {
fn config(mode: &str) -> AcpxProviderSessionConfig {
AcpxProviderSessionConfig {
candidate_admission: None,
transport: AcpxSidecarTransportConfig {
command: PathBuf::from(env!("CARGO_BIN_EXE_fake-acpx-sidecar")),
args: vec!["--mode".to_owned(), mode.to_owned()],
@@ -10,6 +10,7 @@ use paperclip_runner_core::provider_bridge::{authorized_tool_catalog_digest, Aut
fn config(mode: &str) -> AcpxProviderSessionConfig {
let operations = Vec::new();
AcpxProviderSessionConfig {
candidate_admission: None,
transport: AcpxSidecarTransportConfig {
command: PathBuf::from(env!("CARGO_BIN_EXE_fake-acpx-sidecar")),
args: vec!["--mode".to_owned(), mode.to_owned()],
@@ -29,6 +29,7 @@ fn tool_set() -> AuthorizedToolSet {
fn config(mode: &str) -> AcpxProviderSessionConfig {
AcpxProviderSessionConfig {
candidate_admission: None,
transport: AcpxSidecarTransportConfig {
command: PathBuf::from(env!("CARGO_BIN_EXE_fake-acpx-sidecar")),
args: vec!["--mode".to_owned(), mode.to_owned()],
@@ -392,6 +392,78 @@ fn assigned_gateway_binding_reaches_qualified_sidecar_without_unrelated_secrets(
}
}
#[test]
fn antigravity_candidate_forwards_only_selected_credentials_and_binding() {
const CHILD: &str = "PAPERCLIP_TEST_ANTIGRAVITY_ENV_CHILD";
let selected = match std::env::var(CHILD) {
Ok(selected) => selected,
Err(_) => {
for selected in [
"GEMINI_API_KEY",
"PAPERCLIP_ACPX_ANTIGRAVITY_AUTH_JSON_SECRET",
] {
let status = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"antigravity_candidate_forwards_only_selected_credentials_and_binding",
"--nocapture",
])
.env_clear()
.env("PATH", std::env::var_os("PATH").unwrap_or_default())
.env(CHILD, selected)
.env(selected, "fixture-selected-credential")
.env(
"PAPERCLIP_ACPX_CREDENTIAL_BINDING",
"fixture-controller-binding",
)
.envs(
[
"ANTHROPIC_API_KEY",
"OPENAI_API_KEY",
"COPILOT_GITHUB_TOKEN",
"UNRELATED_EVAL_SECRET",
"GEMINI_HOME",
"GOOGLE_APPLICATION_CREDENTIALS",
"GOOGLE_API_KEY",
"GOOGLE_CLOUD_PROJECT",
"AGY_ACP_FORCE_FILE_STORAGE",
"PYTHONPATH",
"PYTHONHOME",
]
.into_iter()
.map(|key| (key, "fixture-ambient-must-not-cross")),
)
.status()
.unwrap();
assert!(
status.success(),
"isolated Antigravity environment test failed"
);
}
return;
}
};
let mut sidecar = AcpxSidecarTransport::start_for_agent(
&AcpxSidecarTransportConfig {
command: PathBuf::from(env!("CARGO_BIN_EXE_fake-acpx-sidecar")),
args: vec!["--mode".into(), "mcp-environment".into()],
verified_launch: None,
request_timeout: Duration::from_secs(2),
shutdown_grace: Duration::from_millis(50),
},
"antigravity",
)
.unwrap();
let response = sidecar
.request(GeneratedAcpxSidecarCommand::Initialize, json!({}))
.unwrap();
assert_eq!(response["credentialKeys"], json!([selected]));
assert_eq!(response["credentialBinding"], "fixture-controller-binding");
assert_eq!(response["hasUnrelatedSecret"], false);
assert_eq!(response["ambientAntigravityKeys"], json!([]));
sidecar.shutdown().unwrap();
}
#[test]
fn pi_ordinary_request_timeout_still_retires_the_sidecar() {
let mut sidecar = AcpxSidecarTransport::start_for_agent(
@@ -62,6 +62,7 @@ fn checkpoints_the_sidecar_and_rebinds_consecutive_warm_runs_before_accepting_wo
runner_version: "0.0.0".to_owned(),
runner_digest: format!("sha256:{}", "a".repeat(64)),
acpx_launch_profile: Some(AcpxLaunchProfile {
candidate_admission: None,
authority_digest: format!("sha256:{}", "d".repeat(64)),
command: sidecar.clone(),
args: args.clone(),
@@ -17,7 +17,7 @@ use sha2::{Digest, Sha256};
const CODEX_ACPX_DIGEST: &str =
"sha256:443491f4e814501a5dc47347e4510e84faeec9c9382e2d4e53bfe69386d5aabc";
const PI_ACPX_DIGEST: &str =
"sha256:ed4b32cc8fbf36309d38b3bd378f41f8e7b9e96a2405d275cc55e89042d94c7f";
"sha256:9473de6ef4baf2bd82fe8b6757239bdc160960b0a78ae6f47fa9e0996544226c";
fn temporary_directory(label: &str) -> PathBuf {
let nonce = SystemTime::now()
@@ -142,6 +142,7 @@ fn acpx_config(state_dir: &Path, mode: &str) -> DurableRunnerConfig {
let mut config = config(state_dir);
let command = PathBuf::from(env!("CARGO_BIN_EXE_fake-acpx-sidecar"));
config.acpx_launch_profile = Some(AcpxLaunchProfile {
candidate_admission: None,
authority_digest: format!("sha256:{}", "d".repeat(64)),
command: command.clone(),
args: vec![
@@ -158,6 +159,116 @@ fn acpx_config(state_dir: &Path, mode: &str) -> DurableRunnerConfig {
config
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn candidate_fixture(directory: &Path) -> (DurableRunnerConfig, Value) {
use paperclip_runner_core::acpx_candidate_admission::AcpxCandidateAdmission;
let profiles: Value =
serde_json::from_str(include_str!("../../../../acpx-profiles.json")).unwrap();
let distributions: Value =
serde_json::from_str(include_str!("../../../../antigravity-distributions.json")).unwrap();
let profile = &profiles["profiles"]["antigravity"];
let admission = AcpxCandidateAdmission::parse_json(&json!({
"schema":"paperclip.acpx-candidate-admission.v1", "companyId":"company-1",
"agent":"antigravity", "model":"explicit-test-model", "profileDigest":profile["commandDigest"],
"nativeClosureDigest":format!("sha256:{}",distributions["platforms"]["darwin-arm64"]["closureSha256"].as_str().unwrap()),
"platform":"darwin", "architecture":"arm64", "expiresAt":SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as u64 + 60_000,
"runId":"run-1", "normalizedSessionId":"session-1"
}).to_string()).unwrap();
let mut config = acpx_config(directory, "turns-reserved-result-terminal");
let launch = config.acpx_launch_profile.as_mut().unwrap();
*launch.args.last_mut().unwrap() = admission.profile_digest.clone();
launch.candidate_admission = Some(admission);
let mut payload = prepare_payload(directory, "antigravity");
let provider = &mut payload["provider"];
for field in [
"agentServerPackage",
"agentServerVersion",
"agentRuntimePackage",
"agentRuntimeVersion",
"commandDigest",
] {
provider[field] = profile[field].clone();
}
provider["sidecarArgs"] = json!(launch.args);
provider["providerPolicy"] = json!({"readOnly":true});
(config, payload)
}
#[test]
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn candidate_cannot_change_provider_kind_or_adopt_prior_activation() {
let directory = temporary_directory("candidate-provider-authority");
let (config, mut payload) = candidate_fixture(&directory);
for kind in [
"codex",
"opencode",
"openai_dot",
"grok_bot",
"muse",
"claude_managed",
"aws_agentcore",
] {
let mut executor = NativeProviderCommandExecutor::with_runner_config(&directory, &config);
let error = executor
.execute(&command(
1,
"run.prepare",
json!({"provider":{"kind":kind}}),
))
.unwrap_err();
assert!(
error
.to_string()
.contains("cannot authorize another provider kind"),
"{kind}: {error}"
);
assert!(!directory.join("acpx-runtime").exists());
}
payload["paperclipNextAuthority"] = json!({});
let mut executor = NativeProviderCommandExecutor::with_runner_config(&directory, &config);
assert!(executor
.execute(&command(1, "run.attach", payload))
.unwrap_err()
.to_string()
.contains("cannot rotate a live runner authority"));
assert!(!directory.join("acpx-provider-state.json").exists());
fs::write(directory.join("codex-provider-state.json"), b"{}").unwrap();
let mut recovered = NativeProviderCommandExecutor::with_runner_config(&directory, &config);
assert!(recovered
.poll_events()
.unwrap_err()
.to_string()
.contains("cannot adopt a prior durable runner activation"));
fs::remove_dir_all(directory).unwrap();
}
#[test]
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn candidate_verified_cold_session_retires_on_suspend_instead_of_warm_adoption() {
let directory = temporary_directory("candidate-cold-session");
let (config, payload) = candidate_fixture(&directory);
let mut executor = NativeProviderCommandExecutor::with_runner_config(&directory, &config);
executor
.execute(&command(1, "run.prepare", payload))
.unwrap();
let opened = executor
.execute(&command(2, "session.open", json!({})))
.unwrap();
assert_eq!(opened.result["status"], "started");
let closed = executor
.execute(&command(3, "runner.suspend", json!({})))
.unwrap();
assert_eq!(closed.result["status"], "closed");
let state: Value =
serde_json::from_slice(&fs::read(directory.join("acpx-provider-state.json")).unwrap())
.unwrap();
assert_eq!(state["lifecycle"], "closed");
executor.shutdown().unwrap();
let mut recovered = NativeProviderCommandExecutor::with_runner_config(&directory, &config);
assert!(recovered.poll_events().is_err());
fs::remove_dir_all(directory).unwrap();
}
fn pi_acpx_config(state_dir: &Path, mode: &str) -> DurableRunnerConfig {
let mut config = acpx_config(state_dir, mode);
*config
@@ -2,14 +2,14 @@
// restart whenever the resulting identity changes. Historical identities remain.
import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { existsSync, readFileSync, writeFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { resolve, dirname } from "node:path";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const fixture = resolve(root, "test/fixtures/copilot-profile-v81-identity.json");
const fixture = resolve(root, "test/fixtures/copilot-profile-v84-identity.json");
const historical = JSON.parse(readFileSync(resolve(root, "test/fixtures/copilot-profile-v12-identity.json"), "utf8"));
const declaration = { ...historical.declaration, agentProfileVersion: 81 };
const declaration = { ...historical.declaration, agentProfileVersion: 84 };
delete declaration.permissionIdentitySourceSha256;
const sources = {
displayEventSourceSha256: "src/drivers/acpx/copilot-events.ts",
@@ -85,6 +85,9 @@ for (const [field, path] of Object.entries(sources)) {
const sorted = Object.fromEntries(Object.entries(declaration).sort(([a], [b]) => a.localeCompare(b)));
const commandDigest = `sha256:${createHash("sha256").update(JSON.stringify(sorted)).digest("hex")}`;
const identity = `${JSON.stringify({ commandDigest, declaration }, null, 2)}\n`;
if (!process.argv.includes("--check") && existsSync(fixture) && readFileSync(fixture, "utf8") !== identity) {
throw new Error("Copilot profile identity is immutable; advance its version before changing bound source");
}
// The shared manifest now generates both TypeScript and Rust admission. Update
// only Copilot; other providers retain their upstream release attestations.
const manifestPath = resolve(root, "acpx-profiles.json");
@@ -8,6 +8,7 @@ const root = new URL("../", import.meta.url);
const readJson = async path => JSON.parse(await readFile(new URL(path, root), "utf8"));
const manifest = await readJson("acpx-profiles.json");
const distributions = await readJson("cursor-distributions.json");
const antigravityDistributions = await readJson("antigravity-distributions.json");
const contract = await readJson("cursor-contract.json");
const pkg = await readJson("package.json");
assert.equal(manifest.schema, "paperclip.acpx-profiles.v1");
@@ -41,6 +42,8 @@ const canonicalJson = value => value && typeof value === "object"
? Array.isArray(value) ? `[${value.map(canonicalJson).join(",")}]`
: `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`
: JSON.stringify(value);
assert.equal(manifest.profiles.antigravity.agentServerVersion, antigravityDistributions.version);
assert.equal(manifest.profiles.antigravity.commandDigest, `sha256:${createHash("sha256").update(canonicalJson(antigravityDistributions)).digest("hex")}`, "Antigravity distribution identity is stale");
const { commandDigest, ...attestation } = contract;
assert.equal(commandDigest, `sha256:${createHash("sha256").update(canonicalJson(attestation)).digest("hex")}`);
assert.equal(contract.acpxPatchSha256, createHash("sha256")
@@ -74,6 +77,8 @@ export const ACPX_DRIVER_PROTOCOL_VERSION = ${manifest.protocolVersion} as const
export const QUALIFIED_ACPX_PROFILE_DATA = ${JSON.stringify(profiles, null, 2)} as const;
export const CURSOR_DISTRIBUTION_PINS = ${JSON.stringify(Object.fromEntries(Object.entries(distributions.platforms).map(([platform, { closureSha256, executable, entrypoint }]) => [platform, { closureSha256, executable, entrypoint }])), null, 2)} as const;
export const ANTIGRAVITY_DISTRIBUTION_PINS = ${JSON.stringify(Object.fromEntries(Object.entries(antigravityDistributions.platforms).filter(([, pin]) => pin.provisioning === "verified-closure").map(([platform, { closureSha256, executable, args }]) => [platform, { closureSha256, executable, args }])), null, 2)} as const;
`;
const option = value => value === null ? "None" : `Some(${quote(value)})`;
const rust = `// ${header}
@@ -88,6 +93,7 @@ pub(crate) struct AcpxReleaseProfile {
pub agent_runtime_version: Option<&'static str>,
pub command_digest: &'static str,
pub requires_provider_policy: bool,
pub qualification_pending: bool,
}
pub(crate) fn acpx_release_profile(agent: &str) -> Option<AcpxReleaseProfile> {
@@ -100,6 +106,7 @@ ${entries.map(([agent, p]) => ` ${quote(agent)} => AcpxReleaseProfile {
command_digest:
${quote(p.commandDigest)},
requires_provider_policy: ${p.requiresProviderPolicy},
qualification_pending: ${p.qualificationStatus === "pending"},
},`).join("\n")}
_ => return None,
})
@@ -7,9 +7,9 @@ import { fileURLToPath } from "node:url";
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const repositoryRoot = resolve(packageRoot, "../..");
// Versions 1–11 remain decodable historical identities. Versioned fixtures are
// Versions 1–12 remain decodable historical identities. Versioned fixtures are
// immutable; advance this version before changing a versioned declaration.
export const GROK_PROFILE_VERSION = 12;
export const GROK_PROFILE_VERSION = 13;
// Contract surfaces: ACPX wire patch; sidecar parsing and error translation;
// native effort/search/credential admission; host/sandbox/tool-bridge execution;
// launcher integrity and platform pins; Rust descriptor writes, session state,
@@ -47,7 +47,7 @@ test("the generator cannot overwrite a versioned identity after source changes",
const launcher = `${prefix}src/providers/grok/launcher.cjs`;
try {
for (const path of new Set([...GROK_PROFILE_SOURCE_FILES, ...[
"acpx-profiles.json", "cursor-distributions.json", "cursor-contract.json", "package.json",
"acpx-profiles.json", "cursor-distributions.json", "cursor-contract.json", "antigravity-distributions.json", "package.json",
"scripts/grok-profile-identity.mjs", "scripts/generate-acpx-profiles.mjs",
`test/fixtures/copilot-profile-v${manifest.profiles.copilot.agentProfileVersion}-identity.json`,
`test-fixtures/pi-acp/profile-v${manifest.profiles.pi.agentProfileVersion}-identity.json`,
@@ -50,7 +50,7 @@ test("public server tar layout carries a self-contained host provisioner and exa
assert.deepEqual(await readdir(join(installedRunner, "provider-assets/pi")), [`${process.platform}-${process.arch}`]);
});
test("explicit CLI setup passes only network settings to its real child and enables Node proxy handling", async t => {
for (const provider of ["pi", "antigravity"]) test(`${provider} CLI setup passes only network settings to its real child and enables Node proxy handling`, async t => {
const root = await mkdtemp(join(tmpdir(), "paperclip-pi-setup-environment-"));
t.after(() => rm(root, { recursive: true, force: true }));
const server = join(root, "node_modules/@paperclipai/server");
@@ -59,7 +59,7 @@ test("explicit CLI setup passes only network settings to its real child and enab
await writeFile(join(server, "package.json"), JSON.stringify({ name: "@paperclipai/server", type: "module", exports: "./dist/index.js" }));
await writeFile(join(server, "dist/index.js"), "throw Error('server must not start')");
// A capture child models the public layout only. It cannot download or launch Pi.
await writeFile(join(cli, "provision-pi.cjs"), `const assert=require('node:assert/strict');
await writeFile(join(cli, `provision-${provider}.cjs`), `const assert=require('node:assert/strict');
assert.deepEqual(process.execArgv,['--use-env-proxy']);
assert.equal(process.env.HTTP_PROXY,'http://127.0.0.1:9');
assert.equal(process.env.HTTPS_PROXY,'http://127.0.0.1:9');
@@ -71,7 +71,8 @@ test("explicit CLI setup passes only network settings to its real child and enab
console.log('SETUP_NETWORK_BOUNDARY_PASS');`);
const modulePath = join(root, "runtime.mjs");
await build({ entryPoints: [resolve(dirname(new URL(import.meta.url).pathname), "../../../cli/src/commands/runtime.ts")], outfile: modulePath, bundle: true, platform: "node", format: "esm", target: "node24", logLevel: "silent" });
const result = spawnSync(process.execPath, ["--input-type=module", "-e", "const runtime=await import(process.argv[1]);await runtime.setupPiRuntime();", modulePath], {
const setupFunction = provider === "pi" ? "setupPiRuntime" : "setupAntigravityRuntime";
const result = spawnSync(process.execPath, ["--input-type=module", "-e", `const runtime=await import(process.argv[1]);await runtime.${setupFunction}();`, modulePath], {
encoding: "utf8", timeout: 10_000, env: { PATH: "/usr/bin:/bin", HTTP_PROXY: "http://127.0.0.1:9", HTTPS_PROXY: "http://127.0.0.1:9", NO_PROXY: "localhost", http_proxy: "http://127.0.0.1:9", https_proxy: "http://127.0.0.1:9", no_proxy: "localhost", SSL_CERT_FILE: "/public/ca.pem", SSL_CERT_DIR: "/public/certs", NODE_EXTRA_CA_CERTS: "/dev/null", OPENROUTER_API_KEY: "sensitive-canary", NPM_TOKEN: "sensitive-canary", HOME: "/private/home", NODE_PATH: "/foreign", NODE_TLS_REJECT_UNAUTHORIZED: "0" },
});
assert.ifError(result.error); assert.equal(result.status, 0, result.stderr);
@@ -1,3 +1,5 @@
import { bindAcpxCandidateAdmission } from "../drivers/acpx/candidate-admission.js";
import { ANTIGRAVITY_DISTRIBUTION_PINS } from "../drivers/acpx/generated-profiles.js";
import { canonicalNativeRuntimeContextDigest } from "../contracts/runtime-context.js";
import { acpxProfileActivity } from "../drivers/acpx/profile-activity.js";
import { redactObservabilityDiagnostic } from "../drivers/agent-observability.js";
@@ -1269,6 +1271,30 @@ describe("qualified ACPX runtime sidecar", () => {
expect(ACPX_CAPABILITY_PROFILES[agent].qualification).toBe(agent === "cursor" ? "qualified" : "pending");
});
it("refuses pending Antigravity bootstrap before any native process opens", async () => {
const sidecar = startSidecar();
sidecar.write(initializeRequest(1, "antigravity", "caller-exact-model"));
expect(await sidecar.next(frame => frame.id === 1)).toMatchObject({ id: 1, ok: false,
error: { message: expect.stringContaining("qualification is pending"), retryable: false } });
});
it.skipIf(process.platform !== "darwin" || process.arch !== "arm64")("admits only matching candidate startup and rejects session replay before provider launch", async () => {
const sidecar = startSidecar();
const model = "gemini-3.8-flash-low";
const candidateAdmission = bindAcpxCandidateAdmission({ schema: "paperclip.acpx-candidate-admission.v1", companyId: "company-candidate", agent: "antigravity", model,
profileDigest: resolveQualifiedAcpxProfile("antigravity", model).commandDigest,
nativeClosureDigest: `sha256:${ANTIGRAVITY_DISTRIBUTION_PINS["darwin-arm64"].closureSha256}`,
platform: "darwin", architecture: "arm64", expiresAt: Date.now() + 60_000 }, { runId: "run-candidate", normalizedSessionId: "session-candidate" });
const request = initializeRequest(1, "antigravity", model);
sidecar.write({ ...request, params: { ...request.params, candidateAdmission } });
expect(await sidecar.next(frame => frame.id === 1)).toMatchObject({ ok: true, result: { profile: { agent: "antigravity", qualificationStatus: "pending", reportedModelId: model } } });
sidecar.write({ protocolVersion: ACPX_SIDECAR_PROTOCOL_VERSION, id: 2, command: "session.open", params: {
runtimeDirectory: "/unused", workingDirectory: "/unused", normalizedSessionId: "other-session", agent: "antigravity", model,
candidateAdmission, permissionMode: "deny-all", permissionModePinned: true, providerPolicy: { readOnly: true }, systemInstructions: "", tools: [],
} });
expect(await sidecar.next(frame => frame.id === 2)).toMatchObject({ ok: false, error: { message: expect.stringContaining("candidate admission") } });
});
it("fails closed after an unsupported provider bootstrap", async () => {
const sidecar = startSidecar();
sidecar.write(
@@ -1286,7 +1312,7 @@ describe("qualified ACPX runtime sidecar", () => {
ok: false,
error: {
code: "acpx_sidecar_command_failed",
message: "ACPX agent must be claude, codex, grok, cursor, copilot, or pi",
message: "ACPX agent must be claude, codex, grok, cursor, copilot, pi, or antigravity",
retryable: false,
},
});
@@ -4,6 +4,7 @@ import { redactObservabilityDiagnostic } from "../drivers/agent-observability.js
import { resolveClaudeEffort } from "../drivers/acpx/claude-effort.js";
import { resolveGrokEffort } from "../drivers/acpx/grok-effort.js";
import { parseProviderMode } from "../contracts/provider-mode.js";
import { assertAcpxExecutionAdmission, parseAcpxCandidateStartupAdmission, type AcpxCandidateStartupAdmission } from "../drivers/acpx/candidate-admission.js";
import { acpxProfileActivity, type AcpxActivityAdapter, type AcpxToolEvidence } from "../drivers/acpx/profile-activity.js";
import { resolvePiThinkingLevel } from "../drivers/acpx/pi-thinking.js";
@@ -45,6 +46,7 @@ import { normalizeAcpxPermission, type NormalizedAcpxPermission } from "../drive
import { acpxGoalProjection } from "../drivers/acpx/session-goals.js";
import { acpxProviderSessionIdentity } from "../drivers/acpx/recovery-identity.js";
import {
assertAcpxProfileReleased,
resolveQualifiedAcpxProfile,
type QualifiedAcpxAgent,
} from "../drivers/acpx/qualified-profiles.js";
@@ -169,6 +171,7 @@ let pendingInput = Promise.resolve();
let bootstrapFailure: Error | null = null;
let initializedAgent: QualifiedAcpxAgent | null = null;
let initializedModel: string | null = null;
let initializedCandidateAdmission: AcpxCandidateStartupAdmission | undefined;
let inputClosed = false;
const tools = new Map<string, PendingTool>();
const normalizingTools = new Set<string>();
@@ -256,11 +259,14 @@ async function dispatch(
if (request.command === "initialize") {
if (initializedModel)
throw new Error("ACPX sidecar is already initialized");
const agent = requireQualifiedAgent(request.params.agent);
const model = requiredText(request.params.model, "model");
const candidateAdmission = request.params.candidateAdmission === undefined ? undefined
: parseAcpxCandidateStartupAdmission(request.params.candidateAdmission, { agent: String(request.params.agent), model });
const agent = requireQualifiedAgent(request.params.agent, model, candidateAdmission);
const profile = resolveQualifiedAcpxProfile(agent, model);
initializedAgent = agent;
initializedModel = model;
initializedCandidateAdmission = candidateAdmission;
return {
protocolVersion: ACPX_SIDECAR_PROTOCOL_VERSION,
sidecarPid: process.pid,
@@ -289,11 +295,13 @@ async function dispatch(
if (
params.agent !== initializedAgent ||
params.model !== initializedModel
|| canonicalJson(params.candidateAdmission ?? null) !== canonicalJson(initializedCandidateAdmission ?? null)
) {
throw new Error("ACPX session profile differs from its initialization");
}
const openedHost = await AcpxRuntimeHost.open(
{
candidateAdmission: params.candidateAdmission,
runtimeDirectory: params.runtimeDirectory,
normalizedSessionId: params.normalizedSessionId,
workingDirectory: params.workingDirectory,
@@ -375,6 +383,7 @@ async function dispatch(
throw new Error("ACPX run tool catalog differs from the opened session");
}
const attachment = parseAcpxRunAttachment(request.params);
if (initializedCandidateAdmission) parseAcpxCandidateStartupAdmission(initializedCandidateAdmission, { agent: initializedAgent!, model: initializedModel!, runId: attachment.runId, normalizedSessionId: openParams!.normalizedSessionId });
runId = attachment.runId;
return {
runId: attachment.runId,
@@ -385,6 +394,7 @@ async function dispatch(
const activeHost = requireHost();
if (!runId) throw new Error("attach a run before starting an ACPX turn");
if (turnId) throw new Error("ACPX sidecar already has an active turn");
if (initializedCandidateAdmission) parseAcpxCandidateStartupAdmission(initializedCandidateAdmission, { agent: initializedAgent!, model: initializedModel!, runId, normalizedSessionId: openParams!.normalizedSessionId });
const currentTurnId = boundedIdentity(request.params.turnId, "turnId");
const activeAgent = openParams!.agent;
turnId = currentTurnId;
@@ -1241,10 +1251,12 @@ function safeOutput(value: unknown): Record<string, unknown> {
function parseOpenParams(
value: Record<string, unknown>,
): AcpxSidecarOpenParams {
const fields = new Set(["runtimeDirectory", "normalizedSessionId", "workingDirectory", "agent", "model", "permissionMode", "mode", "piThinkingLevel", "claudeEffort", "grokEffort", "grokDisableWebSearch", "permissionModePinned", "providerPolicy", "systemInstructions", "runtimeContext", "tools", "providerSessionKey", "expectedIdentity"]);
const fields = new Set(["runtimeDirectory", "normalizedSessionId", "workingDirectory", "agent", "model", "permissionMode", "mode", "piThinkingLevel", "claudeEffort", "grokEffort", "grokDisableWebSearch", "permissionModePinned", "providerPolicy", "systemInstructions", "runtimeContext", "tools", "providerSessionKey", "expectedIdentity", "candidateAdmission"]);
if (Object.keys(value).some(key => !fields.has(key))) throw new Error("ACPX open parameters include an unsupported field");
const agent = requireQualifiedAgent(value.agent);
const model = requiredText(value.model, "model");
const candidateAdmission = value.candidateAdmission === undefined ? undefined
: parseAcpxCandidateStartupAdmission(value.candidateAdmission, { agent: String(value.agent), model, normalizedSessionId: String(value.normalizedSessionId) });
const agent = requireQualifiedAgent(value.agent, model, candidateAdmission);
if (value.mode !== undefined && agent !== "cursor") throw new Error("mode is supported only for Cursor");
const piThinkingLevel = resolvePiThinkingLevel(agent, value.piThinkingLevel);
const claudeEffort = resolveClaudeEffort(agent, value.claudeEffort);
@@ -1260,6 +1272,7 @@ function parseOpenParams(
);
}
return {
...(candidateAdmission ? { candidateAdmission } : {}),
runtimeDirectory: requiredText(value.runtimeDirectory, "runtimeDirectory"),
normalizedSessionId: boundedIdentity(
value.normalizedSessionId,
@@ -1453,10 +1466,12 @@ function requireHost(
return requireSidecarCommandHost(host, activeHostCleanup, options);
}
function requireQualifiedAgent(value: unknown): QualifiedAcpxAgent {
if (value !== "grok" && value !== "codex" && value !== "claude" && value !== "pi" && value !== "cursor" && value !== "copilot") {
throw new Error("ACPX agent must be claude, codex, grok, cursor, copilot, or pi");
function requireQualifiedAgent(value: unknown, model?: string, candidateAdmission?: AcpxCandidateStartupAdmission): QualifiedAcpxAgent {
if (value !== "grok" && value !== "codex" && value !== "claude" && value !== "pi" && value !== "cursor" && value !== "copilot" && value !== "antigravity") {
throw new Error("ACPX agent must be claude, codex, grok, cursor, copilot, pi, or antigravity");
}
if (candidateAdmission) assertAcpxExecutionAdmission(resolveQualifiedAcpxProfile(value, model ?? ""), candidateAdmission);
else assertAcpxProfileReleased(value);
return value;
}
@@ -93,7 +93,7 @@ export interface NativeAwsAgentCoreProfileSnapshot {
eventExpiryDays: 90;
}
export type NativeAcpxAgent = "pi" | "claude" | "codex" | "grok" | "cursor" | "copilot";
export type NativeAcpxAgent = "pi" | "claude" | "codex" | "grok" | "cursor" | "copilot" | "antigravity";
export type NativeCodexApprovalPolicy = "never" | "on-request" | "untrusted";
export type NativeOpenCodePermissionMode = "allow" | "ask" | "deny";
export type NativeAcpxPermissionMode = "approve-all" | "approve-paperclip" | "approve-reads" | "deny-all";
@@ -709,8 +709,8 @@ export function parseNativeExecutionInput(value: unknown): NativeExecutionInput
if (providerModel === null) {
throw new NativeExecutionInputError("input.provider.model is required for acpx");
}
if (provider.agent !== "pi" && provider.agent !== "claude" && provider.agent !== "codex" && provider.agent !== "grok" && provider.agent !== "cursor" && provider.agent !== "copilot") {
throw new NativeExecutionInputError("input.provider.agent must be pi, claude, codex, grok, cursor, or copilot");
if (provider.agent !== "pi" && provider.agent !== "claude" && provider.agent !== "codex" && provider.agent !== "grok" && provider.agent !== "cursor" && provider.agent !== "copilot" && provider.agent !== "antigravity") {
throw new NativeExecutionInputError("input.provider.agent must be pi, claude, codex, grok, cursor, copilot, or antigravity");
}
if (isV4) {
if (provider.permissionMode !== "approve-all" && provider.permissionMode !== "approve-paperclip" && provider.permissionMode !== "approve-reads" && provider.permissionMode !== "deny-all") {
@@ -1,4 +1,4 @@
import { execFileSync, spawn } from "node:child_process";
import { execFileSync, spawn, type ChildProcess } from "node:child_process";
import { once } from "node:events";
import { createHash } from "node:crypto";
import { createServer as createNetServer } from "node:net";
@@ -7,10 +7,12 @@ import { tmpdir } from "node:os";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { beforeAll, expect, it, vi } from "vitest";
import { DurablePrpControlPlane, decodeDurablePrpCheckpoint, type DurablePrpCheckpoint } from "./durable-prp-control-plane.js";
import { DurablePrpControlPlane, decodeDurablePrpCheckpoint, spawnRunner, type DurablePrpCheckpoint } from "./durable-prp-control-plane.js";
import { copyOwnedNodeRuntime } from "../test-support/owned-node-runtime.js";
import { GENERATED_ACPX_SIDECAR_PROTOCOL_VERSION } from "../drivers/acpx/generated-sidecar-contract.js";
import { createCapabilityRunnerdCodexTransport } from "../live/runnerd-codex-transport.js";
import { bindAcpxCandidateAdmission, type AcpxCandidateStartupAdmission } from "../drivers/acpx/candidate-admission.js";
import { ANTIGRAVITY_DISTRIBUTION_PINS } from "../drivers/acpx/generated-profiles.js";
const packageRoot = fileURLToPath(new URL("../..", import.meta.url));
const binary = process.env.PAPERCLIP_ACPX_REPRO_BINARY ?? resolve(packageRoot, "runner/target/debug/paperclip-runnerd");
@@ -30,11 +32,11 @@ beforeAll(() => {
expectedRunnerDigest = digest(binary);
}, 150_000);
function sidecarSource(root: string) {
function sidecarSource(root: string, selectedProfile = profile, agent = "claude") {
return [
"const fs = require('node:fs'); const net = require('node:net'); const rl = require('node:readline'); const {randomInt}=require('node:crypto');",
"const root = " + JSON.stringify(root) + ";",
"const profileDigest = " + JSON.stringify(profile.commandDigest) + ";",
"const profileDigest = " + JSON.stringify(selectedProfile.commandDigest) + ";",
"const protocolVersion = " + GENERATED_ACPX_SIDECAR_PROTOCOL_VERSION + ";",
"const send = value => process.stdout.write(JSON.stringify(value) + '\\n');",
"let identity, runId, turnId = null, sequence = 0, lastAction = '';",
@@ -48,7 +50,7 @@ function sidecarSource(root: string) {
" const lines=rl.createInterface({input:process.stdin}); lines.on('close',()=>process.exit(0)); lines.on('line', line => {",
" const r=JSON.parse(line), p=r.params || {};",
" switch(r.command) {",
" case 'initialize': reply(r,{protocolVersion,sidecarPid:process.pid,profile:{agent:'claude'},capabilities:{persistentSessions:true,exactModelVerification:true,permissions:'runner_policy',semanticTools:'runner_bridge',structuredInput:'paperclip.question_set.v1'}});break;",
" case 'initialize': reply(r,{protocolVersion,sidecarPid:process.pid,profile:{agent:" + JSON.stringify(agent) + "},capabilities:{persistentSessions:true,exactModelVerification:true,permissions:'runner_policy',semanticTools:'runner_bridge',structuredInput:'paperclip.question_set.v1'}});break;",
" case 'session.open': identity={kind:'acpx',normalizedSessionId:p.normalizedSessionId,acpxRecordId:'record-1',backendSessionId:'backend-1',agentSessionId:'agent-1',profileDigest,workspaceDigest:'sha256:'+'2'.repeat(64),requestedModel:p.model,effectiveModel:p.model,permissionMode:p.permissionMode,mode:p.mode,piThinkingLevel:p.piThinkingLevel,claudeEffort:p.claudeEffort,grokEffort:p.grokEffort,grokDisableWebSearch:p.grokDisableWebSearch,providerLifetimeFenceCandidates:ports};reply(r,{sidecarPid:process.pid,identity,status:{},turnControls:{steering:false,queuedFollowUp:false}});break;",
" case 'run.attach': runId=p.runId; reply(r,{runId,catalogRevision:p.catalogRevision});break;",
" case 'session.goal.get': reply(r,{schema:'paperclip.session_goal.snapshot.v1',goal:null,workingNow:false,sessionGoals:{availability:'unsupported',actions:[],autonomousUpdates:false,persistentAcrossResume:false,maxObjectiveChars:4000,tokenBudgetControl:false,usageReporting:false}});break;",
@@ -65,20 +67,22 @@ function sidecarSource(root: string) {
].join("\n");
}
async function fixture() {
async function fixture(maxLifetimeMs?: number, candidateAdmission?: AcpxCandidateStartupAdmission, beforeLaunchDelayMs = 0) {
const root = mkdtempSync(resolve(tmpdir(), "acpx-controller-recovery-"));
const { executable: nodeCommand } = await copyOwnedNodeRuntime(resolve(root, "node"));
const stateDirectory = resolve(root, "control-plane");
const script = resolve(root, "sidecar.cjs");
writeFileSync(script, sidecarSource(root), { mode: 0o700 });
const agent = candidateAdmission ? "antigravity" : "claude", selectedProfile = profiles.profiles[agent];
writeFileSync(script, sidecarSource(root, selectedProfile, agent), { mode: 0o700 });
const runtime = resolve(root, "runtime");
const descriptor = { kind: "acpx", provider: "acpx", driver: "acpx_runtime", providerVersion: profiles.acpxVersion,
agent: "claude", model: "scripted-model", acpxVersion: profiles.acpxVersion,
agentServerPackage: profile.agentServerPackage, agentServerVersion: profile.agentServerVersion,
agentRuntimePackage: profile.agentRuntimePackage, agentRuntimeVersion: profile.agentRuntimeVersion,
commandDigest: profile.commandDigest, sidecarCommand: nodeCommand, sidecarArgs: [script],
agent, model: candidateAdmission?.model ?? "scripted-model", acpxVersion: profiles.acpxVersion,
agentServerPackage: selectedProfile.agentServerPackage, agentServerVersion: selectedProfile.agentServerVersion,
agentRuntimePackage: selectedProfile.agentRuntimePackage, agentRuntimeVersion: selectedProfile.agentRuntimeVersion,
commandDigest: selectedProfile.commandDigest, sidecarCommand: nodeCommand, sidecarArgs: [script],
runtimeDirectory: runtime, normalizedSessionId: identity.normalizedSessionId, runId: identity.runId,
cwd: root, instructions: "Do the supplied synthetic task.", permissionMode: "approve-reads", permissionModePinned: true,
...(selectedProfile.requiresProviderPolicy ? { providerPolicy: { readOnly: true } } : {}),
runtimeContext: null };
let checkpoint!: DurablePrpCheckpoint;
const create = () => {
@@ -89,8 +93,12 @@ async function fixture() {
let controller = create();
await controller.start();
const port = Number(new URL(controller.connectUrl).port);
const ticket = controller.issueBootstrapTicket();
// Establish one absolute fixture grant before issuing its one-use ticket.
// Bootstrap preparation and the actual daemon share this deadline.
const deadlineAt = candidateAdmission?.expiresAt ?? (maxLifetimeMs === undefined ? undefined : Date.now() + maxLifetimeMs);
const ticket = controller.issueBootstrapTicket(deadlineAt === undefined ? 5_000 : deadlineAt - Date.now());
await controller.flushCheckpoint();
if (beforeLaunchDelayMs) await new Promise(resolveDelay => setTimeout(resolveDelay, beforeLaunchDelayMs));
let diagnostics = "";
const args = ["--connect-url",controller.connectUrl,"--state-dir",resolve(root,"runner"),
"--runner-id",identity.runnerInstanceId,"--environment-lease-id",identity.environmentLeaseId,
@@ -98,24 +106,46 @@ async function fixture() {
"--runner-version",expectedRunnerVersion,"--runner-digest",expectedRunnerDigest,"--max-runtime-ms","0",
"--reconnect-grace-ms","15000","--reconnect-delay-ms","25","--acpx-launch-authority-digest","sha256:"+"d".repeat(64),
"--acpx-sidecar-command",nodeCommand,"--acpx-sidecar-command-sha256",digest(nodeCommand),
"--acpx-sidecar-script",script,"--acpx-sidecar-script-sha256",digest(script)];
"--acpx-sidecar-script",script,"--acpx-sidecar-script-sha256",digest(script),
...(candidateAdmission ? ["--acpx-candidate-admission-json", JSON.stringify(candidateAdmission)] : [])];
const startedAt = new Date().toISOString();
const child = spawn(binary,args,{env:{PATH:process.env.PATH,PAPERCLIP_RUNNER_BOOTSTRAP_TICKET:ticket},stdio:["ignore","ignore","pipe"],detached:true});
// Exercise daemon authority independently of the TypeScript TTL clamp.
const bounded = maxLifetimeMs === undefined ? undefined : spawnRunner({
connectUrl: controller.connectUrl, stateDirectory: resolve(root, "runner"), identity, ticket,
maxOutboxBytes: 16 * 1024 * 1024, p0ReserveBytes: 1024 * 1024,
maxRuntimeMs: 0, maxLifetimeMs: deadlineAt! - Date.now(), reconnectGraceMs: 15_000,
runnerBinaryPath: binary, runnerVersion: expectedRunnerVersion, runnerDigest: expectedRunnerDigest,
acpxLaunchProfile: { ...(candidateAdmission ? { candidateAdmission } : {}), authorityDigest: `sha256:${"d".repeat(64)}`, command: nodeCommand, commandSha256: digest(nodeCommand),
sidecarScript: script, sidecarScriptSha256: digest(script) },
environment: { PATH: process.env.PATH },
});
const child = bounded?.child as ChildProcess | undefined
?? spawn(binary,args,{env:{PATH:process.env.PATH,PAPERCLIP_RUNNER_BOOTSTRAP_TICKET:ticket},stdio:["ignore","ignore","pipe"],detached:true});
const exit = once(child,"exit");
child.stderr!.on("data",bytes=>{diagnostics=(diagnostics+String(bytes)).slice(-4096);});
let commandNumber=0;
async function command(type:string,payload:Record<string,unknown>={}) {
const id="command-"+(++commandNumber);
controller.queueCommand(type,payload,id,true);
await vi.waitFor(()=>{
expect(child.exitCode,"runner exit; "+diagnostics).toBeNull();
expect(child.signalCode,"runner signal; "+diagnostics).toBeNull();
expect(["completed","failed"]).toContain(controller.getCommand(id)?.status);
},{timeout:8000});
try {
await vi.waitFor(()=>{
expect(child.exitCode,"runner exit; "+diagnostics).toBeNull();
expect(child.signalCode,"runner signal; "+diagnostics).toBeNull();
expect(["completed","failed"]).toContain(controller.getCommand(id)?.status);
},{timeout:8000});
} catch (error) {
let runnerDiagnostics: unknown;
try { runnerDiagnostics = JSON.parse(readFileSync(resolve(root, "runner/runner-state.json"), "utf8")).diagnostics?.slice(-8); } catch { /* Startup may not have created state. */ }
throw new Error("Hermetic Runner command did not settle: " + JSON.stringify({ id, type,
authenticatedConnections: controller.activeRunnerConnectionCount(), command: controller.getCommand(id),
remainingLifetimeMs: deadlineAt === undefined ? null : deadlineAt - Date.now(),
exitCode: child.exitCode, signalCode: child.signalCode, stderr: diagnostics, runnerDiagnostics }).slice(-8192), { cause: error });
}
return controller.getCommand(id)!;
}
const ready = async () => {
expect(await command("run.prepare",{provider:descriptor})).toMatchObject({status:"completed"});
const prepared = await command("run.prepare",{provider:descriptor});
expect(prepared, JSON.stringify(prepared)).toMatchObject({status:"completed"});
const opened = await command("session.open");
expect(opened, JSON.stringify(opened)).toMatchObject({status:"completed"});
};
@@ -126,7 +156,7 @@ async function fixture() {
rmSync(stateDirectory,{recursive:true,force:true});
decodeDurablePrpCheckpoint(checkpoint.records).restoreSessionDirectory(root);
};
return {root,child,exit,command,ready,startedAt,
return {root,child,exit,command,ready,startedAt,deadlineAt,processGroupId: bounded?.processGroupId ?? child.pid,
controller:()=>controller,
stopController:stopped,
restoreCheckpoint,
@@ -162,6 +192,49 @@ async function fixture() {
};
}
async function assertActiveExpiry(candidate = false) {
// One fixed grant covers the six-second cold delay, four existing eight-
// second command budgets, and seven seconds of active-turn headroom. Loaded
// CI exhausted the earlier fifteen-second grant before session.open settled.
// Never renew this authority after setup or the active-turn witness.
const lifetimeMs = 45_000;
const candidateAdmission = candidate ? bindAcpxCandidateAdmission({ schema: "paperclip.acpx-candidate-admission.v1", companyId: "company-expiry",
agent: "antigravity", model: "gemini-3.8-flash-low", profileDigest: profiles.profiles.antigravity.commandDigest,
nativeClosureDigest: `sha256:${ANTIGRAVITY_DISTRIBUTION_PINS["darwin-arm64"].closureSha256}`,
platform: "darwin", architecture: "arm64", expiresAt: Date.now() + lifetimeMs }, identity) : undefined;
// A controlled cold start outlasts the default five-second bootstrap ticket.
// It must still activate inside the original fixed absolute grant.
const run = await fixture(candidate ? undefined : lifetimeMs, candidateAdmission, candidate ? 0 : 6_000);
const deadlineAt = run.deadlineAt!;
try {
await run.ready();
expect(await run.command("turn.start", { turnId: "provider-expiring-turn", text: "Keep this hermetic turn active" })).toMatchObject({ status: "completed" });
const launch = run.launches()[0];
expect(launch.pid).toBeGreaterThan(1);
expect(() => process.kill(launch.pid, 0)).not.toThrow();
expect(await run.command("session.snapshot")).toMatchObject({ result: { result: { activeProviderTurnId: "provider-expiring-turn" } } });
expect(Date.now()).toBeLessThan(deadlineAt);
// No stop/cancel request is sent. The real daemon must enforce the launch
// deadline while the provider is active, then finish physical cleanup.
await vi.waitFor(() => expect(run.child.exitCode ?? run.child.signalCode).not.toBeNull(), { timeout: Math.max(1, deadlineAt - Date.now()) + 4_000 });
expect(run.child.exitCode).not.toBe(0);
expect(() => process.kill(launch.pid, 0)).toThrow();
expect(run.processGroupId).toBeGreaterThan(1);
expect(() => process.kill(-run.processGroupId!, 0)).toThrow();
const fences = await Promise.all((launch.ports as number[]).map(port => new Promise<ReturnType<typeof createNetServer>>((resolveFree, reject) => {
const probe = createNetServer(); probe.once("error", reject); probe.listen(port, "127.0.0.1", () => resolveFree(probe));
})));
await Promise.all(fences.map(server => new Promise<void>((resolveFree, reject) => server.close(error => error ? reject(error) : resolveFree()))));
const state = JSON.parse(readFileSync(resolve(run.root, "runner/runner-state.json"), "utf8"));
expect(state.recoverableFailure).toBe("transport_reconnect_deadline_exceeded");
} finally { await run.cleanup(); }
}
it("expires an active provider turn after a cold bootstrap delay and physically retires its owned processes", () => assertActiveExpiry(), 60_000);
it.skipIf(process.platform !== "darwin" || process.arch !== "arm64")(
"retires an active exact Antigravity candidate at absolute operator expiry with no caller runtime limit",
() => assertActiveExpiry(true), 60_000);
it("keeps the original native ACPX process and active turn through controller replacement",async()=>{
const run=await fixture();
try{
@@ -3914,6 +3914,7 @@ const runnerExplicitProviderEnvironmentKeys = [
"CODEX_API_KEY",
"PAPERCLIP_ACPX_CODEX_AUTH_JSON_SECRET",
"PAPERCLIP_ACPX_GROK_AUTH_JSON_SECRET",
"PAPERCLIP_ACPX_ANTIGRAVITY_AUTH_JSON_SECRET",
"XAI_API_KEY",
"AWS_REGION",
"AWS_DEFAULT_REGION",
@@ -4016,6 +4017,7 @@ export function spawnRunner(options: {
runnerVersion: string;
runnerDigest: string;
acpxLaunchProfile?: {
candidateAdmission?: import("../drivers/acpx/candidate-admission.js").AcpxCandidateStartupAdmission;
authorityDigest: string;
command: string;
commandSha256: string;
@@ -4034,6 +4036,23 @@ export function spawnRunner(options: {
processLauncher?: (spec: RunnerProcessLaunchSpec) => RunnerProcessHandle;
diagnosticsDirectory?: string;
}): RunnerProcessHandle {
for (const value of [options.maxRuntimeMs, options.maxLifetimeMs]) {
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) throw new Error("runner_process_lifetime_invalid");
}
const launchedAt = Date.now();
const candidateExpiresAt = options.acpxLaunchProfile?.candidateAdmission?.expiresAt;
const lifetimeDeadlines = [
...(options.maxLifetimeMs && options.maxLifetimeMs > 0 ? [launchedAt + options.maxLifetimeMs] : []),
...(candidateExpiresAt === undefined ? [] : [candidateExpiresAt]),
];
const lifetimeDeadline = lifetimeDeadlines.length ? Math.min(...lifetimeDeadlines) : undefined;
if (lifetimeDeadline !== undefined && lifetimeDeadline <= launchedAt) throw new Error("runner_process_lifetime_expired");
const runtimeBounds = [
...(options.maxRuntimeMs && options.maxRuntimeMs > 0 ? [options.maxRuntimeMs] : []),
...(lifetimeDeadline === undefined ? [] : [lifetimeDeadline - launchedAt]),
];
const maxRuntimeMs = runtimeBounds.length ? Math.min(...runtimeBounds)
: options.maxRuntimeMs ?? (options.maxLifetimeMs === undefined ? undefined : 0);
const connection =
options.connection ??
(options.connectUrl
@@ -4091,6 +4110,7 @@ export function spawnRunner(options: {
options.acpxLaunchProfile.sidecarScript,
"--acpx-sidecar-script-sha256",
options.acpxLaunchProfile.sidecarScriptSha256,
...(options.acpxLaunchProfile.candidateAdmission ? ["--acpx-candidate-admission-json", JSON.stringify(options.acpxLaunchProfile.candidateAdmission)] : []),
]
: []),
...(options.opencodeLaunchProfile
@@ -4120,11 +4140,9 @@ export function spawnRunner(options: {
"--reconnect-delay-ms",
"250",
];
if (options.maxLifetimeMs !== undefined) {
args.push("--max-lifetime-ms", String(options.maxLifetimeMs));
} else if (options.maxRuntimeMs !== undefined) {
args.push("--max-runtime-ms", String(options.maxRuntimeMs));
}
// The daemon enforces its total process deadline through max-runtime-ms.
// Keep the same bounded value in structured launches for external owners.
if (maxRuntimeMs !== undefined) args.push("--max-runtime-ms", String(maxRuntimeMs));
if (options.reconnectGraceMs !== undefined) {
args.push("--reconnect-grace-ms", String(options.reconnectGraceMs));
}
@@ -4151,7 +4169,9 @@ export function spawnRunner(options: {
if (handle.isExitConfirmed?.() === false) {
throw new Error("runner_process_termination_unconfirmed");
}
return spawnRunner({ ...options, ticket });
if (lifetimeDeadline !== undefined && lifetimeDeadline <= Date.now()) throw new Error("runner_process_lifetime_expired");
return spawnRunner({ ...options, ticket,
...(lifetimeDeadline === undefined ? {} : { maxLifetimeMs: lifetimeDeadline - Date.now() }) });
},
});
if (options.processLauncher !== undefined) {
@@ -4161,7 +4181,7 @@ export function spawnRunner(options: {
identity: options.identity, ticket: options.ticket, connection, stateDirectory: options.stateDirectory,
runnerVersion: options.runnerVersion, runnerDigest: options.runnerDigest,
maxOutboxBytes: options.maxOutboxBytes, p0ReserveBytes: options.p0ReserveBytes,
maxRuntimeMs: options.maxRuntimeMs, maxLifetimeMs: options.maxLifetimeMs,
maxRuntimeMs, maxLifetimeMs: lifetimeDeadline === undefined ? options.maxLifetimeMs : maxRuntimeMs,
reconnectGraceMs: options.reconnectGraceMs, lifecyclePolicy: options.lifecyclePolicy,
acpxLaunchProfile: options.acpxLaunchProfile, opencodeLaunchProfile: options.opencodeLaunchProfile,
diagnosticsDirectory: options.diagnosticsDirectory,
@@ -0,0 +1,172 @@
import { spawn, type ChildProcess } from "node:child_process";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, expect, it } from "vitest";
import { createAcpRuntime } from "acpx/runtime";
import { createAntigravityAdmission } from "./antigravity-admission.js";
const cleanups: Array<() => Promise<void>> = [];
afterEach(async () => { for (const cleanup of cleanups.splice(0).reverse()) await cleanup(); });
/** The real installed manager/stdio transport, with only the remote provider replaced. */
async function fixture(behavior: "accept" | "missing" | "mismatch" | "question" | "family" | "family-change" = "accept") {
const cwd = await mkdtemp(join(tmpdir(), "antigravity-acpx-contract-"));
const requests: Array<Record<string, any>> = [];
const nativeResponses: Array<Record<string, any>> = [];
const sdkResponses: Array<Record<string, any>> = [];
const children: ChildProcess[] = [];
const records = new Map();
const runtimes: Array<{ runtime: ReturnType<typeof createAcpRuntime>; handle: any }> = [];
const content = " Exact 🧷 governance\n\n";
const family = behavior.startsWith("family");
const model = family ? "gemini-3.8-flash-low" : "caller-model-absent-from-catalog";
const admission = createAntigravityAdmission(content, model);
cleanups.push(async () => {
try {
for (const owner of runtimes) if (owner.handle) await owner.runtime.close({ handle: owner.handle, reason: "test cleanup" }).catch(() => {});
} finally {
for (const child of children) {
if (child.exitCode === null && child.signalCode === null) {
const closed = new Promise<void>(resolve => child.once("close", () => resolve()));
child.kill("SIGKILL"); await closed;
}
}
await rm(cwd, { recursive: true, force: true });
}
});
const options = {
cwd, timeoutMs: 2000, permissionMode: "approve-all" as const, elicitationModes: ["form"] as ["form"],
sessionStore: { async load(id: string) { return records.get(id); }, async save(record: any) { records.set(record.acpxRecordId, record); } },
agentRegistry: { resolve: () => "verified-antigravity-test-peer", list: () => ["antigravity"] },
protocolGuardFactory: () => {
const guard = admission.createGuard();
return (direction: "inbound" | "outbound", message: unknown) => {
if (direction === "inbound") nativeResponses.push(structuredClone(message) as Record<string, any>);
guard(direction, message);
if (direction === "inbound") sdkResponses.push(structuredClone(message) as Record<string, any>);
if (direction === "outbound") requests.push(structuredClone(message) as Record<string, any>);
};
},
spawnAgent: () => {
const code = `
const readline = require('node:readline'); let promptId;
const send=value=>process.stdout.write(JSON.stringify({jsonrpc:'2.0',...value})+'\\n');
const family=${JSON.stringify(family)};
const nativeOptions=()=>family?[
{id:'model',name:'Model',type:'select',category:'model',currentValue:'gemini-3.8-flash',options:[{value:'gemini-3.8-flash',name:'Gemini 3.8 Flash'}]},
{id:'thinking_level',name:'Thinking level',type:'select',category:'thought_level',currentValue:'low',options:[{value:'high',name:'High'},{value:'medium',name:'Medium'},{value:'low',name:'Low'}]},
{id:'mode',name:'Mode',type:'select',category:'mode',currentValue:'default',options:[{value:'default',name:'Default'},{value:'auto_edit',name:'Auto edit'},{value:'yolo',name:'YOLO'}]}
]:null;
readline.createInterface({input:process.stdin}).on('line',line=>{
const m=JSON.parse(line);
if(m.method==='initialize') send({id:m.id,result:{protocolVersion:1,agentInfo:{name:'antigravity-acp',version:'1.4.3'},
agentCapabilities:{loadSession:true,_meta:{antigravity:{paperclipAdmission:1}}},authMethods:[]}});
else if(['session/new','session/load','session/resume','session/set_config_option','session/set_model'].includes(m.method)) {
const a=m.params._meta.antigravity.paperclipAdmission;
const model=${JSON.stringify(behavior)}==='mismatch'?'fallback':a.model;
send({id:m.id,result:{...(!family||m.method==='session/new'?{sessionId:'native-session'}:{}),...(!family||['session/new','session/load','session/resume'].includes(m.method)?{modes:{currentModeId:'default'}}:{}),
...(!family||['session/new','session/load','session/resume'].includes(m.method)?{models:{currentModelId:model,availableModels:family?[{modelId:'gemini-3.8-flash-high',name:'Gemini 3.8 Flash (High)'},{modelId:'gemini-3.8-flash-medium',name:'Gemini 3.8 Flash (Medium)'},{modelId:'gemini-3.8-flash-low',name:'Gemini 3.8 Flash (Low)'}]:[{modelId:'advertised-only',name:'Advertised'}]}}:{}),
configOptions:nativeOptions()??[{id:'model',name:'Model',type:'select',category:'model',currentValue:model,options:[{value:'advertised-only',name:'Advertised'}]}],
...(${JSON.stringify(behavior)}==='missing'?{}:{_meta:{paperclipAntigravityAdmission:{...a,model,sessionId:'native-session',connectionId:'owned-native-connection'}}})}});
if(family&&m.method==='session/set_config_option')send({method:'session/update',params:{sessionId:'native-session',update:{sessionUpdate:'config_option_update',configOptions:nativeOptions()}}});
} else if(m.method==='session/prompt') {
promptId=m.id;
if(${JSON.stringify(behavior)}==='family-change') {
const changed=nativeOptions();changed.find(o=>o.id==='thinking_level').currentValue='high';
send({method:'session/update',params:{sessionId:'native-session',update:{sessionUpdate:'config_option_update',configOptions:changed}}});
}
if(${JSON.stringify(behavior)}==='question') send({id:0,method:'session/request_permission',params:{sessionId:'native-session',
toolCall:{toolCallId:'interaction_choices',title:'Pick one',status:'pending',rawInput:{}},
options:[{optionId:'yes',name:'Yes',kind:'allow_once'},{optionId:'deny',name:'No',kind:'reject_once'}]}});
else send({id:m.id,result:{stopReason:'end_turn'}});
} else if(m.id===0 && m.result?.outcome) {
if(m.result.outcome.outcome!=='selected'||m.result.outcome.optionId!=='deny') process.exit(7);
send({id:promptId,result:{stopReason:'end_turn'}});
} else if(m.id!==undefined) send({id:m.id,result:{}});
});`;
const child = spawn(process.execPath, ["-e", code], { cwd, env: {}, stdio: ["pipe", "pipe", "pipe"] });
children.push(child); return child;
},
};
return {
requests, nativeResponses, sdkResponses, content, model,
async open() {
const owner = { runtime: createAcpRuntime(options), handle: null as any };
runtimes.push(owner);
owner.handle = await owner.runtime.ensureSession({ sessionKey: "antigravity-contract", agent: "antigravity", mode: "persistent", cwd,
sessionOptions: { model, systemPrompt: { append: content } } });
return owner;
},
async turn(owner: { runtime: ReturnType<typeof createAcpRuntime>; handle: any }, onQuestion?: (request: any) => void) {
const turn = owner.runtime.startTurn({ handle: owner.handle, text: "Do the task", mode: "prompt", requestId: "turn-contract",
onElicitation: async request => { onQuestion?.(request); return { action: "accept", content: { answer: "deny" } }; } });
const drained = (async () => { for await (const _event of turn.events) {} })();
const result = await turn.result; await drained; return result;
},
};
}
it("passes exact unlisted model and instruction bytes through installed ACPX on create and load", async () => {
const f = await fixture(); const first = await f.open();
expect((await f.turn(first)).status).toBe("completed");
await first.runtime.close({ handle: first.handle, reason: "reload" });
const second = await f.open(); expect((await f.turn(second)).status).toBe("completed");
const opens = f.requests.filter(message => ["session/new", "session/load"].includes(message.method));
expect(opens.some(message => message.method === "session/load")).toBe(true);
for (const message of opens) {
expect(message.params._meta.antigravity.systemPrompt.append).toBe(f.content);
expect(message.params._meta.antigravity.paperclipAdmission.model).toBe(f.model);
}
expect(new Set(opens.map(message => message.params._meta.antigravity.paperclipAdmission.nonce)).size).toBe(opens.length);
});
it.each(["missing", "mismatch"] as const)("blocks prompts when the actual installed manager receives %s native admission", async behavior => {
const f = await fixture(behavior);
await expect(f.open()).rejects.toThrow(/admission/i);
expect(f.requests.some(message => message.method === "session/prompt")).toBe(false);
});
it("delivers a native deny answer through the form path even with approve-all permissions", async () => {
const f = await fixture("question"); const owner = await f.open(); const seen: unknown[] = [];
expect((await f.turn(owner, request => seen.push(request))).status).toBe("completed");
expect(seen).toHaveLength(1);
expect(seen[0]).toMatchObject({ mode: "form", requestedSchema: { required: ["answer"] } });
expect(f.requests.find(message => message.id === 0 && message.result)).toMatchObject({ result: { outcome: { outcome: "selected", optionId: "deny" } } });
});
it("admits the concrete model through installed ACPX when native new/load/setter metadata groups it by family and thinking level", async () => {
const f = await fixture("family");
const first = await f.open();
expect((await f.turn(first)).status).toBe("completed");
await first.runtime.setConfigOption({ handle: first.handle, key: "model", value: f.model });
expect((await f.turn(first)).status).toBe("completed");
await first.runtime.close({ handle: first.handle, reason: "native family reload" });
const loaded = await f.open();
expect((await f.turn(loaded)).status).toBe("completed");
expect(f.requests.some(message => message.method === "session/load")).toBe(true);
expect(f.requests.some(message => message.method === "session/set_config_option")).toBe(true);
const native = f.nativeResponses.filter(message => message.result?.configOptions);
expect(native.length).toBeGreaterThanOrEqual(3);
const setter = native.find(message => !message.result.models);
expect(setter).toBeDefined();
expect(setter!.result.modes).toBeUndefined();
expect(setter!.result._meta.paperclipAntigravityAdmission.model).toBe(f.model);
for (const message of native) {
expect(message.result.configOptions.find((option: any) => option.id === "model").currentValue).toBe("gemini-3.8-flash");
expect(message.result.configOptions.find((option: any) => option.id === "thinking_level").currentValue).toBe("low");
}
for (const message of f.sdkResponses.filter(message => message.result?.configOptions)) {
expect(message.result.configOptions.find((option: any) => option.id === "model").currentValue).toBe(f.model);
}
const updates = f.sdkResponses.filter(message => message.params?.update?.sessionUpdate === "config_option_update");
expect(updates.length).toBeGreaterThan(0);
for (const message of updates) expect(message.params.update.configOptions.find((option: any) => option.id === "model").currentValue).toBe(f.model);
});
it("rejects an unacknowledged thinking-level change after a native family model was admitted", async () => {
const f = await fixture("family-change"), owner = await f.open();
const result = await f.turn(owner);
expect(result.status).toBe("failed");
expect(result.error?.message).toMatch(/admission/i);
});
@@ -0,0 +1,177 @@
import { describe, expect, it } from "vitest";
import { ANTIGRAVITY_ADMISSION_SCHEMA, createAntigravityAdmission } from "./antigravity-admission.js";
const content = " Exact governance 🧷\n\n";
const model = "caller/model-not-in-catalog";
const initialize = { agentInfo: { name: "antigravity-acp", version: "1.4.3" }, agentCapabilities: { _meta: { antigravity: { paperclipAdmission: 1 } } } };
function fixture() {
const admission = createAntigravityAdmission(content, model);
const guard = admission.createGuard();
guard("outbound", { id: "init", method: "initialize", params: {} });
guard("inbound", { id: "init", result: structuredClone(initialize) });
return { admission, guard };
}
function request(f: ReturnType<typeof fixture>, method = "session/new", id: string | number = 0) {
const message = { id, method, params: { ...(method === "session/new" ? {} : { sessionId: "native-session" }) } as Record<string, unknown> };
f.guard("outbound", message);
return message;
}
function response(message: ReturnType<typeof request>, changes: Record<string, unknown> = {}) {
const meta = message.params._meta as { antigravity: { paperclipAdmission: Record<string, unknown> } };
return { id: message.id, result: {
sessionId: "native-session", models: { currentModelId: model }, modes: { currentModeId: "default" },
_meta: { paperclipAntigravityAdmission: {
...meta.antigravity.paperclipAdmission, sessionId: "native-session", connectionId: "native-connection", ...changes,
} },
} };
}
const prompt = () => ({ id: "turn", method: "session/prompt", params: { sessionId: "native-session", prompt: [{ type: "text", text: "Do the task" }] } });
describe("Antigravity exact native admission", () => {
it("preserves whitespace/Unicode and binds a native receipt to request ID zero", () => {
const f = fixture(); const sent = request(f);
const meta = sent.params._meta as { antigravity: Record<string, unknown> };
expect(meta.antigravity.systemPrompt).toEqual({ append: content });
expect(meta.antigravity.paperclipAdmission).toMatchObject({ schema: ANTIGRAVITY_ADMISSION_SCHEMA, model, byteLength: Buffer.byteLength(content) });
f.guard("inbound", response(sent));
expect(() => f.admission.assertReady()).not.toThrow();
expect(() => f.guard("outbound", prompt())).not.toThrow();
});
it.each(["session/load", "session/resume"])("requires a new exact receipt on %s", method => {
const f = fixture(); const first = request(f); f.guard("inbound", response(first));
const load = request(f, method, "reloaded");
expect(() => f.admission.assertReady()).toThrow("receipt is missing");
const ack = response(load);
expect(ack.result._meta.paperclipAntigravityAdmission.nonce).not.toBe(response(first).result._meta.paperclipAntigravityAdmission.nonce);
f.guard("inbound", ack);
expect(() => f.guard("outbound", prompt())).not.toThrow();
});
it.each([
{ digest: "sha256:wrong" }, { byteLength: 1 }, { nonce: "stale-request" },
{ sessionId: "other-session" }, { model: "fallback" }, { connectionId: "" }, { schema: "wrong" },
])("rejects a mismatched native receipt %j before prompting", mutation => {
const f = fixture(); const sent = request(f);
expect(() => f.guard("inbound", response(sent, mutation))).toThrow("receipt");
expect(() => f.guard("outbound", prompt())).toThrow();
});
it("rejects an ordinary successful ACP response without native content proof", () => {
const f = fixture(); const sent = request(f);
expect(() => f.guard("inbound", { id: sent.id, result: { sessionId: "native-session" } })).toThrow("receipt");
});
it("does not inherit authority from a previous stdio connection", () => {
const f = fixture(); const sent = request(f); f.guard("inbound", response(sent));
const next = f.admission.createGuard();
expect(() => f.guard("outbound", prompt())).toThrow("replaced");
expect(() => next("outbound", prompt())).toThrow("unacknowledged");
});
it("preserves the provider's original session error and never admits a prompt", () => {
const f = fixture(); const sent = request(f);
expect(() => f.guard("inbound", { id: sent.id, error: { code: -32000, message: "Authentication required" } })).not.toThrow();
expect(() => f.admission.assertReady()).toThrow("receipt is missing");
expect(() => f.guard("outbound", prompt())).toThrow();
});
it("rejects native model replacement and permission-mode changes", () => {
for (const update of [
{ sessionUpdate: "config_option_update", models: { currentModelId: "fallback" } },
{ sessionUpdate: "current_mode_update", currentModeId: "yolo" },
]) {
const f = fixture(); const sent = request(f); f.guard("inbound", response(sent));
expect(() => f.guard("inbound", { method: "session/update", params: { sessionId: "native-session", update } })).toThrow();
expect(() => f.guard("outbound", prompt())).toThrow();
}
});
it("requires a fresh configuration receipt after exact-model control", () => {
const f = fixture(); const sent = request(f); f.guard("inbound", response(sent));
const control = { id: "model-control", method: "session/set_config_option", params: { sessionId: "native-session", configId: "model", value: model } as Record<string, unknown> };
f.guard("outbound", control);
expect(() => f.admission.assertReady()).toThrow();
f.guard("inbound", response(control));
expect(() => f.admission.assertReady()).not.toThrow();
});
it("rejects native slash commands, non-text prompts, and arbitrary configuration", () => {
for (const message of [
{ ...prompt(), params: { ...prompt().params, prompt: [{ type: "text", text: " /logout" }] } },
{ ...prompt(), params: { ...prompt().params, prompt: [{ type: "image", data: "unverified" }] } },
{ id: "control", method: "session/set_config_option", params: { sessionId: "native-session", configId: "mode", value: "yolo" } },
]) {
const f = fixture(); const sent = request(f); f.guard("inbound", response(sent));
expect(() => f.guard("outbound", message)).toThrow();
}
});
it.each(["Do the task\n/logout", "Do the task\r\n /model replace", "Do the task\r /login"])("rejects a native slash command on any prompt line: %j", text => {
const f = fixture(); const sent = request(f); f.guard("inbound", response(sent));
expect(() => f.guard("outbound", { ...prompt(), params: { ...prompt().params, prompt: [{ type: "text", text }] } })).toThrow("slash commands");
});
it("rejects a native slash command in a later text block", () => {
const f = fixture(); const sent = request(f); f.guard("inbound", response(sent));
expect(() => f.guard("outbound", { ...prompt(), params: { ...prompt().params, prompt: [{ type: "text", text: "Do the task" }, { type: "text", text: "/logout" }] } })).toThrow("slash commands");
});
it("rejects unpatched native runtimes before creating a session", () => {
const f = createAntigravityAdmission("", model); const guard = f.createGuard();
guard("outbound", { id: 0, method: "initialize", params: {} });
expect(() => guard("inbound", { id: 0, result: { agentInfo: initialize.agentInfo, agentCapabilities: {} } })).toThrow("patch");
});
});
describe("Antigravity native questions", () => {
function admitted() {
const f = fixture(); const sent = request(f); f.guard("inbound", response(sent)); return f;
}
const question = () => ({ id: 0, method: "session/request_permission", params: {
sessionId: "native-session", toolCall: { toolCallId: "interaction_123", title: "Choose a deployment" },
options: [{ optionId: "allow/weird:α", name: "Deploy", kind: "allow_once" }, { optionId: "deny", name: "Keep it local", kind: "reject_once" }],
} });
it("converts permission-shaped choices into a form and preserves a deny choice as an answer", () => {
const f = admitted(); const q = question(); f.guard("inbound", q);
expect(q.method).toBe("elicitation/create");
expect(q.params).toMatchObject({ mode: "form", requestedSchema: { required: ["answer"], properties: {
answer: { oneOf: [{ const: "allow/weird:α", title: "Deploy" }, { const: "deny", title: "Keep it local" }] },
} } });
const answer = { id: 0, result: { action: "accept", content: { answer: "deny" } } as Record<string, unknown> };
f.guard("outbound", answer);
expect(answer.result).toEqual({ outcome: { outcome: "selected", optionId: "deny" } });
});
it("passes ordinary tool permissions through unchanged", () => {
const f = admitted(); const q = question(); q.params.toolCall.toolCallId = "command_123";
const before = structuredClone(q); f.guard("inbound", q); expect(q).toEqual(before);
});
it.each(["forged", ["deny", "allow/weird:α"]])("rejects forged or multiple answers %j", answer => {
const f = admitted(); const q = question(); f.guard("inbound", q);
expect(() => f.guard("outbound", { id: 0, result: { action: "accept", content: { answer } } })).toThrow("offered choice");
});
it("maps explicit cancellation and rejects duplicate native request identities", () => {
const f = admitted(); f.guard("inbound", question());
const answer = { id: 0, result: { action: "cancel" } as Record<string, unknown> };
f.guard("outbound", answer); expect(answer.result).toEqual({ outcome: { outcome: "cancelled" } });
f.guard("inbound", question());
expect(() => f.guard("inbound", question())).toThrow("identity");
});
it("retires old questions when a new native session replaces their authority", () => {
const f = admitted(); f.guard("inbound", question());
const sent = request(f, "session/new", "new-session-request"), ack = response(sent, { sessionId: "new-native-session" });
ack.result.sessionId = "new-native-session"; f.guard("inbound", ack);
const late = { id: 0, result: { action: "accept", content: { answer: "deny" } } };
expect(() => f.guard("outbound", late)).not.toThrow();
expect(late.result).toEqual({ action: "accept", content: { answer: "deny" } });
const next = question(); next.params.sessionId = "new-native-session";
expect(() => f.guard("inbound", next)).not.toThrow();
expect(next.method).toBe("elicitation/create");
expect(() => f.admission.assertReady()).not.toThrow();
});
});
@@ -0,0 +1,185 @@
import { createHash, randomBytes } from "node:crypto";
import { createAntigravityQuestionBridge } from "./antigravity-questions.js";
export const ANTIGRAVITY_ADMISSION_SCHEMA = "paperclip.antigravity.admission.v1";
const SESSION_METHODS = new Set(["session/new", "session/load", "session/resume"]);
export class AntigravityAdmissionError extends Error {
readonly code = "ANTIGRAVITY_ADMISSION_FAILED";
readonly retryable = false;
constructor(detail: string) { super(`Antigravity admission failed: ${detail}`); this.name = "AntigravityAdmissionError"; }
}
export function antigravityInstructionBinding(content: string) {
if (typeof content !== "string" || content.includes("\0") || Buffer.byteLength(content) > 256 * 1024) {
throw new AntigravityAdmissionError("instructions exceed their size limit or contain NUL");
}
return { digest: `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`, byteLength: Buffer.byteLength(content) };
}
export function assertAntigravityPromptPolicy(content: string): void {
// Native slash commands can alter account/configuration before inference.
if (content.split(/\r\n|\r|\n/u).some(line => line.trimStart().startsWith("/"))) throw new AntigravityAdmissionError("native slash commands are unavailable in a managed task");
}
/** No saved ACPX record can substitute for this actual connection's receipt. */
export function createAntigravityAdmission(content: string, model: string) {
const binding = antigravityInstructionBinding(content);
if (typeof model !== "string" || !model.trim() || model.includes("\0") || model.length > 512) {
throw new AntigravityAdmissionError("an explicit model is required");
}
let current: { sessionId: string | null; error: Error | null } | null = null;
const assertReady = () => {
if (current?.error) throw current.error;
if (!current?.sessionId) throw new AntigravityAdmissionError("the native configuration receipt is missing");
};
return {
assertReady,
createGuard() {
const state = { sessionId: null as string | null, error: null as Error | null };
current = state;
let initialized = false;
const sessions = new Map<string, string>();
let nativeSelection = new Map<string, string>();
const pending = new Map<string | number, { method: string; nonce?: string; sessionId?: string }>();
const questions = createAntigravityQuestionBridge(id => typeof id === "string" && sessions.has(id));
const fail = (detail: string): never => { throw new AntigravityAdmissionError(detail); };
return (direction: "inbound" | "outbound", value: unknown): void => {
if (current !== state) throw new AntigravityAdmissionError("connection authority was replaced");
if (state.error) throw state.error;
const message = record(value);
const params = record(message.params);
const method = message.method;
const id = message.id;
try {
if (direction === "outbound" && typeof method === "string") {
const isSession = SESSION_METHODS.has(method);
const isModel = method === "session/set_model" || method === "session/set_config_option";
if (method.startsWith("session/") && !isSession) {
if (typeof params.sessionId !== "string" || !sessions.has(params.sessionId)) fail("operation belongs to an unacknowledged session");
}
if (isSession || isModel) {
if (!initialized) fail("the runtime does not advertise the required native admission patch");
if (isModel && !(method === "session/set_model" ? params.modelId === model : params.configId === "model" && params.value === model)) {
fail("only the exact selected model can be configured");
}
if (typeof id !== "string" && typeof id !== "number") fail("native admission requires a request identity");
if ([...pending.values()].some(request => request.nonce)) fail("a native configuration request is already pending");
const nonce = randomBytes(24).toString("hex");
const meta = record(params._meta);
params._meta = { ...meta, antigravity: {
...record(meta.antigravity), systemPrompt: { append: content },
paperclipAdmission: { schema: ANTIGRAVITY_ADMISSION_SCHEMA, nonce, model, ...binding },
} };
message.params = params;
state.sessionId = null;
sessions.clear();
nativeSelection.clear();
questions.clear();
if (pending.has(id as string | number) || pending.size >= 128) fail("duplicate or excessive native admission requests");
pending.set(id as string | number, { method, nonce, ...(typeof params.sessionId === "string" ? { sessionId: params.sessionId } : {}) });
} else if (method === "session/prompt") {
assertReady();
if (params.sessionId !== state.sessionId || !Array.isArray(params.prompt)
|| params.prompt.some(value => record(value).type !== "text" || typeof record(value).text !== "string")) fail("prompt is outside the admitted text session");
assertAntigravityPromptPolicy((params.prompt as unknown[]).map(value => record(value).text).join("\n"));
} else if (!["initialize", "session/cancel", "session/close"].includes(method)) {
fail("native control is unavailable in a managed task");
}
if (method === "initialize" && (typeof id === "string" || typeof id === "number")) {
if (pending.has(id)) fail("duplicate initialize identity");
pending.set(id, { method });
}
if (method === "session/close" && typeof params.sessionId === "string") {
sessions.delete(params.sessionId); state.sessionId = null; nativeSelection.clear(); questions.clear();
}
}
if (direction === "inbound" && method === undefined && (typeof id === "string" || typeof id === "number")) {
const request = pending.get(id);
if (request) {
pending.delete(id);
if (message.error !== undefined) return; // Keep the provider's typed error.
const result = record(message.result);
if (request.method === "initialize") {
const info = record(result.agentInfo);
const native = record(record(record(result.agentCapabilities)._meta).antigravity);
if (info.name !== "antigravity-acp" || info.version !== "1.4.3" || native.paperclipAdmission !== 1) fail("runtime identity or native admission patch is unavailable");
initialized = true;
} else if (request.nonce) {
const ack = record(record(result._meta).paperclipAntigravityAdmission);
const sessionId = request.sessionId ?? result.sessionId;
if (typeof sessionId !== "string" || !sessionId || ack.schema !== ANTIGRAVITY_ADMISSION_SCHEMA
|| ack.nonce !== request.nonce || ack.sessionId !== sessionId || ack.model !== model
|| ack.digest !== binding.digest || ack.byteLength !== binding.byteLength
|| typeof ack.connectionId !== "string" || !ack.connectionId || ack.connectionId.length > 256) fail("native configuration receipt is missing, stale, or mismatched");
const selected = selectedModel(result);
if (selected !== undefined && selected !== model) fail("provider acknowledged a different model");
const mode = record(result.modes).currentModeId;
if (mode !== undefined && mode !== "default") fail("native permissions are outside the admitted default mode");
nativeSelection = selectionValues(result);
projectConcreteModel(result, model);
sessions.set(sessionId as string, ack.connectionId as string);
state.sessionId = sessionId as string;
}
}
}
if (direction === "inbound" && method === "session/update") {
const update = record(params.update);
if (update.sessionUpdate === "config_option_update" && sessions.has(String(params.sessionId))
&& selectedModel(update) !== undefined && selectedModel(update) !== model) fail("provider changed the admitted model");
if (update.sessionUpdate === "config_option_update" && sessions.has(String(params.sessionId))) {
for (const [id, value] of selectionValues(update)) {
if (nativeSelection.get(id) !== value) fail("provider changed the admitted model, thinking level, or permission mode");
}
projectConcreteModel(update, model);
}
if (update.sessionUpdate === "current_mode_update" && update.currentModeId !== "default") fail("provider changed the admitted permission mode");
}
questions.guard(direction, message);
} catch (error) {
state.error = error instanceof AntigravityAdmissionError ? error : new AntigravityAdmissionError(error instanceof Error ? error.message : "invalid native protocol");
state.sessionId = null; sessions.clear(); nativeSelection.clear(); pending.clear(); questions.clear();
throw state.error;
}
};
},
};
}
function selectionValues(value: Record<string, unknown>): Map<string, string> {
const selected = new Map<string, string>();
if (!Array.isArray(value.configOptions)) return selected;
for (const entry of value.configOptions) {
const option = record(entry);
if (!["model", "thinking_level", "mode"].includes(String(option.id))) continue;
if (typeof option.currentValue !== "string" || selected.has(String(option.id))) {
throw new AntigravityAdmissionError("invalid native selection state");
}
selected.set(String(option.id), option.currentValue);
}
return selected;
}
/** Native model options group variants into a family plus thinking level.
* ACPX treats currentValue as a concrete model ID. Project only after the
* native receipt binds the actual initialized variant; keep the native values
* above so subsequent family/thinking changes cannot inherit that authority.
*/
function projectConcreteModel(value: Record<string, unknown>, model: string): void {
if (!Array.isArray(value.configOptions)) return;
for (const entry of value.configOptions) {
const option = record(entry);
if (option.id === "model") option.currentValue = model;
}
}
function selectedModel(value: Record<string, unknown>): unknown {
// The native model selector can group concrete variants under a display
// family. The exact initialized model is bound by the native receipt;
// only models.currentModelId is an additional concrete-model assertion.
return record(value.models).currentModelId;
}
function record(value: unknown): Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
@@ -0,0 +1,197 @@
import { execFileSync, spawn, type ChildProcess } from "node:child_process";
import { access, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
const setup = vi.hoisted(() => ({ spawn: vi.fn(), close: vi.fn(async () => undefined), verify: vi.fn() }));
vi.mock("./antigravity-installation.js", () => ({ antigravityNativeDistributionSpec: () => ({ pinned: true }) }));
vi.mock("./installation-integrity.js", () => ({ verifyNativeAcpxInstallation: setup.verify }));
import { sanitizeAntigravityCredential, startAntigravityBrowserLogin, validateAntigravityAuthorizationUrl, verifyAntigravityCredential } from "./antigravity-auth.js";
const CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
const credential = { client_id: CLIENT_ID, client_secret: "fixture-client-secret", refresh_token: "fixture-refresh-token", token_uri: "https://oauth2.googleapis.com/token",
scopes: ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/aicode"], project_id: "fixture-project" };
function authorizationUrl() {
return "https://accounts.google.com/o/oauth2/v2/auth?" + new URLSearchParams({ client_id: CLIENT_ID, response_type: "code", redirect_uri: "http://127.0.0.1:18973/",
scope: credential.scopes.join(" "), state: "fixture_state_0123456789", code_challenge: "a".repeat(43), code_challenge_method: "S256", access_type: "offline", prompt: "consent" });
}
const roots: string[] = [], children: ChildProcess[] = [];
afterEach(async () => {
vi.restoreAllMocks();
for (const child of children.splice(0)) if (child.exitCode === null && child.signalCode === null) { try { process.kill(-child.pid!, "SIGKILL"); } catch {} }
await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true })));
vi.unstubAllGlobals(); vi.clearAllMocks();
});
async function fixture(mode = "success") {
const home = await realpath(await mkdtemp(join(tmpdir(), "paperclip-antigravity-login-test-"))); roots.push(home);
const peer = join(home, "peer.cjs");
await writeFile(peer, `
const fs = require('node:fs');
const path = require('node:path');
const readline = require('node:readline');
const home = process.argv[2], mode = process.argv[3];
const requests = [];
if(mode==='stubborn') {
process.on('SIGTERM',()=>{});
const child=require('node:child_process').spawn(process.execPath,['-e',"process.on('SIGTERM',()=>{});setInterval(()=>{},1000)"],{stdio:'ignore'});
fs.writeFileSync(path.join(home,'descendant.pid'),String(child.pid));
}
const reader = readline.createInterface({input:process.stdin});
reader.on('line', line => {
const request=JSON.parse(line); requests.push(request);
fs.writeFileSync(path.join(home,'requests.json'),JSON.stringify(requests));
if(request.method==='initialize') {
process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:request.id,result:{protocolVersion:1,agentInfo:{name:'antigravity-acp',version:'1.4.3'},agentCapabilities:{_meta:{antigravity:{paperclipAdmission:1}}},authMethods:[{id:'oauth-personal'}]}})+'\\n');
} else if(request.method==='authenticate') {
const url = mode==='bad-url' ? 'https://example.invalid/steal' : ${JSON.stringify(authorizationUrl())};
const message='Open the following link to authenticate the ACP server: '+url+'\\n';
process.stderr.write(message.slice(0,42));process.stderr.write(message.slice(42));
if(mode==='blocked' || mode==='bad-url' || mode==='stubborn' || mode==='retained') return;
if(mode==='error') {process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:request.id,error:{code:-32000,message:'do not expose fixture-refresh-token'}})+'\\n');return;}
fs.mkdirSync(path.join(home,'antigravity-acp'),{mode:0o700});
fs.writeFileSync(path.join(home,'antigravity-acp/acp_token.json'),${JSON.stringify(JSON.stringify(credential))},{mode:0o600});
process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:request.id,result:{}})+'\\n');
} else {throw new Error('inference/session request forbidden');}
});
reader.on('close',()=>mode==='stubborn'||mode==='retained'?setInterval(()=>{},1000):process.exit(0));
`);
setup.verify.mockResolvedValue({ openCommand: async () => ({ spawn: setup.spawn, close: setup.close }) });
setup.spawn.mockImplementation((_args, options) => {
const child = spawn(process.execPath, [peer, home, mode], { ...options, detached: true }); children.push(child); return child;
});
return home;
}
function googleIdentity(emailVerified = true) {
return vi.fn().mockImplementation(async (url: string, options: RequestInit) => {
if (url === "https://oauth2.googleapis.com/token") {
expect(options.redirect).toBe("error");
expect(String(options.body)).toContain("grant_type=refresh_token");
return new Response(JSON.stringify({ access_token: "fixture-access-token", refresh_token: "fixture-rotated-refresh", token_type: "Bearer" }));
}
expect(url).toBe("https://www.googleapis.com/oauth2/v3/userinfo");
expect(options.headers).toEqual({ Authorization: "Bearer fixture-access-token" });
return new Response(JSON.stringify({ email: "person@example.test", email_verified: emailVerified }));
});
}
describe("Antigravity setup-only Google authentication", () => {
it.each(["network", "unverified"])("preserves an OAuth rotation when userinfo %s verification fails", async failure => {
const home = await realpath(await mkdtemp(join(tmpdir(), "paperclip-antigravity-rotation-test-"))); roots.push(home);
const { mkdir } = await import("node:fs/promises");
await mkdir(join(home, "antigravity-acp"), { mode: 0o700 });
const token = join(home, "antigravity-acp", "acp_token.json");
await writeFile(token, JSON.stringify(credential), { mode: 0o600 });
const fetch = vi.fn().mockResolvedValueOnce(Response.json({ access_token: "fixture-access-token", refresh_token: "fixture-newest-rotation", token_type: "Bearer" }));
if (failure === "network") fetch.mockRejectedValueOnce(new Error("fixture userinfo unavailable"));
else fetch.mockResolvedValueOnce(Response.json({ email: "person@example.test", email_verified: false }));
vi.stubGlobal("fetch", fetch);
await expect(verifyAntigravityCredential(home)).rejects.toThrow();
expect(JSON.parse(await readFile(token, "utf8")).refresh_token).toBe("fixture-newest-rotation");
expect(fetch).toHaveBeenCalledTimes(2);
});
it("performs native authenticate, retires it, verifies identity, and preserves refresh rotation without repeated status network calls", async () => {
const home = await fixture(), fetch = googleIdentity(); vi.stubGlobal("fetch", fetch);
const login = startAntigravityBrowserLogin(home);
await vi.waitFor(() => expect(login.outcome).toBe("success"), { timeout: 5_000 });
expect(login.authorizationUrl).toBe(authorizationUrl());
expect(JSON.parse(await readFile(join(home, "requests.json"), "utf8")).map((request: { method: string }) => request.method)).toEqual(["initialize", "authenticate"]);
expect(setup.spawn.mock.calls[0]![1].env).toMatchObject({ GEMINI_HOME: home, AGY_ACP_FORCE_FILE_STORAGE: "1", BROWSER: "/usr/bin/true" });
expect(Object.keys(setup.spawn.mock.calls[0]![1].env).some(key => /^(?:GOOGLE_|GEMINI_API|ANTIGRAVITY_|PYTHON|NODE_OPTIONS|HTTP_PROXY)/.test(key))).toBe(false);
expect(setup.close).toHaveBeenCalledOnce();
expect(children[0]!.exitCode).toBe(0);
const verified = await verifyAntigravityCredential(home);
expect(verified.accountEmail).toBe("person@example.test");
expect(JSON.parse(verified.credential).refresh_token).toBe("fixture-rotated-refresh");
expect(JSON.parse(await readFile(join(home, "antigravity-acp/acp_token.json"), "utf8")).refresh_token).toBe("fixture-rotated-refresh");
expect(fetch).toHaveBeenCalledTimes(2);
await login.abort();
// Runtime verification outside a live browser attempt must not retain a
// credential indefinitely in the browser status cache.
await verifyAntigravityCredential(home);
await verifyAntigravityCredential(home);
expect(fetch).toHaveBeenCalledTimes(6);
});
it.each(["error", "bad-url"])("rejects %s without leaking provider text or calling Google", async mode => {
const home = await fixture(mode), fetch = vi.fn(); vi.stubGlobal("fetch", fetch);
const login = startAntigravityBrowserLogin(home);
await vi.waitFor(() => expect(login.outcome).toBe("failure"), { timeout: 5_000 });
await vi.waitFor(() => expect(setup.close).toHaveBeenCalledOnce(), { timeout: 5_000 });
expect(login.error).not.toContain("fixture");
if (mode === "bad-url") expect(login.authorizationUrl).toBeUndefined();
expect(fetch).not.toHaveBeenCalled();
});
it("abort retires a waiting native flow without session requests", async () => {
const home = await fixture("blocked"), fetch = vi.fn(); vi.stubGlobal("fetch", fetch);
const login = startAntigravityBrowserLogin(home);
await vi.waitFor(() => expect(login.authorizationUrl).toBeDefined());
login.abort();
await vi.waitFor(() => expect(setup.close).toHaveBeenCalledOnce(), { timeout: 5_000 });
expect(login.outcome).toBe("failure"); expect(fetch).not.toHaveBeenCalled();
expect(children[0]!.exitCode).toBe(0);
});
it("abort waits for SIGKILL retirement of a login and inherited descendant that ignore EOF and SIGTERM", async () => {
const home = await fixture("stubborn"); vi.stubGlobal("fetch", vi.fn());
const login = startAntigravityBrowserLogin(home);
await vi.waitFor(() => expect(login.authorizationUrl).toBeDefined());
const descendant = (await readFile(join(home, "descendant.pid"), "utf8")).trim();
await login.abort();
expect(children[0]!.signalCode).toBe("SIGKILL");
expect(setup.close).toHaveBeenCalledOnce();
let state = "";
try { state = execFileSync("/bin/ps", ["-o", "stat=", "-p", descendant], { encoding: "utf8" }).trim(); } catch {}
expect(state === "" || state.startsWith("Z")).toBe(true);
});
it("keeps abort, the command lease and the private home owned through uncertain retirement until the actual child exits", async () => {
const home = await fixture("retained"); vi.stubGlobal("fetch", vi.fn());
const login = startAntigravityBrowserLogin(home);
await vi.waitFor(() => expect(login.authorizationUrl).toBeDefined());
const child = children[0]!, realKill = process.kill.bind(process);
let blockProof = true;
vi.spyOn(process, "kill").mockImplementation((pid, signal) => {
if (blockProof && pid === -child.pid! && signal === 0) throw Object.assign(new Error("controlled process-state uncertainty"), { code: "EACCES" });
return realKill(pid, signal);
});
const stopped = login.abort(), releaseHome = vi.fn(() => rm(home, { recursive: true, force: true }));
const cleaned = stopped.then(releaseHome);
await vi.waitFor(() => expect(login.error).toContain("still stopping"), { timeout: 5_000 });
expect(child.exitCode).toBeNull(); expect(child.signalCode).toBeNull();
expect(setup.close).not.toHaveBeenCalled(); expect(releaseHome).not.toHaveBeenCalled();
await expect(access(home)).resolves.toBeUndefined();
blockProof = false;
realKill(-child.pid!, "SIGKILL");
await cleaned;
expect(child.signalCode).toBe("SIGKILL");
expect(setup.close).toHaveBeenCalledOnce(); expect(releaseHome).toHaveBeenCalledOnce();
await expect(access(home)).rejects.toMatchObject({ code: "ENOENT" });
});
it("does not accept an unverified Google account after native authentication", async () => {
const home = await fixture(); vi.stubGlobal("fetch", googleIdentity(false));
const login = startAntigravityBrowserLogin(home);
await vi.waitFor(() => expect(login.outcome).toBe("failure"), { timeout: 5_000 });
expect(setup.close).toHaveBeenCalledOnce();
});
it("requires the pinned OAuth client and token destination and strips unrelated native fields", () => {
expect(JSON.parse(sanitizeAntigravityCredential(JSON.stringify({ ...credential, endpoint: "https://example.invalid", access_token: "discard" })))).toEqual(credential);
expect(sanitizeAntigravityCredential(JSON.stringify({ ...credential, scopes: [...credential.scopes].reverse() }))).toBe(sanitizeAntigravityCredential(JSON.stringify(credential)));
expect(() => sanitizeAntigravityCredential(JSON.stringify({ ...credential, token_uri: "https://example.invalid/token" }))).toThrow("Invalid Antigravity credential");
expect(() => sanitizeAntigravityCredential(JSON.stringify({ ...credential, client_id: "another-client" }))).toThrow("Invalid Antigravity credential");
expect(() => sanitizeAntigravityCredential(JSON.stringify({ ...credential, scopes: ["openid"] }))).toThrow("Invalid Antigravity credential");
});
it("requires Google PKCE and a numeric loopback redirect", () => {
expect(validateAntigravityAuthorizationUrl(authorizationUrl())).toBe(authorizationUrl());
for (const url of [authorizationUrl().replace("accounts.google.com", "example.invalid"), authorizationUrl().replace("S256", "plain"), authorizationUrl().replace("127.0.0.1", "localhost"), authorizationUrl() + "&state=duplicate"]) {
expect(() => validateAntigravityAuthorizationUrl(url)).toThrow();
}
});
});
@@ -0,0 +1,260 @@
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { constants } from "node:fs";
import { lstat, open, rename, rm } from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { join } from "node:path";
import { StringDecoder } from "node:string_decoder";
import { antigravityPrivateEnvironment, checkAntigravityPrivateHome, retireAntigravityProcess } from "./antigravity-process.js";
import { antigravityNativeDistributionSpec } from "./antigravity-installation.js";
import { verifyNativeAcpxInstallation, type VerifiedAcpxCommandLease } from "./installation-integrity.js";
const CLIENT_ID = "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com";
const TOKEN_URI = "https://oauth2.googleapis.com/token";
const SCOPES = ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/aicode"];
const AUTH_PREFIX = "Open the following link to authenticate the ACP server: ";
const MAX_BYTES = 64 * 1024;
const LOGIN_ERROR = "Google sign-in did not complete. Start sign-in again.";
const record = (value: unknown): Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
const verifiedCredentials = new Map<string, { credential: string; accountEmail: string }>();
const activeBrowserHomes = new Set<string>();
export type AntigravityBrowserLoginState = {
authorizationUrl?: string;
outcome?: "success" | "failure";
error?: string;
/** Resolves only after physical process retirement and lease release. */
abort(): Promise<void>;
};
/** Pure validation for the native credential projection and refresh writeback. */
export function sanitizeAntigravityCredential(serialized: string): string {
if (Buffer.byteLength(serialized) > MAX_BYTES) throw new Error("Invalid Antigravity credential");
let value: Record<string, unknown>;
try { value = record(JSON.parse(serialized)); } catch { throw new Error("Invalid Antigravity credential"); }
const text = (entry: unknown, max: number): entry is string => typeof entry === "string" && entry.length > 0 && entry.length <= max && !/[\x00-\x20\x7f]/.test(entry);
if (value.client_id !== CLIENT_ID || value.token_uri !== TOKEN_URI
|| !text(value.client_secret, 1024) || !text(value.refresh_token, 16_384)
|| !Array.isArray(value.scopes) || value.scopes.length > 4
|| !value.scopes.every(scope => typeof scope === "string" && [...SCOPES, "openid"].includes(scope))
|| !SCOPES.every(scope => (value.scopes as unknown[]).includes(scope))
|| new Set(value.scopes).size !== value.scopes.length
|| (value.project_id !== undefined && !text(value.project_id, 1024))) throw new Error("Invalid Antigravity credential");
return JSON.stringify({ client_id: CLIENT_ID, client_secret: value.client_secret, refresh_token: value.refresh_token,
token_uri: TOKEN_URI, scopes: [...SCOPES, ...((value.scopes as unknown[]).includes("openid") ? ["openid"] : [])], ...(value.project_id !== undefined ? { project_id: value.project_id } : {}) });
}
/** Only the official PKCE loopback authorization URL may reach the browser. */
export function validateAntigravityAuthorizationUrl(raw: string): string {
if (raw.length > 16_384 || /[\x00-\x20\x7f]/.test(raw)) throw new Error(LOGIN_ERROR);
const url = new URL(raw), query = url.searchParams;
const keys = ["client_id", "response_type", "redirect_uri", "scope", "state", "code_challenge", "code_challenge_method", "access_type", "prompt"];
if (url.origin !== "https://accounts.google.com" || url.pathname !== "/o/oauth2/v2/auth" || url.hash || url.username || url.password
|| [...query.keys()].some(key => !keys.includes(key) || query.getAll(key).length !== 1)
|| query.get("client_id") !== CLIENT_ID || query.get("response_type") !== "code"
|| query.get("access_type") !== "offline" || query.get("prompt") !== "consent"
|| query.get("code_challenge_method") !== "S256" || !/^[A-Za-z0-9_-]{43}$/.test(query.get("code_challenge") ?? "")
|| !/^[A-Za-z0-9._~-]{16,1024}$/.test(query.get("state") ?? "")) throw new Error(LOGIN_ERROR);
const redirect = new URL(query.get("redirect_uri") ?? "");
if (redirect.protocol !== "http:" || redirect.hostname !== "127.0.0.1" || !redirect.port || redirect.pathname !== "/"
|| redirect.username || redirect.password || redirect.search || redirect.hash) throw new Error(LOGIN_ERROR);
const scopes = (query.get("scope") ?? "").split(" ");
if (scopes.length > 4 || !SCOPES.every(scope => scopes.includes(scope)) || scopes.some(scope => ![...SCOPES, "openid"].includes(scope))) throw new Error(LOGIN_ERROR);
return url.href;
}
async function credentialFile(home: string): Promise<string> {
await checkAntigravityPrivateHome(home);
const parent = join(home, "antigravity-acp"), stat = await lstat(parent);
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || (stat.mode & 0o777) !== 0o700) throw new Error("Invalid Antigravity credential");
// Darwin's O_NOFOLLOW_ANY rejects symlinks in every component, including a
// concurrently replaced parent. This candidate is macOS ARM64 only.
const noFollow = process.platform === "darwin" ? 0x20000000 : constants.O_NOFOLLOW;
const file = await open(join(parent, "acp_token.json"), constants.O_RDONLY | noFollow | constants.O_NONBLOCK);
try {
const info = await file.stat();
if (!info.isFile() || info.nlink !== 1 || info.uid !== process.getuid?.() || (info.mode & 0o777) !== 0o600 || info.size > MAX_BYTES) throw new Error("Invalid Antigravity credential");
const bytes = Buffer.alloc(MAX_BYTES + 1);
let length = 0;
while (length < bytes.length) {
const { bytesRead } = await file.read(bytes, length, bytes.length - length, length);
if (!bytesRead) break;
length += bytesRead;
}
if (length > MAX_BYTES) throw new Error("Invalid Antigravity credential");
return sanitizeAntigravityCredential(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, length)));
} finally { await file.close(); }
}
async function googleJson(url: string, options: RequestInit): Promise<Record<string, unknown>> {
const response = await fetch(url, { ...options, redirect: "error", signal: options.signal ?? AbortSignal.timeout(10_000) });
if (!response.ok || !response.body) throw new Error(LOGIN_ERROR);
const reader = response.body.getReader(), parts: Uint8Array[] = [];
let length = 0;
try {
while (true) {
const next = await reader.read();
if (next.done) break;
length += next.value.length;
if (length > MAX_BYTES) throw new Error(LOGIN_ERROR);
parts.push(next.value);
}
return record(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(parts))));
} finally { await reader.cancel().catch(() => undefined); }
}
/** Refresh and check Google identity privately; this never calls a model API.
* Callers must also require the setup process's successful authenticate result.
*/
export async function verifyAntigravityCredential(home: string, options: { signal?: AbortSignal } = {}): Promise<{ credential: string; accountEmail: string }> {
options.signal?.throwIfAborted();
const credential = await credentialFile(home), cached = verifiedCredentials.get(home);
if (cached?.credential === credential) return { ...cached };
const value = record(JSON.parse(credential));
const token = await googleJson(TOKEN_URI, { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" },
signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(10_000)]) : AbortSignal.timeout(10_000),
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: value.refresh_token as string, client_id: CLIENT_ID, client_secret: value.client_secret as string }) });
if (typeof token.access_token !== "string" || token.access_token.length > 16_384 || /[\x00-\x20\x7f]/.test(token.access_token) || !token.access_token
|| (token.token_type !== undefined && String(token.token_type).toLowerCase() !== "bearer")) throw new Error(LOGIN_ERROR);
let next = credential;
if (token.refresh_token !== undefined && token.refresh_token !== value.refresh_token) {
next = sanitizeAntigravityCredential(JSON.stringify({ ...value, refresh_token: token.refresh_token }));
// Preserve a consumed rotation before another request can fail. This is a
// private, unverified receipt until userinfo succeeds; the connection owner
// still performs identity and generation-checked encrypted writeback.
await checkAntigravityPrivateHome(home);
const target = join(home, "antigravity-acp", "acp_token.json"), temporary = `${target}.${randomUUID()}.tmp`;
const file = await open(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
try {
await file.writeFile(next);
await file.sync();
} finally { await file.close(); }
try {
await rename(temporary, target);
const directory = await open(join(home, "antigravity-acp"), constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
try { await directory.sync(); } finally { await directory.close(); }
} finally { await rm(temporary, { force: true }); }
}
const identity = await googleJson("https://www.googleapis.com/oauth2/v3/userinfo", { headers: { Authorization: `Bearer ${token.access_token}` },
signal: options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(10_000)]) : AbortSignal.timeout(10_000) });
if (identity.email_verified !== true || typeof identity.email !== "string" || identity.email.length > 320 || !/^[^\s@]+@[^\s@]+$/.test(identity.email)) throw new Error("Google could not verify the signed-in account.");
const result = { credential: next, accountEmail: identity.email };
// Runtime refresh checks use short-lived prepared homes. Retain credentials
// only for an active, bounded browser attempt, whose abort owns eviction.
if (activeBrowserHomes.has(home)) verifiedCredentials.set(home, result);
return { ...result };
}
/** A setup-only native ACP client. Its complete outgoing method set is
* initialize/authenticate; pending provider qualification remains enforced for
* every session and prompt by the ordinary runtime installation path.
*/
export function startAntigravityBrowserLogin(home: string): AntigravityBrowserLoginState {
const controller = new AbortController();
activeBrowserHomes.add(home);
let resolveStopped: () => void;
const stopped = new Promise<void>(resolve => { resolveStopped = resolve; });
const state: AntigravityBrowserLoginState = { abort() { controller.abort(); activeBrowserHomes.delete(home); verifiedCredentials.delete(home); return stopped; } };
void (async () => {
let lease: VerifiedAcpxCommandLease | undefined;
let child: ChildProcessWithoutNullStreams | undefined, closed: Promise<void> | undefined;
let timeout: ReturnType<typeof setTimeout> | undefined, failPending: ((error: Error) => void) | undefined;
try {
await checkAntigravityPrivateHome(home);
const installation = await verifyNativeAcpxInstallation(antigravityNativeDistributionSpec());
lease = await installation.openCommand({ signal: controller.signal });
const environment = await antigravityPrivateEnvironment(home);
if (controller.signal.aborted) throw new Error(LOGIN_ERROR);
child = lease.spawn([], { cwd: home, stdio: "pipe", detached: process.platform !== "win32", env: environment }) as ChildProcessWithoutNullStreams;
closed = new Promise(resolve => child!.once("close", () => resolve()));
const peer = child;
let phase = 1, stdout = "", stderr = "", total = 0, failure: Error | undefined;
const outputDecoder = new StringDecoder("utf8"), errorDecoder = new StringDecoder("utf8");
const authenticated = new Promise<void>((resolve, reject) => {
const fail = (error = new Error(LOGIN_ERROR)) => { failure ??= error; reject(failure); };
failPending = fail;
const send = (id: 1 | 2) => peer.stdin.write(`${JSON.stringify(id === 1
? { jsonrpc: "2.0", id, method: "initialize", params: { protocolVersion: 1, clientInfo: { name: "paperclip-runner", version: "1" }, clientCapabilities: {} } }
: { jsonrpc: "2.0", id, method: "authenticate", params: { methodId: "oauth-personal" } })}\n`);
peer.stdout.on("data", (chunk: Buffer) => {
total += chunk.length;
if (total > 2 * 1024 * 1024) { fail(); return; }
stdout += outputDecoder.write(chunk);
while (stdout.includes("\n") && !failure) {
const end = stdout.indexOf("\n"), line = stdout.slice(0, end); stdout = stdout.slice(end + 1);
if (!line.trim()) continue;
let message: Record<string, unknown>;
try { message = record(JSON.parse(line)); } catch { fail(); return; }
if (phase > 2 || message.jsonrpc !== "2.0" || message.id !== phase || "method" in message || "error" in message || !Object.hasOwn(message, "result")) { fail(); return; }
if (phase === 1) {
const initialized = record(message.result), info = record(initialized.agentInfo);
if (initialized.protocolVersion !== 1 || info.name !== "antigravity-acp" || info.version !== "1.4.3"
|| record(record(record(initialized.agentCapabilities)._meta).antigravity).paperclipAdmission !== 1
|| !Array.isArray(initialized.authMethods) || !initialized.authMethods.some(method => record(method).id === "oauth-personal")) { fail(); return; }
phase = 2; send(2);
} else { phase = 3; resolve(); }
}
});
peer.stderr.on("data", (chunk: Buffer) => {
total += chunk.length;
if (total > 2 * 1024 * 1024) { fail(); return; }
stderr += errorDecoder.write(chunk);
while (stderr.includes("\n") && !failure) {
const end = stderr.indexOf("\n"), line = stderr.slice(0, end); stderr = stderr.slice(end + 1);
if (!line.startsWith(AUTH_PREFIX)) continue;
try {
const url = validateAntigravityAuthorizationUrl(line.slice(AUTH_PREFIX.length));
if (phase !== 2 || state.authorizationUrl !== undefined) throw new Error(LOGIN_ERROR);
state.authorizationUrl = url;
} catch { fail(); }
}
if (stderr.length > MAX_BYTES) fail();
});
peer.once("error", () => fail());
peer.once("close", () => { if (phase !== 3) fail(); });
peer.stdin.on("error", () => { if (phase !== 3) fail(); });
timeout = setTimeout(() => fail(), 360_000);
send(1);
});
const abort = () => failPending?.(new Error(LOGIN_ERROR));
controller.signal.addEventListener("abort", abort, { once: true });
try { if (controller.signal.aborted) abort(); await authenticated; }
finally { controller.signal.removeEventListener("abort", abort); }
if (timeout) clearTimeout(timeout);
await retireAntigravityProcess(peer, closed); child = undefined;
await lease.close(); lease = undefined;
if (failure || controller.signal.aborted) throw new Error(LOGIN_ERROR);
await verifyAntigravityCredential(home, { signal: controller.signal });
if (controller.signal.aborted) throw new Error(LOGIN_ERROR);
state.outcome = "success";
} catch {
state.outcome = "failure"; state.error = LOGIN_ERROR;
} finally {
if (timeout) clearTimeout(timeout);
let observedClose = child === undefined;
let cleanupWasUncertain = false;
void closed?.then(() => { observedClose = true; });
// The active login owns these references until cleanup is proven. Only
// this unresolved obligation schedules retries; physical close wakes it
// early. Callers keep the private home while awaiting abort's completion.
while (true) {
try {
if (child && closed) await retireAntigravityProcess(child, closed);
await lease?.close();
break;
} catch {
cleanupWasUncertain = true;
state.outcome = "failure";
state.error = "Google sign-in is still stopping. Wait before trying again.";
let retryTimer: ReturnType<typeof setTimeout> | undefined;
const retry = new Promise<void>(resolve => { retryTimer = setTimeout(resolve, 1_000); });
try { await (observedClose || !closed ? retry : Promise.race([closed, retry])); }
finally { if (retryTimer) clearTimeout(retryTimer); }
}
}
if (cleanupWasUncertain) state.error = LOGIN_ERROR;
if (state.outcome !== "success") { activeBrowserHomes.delete(home); verifiedCredentials.delete(home); }
resolveStopped!();
}
})();
return state;
}
@@ -0,0 +1,126 @@
import { mkdtemp, readFile, realpath, rm, stat, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, expect, it, vi } from "vitest";
import { ANTIGRAVITY_AUTH_ENV, stageManagedAntigravityCredential } from "./antigravity-credentials.js";
import { ACPX_CREDENTIAL_BINDING_ENV, createAcpxCredentialBinding, createAcpxSidecarHostEnvironment, createSanitizedAcpxSpawnInput } from "./environment.js";
import { configuredEnvironmentProjection } from "../../configured-environment.js";
import * as codexCredentials from "./codex-credentials.js";
const roots: string[] = [];
const credential = JSON.stringify({ client_id: "1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com",
client_secret: "fixture-client", refresh_token: "fixture-refresh", token_uri: "https://oauth2.googleapis.com/token",
scopes: ["https://www.googleapis.com/auth/cloud-platform", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/aicode"] });
async function home() { const value = await realpath(await mkdtemp(join(tmpdir(), "antigravity-credential-test-"))); roots.push(value); return value; }
afterEach(async () => { vi.restoreAllMocks(); vi.unstubAllEnvs(); await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))); });
it.each([{}, { [ANTIGRAVITY_AUTH_ENV]: "{}" }, { [ANTIGRAVITY_AUTH_ENV]: credential, GEMINI_API_KEY: "fixture-key" }])("rejects absent, malformed or ambiguous credentials", async environment => {
await expect(stageManagedAntigravityCredential({ agentHomeDirectory: await home(), environment })).rejects.toThrow(/credential/);
});
it("keeps OAuth bound to its sidecar session and out of the actual native environment", () => {
const source = { [ANTIGRAVITY_AUTH_ENV]: credential };
const env = { ...source, [ACPX_CREDENTIAL_BINDING_ENV]: createAcpxCredentialBinding(source, "antigravity", "one") };
expect(createAcpxSidecarHostEnvironment(env, "antigravity", "one")[ANTIGRAVITY_AUTH_ENV]).toBe(credential);
expect(() => createAcpxSidecarHostEnvironment(env, "antigravity", "other")).toThrow(/binding/);
expect(() => createAcpxSidecarHostEnvironment(source, "antigravity", "one")).toThrow(/binding/);
expect(createSanitizedAcpxSpawnInput(source, "antigravity").env[ANTIGRAVITY_AUTH_ENV]).toBeUndefined();
vi.stubEnv(ANTIGRAVITY_AUTH_ENV, credential);
expect(createSanitizedAcpxSpawnInput(undefined, "antigravity").env[ANTIGRAVITY_AUTH_ENV]).toBeUndefined();
});
it("fences refresh owners, preserves native rotation, scrubs live files and removes stale handoffs on reuse", async () => {
const root = await home(), directory = join(root, "antigravity-acp");
const input = { agentHomeDirectory: root, environment: { [ANTIGRAVITY_AUTH_ENV]: credential } };
const first = await stageManagedAntigravityCredential(input);
try {
expect((await stat(join(directory, "acp_token.json"))).mode & 0o777).toBe(0o600);
await expect(stageManagedAntigravityCredential(input)).rejects.toThrow(/active lease|owned/);
const rotated = credential.replace("fixture-refresh", "rotated-refresh");
await writeFile(join(directory, "acp_token.json"), rotated, { mode: 0o600 });
await Promise.all([first.close(), first.close()]);
expect(await readFile(join(directory, "auth-refresh.json"), "utf8")).toBe(rotated);
for (const name of ["auth.json", "acp_token.json"]) await expect(stat(join(directory, name))).rejects.toMatchObject({ code: "ENOENT" });
const second = await stageManagedAntigravityCredential(input);
try { await expect(stat(join(directory, "auth-refresh.json"))).rejects.toMatchObject({ code: "ENOENT" }); }
finally { await second.close(); }
} finally { await first.close(); }
});
it("retains ownership when a token is replaced by a symlink, and never reads its target", async () => {
const root = await home(), token = join(root, "antigravity-acp", "acp_token.json");
const input = { agentHomeDirectory: root, environment: { [ANTIGRAVITY_AUTH_ENV]: credential } };
const lease = await stageManagedAntigravityCredential(input), target = join(await home(), "unrelated");
await writeFile(target, "unrelated", { mode: 0o600 });
await rm(token); await symlink(target, token);
await expect(lease.close()).rejects.toThrow();
await expect(stageManagedAntigravityCredential(input)).rejects.toThrow(/active lease|owned/);
expect(await readFile(target, "utf8")).toBe("unrelated");
await rm(token); await writeFile(token, credential, { mode: 0o600 }); await lease.close();
});
it("preserves a native OAuth rotation when startup admission never succeeds", async () => {
const root = await home(), directory = join(root, "antigravity-acp");
const lease = await stageManagedAntigravityCredential({ agentHomeDirectory: root,
environment: { [ANTIGRAVITY_AUTH_ENV]: credential }, retainRefresh: () => false });
const rotated = credential.replace("fixture-refresh", "failed-startup-rotation");
await writeFile(join(directory, "acp_token.json"), rotated, { mode: 0o600 });
await lease.close();
expect(await readFile(join(directory, "auth-refresh.json"), "utf8")).toBe(rotated);
expect((await stat(join(directory, "auth-refresh.json"))).mode & 0o777).toBe(0o600);
await expect(stat(join(directory, "acp_token.json"))).rejects.toMatchObject({ code: "ENOENT" });
});
it("retries the original lifetime lease after its first close fails without losing the rotated handoff", async () => {
const stage = codexCredentials.stageManagedCodexCredential;
let attempts = 0;
vi.spyOn(codexCredentials, "stageManagedCodexCredential").mockImplementation(async input => {
const actual = await stage(input);
return { ...actual, close: async () => {
if (++attempts === 1) throw new Error("injected lifetime close failure");
await actual.close();
} };
});
const root = await home(), directory = join(root, "antigravity-acp");
const input = { agentHomeDirectory: root, environment: { [ANTIGRAVITY_AUTH_ENV]: credential } };
const lease = await stageManagedAntigravityCredential(input);
const rotated = credential.replace("fixture-refresh", "retry-close-rotation");
await writeFile(join(directory, "acp_token.json"), rotated, { mode: 0o600 });
try {
await expect(lease.close()).rejects.toThrow("injected lifetime close failure");
await expect(lease.close()).resolves.toBeUndefined();
expect(attempts).toBe(2);
expect(await readFile(join(directory, "auth-refresh.json"), "utf8")).toBe(rotated);
const next = await stageManagedAntigravityCredential(input); await next.close();
} finally { await lease.close(); }
});
it("leaves no unchanged OAuth handoff after failed admission or an API-key session", async () => {
for (const environment of [{ [ANTIGRAVITY_AUTH_ENV]: credential }, { GEMINI_API_KEY: "fixture-key" }]) {
const root = await home();
const lease = await stageManagedAntigravityCredential({ agentHomeDirectory: root, environment, retainRefresh: () => false });
await lease.close();
for (const name of ["auth.json", "acp_token.json", "auth-refresh.json"]) await expect(stat(join(root, "antigravity-acp", name))).rejects.toMatchObject({ code: "ENOENT" });
}
});
it("keeps the selected key while removing conflicting explicitly projected Google auth and runtime controls", () => {
const source = configuredEnvironmentProjection({
GEMINI_API_KEY: "selected-key", GOOGLE_API_KEY: "other-key", GOOGLE_APPLICATION_CREDENTIALS: "/foreign/adc.json",
GOOGLE_GENAI_USE_VERTEXAI: "true", GOOGLE_CLOUD_PROJECT: "foreign-project", GEMINI_HOME: "/foreign/home",
AGY_LLM_GATEWAY_URL: "https://foreign.invalid", ANTIGRAVITY_HARNESS_PATH: "/foreign/helper",
CLOUDSDK_CONFIG: "/foreign/cloud", AGY_ACP_FORCE_FILE_STORAGE: "0", TASK_INPUT: "retained",
});
const env = createSanitizedAcpxSpawnInput(source, "antigravity").env;
expect(env.GEMINI_API_KEY).toBe("selected-key");
expect(env.TASK_INPUT).toBe("retained");
for (const name of Object.keys(source)) {
if (/^(?:GOOGLE_|GEMINI_|AGY_|ANTIGRAVITY_|CLOUDSDK_)/.test(name) && name !== "GEMINI_API_KEY") expect(env[name]).toBeUndefined();
}
expect(JSON.parse(env.PAPERCLIP_CONFIGURED_ENV_KEYS!)).toEqual(["GEMINI_API_KEY", "TASK_INPUT"]);
});
it("does not borrow an ambient Gemini key", () => {
vi.stubEnv("GEMINI_API_KEY", "ambient-key");
expect(createSanitizedAcpxSpawnInput(undefined, "antigravity").env.GEMINI_API_KEY).toBeUndefined();
});
@@ -0,0 +1,93 @@
import { constants } from "node:fs";
import { lstat, mkdir, open, realpath, rename, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { sanitizeAntigravityCredential } from "./antigravity-auth.js";
import { stageManagedCodexCredential, type AcpxProviderLifetimeLease } from "./codex-credentials.js";
export const ANTIGRAVITY_AUTH_ENV = "PAPERCLIP_ACPX_ANTIGRAVITY_AUTH_JSON_SECRET";
/** The controller's selected credential crosses stdio admission once, then lives
* only in a private, fenced native home. It never enters provider environment.
*/
export async function stageManagedAntigravityCredential(input: {
agentHomeDirectory: string;
environment?: NodeJS.ProcessEnv;
/** Unchanged copies may be omitted; consumed refresh rotations always survive. */
retainRefresh?: () => boolean;
}): Promise<AcpxProviderLifetimeLease> {
const source = input.environment ?? {};
const inline = source[ANTIGRAVITY_AUTH_ENV];
const key = source.GEMINI_API_KEY?.trim();
if (Boolean(inline) === Boolean(key)) throw new Error("Antigravity requires exactly one selected runtime credential");
const canonical = inline ? sanitizeAntigravityCredential(inline) : undefined;
const directory = join(input.agentHomeDirectory, "antigravity-acp");
await mkdir(directory, { recursive: true, mode: 0o700 });
const metadata = await lstat(directory);
if (!metadata.isDirectory() || metadata.isSymbolicLink() || (metadata.mode & 0o077) !== 0
|| metadata.uid !== process.getuid?.() || await realpath(directory) !== directory) {
throw new Error("Antigravity credential home is unsafe");
}
// Reuse the established crash scrub, cleanup intent, and inherited quorum.
// The extra native token filename is scrubbed while that quorum is held.
const lease = await stageManagedCodexCredential({ agentHomeDirectory: directory,
environment: canonical ? { PAPERCLIP_ACPX_CODEX_AUTH_JSON_SECRET: canonical } : { OPENAI_API_KEY: key } });
const token = join(directory, "acp_token.json");
const refresh = join(directory, "auth-refresh.json");
try {
for (const filename of [token, refresh, `${refresh}.tmp`]) await rm(filename, { force: true });
if (canonical) await writeFile(token, canonical, { flag: "wx", mode: 0o600 });
} catch (error) {
await rm(token, { force: true });
await lease.close();
throw error;
}
let closed = false;
let tokenProcessed = false;
let closing: Promise<void> | undefined;
return {
lifetimeFenceCandidates: lease.lifetimeFenceCandidates,
lifetimeFenceFds: lease.lifetimeFenceFds,
activateLifetimeOwner: pid => lease.activateLifetimeOwner(pid),
close() {
if (closed) return Promise.resolve();
return closing ??= (async () => {
if (canonical && !tokenProcessed) {
const handle = await open(token, constants.O_RDONLY | constants.O_NOFOLLOW);
try {
const stat = await handle.stat();
if (!stat.isFile() || stat.nlink !== 1 || stat.uid !== process.getuid?.()
|| (stat.mode & 0o777) !== 0o600 || stat.size > 65_536) throw new Error("Antigravity refreshed credential is unsafe");
const buffer = Buffer.alloc(65_537);
try {
let size = 0;
while (size < buffer.length) {
const read = await handle.read(buffer, size, buffer.length - size, size);
if (!read.bytesRead) break;
size += read.bytesRead;
}
if (size > 65_536) throw new Error("Antigravity refreshed credential exceeds its bound");
const refreshed = sanitizeAntigravityCredential(buffer.subarray(0, size).toString("utf8"));
// Failed/cancelled startup can still consume a refresh token.
// Admission controls retaining an unchanged copy, never rotation.
if (refreshed !== canonical || (input.retainRefresh?.() ?? true)) {
await rm(`${refresh}.tmp`, { force: true });
const handoff = await open(`${refresh}.tmp`, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
try { await handoff.writeFile(refreshed); await handoff.sync(); } finally { await handoff.close(); }
await rename(`${refresh}.tmp`, refresh);
const parent = await open(directory, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
try { await parent.sync(); } finally { await parent.close(); }
}
} finally { buffer.fill(0); await rm(`${refresh}.tmp`, { force: true }); }
} finally { await handle.close(); }
// A failed underlying lease close must retry that same lease without
// reopening the token removed only after this durable handoff.
tokenProcessed = true;
}
await rm(token, { force: true });
await rm(join(directory, "logs"), { recursive: true, force: true });
await lease.close();
closed = true;
})().finally(() => { closing = undefined; });
},
};
}
@@ -0,0 +1,16 @@
import { CONFIGURED_ENVIRONMENT_KEYS, configuredEnvironmentKeys } from "../../configured-environment.js";
/** The selected Gemini key owns authentication; task variables cannot reroute it. */
export function sanitizeAntigravityEnvironment(environment: NodeJS.ProcessEnv): void {
const removed = new Set<string>();
const names = configuredEnvironmentKeys(environment);
for (const name of Object.keys(environment)) {
if (name === "GEMINI_API_KEY") continue;
if (/^(?:GOOGLE_|GEMINI_|AGY_|ANTIGRAVITY_|GCLOUD_|CLOUDSDK_|PYTHON)/i.test(name)) {
delete environment[name]; removed.add(name);
}
}
if (environment[CONFIGURED_ENVIRONMENT_KEYS] !== undefined) {
environment[CONFIGURED_ENVIRONMENT_KEYS] = JSON.stringify(names.filter(name => !removed.has(name)));
}
}
@@ -0,0 +1,53 @@
import { assertAcpxExecutionAdmission, type AcpxCandidateStartupAdmission } from "./candidate-admission.js";
import { lstatSync } from "node:fs";
import { userInfo } from "node:os";
import { isAbsolute, join } from "node:path";
import { ANTIGRAVITY_DISTRIBUTION_PINS } from "./generated-profiles.js";
import { verifyNativeAcpxInstallation, type VerifiedAcpxInstallation } from "./installation-integrity.js";
import type { NativeAcpxDistributionInput } from "./native-distribution-integrity.js";
import { resolveRunnerProviderAssetsRoot } from "./provider-assets-root.js";
import { QUALIFIED_ACPX_PROFILES, type QualifiedAcpxProfile } from "./qualified-profiles.js";
/** Owned cache resolution is independent of the isolated provider's HOME. */
export function antigravityRuntimeCachePath(closure: string, platform: string, architecture: string, home = userInfo().homedir): string {
if (!/^[a-f0-9]{64}$/.test(closure) || !Object.hasOwn(ANTIGRAVITY_DISTRIBUTION_PINS, `${platform}-${architecture}`)
|| !isAbsolute(home) || home.includes("\0")) throw new Error("Invalid Antigravity runtime cache identity");
let directory = home;
for (const part of [".paperclip", "runtimes", "antigravity", `${platform}-${architecture}`, closure]) {
directory = join(directory, part);
try {
const stat = lstatSync(directory);
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("Antigravity runtime cache must contain real directories");
} catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
}
return directory;
}
export function antigravityNativeDistributionSpec(platform: NodeJS.Platform = process.platform, architecture: string = process.arch): NativeAcpxDistributionInput {
const key = `${platform}-${architecture}`;
if (!Object.hasOwn(ANTIGRAVITY_DISTRIBUTION_PINS, key)) throw new Error(`Antigravity has no admitted complete runtime closure for ${key}`);
const pin = ANTIGRAVITY_DISTRIBUTION_PINS[key as keyof typeof ANTIGRAVITY_DISTRIBUTION_PINS];
const packaged = join(resolveRunnerProviderAssetsRoot(import.meta.url, "antigravity"), key);
let distributionRoot = packaged;
try {
const stat = lstatSync(packaged);
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("Antigravity packaged runtime must be a real directory");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
distributionRoot = antigravityRuntimeCachePath(pin.closureSha256, platform, architecture);
}
return { distributionRoot, manifestPath: join(distributionRoot, ".paperclip-antigravity-closure.json"), expectedClosureSha256: pin.closureSha256,
executable: pin.executable, fixedArguments: [...pin.args] };
}
export async function verifyAntigravityInstallation(profile: QualifiedAcpxProfile, candidateAdmission?: AcpxCandidateStartupAdmission): Promise<VerifiedAcpxInstallation> {
assertAcpxExecutionAdmission(profile, candidateAdmission);
const trusted = QUALIFIED_ACPX_PROFILES.antigravity;
const modelFields = new Set(["qualificationModel", "reportedModelId"]);
if (Object.keys(profile).some(key => !modelFields.has(key) && !Object.hasOwn(trusted, key))
|| Object.entries(trusted).some(([key, value]) => profile[key as keyof QualifiedAcpxProfile] !== value)
|| !profile.qualificationModel?.trim() || profile.reportedModelId !== profile.qualificationModel) {
throw new Error("Antigravity installation requires its exact pinned profile and an explicit model");
}
return { ...await verifyNativeAcpxInstallation(antigravityNativeDistributionSpec()), commandDigest: trusted.commandDigest };
}
@@ -0,0 +1,183 @@
import { execFileSync, spawn, type ChildProcess } from "node:child_process";
import { access, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it, vi } from "vitest";
import { CodexAcpxDriver } from "./codex-acpx-driver.js";
import { acpxCapabilities, acpxDriverDescriptor, validateAcpxDriverConfig } from "./driver-profile.js";
import { verifyQualifiedAcpxInstallation } from "./installation-integrity.js";
import { verifyAcpxProfileInstallation } from "./profile-installation.js";
import { assertAcpxProfileReleased, resolveQualifiedAcpxProfile } from "./qualified-profiles.js";
const setup = vi.hoisted(() => ({ spawn: vi.fn(), close: vi.fn(async () => undefined), verify: vi.fn(), uncertainRetirement: false }));
vi.mock("./antigravity-installation.js", () => ({ antigravityNativeDistributionSpec: () => ({ pinned: true }) }));
vi.mock("./installation-integrity.js", async original => ({
...await original<typeof import("./installation-integrity.js")>(), verifyNativeAcpxInstallation: setup.verify,
}));
vi.mock("./antigravity-process.js", async original => {
const actual = await original<typeof import("./antigravity-process.js")>();
return { ...actual, retireAntigravityProcess: (...args: Parameters<typeof actual.retireAntigravityProcess>) => setup.uncertainRetirement
? Promise.reject(new Error("controlled retirement uncertainty")) : actual.retireAntigravityProcess(...args) };
});
import { AntigravityModelDiscoveryCleanupError, discoverAntigravityModels } from "./antigravity-models.js";
const roots: string[] = [], children: ChildProcess[] = [];
afterEach(async () => {
for (const child of children.splice(0)) { try { process.kill(-child.pid!, "SIGKILL"); } catch {} }
await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true })));
setup.uncertainRetirement = false;
vi.unstubAllGlobals(); vi.clearAllMocks();
});
async function fixture(mode = "success") {
const home = await realpath(await mkdtemp(join(tmpdir(), "paperclip-antigravity-models-test-"))); roots.push(home);
const peer = join(home, "peer.cjs");
await writeFile(peer, `
const fs=require('node:fs'),path=require('node:path'),readline=require('node:readline');
const home=process.argv[2],mode=process.argv[3],requests=[];
if(mode==='stubborn') {
process.on('SIGTERM',()=>{});
const child=require('node:child_process').spawn(process.execPath,['-e',"process.on('SIGTERM',()=>{});setInterval(()=>{},1000)"],{stdio:'ignore'});
fs.writeFileSync(path.join(home,'descendant.pid'),String(child.pid));
}
const reader=readline.createInterface({input:process.stdin});
const reply=(id,result)=>process.stdout.write(JSON.stringify({jsonrpc:'2.0',id,result})+'\\n');
reader.on('line',line=>{
const request=JSON.parse(line);requests.push(request);fs.writeFileSync(path.join(home,'requests.json'),JSON.stringify(requests));
if(request.method==='initialize') {
reply(request.id,{protocolVersion:1,agentInfo:{name:'antigravity-acp',version:'1.4.3'},agentCapabilities:{_meta:{antigravity:{paperclipAdmission:1,paperclipModels:mode==='old-patch'?0:1}}},authMethods:[{id:'oauth-personal'}]});
} else if(request.method==='_paperclip/models') {
if(mode==='blocked'||mode==='stubborn') return;
if(mode==='error') {process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:request.id,error:{code:-32000,message:'private-token-do-not-expose'}})+'\\n');return;}
if(mode==='bad-utf8') {process.stdout.write(Buffer.from([255,10]));return;}
if(mode==='overflow') {process.stdout.write('x'.repeat(1048577));return;}
const result={schema:request.params.schema,nonce:request.params.nonce,authMethod:'oauth-personal',models:[{id:'gemini-pro-agent',label:'Gemini Pro (High)'},{id:'gemini-3.1-pro-low',label:'Gemini Pro (Low)'}],defaultModelId:'gemini-pro-agent',ignoredPrivateData:'private-token-do-not-expose'};
if(mode==='wrong-nonce') result.nonce='stale-nonce';
if(mode==='wrong-auth') result.authMethod='gemini-api-key';
if(mode==='wrong-schema') result.schema='another-schema';
if(mode==='duplicate') result.models.push(result.models[0]);
if(mode==='unknown-default') result.defaultModelId='unavailable-model';
if(mode==='bad-label') result.models[0].label='bad\\u0000label';
if(mode==='auto-model') result.models[0].id='auto';
if(mode==='many-models') result.models=Array.from({length:201},(_,i)=>({id:'gemini-'+i,label:'Gemini '+i}));
reply(mode==='wrong-id'?91:request.id,result);
} else throw new Error('setup sent forbidden authentication/session/prompt method');
});
reader.on('close',()=>mode==='stubborn'||mode==='blocked'?setInterval(()=>{},1000):process.exit(0));
`);
setup.verify.mockResolvedValue({ openCommand: async () => ({ spawn: setup.spawn, close: setup.close }) });
setup.spawn.mockImplementation((_args, options) => {
const child = spawn(process.execPath, [peer, home, mode], { ...options, detached: true }); children.push(child); return child;
});
return home;
}
describe("Antigravity session-free account model discovery", () => {
it("returns only nonce-bound account models after process retirement with no ambient auth or direct account requests", async () => {
const home = await fixture(), fetch = vi.fn(() => { throw new Error("direct account requests forbidden"); }); vi.stubGlobal("fetch", fetch);
const result = await discoverAntigravityModels(home);
expect(result).toEqual({ models: [{ id: "gemini-pro-agent", label: "Gemini Pro (High)" }, { id: "gemini-3.1-pro-low", label: "Gemini Pro (Low)" }], defaultModelId: "gemini-pro-agent" });
const requests = JSON.parse(await readFile(join(home, "requests.json"), "utf8"));
expect(requests.map((value: { method: string }) => value.method)).toEqual(["initialize", "_paperclip/models"]);
expect(requests[1].params).toEqual({ schema: "paperclip.antigravity.models.v1", nonce: expect.any(String), authMethod: "oauth-personal" });
expect(setup.spawn.mock.calls[0]![1]).toMatchObject({ detached: true, env: { GEMINI_HOME: home, AGY_ACP_FORCE_FILE_STORAGE: "1", BROWSER: "/usr/bin/true" } });
expect(Object.keys(setup.spawn.mock.calls[0]![1].env).some(key => /^(?:GOOGLE_|GEMINI_API|ANTIGRAVITY_|PYTHON|NODE_OPTIONS|HTTP_PROXY)/.test(key))).toBe(false);
expect(children[0]!.exitCode).toBe(0); expect(setup.close).toHaveBeenCalledOnce(); expect(fetch).not.toHaveBeenCalled();
});
it.each(["old-patch", "wrong-nonce", "wrong-auth", "wrong-schema", "wrong-id", "duplicate", "unknown-default", "bad-label", "auto-model", "many-models", "bad-utf8", "overflow", "error"])("rejects %s and retires the process without exposing native text", async mode => {
const home = await fixture(mode);
await expect(discoverAntigravityModels(home)).rejects.toThrow("Could not load models for this Google account");
expect(children[0]!.exitCode).toBe(0); expect(setup.close).toHaveBeenCalledOnce();
if (mode === "old-patch") expect(JSON.parse(await readFile(join(home, "requests.json"), "utf8"))).toHaveLength(1);
});
it("cancellation retires a blocked catalog process and inherited descendant before rejecting", async () => {
const home = await fixture("stubborn"), controller = new AbortController();
const result = discoverAntigravityModels(home, { signal: controller.signal });
void result.catch(() => undefined);
await vi.waitFor(async () => expect(JSON.parse(await readFile(join(home, "requests.json"), "utf8"))).toHaveLength(2));
const descendant = (await readFile(join(home, "descendant.pid"), "utf8")).trim();
controller.abort();
await expect(result).rejects.toThrow("Could not load models for this Google account");
expect(children[0]!.signalCode).toBe("SIGKILL"); expect(setup.close).toHaveBeenCalledOnce();
let state = "";
try { state = execFileSync("/bin/ps", ["-o", "stat=", "-p", descendant], { encoding: "utf8" }).trim(); } catch {}
expect(state === "" || state.startsWith("Z")).toBe(true);
});
it("retains the actual child, lease and caller home through cleanup uncertainty, then releases exactly once after physical exit", async () => {
const home = await fixture("error"); setup.uncertainRetirement = true;
const error = await discoverAntigravityModels(home).catch(error => error);
expect(error).toBeInstanceOf(AntigravityModelDiscoveryCleanupError);
expect(error.operationError.message).toContain("Could not load models");
expect(error.cleanupError.message).toBe("controlled retirement uncertainty");
expect(error.message).not.toContain("private-token");
expect(children[0]!.exitCode).toBeNull(); expect(children[0]!.signalCode).toBeNull();
expect(setup.close).not.toHaveBeenCalled();
const releaseHome = vi.fn(() => rm(home, { recursive: true, force: true }));
const cleaned = error.retired.then(releaseHome);
await new Promise(resolve => setTimeout(resolve, 50));
expect(releaseHome).not.toHaveBeenCalled(); await expect(access(home)).resolves.toBeUndefined();
setup.uncertainRetirement = false;
process.kill(-children[0]!.pid!, "SIGTERM");
await cleaned;
expect(setup.close).toHaveBeenCalledOnce(); expect(releaseHome).toHaveBeenCalledOnce();
expect(children[0]!.signalCode).toBe("SIGTERM");
await expect(access(home)).rejects.toMatchObject({ code: "ENOENT" });
});
it("enforces its real 30-second overall deadline and physically retires a blocked metadata call", async () => {
const home = await fixture("blocked"), start = Date.now();
await expect(discoverAntigravityModels(home)).rejects.toThrow("Could not load models for this Google account");
expect(Date.now() - start).toBeGreaterThanOrEqual(29_500);
expect(Date.now() - start).toBeLessThan(35_000);
expect(children[0]!.signalCode).toBe("SIGTERM"); expect(setup.close).toHaveBeenCalledOnce();
expect(JSON.parse(await readFile(join(home, "requests.json"), "utf8")).map((value: { method: string }) => value.method)).toEqual(["initialize", "_paperclip/models"]);
}, 40_000);
});
describe("Antigravity pending release boundaries", () => {
it("retains release admission for existing providers, including Copilot's separate capability status", () => {
for (const agent of ["claude", "codex", "grok", "cursor", "pi", "copilot"] as const) {
expect(() => assertAcpxProfileReleased(agent)).not.toThrow();
}
expect(acpxCapabilities("copilot")).toMatchObject({ typedEvents: true, dynamicTools: true });
});
it("preserves the caller's exact model separately from release admission", () => {
expect(resolveQualifiedAcpxProfile("antigravity", "caller-exact-model")).toMatchObject({
agent: "antigravity", qualificationStatus: "pending", qualificationModel: "caller-exact-model", reportedModelId: "caller-exact-model",
});
expect(validateAcpxDriverConfig({ agent: "antigravity", model: "caller-exact-model" }))
.toMatchObject({ ok: false, issues: [{ path: "agent", code: "qualification_pending" }] });
expect(() => assertAcpxProfileReleased("antigravity")).toThrow(expect.objectContaining({ code: "ACPX_PROFILE_QUALIFICATION_PENDING" }));
});
it("advertises no accepted runtime or context capabilities", () => {
expect(acpxDriverDescriptor("antigravity")).toMatchObject({ displayName: "Google Antigravity via ACPX",
runtimeContextCapabilities: { instructions: "unsupported", skills: "unsupported", mcp: "unsupported" } });
for (const value of Object.values(acpxCapabilities("antigravity"))) {
if (typeof value === "boolean") expect(value).toBe(false);
}
});
it("rejects direct construction before opening a runtime host", () => {
const openHost = vi.fn();
expect(() => new CodexAcpxDriver({ agent: "antigravity", model: "caller-exact-model", runtimeDirectory: "/unused",
providerPolicy: { readOnly: false } }, { openHost }))
.toThrow(expect.objectContaining({ code: "ACPX_PROFILE_QUALIFICATION_PENDING" }));
expect(openHost).not.toHaveBeenCalled();
});
it("refuses raw installation lookup even after a caller strips the pending marker", async () => {
const profile = { ...resolveQualifiedAcpxProfile("antigravity", "caller-exact-model"), qualificationStatus: undefined };
const resolvePackage = vi.fn();
await expect(verifyQualifiedAcpxInstallation(profile, resolvePackage))
.rejects.toMatchObject({ code: "ACPX_PROFILE_QUALIFICATION_PENDING" });
await expect(verifyAcpxProfileInstallation(profile))
.rejects.toMatchObject({ code: "ACPX_PROFILE_QUALIFICATION_PENDING" });
expect(resolvePackage).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,147 @@
import type { ChildProcessWithoutNullStreams } from "node:child_process";
import { randomUUID } from "node:crypto";
import { antigravityNativeDistributionSpec } from "./antigravity-installation.js";
import { antigravityPrivateEnvironment, checkAntigravityPrivateHome, retireAntigravityProcess } from "./antigravity-process.js";
import { verifyNativeAcpxInstallation, type VerifiedAcpxCommandLease } from "./installation-integrity.js";
const SCHEMA = "paperclip.antigravity.models.v1";
const ERROR = "Could not load models for this Google account. Retry or sign in again.";
const MAX_WIRE_BYTES = 1024 * 1024;
const record = (value: unknown): Record<string, unknown> => value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
export type AntigravityModelCatalog = { models: Array<{ id: string; label: string }>; defaultModelId: string };
/** The caller must keep its credential home until retired resolves. The owned
* cleanup operation retains the child and command lease throughout uncertainty.
*/
export class AntigravityModelDiscoveryCleanupError extends Error {
readonly code = "ANTIGRAVITY_MODELS_CLEANUP_PENDING";
constructor(
readonly retired: Promise<void>,
readonly operationError: Error | undefined,
readonly cleanupError: Error,
) {
super("Antigravity model discovery failed while its process was stopping.");
this.name = "AntigravityModelDiscoveryCleanupError";
}
}
function retainCatalogCleanup(child: ChildProcessWithoutNullStreams | undefined, closed: Promise<void> | undefined, lease: VerifiedAcpxCommandLease): Promise<void> {
let observedClose = child === undefined;
void closed?.then(() => { observedClose = true; });
return (async () => {
// This retry belongs only to an unresolved, active process/lease cleanup.
// Close wakes it immediately; successful cleanup leaves no timer or work.
while (true) {
try {
if (child && closed) await retireAntigravityProcess(child, closed);
await lease.close();
return;
} catch {
let timer: ReturnType<typeof setTimeout> | undefined;
const retry = new Promise<void>(resolve => { timer = setTimeout(resolve, 1_000); });
try { await (observedClose || !closed ? retry : Promise.race([closed, retry])); }
finally { if (timer) clearTimeout(timer); }
}
}
})();
}
function catalog(value: unknown, nonce: string): AntigravityModelCatalog {
const response = record(value);
if (response.schema !== SCHEMA || response.nonce !== nonce || response.authMethod !== "oauth-personal"
|| !Array.isArray(response.models) || response.models.length < 1 || response.models.length > 200) throw new Error(ERROR);
const ids = new Set<string>(), models: AntigravityModelCatalog["models"] = [];
for (const value of response.models) {
const model = record(value);
if (typeof model.id !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,511}$/.test(model.id)
|| ["auto", "default"].includes(model.id.toLowerCase()) || ids.has(model.id)
|| typeof model.label !== "string" || !model.label.trim() || model.label.length > 200 || /[\x00-\x1f\x7f]/.test(model.label)) throw new Error(ERROR);
ids.add(model.id); models.push({ id: model.id, label: model.label });
}
if (typeof response.defaultModelId !== "string" || !ids.has(response.defaultModelId)) throw new Error(ERROR);
return { models, defaultModelId: response.defaultModelId };
}
/** Read-only account catalog discovery through the pinned native implementation.
* The caller owns the private projected credential home and any refreshed-secret
* writeback. Execution retires before ordinary results/errors. A cleanup error
* transfers the home-retention obligation through its retired completion.
*/
export async function discoverAntigravityModels(home: string, options: { signal?: AbortSignal } = {}): Promise<AntigravityModelCatalog> {
const deadline = new AbortController(), timer = setTimeout(() => deadline.abort(), 30_000);
const signal = options.signal ? AbortSignal.any([options.signal, deadline.signal]) : deadline.signal;
let lease: VerifiedAcpxCommandLease | undefined, child: ChildProcessWithoutNullStreams | undefined, closed: Promise<void> | undefined;
let result: AntigravityModelCatalog | undefined, failure: Error | undefined;
let cleanupError: AntigravityModelDiscoveryCleanupError | undefined;
try {
signal.throwIfAborted();
await checkAntigravityPrivateHome(home);
signal.throwIfAborted();
const installation = await verifyNativeAcpxInstallation(antigravityNativeDistributionSpec());
signal.throwIfAborted();
lease = await installation.openCommand({ signal });
const environment = await antigravityPrivateEnvironment(home);
signal.throwIfAborted();
child = lease.spawn([], { cwd: home, stdio: "pipe", detached: process.platform !== "win32", env: environment }) as ChildProcessWithoutNullStreams;
const peer = child;
closed = new Promise(resolve => peer.once("close", () => resolve()));
const nonce = randomUUID();
result = await new Promise<AntigravityModelCatalog>((resolve, reject) => {
let phase = 1, stdout = "", outputBytes = 0, errorBytes = 0;
const decoder = new TextDecoder("utf-8", { fatal: true });
const fail = () => { failure ??= new Error(ERROR); reject(failure); };
const abort = () => fail();
signal.addEventListener("abort", abort, { once: true });
const send = (id: 1 | 2) => peer.stdin.write(`${JSON.stringify(id === 1
? { jsonrpc: "2.0", id, method: "initialize", params: { protocolVersion: 1, clientInfo: { name: "paperclip-runner", version: "1" }, clientCapabilities: {} } }
: { jsonrpc: "2.0", id, method: "_paperclip/models", params: { schema: SCHEMA, nonce, authMethod: "oauth-personal" } })}\n`);
peer.stdout.on("data", (chunk: Buffer) => {
outputBytes += chunk.length;
if (outputBytes > MAX_WIRE_BYTES) { fail(); return; }
try { stdout += decoder.decode(chunk, { stream: true }); } catch { fail(); return; }
while (stdout.includes("\n") && !failure) {
const end = stdout.indexOf("\n"), line = stdout.slice(0, end); stdout = stdout.slice(end + 1);
if (!line.trim()) continue;
let message: Record<string, unknown>;
try { message = record(JSON.parse(line)); } catch { fail(); return; }
if (phase > 2 || message.jsonrpc !== "2.0" || message.id !== phase || "method" in message || "error" in message || !Object.hasOwn(message, "result")) { fail(); return; }
if (phase === 1) {
const initialized = record(message.result), info = record(initialized.agentInfo), capabilities = record(record(record(initialized.agentCapabilities)._meta).antigravity);
if (initialized.protocolVersion !== 1 || info.name !== "antigravity-acp" || info.version !== "1.4.3"
|| capabilities.paperclipAdmission !== 1 || capabilities.paperclipModels !== 1
|| !Array.isArray(initialized.authMethods) || !initialized.authMethods.some(value => record(value).id === "oauth-personal")) { fail(); return; }
phase = 2; send(2);
} else {
try { const models = catalog(message.result, nonce); phase = 3; resolve(models); } catch { fail(); }
}
}
});
peer.stdout.once("end", () => {
try { stdout += decoder.decode(); if (stdout.trim()) fail(); } catch { fail(); }
});
// Native error text may contain private account data. Count and discard.
peer.stderr.on("data", (chunk: Buffer) => { errorBytes += chunk.length; if (errorBytes > MAX_WIRE_BYTES) fail(); });
peer.once("error", fail);
peer.once("close", () => { signal.removeEventListener("abort", abort); if (phase !== 3) fail(); });
peer.stdin.on("error", () => { if (phase !== 3) fail(); });
if (signal.aborted) fail(); else send(1);
});
} catch { failure ??= new Error(ERROR); }
finally {
try {
if (child && closed) await retireAntigravityProcess(child, closed);
await lease?.close();
} catch (error) {
if (lease) cleanupError = new AntigravityModelDiscoveryCleanupError(
retainCatalogCleanup(child, closed, lease), failure,
error instanceof Error ? error : new Error("Antigravity process retirement or command lease release could not be confirmed"),
);
else failure ??= new Error(ERROR);
}
clearTimeout(timer);
}
if (cleanupError) throw cleanupError;
if (signal.aborted || failure || result === undefined) throw failure ?? new Error(ERROR);
return result;
}
@@ -0,0 +1,42 @@
import { spawn } from "node:child_process";
import { mkdir, mkdtemp, rm, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { expect, it } from "vitest";
import { acquireAcpxProviderLifetimeLease } from "./codex-credentials.js";
import { resolveAcpxRuntimeRoot } from "./recovery-identity.js";
import { confirmAntigravityProbeNativeRetirement } from "./antigravity-probe-retirement.js";
it.skipIf(process.platform === "win32")("retains native cleanup until the real descendant releases its inherited kernel quorum", async () => {
const root = await mkdtemp(join(tmpdir(), "antigravity-probe-quorum-"));
const home = join(await resolveAcpxRuntimeRoot(root, "probe-session"), "antigravity-home", "antigravity-acp");
await mkdir(home, { recursive: true, mode: 0o700 });
const lease = await acquireAcpxProviderLifetimeLease({ agentHomeDirectory: home });
const child = spawn(process.execPath, ["-e", "process.stdout.write('ready\\n'); setInterval(() => {}, 1000)"], {
detached: true, env: { PATH: "/usr/bin:/bin" }, stdio: ["ignore", "pipe", "ignore", ...lease.lifetimeFenceFds],
});
const closed = new Promise<void>(resolve => child.once("exit", () => resolve()));
let retirement: Promise<void> | undefined;
try {
await new Promise<void>((resolve, reject) => { child.stdout!.once("data", () => resolve()); child.once("error", reject); });
await lease.close();
let retired = false;
retirement = confirmAntigravityProbeNativeRetirement(root, "probe-session").then(() => { retired = true; });
await new Promise(resolve => setTimeout(resolve, 25));
expect(retired).toBe(false);
expect(child.exitCode).toBeNull();
expect((await stat(home)).isDirectory()).toBe(true);
child.kill("SIGKILL");
await closed;
await retirement;
expect(retired).toBe(true);
const successor = await acquireAcpxProviderLifetimeLease({ agentHomeDirectory: home });
await successor.close();
} finally {
child.kill("SIGKILL");
await closed;
await lease.close();
await retirement;
await rm(root, { recursive: true, force: true });
}
}, 10_000);
@@ -0,0 +1,32 @@
import { lstat } from "node:fs/promises";
import { join } from "node:path";
import { acquireAcpxProviderLifetimeLease, type AcpxProviderLifetimeLease } from "./codex-credentials.js";
import { resolveAcpxRuntimeRoot } from "./recovery-identity.js";
/** After all original startup work and the sidecar have retired, acquiring the
* same kernel quorum proves the guarded native lifetime no longer owns this
* credential home. No token bytes are read, changed, or used for authentication.
*/
export async function confirmAntigravityProbeNativeRetirement(runtimeDirectory: string, normalizedSessionId: string): Promise<void> {
let directory: string;
try {
directory = join(await resolveAcpxRuntimeRoot(runtimeDirectory, normalizedSessionId), "antigravity-home", "antigravity-acp");
await lstat(directory);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
throw error;
}
let lease: AcpxProviderLifetimeLease | undefined;
// This owner retries only a concrete outstanding native lifetime or release.
// It leaves no recurring work once the original native fence is free.
while (true) {
try {
lease ??= await acquireAcpxProviderLifetimeLease({ agentHomeDirectory: directory });
await lease.close();
return;
} catch (error) {
if (!lease && (!(error instanceof Error) || error.message !== "Managed Codex credential home already has an active lease")) throw error;
await new Promise(resolve => setTimeout(resolve, 1_000));
}
}
}
@@ -0,0 +1,73 @@
import { execFile, type ChildProcessWithoutNullStreams } from "node:child_process";
import { lstat, mkdir, realpath } from "node:fs/promises";
import { isAbsolute, join, resolve } from "node:path";
export async function checkAntigravityPrivateHome(home: string): Promise<void> {
if (!isAbsolute(home) || home.includes("\0") || resolve(home) !== home || await realpath(home) !== home) throw new Error("Invalid Antigravity private home");
const stat = await lstat(home);
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || (stat.mode & 0o777) !== 0o700) throw new Error("Invalid Antigravity private home");
}
/** Owned setup environment; never inherits credentials, endpoints, hooks,
* Python startup options, proxy routing, or user configuration from the host. */
export async function antigravityPrivateEnvironment(home: string): Promise<NodeJS.ProcessEnv> {
await checkAntigravityPrivateHome(home);
for (const name of ["host-home", "config", "data", "cache", "tmp"]) {
const directory = join(home, name);
try { await mkdir(directory, { mode: 0o700 }); }
catch (error) { if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error; }
const stat = await lstat(directory);
if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== process.getuid?.() || (stat.mode & 0o777) !== 0o700) throw new Error("Invalid Antigravity private home");
}
return {
PATH: "/usr/bin:/bin", HOME: join(home, "host-home"), GEMINI_HOME: home,
XDG_CONFIG_HOME: join(home, "config"), XDG_DATA_HOME: join(home, "data"), XDG_CACHE_HOME: join(home, "cache"), TMPDIR: join(home, "tmp"),
// Success prevents Python webbrowser from trying its next OS controller.
BROWSER: "/usr/bin/true", AGY_ACP_FORCE_FILE_STORAGE: "1", NO_COLOR: "1", LANG: "en_US.UTF-8",
};
}
export async function retireAntigravityProcess(child: ChildProcessWithoutNullStreams, closed: Promise<void>): Promise<void> {
let retired = false;
void closed.then(() => { retired = true; });
const signal = (value: NodeJS.Signals) => {
try { if (child.pid !== undefined && process.platform !== "win32") process.kill(-child.pid, value); else if (!retired) child.kill(value); }
catch (error) { if (!["ESRCH", "EPERM"].includes((error as NodeJS.ErrnoException).code ?? "")) throw error; }
};
const alive = async () => {
let groupAlive = !retired;
if (process.platform !== "win32" && child.pid !== undefined) {
try { process.kill(-child.pid, 0); groupAlive = true; }
catch (error) {
const code = (error as NodeJS.ErrnoException).code;
if (code === "ESRCH") groupAlive = false;
else if (code === "EPERM" && process.platform === "darwin") {
// Darwin reports EPERM for some groups containing only orphaned
// zombies. Read numeric state only; never process arguments/env.
const output = await new Promise<string>((resolve, reject) => execFile("/bin/ps", ["-axo", "pid=,pgid=,stat="], {
encoding: "utf8", timeout: 1_000, maxBuffer: 1024 * 1024, env: { PATH: "/usr/bin:/bin", LANG: "C" },
}, (error, stdout) => error ? reject(error) : resolve(stdout)));
const rows = output.trim().split("\n").filter(Boolean).map(line => line.trim().split(/\s+/));
if (rows.some(row => row.length !== 3 || !/^\d+$/.test(row[0]!) || !/^\d+$/.test(row[1]!) || !/^[A-Za-z+<>NLsWX-]+$/.test(row[2]!))) throw new Error("Antigravity process retirement could not be confirmed");
groupAlive = rows.some(row => Number(row[1]) === child.pid && !row[2]!.startsWith("Z"));
} else throw error;
}
}
return groupAlive;
};
child.stdin.end();
for (const [value, wait] of [[null, 500], ["SIGTERM", 500], ["SIGKILL", 1_000]] as const) {
if (value) signal(value);
let timer: ReturnType<typeof setTimeout> | undefined;
const deadline = Date.now() + wait;
await Promise.race([closed, new Promise<void>(resolve => { timer = setTimeout(resolve, wait); })]);
if (timer) clearTimeout(timer);
if (!await alive()) return;
// A descendant can keep running after the launcher exits. Observe the
// remainder of this stage's deadline before escalating the owned group.
const remaining = deadline - Date.now();
if (remaining > 0) await new Promise(resolve => setTimeout(resolve, remaining));
if (!await alive()) return;
}
throw new Error("Antigravity process retirement could not be confirmed");
}
@@ -0,0 +1,66 @@
/** Antigravity 1.4.3 encodes fixed-choice questions as permission callbacks. */
export function createAntigravityQuestionBridge(ownsSession: (sessionId: unknown) => boolean) {
const pending = new Map<string | number, { sessionId: string; optionIds: Set<string> }>();
return {
clear() { pending.clear(); },
guard(direction: "inbound" | "outbound", value: unknown): void {
const message = record(value);
const id = message.id;
if (direction === "inbound" && message.method === "session/request_permission") {
const params = record(message.params);
const tool = record(params.toolCall);
if (typeof tool.toolCallId !== "string" || !tool.toolCallId.startsWith("interaction_")) return;
if ((typeof id !== "string" && typeof id !== "number") || pending.has(id) || pending.size >= 16
|| !ownsSession(params.sessionId) || typeof params.sessionId !== "string"
|| typeof tool.title !== "string" || !tool.title.trim()
|| !Array.isArray(params.options) || params.options.length < 1 || params.options.length > 128) {
throw new Error("Antigravity question has invalid identity or incomplete choices");
}
const optionIds = new Set<string>();
const choices = params.options.map(value => {
const option = record(value);
if (typeof option.optionId !== "string" || !option.optionId || option.optionId.length > 1024
|| typeof option.name !== "string" || !option.name.trim() || optionIds.has(option.optionId)) {
throw new Error("Antigravity question has invalid or duplicate choice identities");
}
optionIds.add(option.optionId);
// kind is deliberately ignored: deny/block can be an answer choice.
return { const: option.optionId, title: option.name };
});
const form = {
sessionId: params.sessionId, mode: "form", message: tool.title,
requestedSchema: {
type: "object", title: tool.title, required: ["answer"], additionalProperties: false,
properties: { answer: { type: "string", title: "Answer", oneOf: choices } },
},
};
if (Buffer.byteLength(JSON.stringify(form)) > 196 * 1024) throw new Error("Antigravity question exceeds its complete-content limit");
pending.set(id, { sessionId: params.sessionId, optionIds });
// The installed ACPX form path owns durable input and delivery. Convert
// before its permission policy can interpret a choice as an approval.
message.method = "elicitation/create";
message.params = form;
} else if (direction === "outbound" && message.method === undefined
&& (typeof id === "string" || typeof id === "number") && pending.has(id)) {
const question = pending.get(id)!;
if (!ownsSession(question.sessionId)) throw new Error("Antigravity answer belongs to a stale session");
if (message.error !== undefined) { pending.delete(id); return; }
const result = record(message.result);
if (result.action === "accept") {
const content = record(result.content);
if (Object.keys(content).length !== 1 || typeof content.answer !== "string" || !question.optionIds.has(content.answer)) {
throw new Error("Antigravity answer must select exactly one offered choice");
}
message.result = { outcome: { outcome: "selected", optionId: content.answer } };
} else if (result.action === "decline" || result.action === "cancel") {
message.result = { outcome: { outcome: "cancelled" } };
} else throw new Error("Antigravity question response is invalid");
pending.delete(id);
}
},
};
}
function record(value: unknown): Record<string, unknown> {
return value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
@@ -0,0 +1,97 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { ANTIGRAVITY_DISTRIBUTION_PINS } from "./generated-profiles.js";
import { assertAcpxProfileReleased, resolveQualifiedAcpxProfile } from "./qualified-profiles.js";
import { assertAcpxExecutionAdmission, bindAcpxCandidateAdmission, parseAcpxCandidateAdmission, parseAcpxCandidateStartupAdmission, type AcpxCandidateAdmission } from "./candidate-admission.js";
import { verifyAcpxProfileInstallation } from "./profile-installation.js";
import { AcpxRuntimeHost } from "./runtime-host.js";
import * as integrity from "./installation-integrity.js";
vi.mock("./installation-integrity.js", async importOriginal => ({
...await importOriginal<typeof import("./installation-integrity.js")>(),
verifyNativeAcpxInstallation: vi.fn(),
}));
afterEach(() => vi.restoreAllMocks());
const now = 1_800_000_000_000;
const expected = { companyId: "company-candidate", agent: "antigravity", model: "gemini-3.8-flash-low" };
const candidate = (): AcpxCandidateAdmission => ({ schema: "paperclip.acpx-candidate-admission.v1", ...expected, agent: "antigravity",
profileDigest: resolveQualifiedAcpxProfile("antigravity", expected.model).commandDigest,
nativeClosureDigest: `sha256:${ANTIGRAVITY_DISTRIBUTION_PINS["darwin-arm64"].closureSha256}`,
platform: "darwin", architecture: "arm64", expiresAt: now + 60_000 });
describe("operator candidate execution admission", () => {
it("binds an exact company/model/profile/native closure without changing release admission", () => {
const source = candidate();
const value = parseAcpxCandidateAdmission(source, expected, now);
expect(value).toEqual(source);
expect(value).not.toBe(source);
expect(() => assertAcpxProfileReleased("antigravity")).toThrow("qualification is pending");
expect(() => assertAcpxExecutionAdmission(resolveQualifiedAcpxProfile("antigravity", expected.model))).toThrow("qualification is pending");
});
it.each([
{ companyId: "another-company" }, { agent: "codex" }, { model: "gemini-3.8-flash-high" },
{ profileDigest: `sha256:${"a".repeat(64)}` }, { nativeClosureDigest: `sha256:${"b".repeat(64)}` },
{ platform: "linux" }, { architecture: "x64" }, { expiresAt: now }, { expiresAt: now + 86_400_001 },
{ expiresAt: "1800000060000" }, { unknown: true }, { runId: "injected-run" }, { model: "auto" },
])("rejects mismatched or broadened authority %j", change => {
expect(() => parseAcpxCandidateAdmission({ ...candidate(), ...change }, expected, now)).toThrow(expect.objectContaining({ code: "ACPX_CANDIDATE_ADMISSION_INVALID" }));
});
it("binds actual startup run/session and refuses replay against another identity", () => {
const current = { ...candidate(), expiresAt: Date.now() + 60_000 };
const startup = bindAcpxCandidateAdmission(current, { runId: "actual-run", normalizedSessionId: "actual-session" });
expect(parseAcpxCandidateStartupAdmission(startup, { ...expected, runId: "actual-run", normalizedSessionId: "actual-session" })).toEqual(startup);
for (const identity of [{ runId: "other-run" }, { normalizedSessionId: "other-session" }, { model: "other-model" }]) {
expect(() => parseAcpxCandidateStartupAdmission(startup, { ...expected, ...identity })).toThrow();
}
expect(() => parseAcpxCandidateStartupAdmission({ ...startup, trusted: true }, expected)).toThrow();
expect(() => parseAcpxCandidateStartupAdmission(startup, expected, startup.expiresAt)).toThrow();
});
it("permits only the pinned native candidate at the execution exception", () => {
const startup = bindAcpxCandidateAdmission({ ...candidate(), expiresAt: Date.now() + 60_000 }, { runId: "actual-run", normalizedSessionId: "actual-session" });
const profile = resolveQualifiedAcpxProfile("antigravity", expected.model);
if (process.platform === "darwin" && process.arch === "arm64") expect(() => assertAcpxExecutionAdmission(profile, startup)).not.toThrow();
else expect(() => assertAcpxExecutionAdmission(profile, startup)).toThrow();
expect(() => assertAcpxExecutionAdmission(resolveQualifiedAcpxProfile("codex", expected.model), startup)).toThrow();
expect(() => assertAcpxExecutionAdmission({ ...profile, commandDigest: `sha256:${"c".repeat(64)}` }, startup)).toThrow();
for (const agent of ["codex", "claude", "grok", "cursor", "pi", "copilot"] as const) {
expect(() => assertAcpxExecutionAdmission(resolveQualifiedAcpxProfile(agent, "caller-selected-model"))).not.toThrow();
}
});
});
describe("candidate installation remains cryptographically verified", () => {
const profile = resolveQualifiedAcpxProfile("antigravity", "gemini-3.8-flash-low");
const admission = () => bindAcpxCandidateAdmission({ schema: "paperclip.acpx-candidate-admission.v1", companyId: "company-candidate", agent: "antigravity", model: profile.qualificationModel,
profileDigest: profile.commandDigest, nativeClosureDigest: `sha256:${ANTIGRAVITY_DISTRIBUTION_PINS["darwin-arm64"].closureSha256}`,
platform: "darwin", architecture: "arm64", expiresAt: Date.now() + 60_000 }, { runId: "run-candidate", normalizedSessionId: "session-candidate" });
it.skipIf(process.platform !== "darwin" || process.arch !== "arm64")("delegates the admitted exception to the exact complete native closure verifier", async () => {
const native = { commandDigest: "native-closure-verifier-result" } as integrity.VerifiedAcpxInstallation;
vi.mocked(integrity.verifyNativeAcpxInstallation).mockResolvedValue(native);
const result = await verifyAcpxProfileInstallation(profile, admission());
expect(result.commandDigest).toBe(profile.commandDigest);
expect(integrity.verifyNativeAcpxInstallation).toHaveBeenCalledExactlyOnceWith(expect.objectContaining({
expectedClosureSha256: ANTIGRAVITY_DISTRIBUTION_PINS["darwin-arm64"].closureSha256,
executable: "agy_acp_server.par", fixedArguments: [],
}));
});
it("denies absent, altered, or other-provider authority before any native files are opened", async () => {
vi.mocked(integrity.verifyNativeAcpxInstallation).mockClear();
await expect(verifyAcpxProfileInstallation(profile)).rejects.toMatchObject({ code: "ACPX_PROFILE_QUALIFICATION_PENDING" });
await expect(verifyAcpxProfileInstallation(profile, { ...admission(), model: "gemini-3.8-flash-high" })).rejects.toMatchObject({ code: "ACPX_CANDIDATE_ADMISSION_INVALID" });
await expect(verifyAcpxProfileInstallation(resolveQualifiedAcpxProfile("codex", profile.qualificationModel), admission())).rejects.toMatchObject({ code: "ACPX_CANDIDATE_ADMISSION_INVALID" });
expect(integrity.verifyNativeAcpxInstallation).not.toHaveBeenCalled();
});
it("rejects a candidate session mismatch before installation, credentials, or runtime acquisition", async () => {
const verifyInstallation = vi.fn(), openRuntime = vi.fn();
await expect(AcpxRuntimeHost.open({ candidateAdmission: admission(), runtimeDirectory: "/unused", normalizedSessionId: "another-session", workingDirectory: "/unused", agent: "antigravity", model: profile.qualificationModel,
permissionMode: "deny-all", providerPolicy: { readOnly: true } }, { verifyInstallation, openRuntime, retainAdmissionCleanup: () => {}, reportRetainedCleanupFailure: () => {} })).rejects.toMatchObject({ code: "ACPX_CANDIDATE_ADMISSION_INVALID" });
expect(verifyInstallation).not.toHaveBeenCalled();
expect(openRuntime).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,80 @@
import { ANTIGRAVITY_DISTRIBUTION_PINS } from "./generated-profiles.js";
import { QUALIFIED_ACPX_PROFILES, assertAcpxProfileReleased, type QualifiedAcpxProfile } from "./qualified-profiles.js";
/** Operator authority travels only through trusted Runner startup, never agent configuration. */
export interface AcpxCandidateAdmission {
schema: "paperclip.acpx-candidate-admission.v1";
companyId: string;
agent: "antigravity";
model: string;
profileDigest: string;
nativeClosureDigest: string;
platform: "darwin";
architecture: "arm64";
expiresAt: number;
}
/** Runner binds operator authority to its actual launch identity. */
export interface AcpxCandidateStartupAdmission extends AcpxCandidateAdmission {
runId: string;
normalizedSessionId: string;
}
const BASE_FIELDS = ["schema", "companyId", "agent", "model", "profileDigest", "nativeClosureDigest", "platform", "architecture", "expiresAt"];
const stableId = (value: unknown): value is string => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,239}$/.test(value);
const invalid = (): never => { throw Object.assign(new Error("ACPX candidate admission is missing, expired, or does not match the exact authorized execution"), { code: "ACPX_CANDIDATE_ADMISSION_INVALID", retryable: false }); };
export function parseAcpxCandidateAdmission(
value: unknown,
expected: { companyId: string; agent: string; model: string },
now = Date.now(),
): AcpxCandidateAdmission {
if (value === null || typeof value !== "object" || Array.isArray(value)) return invalid();
const entry = value as Record<string, unknown>;
if (!Number.isSafeInteger(now) || now < 0 || Object.keys(entry).length !== BASE_FIELDS.length || Object.keys(entry).some(key => !BASE_FIELDS.includes(key))
|| entry.schema !== "paperclip.acpx-candidate-admission.v1"
|| !stableId(entry.companyId) || entry.companyId !== expected.companyId
|| entry.agent !== "antigravity" || entry.agent !== expected.agent
|| typeof entry.model !== "string" || entry.model !== expected.model || !entry.model.trim()
|| entry.model !== entry.model.trim() || Buffer.byteLength(entry.model) > 512 || /[\u0000-\u001f\u007f]/.test(entry.model)
|| ["auto", "default"].includes(entry.model.toLowerCase())
|| entry.profileDigest !== QUALIFIED_ACPX_PROFILES.antigravity.commandDigest
|| entry.nativeClosureDigest !== `sha256:${ANTIGRAVITY_DISTRIBUTION_PINS["darwin-arm64"].closureSha256}`
|| entry.platform !== "darwin" || entry.architecture !== "arm64"
|| !Number.isSafeInteger(entry.expiresAt) || Number(entry.expiresAt) <= now
|| Number(entry.expiresAt) > now + 24 * 60 * 60 * 1000) return invalid();
return { ...entry } as unknown as AcpxCandidateAdmission;
}
export function bindAcpxCandidateAdmission(
admission: AcpxCandidateAdmission,
identity: { runId: string; normalizedSessionId: string },
): AcpxCandidateStartupAdmission {
if (!stableId(identity.runId) || identity.runId.length > 160 || !stableId(identity.normalizedSessionId) || identity.normalizedSessionId.length > 160) return invalid();
return { ...parseAcpxCandidateAdmission(admission, admission), runId: identity.runId, normalizedSessionId: identity.normalizedSessionId };
}
/** The company field here is only a receipt of upstream authorization. This
* parser is called on the trusted runner/sidecar startup channel; agent or
* provider configuration must never be promoted into that channel. */
export function parseAcpxCandidateStartupAdmission(
value: unknown,
expected: { agent: string; model: string; runId?: string; normalizedSessionId?: string },
now = Date.now(),
): AcpxCandidateStartupAdmission {
if (value === null || typeof value !== "object" || Array.isArray(value)) return invalid();
const { runId, normalizedSessionId, ...base } = value as Record<string, unknown>;
if (!stableId(runId) || runId.length > 160 || !stableId(normalizedSessionId) || normalizedSessionId.length > 160
|| expected.runId !== undefined && expected.runId !== runId
|| expected.normalizedSessionId !== undefined && expected.normalizedSessionId !== normalizedSessionId) return invalid();
const admission = parseAcpxCandidateAdmission(base, { ...expected, companyId: String(base.companyId) }, now);
return { ...admission, runId, normalizedSessionId };
}
/** This exception is deliberately absent from direct driver/config admission. */
export function assertAcpxExecutionAdmission(profile: QualifiedAcpxProfile, candidate?: AcpxCandidateStartupAdmission): void {
if (candidate === undefined) return assertAcpxProfileReleased(profile.agent);
if (process.platform !== "darwin" || process.arch !== "arm64") return invalid();
parseAcpxCandidateStartupAdmission(candidate, { agent: profile.agent, model: profile.qualificationModel });
if (profile.commandDigest !== candidate.profileDigest) return invalid();
}
@@ -3,11 +3,11 @@ import type { QualifiedAcpxAgent } from "./qualified-profiles.js";
export interface AcpxCapabilityProfile {
readonly displayName: string;
readonly qualification: "qualified" | "pending";
readonly models: "explicit-provider-verified";
readonly permissions: "runner-policy" | "interactive";
readonly models: "explicit-provider-verified" | "unverified";
readonly permissions: "runner-policy" | "interactive" | "unverified";
readonly questions: "form" | "cursor-extension" | "semantic-only" | "not-exposed";
readonly plans: "native" | "cursor-decision" | "semantic-only";
readonly tools: "authenticated-mcp" | "owned-extension";
readonly tools: "authenticated-mcp" | "owned-extension" | "unverified";
readonly recovery: "session-load" | "unverified";
readonly toolRefreshOnResume?: boolean;
readonly usage: "reported" | "unverified";
@@ -20,6 +20,12 @@ export interface AcpxCapabilityProfile {
/** These are runner integration claims, not a proxy for everything a harness can do. */
export const ACPX_CAPABILITY_PROFILES: Readonly<Record<QualifiedAcpxAgent, AcpxCapabilityProfile>> = {
antigravity: {
displayName: "Google Antigravity", qualification: "pending", models: "unverified",
permissions: "unverified", questions: "not-exposed", plans: "semantic-only", tools: "unverified",
recovery: "unverified", usage: "unverified", steering: "unsupported", followUp: "controller-queue",
artifacts: "policy_disabled", extensionRequests: [], extensionNotifications: [],
},
claude: {
toolRefreshOnResume: true,
displayName: "Claude", qualification: "qualified", models: "explicit-provider-verified",
@@ -74,6 +74,7 @@ import {
validateAcpxDriverConfig,
} from "./driver-profile.js";
import {
assertAcpxProfileReleased,
resolveQualifiedAcpxProfile,
type QualifiedAcpxAgent,
} from "./qualified-profiles.js";
@@ -288,6 +289,7 @@ export class CodexAcpxDriver implements HarnessDriver {
options: CodexAcpxDriverOptions,
dependencies: CodexAcpxDriverDependencies = {},
) {
assertAcpxProfileReleased(options.agent ?? "codex");
if (["pi", "cursor", "copilot"].includes(options.agent ?? "codex") && typeof options.providerPolicy?.readOnly !== "boolean") {
throw new Error("ACP candidate requires an explicit provider read-only policy");
}
@@ -35,7 +35,9 @@ import {
import type { AcpxModelStatus } from "./model-verification.js";
import { AcpxApprovalRequiredError, decideAcpxPermission } from "./permission-policy.js";
import { ACPX_CAPABILITY_PROFILES } from "./capability-profiles.js";
import { admitCursorInstructions, createCursorInstructionAdmission } from "./cursor-instructions.js";
import { createCursorInstructionAdmission } from "./cursor-instructions.js";
import { createAntigravityAdmission } from "./antigravity-admission.js";
import { admitAcpxSessionContext } from "./session-context-admission.js";
import { createAcpxModeBinding } from "./provider-mode.js";
import { assertCopilotPromptPolicy, createCopilotProtocolGuard } from "./copilot-policy.js";
@@ -327,6 +329,10 @@ export async function openQualifiedAcpxRuntime(
const cursorInstructions = options.profile.agent === "cursor"
? createCursorInstructionAdmission(options.systemInstructions)
: null;
const antigravityAdmission = options.profile.agent === "antigravity"
? createAntigravityAdmission(options.systemInstructions, options.profile.reportedModelId)
: null;
const contextAdmission = cursorInstructions ?? antigravityAdmission;
const runtimeOptions: GoalAwareAcpRuntimeOptions = {
cwd: options.cwd,
...(cursorInstructions || modeBinding ? { protocolGuardFactory: () => {
@@ -336,6 +342,7 @@ export async function openQualifiedAcpxRuntime(
} } : {}),
...(piThinking ? { protocolGuardFactory: () => piThinking.createGuard() } : {}),
...(effortAdmission ? { protocolGuardFactory: () => effortAdmission.createGuard() } : {}),
...(antigravityAdmission ? { protocolGuardFactory: () => antigravityAdmission.createGuard() } : {}),
sessionStore,
agentRegistry: createRegistry({
// Preserve Claude's ACP capability identity. This is metadata only: the
@@ -514,18 +521,18 @@ export async function openQualifiedAcpxRuntime(
},
}),
);
const handshake = (cursorInstructions || modeBinding
const handshake = (contextAdmission || modeBinding
? ensuredSession.then(async (ensuredHandle) => {
handle = ensuredHandle;
options.signal?.throwIfAborted();
if (cursorInstructions) await admitCursorInstructions(cursorInstructions, {
if (contextAdmission) await admitAcpxSessionContext(contextAdmission, {
providerSpawned: commandLaunches.count > 0,
load: async () => {
// A fresh ACPX manager may reuse a saved record without spawning.
// Its supported exact-model control forces session/load and the
// current instruction acknowledgement before host admission.
if (!runtime.setConfigOption) {
throw new Error("Cursor cold admission requires exact model configuration");
throw new Error("ACP context admission requires exact model configuration");
}
await runtime.setConfigOption({
handle: ensuredHandle, key: "model", value: options.profile.reportedModelId,
@@ -542,7 +549,7 @@ export async function openQualifiedAcpxRuntime(
if (modeBinding && !modeBinding.isReady()) {
if (!runtime.setConfigOption) throw new Error("Provider mode admission requires native mode configuration");
await runtime.setConfigOption({ handle: ensuredHandle, key: modeBinding.configKey, value: modeBinding.selectedMode });
cursorInstructions?.assertReady();
contextAdmission?.assertReady();
modeBinding.assertReady();
await children.verifyLifetimeOwnership();
options.signal?.throwIfAborted();
@@ -597,7 +604,7 @@ export async function openQualifiedAcpxRuntime(
handle = options.signal
? await raceRuntimeHandshakeWithAbort(boundedHandshake, options.signal)
: await boundedHandshake;
cursorInstructions?.assertReady();
contextAdmission?.assertReady();
modeBinding?.assertReady();
piThinking?.assertReady();
effortAdmission?.assertReady();
@@ -87,9 +87,9 @@ vi.mock("./installation-integrity.js", () => ({ verifyNativeAcpxInstallation: vi
describe("Copilot build-owned installation", () => {
it("admits the current profile with environment launch preparation", () => {
expect(QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion).toBe(81);
expect(QUALIFIED_ACPX_PROFILES.copilot.agentProfileVersion).toBe(84);
expect(QUALIFIED_ACPX_PROFILES.copilot.qualificationStatus).toBeUndefined();
expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe("sha256:a51bdf77bba778c4d0265bbfd92a0e6c146f9de5debc3b4a2a268c34e5576e0c");
expect(QUALIFIED_ACPX_PROFILES.copilot.commandDigest).toBe("sha256:a95f1d8e4d0a18d0c2cbc3bffdf998a2c5b8a582bdf51a72085b01d948d0a04e");
});
it("preserves published v65 while binding retained-maintenance waits in v66", async () => {
@@ -1,4 +1,5 @@
import { createHash } from "node:crypto";
import { admitAcpxSessionContext } from "./session-context-admission.js";
const SCHEMA = "paperclip.cursor.instructions.v1";
const MAX_BYTES = 32 * 1024;
@@ -77,11 +78,5 @@ export async function admitCursorInstructions(
admission: ReturnType<typeof createCursorInstructionAdmission>,
input: { providerSpawned: boolean; load: () => Promise<void>; refreshCommand: () => Promise<void> },
): Promise<void> {
if (!input.providerSpawned) {
await input.load();
admission.assertReady();
// ACPX control calls close their temporary native connection. A subsequent
// prompt needs a fresh verified command lease, never a consumed snapshot.
await input.refreshCommand();
} else admission.assertReady();
await admitAcpxSessionContext(admission, input);
}
@@ -9,13 +9,14 @@ import { providerFamilyCapabilities } from "../../provider-events.js";
import {
ACPX_DRIVER_KIND,
QUALIFIED_ACPX_VERSION,
QUALIFIED_ACPX_PROFILES,
resolveQualifiedAcpxProfile,
type QualifiedAcpxAgent,
} from "./qualified-profiles.js";
import { ACPX_CAPABILITY_PROFILES } from "./capability-profiles.js";
const ACPX_AGENTS = ["claude", "codex", "grok", "pi", "cursor", "copilot"] as const;
const ACPX_AGENTS = ["claude", "codex", "grok", "pi", "cursor", "copilot", "antigravity"] as const;
const ACPX_PERMISSION_MODES = [
"approve-all",
"approve-paperclip",
@@ -37,6 +38,12 @@ export function acpxCapabilities(
): NativeSessionCapabilities {
const controls = agent === "pi" ? negotiated : null;
const profile = ACPX_CAPABILITY_PROFILES[agent];
if (QUALIFIED_ACPX_PROFILES[agent].qualificationStatus === "pending") return {
resume: false, toolRefreshOnResume: false, typedEvents: false, steering: false, queuedFollowUp: false,
interruption: false, structuredResult: false, read: false, reconciliation: false, usage: false,
dynamicTools: false, runtimeRequestResolution: false, runtimeRequestHandoff: false, goals: false, threadLineage: false,
unsupported: ["qualification_pending"],
};
return {
resume: profile.recovery === "session-load",
toolRefreshOnResume: profile.recovery === "session-load" && profile.toolRefreshOnResume === true,
@@ -68,15 +75,16 @@ export function acpxCapabilities(
export function acpxDriverDescriptor(
agent: QualifiedAcpxAgent,
): HarnessDriverDescriptor {
const pending = QUALIFIED_ACPX_PROFILES[agent].qualificationStatus === "pending";
return {
kind: ACPX_DRIVER_KIND,
displayName: `${displayAgent(agent)} via ACPX`,
version: QUALIFIED_ACPX_VERSION,
protocolVersion: "acp/v1",
runtimeContextCapabilities: {
instructions: "native",
skills: "native",
mcp: "native",
instructions: pending ? "unsupported" : "native",
skills: pending ? "unsupported" : "native",
mcp: pending ? "unsupported" : "native",
},
capabilities: acpxCapabilities(agent),
};
@@ -105,11 +113,11 @@ export function validateAcpxDriverConfig(
return invalid(
"agent",
"invalid_agent",
"ACPX agent must be claude, codex, grok, cursor, copilot, or pi.",
"ACPX agent must be claude, codex, grok, cursor, copilot, pi, or antigravity.",
);
}
if (ACPX_CAPABILITY_PROFILES[agent].qualification !== "qualified") {
return invalid("agent", "qualification_pending", `${ACPX_CAPABILITY_PROFILES[agent].displayName} requires local and Daytona qualification before use.`);
return invalid("agent", "qualification_pending", `${ACPX_CAPABILITY_PROFILES[agent].displayName} requires native Runner acceptance before use.`);
}
const model = text(config.model);
try {
@@ -115,7 +115,7 @@ describe("ACPX launch environment", () => {
it.each([
["pi", "OPENROUTER_API_KEY"], ["cursor", "CURSOR_API_KEY"],
["cursor", "CURSOR_AUTH_TOKEN"], ["copilot", "COPILOT_GITHUB_TOKEN"],
["cursor", "CURSOR_AUTH_TOKEN"], ["copilot", "COPILOT_GITHUB_TOKEN"], ["antigravity", "GEMINI_API_KEY"],
] as const)("requires an explicit %s credential binding for %s", (agent, name) => {
vi.stubEnv(name, "ambient-secret");
expect(createSanitizedAcpxSpawnInput(undefined, agent).env[name]).toBeUndefined();
@@ -137,7 +137,7 @@ describe("ACPX launch environment", () => {
it.each([
["pi", "OPENROUTER_API_KEY"], ["cursor", "CURSOR_API_KEY"],
["cursor", "CURSOR_AUTH_TOKEN"], ["copilot", "COPILOT_GITHUB_TOKEN"],
["cursor", "CURSOR_AUTH_TOKEN"], ["copilot", "COPILOT_GITHUB_TOKEN"], ["antigravity", "GEMINI_API_KEY"],
] as const)("admits only session-bound %s sidecar credentials for %s", (agent, name) => {
const source = { [name]: "task-secret" };
const binding = createAcpxCredentialBinding(source, agent, "session-1");
@@ -1,4 +1,5 @@
import { isOpenRouterUrl, openRouterClaudeHeaders } from "../openrouter-attribution.js";
import { sanitizeAntigravityEnvironment } from "./antigravity-environment.js";
import { providerObservabilityEnvironment } from "../agent-observability.js";
import { CONFIGURED_ENVIRONMENT_KEYS, configuredEnvironment, configuredEnvironmentKeys } from "../../configured-environment.js";
import { PI_CREDENTIAL_NAMES, PI_GENERAL_IAM_CREDENTIAL_NAMES, piCredentialNames } from "./pi-provider-config.js";
@@ -7,6 +8,7 @@ import type { QualifiedAcpxAgent } from "./qualified-profiles.js";
export const ACPX_CREDENTIAL_BINDING_ENV = "PAPERCLIP_ACPX_CREDENTIAL_BINDING";
export const ACPX_CREDENTIAL_NAMES: Readonly<Record<QualifiedAcpxAgent, readonly string[]>> = {
antigravity: ["GEMINI_API_KEY", "PAPERCLIP_ACPX_ANTIGRAVITY_AUTH_JSON_SECRET"],
grok: ["XAI_API_KEY"],
pi: PI_CREDENTIAL_NAMES,
cursor: ["CURSOR_API_KEY", "CURSOR_AUTH_TOKEN"],
@@ -25,7 +27,7 @@ function removePiIamCredentials(environment: NodeJS.ProcessEnv): void {
}
}
const isCandidate = (agent: QualifiedAcpxAgent) => agent === "pi" || agent === "cursor" || agent === "copilot";
const isCandidate = (agent: QualifiedAcpxAgent) => agent === "pi" || agent === "cursor" || agent === "copilot" || agent === "antigravity";
/** Mint only at the controller's explicit task-environment boundary, never by copying a marker. */
export function createAcpxCredentialBinding(
@@ -77,6 +79,7 @@ export function createAcpxSidecarHostEnvironment(
}
}
if (agent === "pi") removePiIamCredentials(result);
if (agent === "antigravity") sanitizeAntigravityEnvironment(result);
return result;
}
@@ -158,6 +161,10 @@ export function createSanitizedAcpxSpawnInput(
result[key] = value;
}
if (agent === "pi") removePiIamCredentials(result);
if (agent === "antigravity") sanitizeAntigravityEnvironment(result);
// The controller-to-sidecar document is staged under a lifetime fence. The
// native child receives the private token file, never its launch document.
if (agent === "antigravity") delete result.PAPERCLIP_ACPX_ANTIGRAVITY_AUTH_JSON_SECRET;
if (agent === "claude" && isOpenRouterUrl(result.ANTHROPIC_BASE_URL)) {
result.ANTHROPIC_CUSTOM_HEADERS = openRouterClaudeHeaders(result.ANTHROPIC_CUSTOM_HEADERS);
}
@@ -4,17 +4,31 @@ export const ACPX_DRIVER_KIND = "acpx_runtime" as const;
export const ACPX_DRIVER_PROTOCOL_VERSION = 1 as const;
export const QUALIFIED_ACPX_PROFILE_DATA = {
"antigravity": {
"driverKind": "acpx_runtime",
"protocolVersion": 1,
"acpxVersion": "0.13.1",
"agent": "antigravity",
"agentProfileVersion": 1,
"agentServerPackage": "native:antigravity-acp",
"agentServerVersion": "1.4.3",
"agentRuntimePackage": "native:antigravity-localharness",
"agentRuntimeVersion": "1.4.3",
"commandDigest": "sha256:acf5f574b6b9ec4388ff9d081890caa330e66bf1ae3fe3b08e7f0fb0e851b9ce",
"qualificationStatus": "pending",
"permissionPolicy": "interactive"
},
"grok": {
"driverKind": "acpx_runtime",
"protocolVersion": 1,
"acpxVersion": "0.13.1",
"agent": "grok",
"agentProfileVersion": 12,
"agentProfileVersion": 13,
"agentServerPackage": "builtin:grok-acp",
"agentServerVersion": "1",
"agentRuntimePackage": "native:grok",
"agentRuntimeVersion": "1.0.13",
"commandDigest": "sha256:3dc83b2c3493913748cacb09025a8c97b74f3b5bbb1dc204072d1a773fa9af3a",
"commandDigest": "sha256:e311f8111dd12f0c49bf12a80345cfe5f31a1222e0189209adeabf2f9bd0e1b0",
"permissionPolicy": "interactive"
},
"pi": {
@@ -22,12 +36,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
"protocolVersion": 1,
"acpxVersion": "0.13.1",
"agent": "pi",
"agentProfileVersion": 34,
"agentProfileVersion": 35,
"agentServerPackage": "pi-acp",
"agentServerVersion": "0.0.33",
"agentRuntimePackage": "@earendil-works/pi-coding-agent",
"agentRuntimeVersion": "1.0.0",
"commandDigest": "sha256:ed4b32cc8fbf36309d38b3bd378f41f8e7b9e96a2405d275cc55e89042d94c7f",
"commandDigest": "sha256:9473de6ef4baf2bd82fe8b6757239bdc160960b0a78ae6f47fa9e0996544226c",
"permissionPolicy": "interactive"
},
"cursor": {
@@ -48,12 +62,12 @@ export const QUALIFIED_ACPX_PROFILE_DATA = {
"protocolVersion": 1,
"acpxVersion": "0.13.1",
"agent": "copilot",
"agentProfileVersion": 81,
"agentProfileVersion": 84,
"agentServerPackage": "@github/copilot",
"agentServerVersion": "1.0.88",
"agentRuntimePackage": null,
"agentRuntimeVersion": null,
"commandDigest": "sha256:a51bdf77bba778c4d0265bbfd92a0e6c146f9de5debc3b4a2a268c34e5576e0c",
"commandDigest": "sha256:a95f1d8e4d0a18d0c2cbc3bffdf998a2c5b8a582bdf51a72085b01d948d0a04e",
"permissionPolicy": "interactive"
},
"claude": {
@@ -101,3 +115,11 @@ export const CURSOR_DISTRIBUTION_PINS = {
"entrypoint": "index.js"
}
} as const;
export const ANTIGRAVITY_DISTRIBUTION_PINS = {
"darwin-arm64": {
"closureSha256": "044792d793e203bc8b4524a418f1816c4d34db67bb41fa4bae5947a050c49b4a",
"executable": "agy_acp_server.par",
"args": []
}
} as const;
@@ -29,7 +29,7 @@ import {
} from "node:path";
import type { Readable, Writable } from "node:stream";
import { resolveQualifiedAcpxProfile, type QualifiedAcpxProfile } from "./qualified-profiles.js";
import { assertAcpxProfileReleased, resolveQualifiedAcpxProfile, type QualifiedAcpxProfile } from "./qualified-profiles.js";
import { resolveInstalledProviderAuthority } from "./installed-provider-authority.js";
import {
VERIFIED_RUNTIME_EXECUTABLE_ENV,
@@ -651,6 +651,7 @@ export async function verifyQualifiedAcpxInstallation(
profile: QualifiedAcpxProfile,
resolvePackageJson: AcpxPackageJsonResolver = defaultPackageJsonResolver,
): Promise<VerifiedAcpxInstallation> {
assertAcpxProfileReleased(profile.agent);
// Keep the selected package authority available when its bundled provider
// wrapper delegates platform installation to the published server manifest.
if ((profile.agent === "codex" || profile.agent === "claude") && resolvePackageJson === defaultPackageJsonResolver) {
@@ -218,21 +218,21 @@ describe("Pi installation factory", () => {
it("rejects legacy profiles and tampered executable identities", () => {
expect(() => assertPiInstallationProfile(candidate())).not.toThrow();
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 34");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 34");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 34");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 34");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 34");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 34");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 34");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 34");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 34");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 34");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 34");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 34");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 34");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 34");
for (const priorVersion of [15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 34");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 1 })).toThrow("version 35");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 2 })).toThrow("version 35");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 3 })).toThrow("version 35");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 4 })).toThrow("version 35");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 5 })).toThrow("version 35");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 6 })).toThrow("version 35");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 7 })).toThrow("version 35");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 8 })).toThrow("version 35");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 9 })).toThrow("version 35");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 10 })).toThrow("version 35");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 11 })).toThrow("version 35");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 12 })).toThrow("version 35");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 13 })).toThrow("version 35");
expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: 14 })).toThrow("version 35");
for (const priorVersion of [15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34]) expect(() => assertPiInstallationProfile({ ...candidate(), agentProfileVersion: priorVersion })).toThrow("version 35");
for (const changed of [{ agentServerVersion: "latest" }, { commandDigest: `sha256:${"0".repeat(64)}` }, { agentRuntimePackage: "ambient-pi" }]) {
expect(() => assertPiInstallationProfile({ ...candidate(), ...changed })).toThrow("trusted declaration");
}
@@ -5,6 +5,9 @@ import { isSupportedAcpxProfileVersion } from "./profile-compatibility.js";
describe("historical ACPX profile decoding", () => {
it("retains each provider's existing revision boundary", () => {
expect(isSupportedAcpxProfileVersion("grok", 7)).toBe(true);
expect(isSupportedAcpxProfileVersion("pi", 30)).toBe(true);
expect(isSupportedAcpxProfileVersion("copilot", 71)).toBe(true);
expect(isSupportedAcpxProfileVersion("copilot", 17)).toBe(true);
expect(isSupportedAcpxProfileVersion("pi", 17)).toBe(true);
expect(isSupportedAcpxProfileVersion("pi", 18)).toBe(true);
@@ -2,13 +2,13 @@ import { QUALIFIED_ACPX_PROFILE_DATA } from "./generated-profiles.js";
/** Decodable historical profile revisions, not permission to launch them.
* Exact current profile/package/digest admission is checked separately. */
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80;
type HistoricalAcpxProfileVersion = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83;
type CurrentAcpxProfileVersion = typeof QUALIFIED_ACPX_PROFILE_DATA[keyof typeof QUALIFIED_ACPX_PROFILE_DATA]["agentProfileVersion"];
export type AcpxProfileVersion = HistoricalAcpxProfileVersion | CurrentAcpxProfileVersion;
const historicalVersions: Readonly<Record<string, readonly HistoricalAcpxProfileVersion[]>> = {
pi: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5],
grok: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80],
pi: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34], claude: [1, 2, 3, 4, 5], codex: [1, 2, 3, 4, 5],
grok: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], copilot: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83],
cursor: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
};
@@ -1,13 +1,17 @@
import { assertAcpxExecutionAdmission, type AcpxCandidateStartupAdmission } from "./candidate-admission.js";
import { assertCopilotCredentials, classifyCopilotFailure } from "./copilot-profile.js";
import { verifyCopilotInstallation } from "./copilot-installation.js";
import { verifyCursorInstallation } from "./cursor-installation.js";
import { verifyAntigravityInstallation } from "./antigravity-installation.js";
import { assertCursorWorkspacePolicy } from "./cursor-launch-policy.js";
import { resolveQualifiedAcpxProfile, type QualifiedAcpxAgent, type QualifiedAcpxProfile } from "./qualified-profiles.js";
import { verifyPiInstallation } from "./pi-installation.js";
import { verifyQualifiedAcpxInstallation, type VerifiedAcpxInstallation } from "./installation-integrity.js";
/** Closed build-owned registry. Provider branches add their pinned installations here. */
export async function verifyAcpxProfileInstallation(profile: QualifiedAcpxProfile): Promise<VerifiedAcpxInstallation> {
export async function verifyAcpxProfileInstallation(profile: QualifiedAcpxProfile, candidateAdmission?: AcpxCandidateStartupAdmission): Promise<VerifiedAcpxInstallation> {
assertAcpxExecutionAdmission(profile, candidateAdmission);
if (profile.agent === "antigravity") return verifyAntigravityInstallation(profile, candidateAdmission);
if (profile.agent === "pi") return verifyPiInstallation(profile);
if (profile.agent === "copilot") return verifyCopilotInstallation(profile);
if (profile.agent === "cursor") {
@@ -31,6 +35,9 @@ export async function assertAcpxProfileWorkspace(agent: QualifiedAcpxAgent, work
/** Called with the sanitized launch environment, never ambient process.env. */
export function assertAcpxProfileEnvironment(agent: QualifiedAcpxAgent, environment: Readonly<NodeJS.ProcessEnv>): void {
if (agent === "copilot") assertCopilotCredentials(environment);
if (agent === "antigravity" && !environment.GEMINI_API_KEY?.trim() && !environment.PAPERCLIP_ACPX_ANTIGRAVITY_AUTH_JSON_SECRET?.trim()) {
throw Object.assign(new Error("Google Antigravity requires an explicitly bound Google runtime connection. Connect a Google account or Gemini API key before starting a task."), { code: "ANTIGRAVITY_CREDENTIALS_MISSING", retryable: false });
}
if (agent === "cursor" && !environment.CURSOR_API_KEY?.trim() && !environment.CURSOR_AUTH_TOKEN?.trim()) {
throw Object.assign(new Error("Cursor credentials are missing. Bind a company secret to CURSOR_API_KEY or CURSOR_AUTH_TOKEN in the agent environment."), { code: "CURSOR_CREDENTIALS_MISSING", retryable: false });
}
@@ -2,7 +2,7 @@ import { constants, closeSync, fstatSync, lstatSync, openSync, readFileSync, rea
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
type NativeProvider = "cursor" | "copilot" | "pi";
type NativeProvider = "cursor" | "copilot" | "pi" | "antigravity";
const RUNNER_PACKAGE_NAME = "@paperclipai/paperclip-runner";
const SERVER_PACKAGE_NAME = "@paperclipai/server";
function assertServerVendorLayout(root: string): void {
@@ -17,7 +17,7 @@ function assertServerVendorLayout(root: string): void {
/** Resolve only runner-owned package assets, including descriptor-loaded sidecars. */
export function resolveRunnerProviderAssetsRoot(moduleUrl: string, provider: NativeProvider): string {
if (!["cursor", "copilot", "pi"].includes(provider)) throw new Error("Unknown native ACP provider assets");
if (!["cursor", "copilot", "pi", "antigravity"].includes(provider)) throw new Error("Unknown native ACP provider assets");
const boundRoot = process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT;
const boundManifest = process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST;
let packageRoot: string;
@@ -37,6 +37,14 @@ export const QUALIFIED_ACPX_PROFILES: Readonly<
Record<QualifiedAcpxAgent, AcpxReleaseProfile>
> = deepFreeze(QUALIFIED_ACPX_PROFILE_DATA);
/** Release admission uses canonical build metadata, never a caller's snapshot. */
export function assertAcpxProfileReleased(agent: QualifiedAcpxAgent): void {
if (QUALIFIED_ACPX_PROFILES[agent].qualificationStatus === "pending") {
throw Object.assign(new Error(`ACPX ${agent} qualification is pending; this build cannot launch the provider.`),
{ code: "ACPX_PROFILE_QUALIFICATION_PENDING", retryable: false });
}
}
export function resolveQualifiedAcpxProfile(
agent: QualifiedAcpxAgent,
requestedModel: string,
@@ -1,3 +1,4 @@
import { assertAcpxExecutionAdmission, parseAcpxCandidateStartupAdmission, type AcpxCandidateStartupAdmission } from "./candidate-admission.js";
import { resolveGrokDisableWebSearch } from "./grok-web-search.js";
import { recordAcpxAdmissionFailure, type ACPX_ADMISSION_CODES } from "./admission-diagnostics.js";
import { composeCopilotSystemInstructions } from "./copilot-profile.js";
@@ -13,6 +14,7 @@ import { bindAcpxAgentFiles } from "./agent-files-binding.js";
import { assertAcpxProfileEnvironment, assertAcpxProfileWorkspace, classifyAcpxProfileError, verifyAcpxProfileInstallation } from "./profile-installation.js";
import { createAcpxRuntimeSkillLease } from "./runtime-skill-lease.js";
import { stageManagedGrokCredential } from "./grok-credentials.js";
import { stageManagedAntigravityCredential } from "./antigravity-credentials.js";
import { claudeNativeSkillPrompt } from "./native-skill-prompt.js";
import { prepareStdioMcpBindings } from "../stdio-mcp.js";
import { nativeMcpLaunchBinding } from "../native-mcp.js";
@@ -238,6 +240,8 @@ export interface AcpxRuntimeHostDependencies {
}
export interface OpenAcpxRuntimeHostOptions {
/** Trusted startup authority from runnerd; never provider session data. */
candidateAdmission?: AcpxCandidateStartupAdmission;
/** Explicit task execution policy; never inferred from auto-approval. Required for Pi. */
providerPolicy?: AcpxProviderRuntimePolicy;
/** Never persisted as session options or recovery identity. */
@@ -309,6 +313,7 @@ export class AcpxRuntimeHost {
readonly #toolBridge: RunnerToolBridge | null;
readonly #claudeSkillNames: readonly string[];
readonly #assertAgentFilesHeld: (() => void) | undefined;
readonly #candidateAdmission: AcpxCandidateStartupAdmission | undefined;
#activeTurn: AcpxRuntimeTurn | null = null;
#activeTurnInterruption: AcpxRuntimeTurn["cancel"] | null = null;
#closingStarted = false;
@@ -325,6 +330,7 @@ export class AcpxRuntimeHost {
toolBridge: RunnerToolBridge | null;
claudeSkillNames: readonly string[];
assertAgentFilesHeld?: () => void;
candidateAdmission?: AcpxCandidateStartupAdmission;
}) {
this.#runtime = input.runtime;
this.#binding = input.binding;
@@ -335,6 +341,7 @@ export class AcpxRuntimeHost {
this.#toolBridge = input.toolBridge;
this.#claudeSkillNames = [...input.claudeSkillNames];
this.#assertAgentFilesHeld = input.assertAgentFilesHeld;
this.#candidateAdmission = input.candidateAdmission ? structuredClone(input.candidateAdmission) : undefined;
}
static async open(
@@ -365,6 +372,11 @@ export class AcpxRuntimeHost {
throw new Error("assigned native MCP launch binding is unavailable");
}
const profile = resolveQualifiedAcpxProfile(options.agent, options.model);
if (options.candidateAdmission) {
assertAcpxExecutionAdmission(profile, options.candidateAdmission);
options = { ...options, candidateAdmission: parseAcpxCandidateStartupAdmission(options.candidateAdmission, { agent: options.agent, model: options.model, normalizedSessionId: options.normalizedSessionId }) };
if (preconfigured || typeof options.providerPolicy?.readOnly !== "boolean") throw new Error("ACPX candidate requires a fresh isolated session and explicit task policy");
}
const binding = await runAbortableAdmissionStage(
options.signal,
() =>
@@ -403,9 +415,9 @@ export class AcpxRuntimeHost {
const installation = await runAbortableAdmissionStage(
options.signal,
() =>
(dependencies.verifyInstallation ?? verifyAcpxProfileInstallation)(
profile,
),
dependencies.verifyInstallation
? dependencies.verifyInstallation(profile)
: verifyAcpxProfileInstallation(profile, options.candidateAdmission),
dependencies.retainAdmissionCleanup,
).catch(error => { throw recordAcpxAdmissionFailure(error, "installation"); });
if (installation.commandDigest !== profile.commandDigest) {
@@ -477,6 +489,7 @@ export class AcpxRuntimeHost {
dependencies.retainAdmissionCleanup,
);
admissionStage = "credential";
if (options.agent === "antigravity") assertAcpxProfileEnvironment(options.agent, options.environment ?? {});
if (options.agent === "codex" && !preconfigured) {
credential = await acquireAbortableAdmissionResource({
signal: options.signal,
@@ -491,6 +504,14 @@ export class AcpxRuntimeHost {
reportFailure: (failure) =>
dependencies.reportRetainedCleanupFailure(failure),
});
} else if (options.agent === "antigravity") {
credential = await acquireAbortableAdmissionResource({
signal: options.signal,
acquire: () => stageManagedAntigravityCredential({ agentHomeDirectory: sandbox.agentHomeDirectory,
environment: options.environment, retainRefresh: () => admissionSucceeded }),
resource: "credential", releaseLate: late => late.close(),
reportFailure: failure => dependencies.reportRetainedCleanupFailure(failure),
});
} else if (options.agent === "grok") {
credential = await acquireAbortableAdmissionResource({
signal: options.signal,
@@ -528,7 +549,9 @@ export class AcpxRuntimeHost {
);
options.signal?.throwIfAborted();
admissionStage = "credential";
assertAcpxProfileEnvironment(options.agent, sandbox.launchEnvironment);
// Antigravity's selected OAuth document has already been validated and
// staged; it is intentionally absent from the native launch environment.
if (options.agent !== "antigravity") assertAcpxProfileEnvironment(options.agent, sandbox.launchEnvironment);
admissionStage = "configuration";
let launchEnvironment = sandbox.launchEnvironment;
// This copy is registered by the authenticated controller for its company,
@@ -630,6 +653,7 @@ export class AcpxRuntimeHost {
options.assertWorkspaceHeld?.();
agentFiles?.assertHeld();
await assertAcpxProfileWorkspace(options.agent, binding.workspacePath);
if (options.candidateAdmission) assertAcpxExecutionAdmission(profile, options.candidateAdmission);
return dependencies.openRuntime({
...(options.clientCapabilities === undefined ? {} : {
clientCapabilities: structuredClone(options.clientCapabilities),
@@ -740,6 +764,7 @@ export class AcpxRuntimeHost {
command,
toolBridge,
assertAgentFilesHeld: agentFiles?.assertHeld,
candidateAdmission: options.candidateAdmission,
claudeSkillNames: options.agent === "claude"
? (options.runtimeContext?.skills.map((skill) => skill.runtimeName) ?? [])
: [],
@@ -864,6 +889,7 @@ export class AcpxRuntimeHost {
}
startTurn(input: AcpxRuntimeTurnInput): AcpxRuntimeTurn {
if (this.#candidateAdmission) parseAcpxCandidateStartupAdmission(this.#candidateAdmission, { agent: this.#candidateAdmission.agent, model: this.#binding.requestedModel, normalizedSessionId: this.#binding.normalizedSessionId });
this.#assertAgentFilesHeld?.();
if (this.#closed || this.#closingStarted) {
throw new Error("ACPX runtime host is closing");
@@ -167,6 +167,20 @@ describe("ACPX runtime sandbox", () => {
for (const value of ["provider-secret", "configured-secret", "scoped-run-token", "private-identity"]) expect(config).not.toContain(value);
});
it("seeds an isolated Antigravity API-key profile without inherited login/configuration", async () => {
const fixture = await sandboxFixture("antigravity");
const sandbox = await prepareAcpxRuntimeSandbox({ binding: fixture.binding, agent: "antigravity",
environment: { GEMINI_API_KEY: "bound-key", GEMINI_HOME: "/foreign/profile", GOOGLE_APPLICATION_CREDENTIALS: "/foreign/adc.json" } });
await sandbox.initializeOwnedConfiguration?.();
expect(sandbox.launchEnvironment.GEMINI_HOME).toBe(sandbox.agentHomeDirectory);
expect(sandbox.launchEnvironment.AGY_ACP_FORCE_FILE_STORAGE).toBe("1");
expect(sandbox.launchEnvironment.GOOGLE_APPLICATION_CREDENTIALS).toBeUndefined();
const settings = join(sandbox.agentHomeDirectory, "antigravity-acp/settings.json");
expect(JSON.parse(await readFile(settings, "utf8"))).toEqual({ auth: { type: "gemini-api-key" } });
expect((await stat(settings)).mode & 0o777).toBe(0o600);
expect(JSON.stringify(sandbox.persistedEnvironment)).not.toContain("bound-key");
});
it.each(["claude-sonnet-5", "sonnet", "custom-deployment-id"])(
"preserves the requested Claude model %s in its isolated settings",
async (model) => {
@@ -551,7 +565,7 @@ it("refreshes Copilot native instructions privately on every admission, includin
expect(await readFile(path, "utf8")).toBe(composed(""));
});
async function sandboxFixture(agent: "pi" | "claude" | "codex" | "grok" | "cursor" | "copilot") {
async function sandboxFixture(agent: "pi" | "claude" | "codex" | "grok" | "cursor" | "copilot" | "antigravity") {
const root = await mkdtemp(join(tmpdir(), "paperclip-acpx-sandbox-"));
temporaryDirectories.push(root);
const workspace = join(root, "workspace");
@@ -564,6 +578,7 @@ async function sandboxFixture(agent: "pi" | "claude" | "codex" | "grok" | "curso
codex: "gpt-5.6-sol",
cursor: "provider-model-cursor",
copilot: "provider-model-copilot",
antigravity: "provider-model-antigravity",
} as const;
const binding = await createAcpxRecoveryBinding({
runtimeDirectory,
@@ -440,6 +440,10 @@ export async function prepareAcpxRuntimeSandbox(input: {
: {}),
})}\n`,
);
} : input.agent === "antigravity" ? async () => {
const profile = await ensurePrivateDirectory(join(agentHomeDirectory, "antigravity-acp"), agentHomeDirectory);
const method = input.environment?.PAPERCLIP_ACPX_ANTIGRAVITY_AUTH_JSON_SECRET ? "oauth-personal" : "gemini-api-key";
await writePrivateFile(join(profile, "settings.json"), `${JSON.stringify({ auth: { type: method } })}\n`);
} : undefined;
if (input.agent === "grok") {
// Native project rules merge with global rules. Ask rules win over project
@@ -529,6 +533,7 @@ export async function prepareAcpxRuntimeSandbox(input: {
XDG_CACHE_HOME: cacheDirectory,
PAPERCLIP_ACPX_PROFILE: input.agent,
PAPERCLIP_ACPX_ISOLATED_CONTEXT: "1",
...(input.agent === "antigravity" ? { GEMINI_HOME: agentHomeDirectory, AGY_ACP_FORCE_FILE_STORAGE: "1" } : {}),
...(input.agent === "grok" ? { GROK_HOME: agentHomeDirectory, NO_BROWSER: "1", GROK_DISABLE_AUTOUPDATER: "1" } : {}),
...(input.agent === "pi"
? {
@@ -764,6 +769,7 @@ function isPersistableEnvironmentName(name: string): boolean {
/^(?:PI_CODING_AGENT_DIR|PI_SKIP_VERSION_CHECK|PI_TELEMETRY)$/.test(name) ||
/^(?:CURSOR_CONFIG_DIR|CURSOR_DATA_DIR|AGENT_CLI_CREDENTIAL_STORE|NO_OPEN_BROWSER|NODE_DISABLE_COMPILE_CACHE)$/.test(name) ||
/^(?:COPILOT_HOME|COPILOT_CACHE_HOME|COPILOT_AUTO_UPDATE|COPILOT_ALLOW_ALL|COPILOT_DISABLE_TERMINAL_TITLE|NO_COLOR)$/.test(name) ||
/^(?:GEMINI_HOME|AGY_ACP_FORCE_FILE_STORAGE)$/.test(name) ||
/^(?:CLAUDE_CONFIG_DIR|CODEX_HOME|NO_BROWSER|DEFAULT_AUTH_REQUEST|PAPERCLIP_RUNNER_PREPARED_HARNESS_HOME)$/.test(
name,
)
@@ -0,0 +1,13 @@
/** A disk record is not proof that this connection received the session context. */
export async function admitAcpxSessionContext(
admission: { assertReady(): void },
input: { providerSpawned: boolean; load: () => Promise<void>; refreshCommand: () => Promise<void> },
): Promise<void> {
if (!input.providerSpawned) {
await input.load();
admission.assertReady();
// ACPX control calls close their temporary connection. Its consumed native
// command lease cannot be reused for the subsequent prompt connection.
await input.refreshCommand();
} else admission.assertReady();
}
@@ -1,3 +1,4 @@
import type { AcpxCandidateStartupAdmission } from "./candidate-admission.js";
import type { QualifiedAcpxAgent } from "./qualified-profiles.js";
import type { NativeRuntimeContextSnapshot } from "../../contracts/runtime-context.js";
import type { NativeAcpxPermissionMode } from "../../contracts/native-execution.js";
@@ -39,6 +40,7 @@ export interface AcpxSidecarEvent {
}
export interface AcpxSidecarOpenParams {
candidateAdmission?: AcpxCandidateStartupAdmission;
runtimeDirectory: string;
normalizedSessionId: string;
workingDirectory: string;
@@ -46,6 +46,8 @@ export interface CodexAppServerTransport {
notify(method: string, params?: Record<string, unknown>): void;
notifications(): AsyncIterable<CodexRpcNotification>;
setServerRequestHandler(handler: CodexServerRequestHandler): void;
/** Establish a fresh owned provider lifetime before reading a retained conversation. */
prepareConversationResume?(params: Record<string, unknown>): Promise<void>;
/** One-shot requests authenticated by an adopted live provider, never a saved checkpoint alone. */
takeRestoredRuntimeRequests?(): CodexRpcServerRequest[];
/**
@@ -406,6 +406,14 @@ export class CodexAppServerDriver implements HarnessDriver {
try {
await cancellation.wait(this.#persistProcessOwnership(transport));
const initialize = await cancellation.wait(this.#initialize(transport));
if (transport.prepareConversationResume) {
const resumeDirectory = snapshot.workingDirectory
? validateWorkingDirectory(snapshot.workingDirectory, this.#options.environment, this.#options.workingDirectoryAuthority)
: undefined;
await cancellation.wait(transport.prepareConversationResume(resumeDirectory
? this.#resumeThreadParams(snapshot.driverSessionId, resumeDirectory)
: { threadId: snapshot.driverSessionId }));
}
const existing = await cancellation.wait(
readCodexThreadState(transport, snapshot.driverSessionId),
);
@@ -436,23 +444,7 @@ export class CodexAppServerDriver implements HarnessDriver {
};
}
const response = await cancellation.wait(
transport.request("thread/resume", {
excludeTurns: true,
threadId: snapshot.driverSessionId,
...createSecuredCodexThreadParams(
workingDirectory,
this.#options.requestedCollaborationMode ?? "default",
this.#options.includeCollaborationModeInstructions ?? true,
this.#options.includeSkillInstructions ?? false,
this.#options.environment,
),
...this.#instructionParams(this.#direct() ? "" : this.#baseInstructions()),
approvalPolicy: this.#options.approvalPolicy ?? "never",
...(this.#options.model ? { model: this.#options.model } : {}),
dynamicTools: this.#providerDynamicTools(),
...(this.#prepared() ? { conversationMode: "prepared" } : {}),
persistExtendedHistory: false,
}),
transport.request("thread/resume", this.#resumeThreadParams(snapshot.driverSessionId, workingDirectory)),
);
await cancellation.wait(this.#persistProcessOwnership(transport));
const collaborationMode = await cancellation.wait(
@@ -686,6 +678,22 @@ export class CodexAppServerDriver implements HarnessDriver {
}
}
#resumeThreadParams(threadId: string, workingDirectory: string): Record<string, unknown> {
return {
excludeTurns: true, threadId,
...createSecuredCodexThreadParams(workingDirectory,
this.#options.requestedCollaborationMode ?? "default",
this.#options.includeCollaborationModeInstructions ?? true,
this.#options.includeSkillInstructions ?? false, this.#options.environment),
...this.#instructionParams(this.#direct() ? "" : this.#baseInstructions()),
approvalPolicy: this.#options.approvalPolicy ?? "never",
...(this.#options.model ? { model: this.#options.model } : {}),
dynamicTools: this.#providerDynamicTools(),
...(this.#prepared() ? { conversationMode: "prepared" } : {}),
persistExtendedHistory: false,
};
}
#transport(context?: {
workingDirectory?: string;
providerRecoveryPolicy?: PersistedHarnessSession["providerRecoveryPolicy"];
@@ -0,0 +1,59 @@
import { CodexAppServerDriver, FakeCodexTransport, WORKSPACE, collectUntilTerminal, envelope, expect, it, vi, type CodexAppServerTransport } from "./codex-app-server-driver.test-support.js";
const model = "gemini-3.8-flash-low";
const identity = { kind: "acpx", normalizedSessionId: "native-conversation", acpxRecordId: "record-1", backendSessionId: "backend-1", agentSessionId: "native-1",
profileDigest: `sha256:${"a".repeat(64)}`, workspaceDigest: `sha256:${"b".repeat(64)}`, requestedModel: model, effectiveModel: model,
permissionMode: "approve-all", providerLifetimeFenceCandidates: [60001, 60002, 60003] };
function peer(nativeIdentity = identity) {
const transport = new FakeCodexTransport("driver-1", "native-1", nativeIdentity) as FakeCodexTransport & Pick<CodexAppServerTransport, "prepareConversationResume">;
const request = transport.request.bind(transport);
transport.request = async (method, params) => {
const result = await request(method, params);
if (method === "thread/start" || method === "thread/resume") result.model = model;
return result;
};
transport.readResponse = { thread: { id: "driver-1", sessionId: "native-1", cwd: WORKSPACE, turns: [] } };
return transport;
}
it.each(["loaded", "missing-conversation", "different-thread", "different-native-identity"])("normal driver continuation prepares a fresh lifetime before authoritative reads (%s)", async mode => {
const first = peer(), next = peer(mode === "different-native-identity" ? { ...identity, agentSessionId: "replaced-native-session" } : identity);
const instructions = "Current assigned instruction bytes.\n";
const prepare = vi.fn(async (params: Record<string, unknown>) => {
next.calls.push({ method: "owned-conversation-bootstrap", params: structuredClone(params) });
expect(next.calls.some(call => call.method === "thread/read")).toBe(false);
if (mode === "missing-conversation") throw new Error("retained native conversation is unavailable");
if (mode === "different-thread") next.readResponse = { thread: { id: "different-driver", sessionId: "different-native", cwd: WORKSPACE, turns: [] } };
});
next.prepareConversationResume = prepare;
let index = 0;
const driver = new CodexAppServerDriver({ taskEnvelope: envelope, model, baseInstructions: instructions,
requireProviderSessionIdentity: true, driverIdentity: { kind: "acpx", displayName: "Antigravity", version: "test" }, requestedCollaborationMode: "default", environment: {}, transportFactory: () => [first, next][index++]! });
const original = await driver.openSession({ runId: "first-run", normalizedSessionId: "native-conversation", workingDirectory: WORKSPACE });
await original.startTurn({ message: { role: "user", text: "Finish the first turn." } });
first.push("turn/completed", { threadId: "driver-1", turn: { id: "turn-1", status: "completed", items: [] } });
await collectUntilTerminal(original.events());
const snapshot = await original.snapshot();
expect(snapshot.activeTurnId).toBeNull();
await original.close({ reason: "first per-turn lifetime retired" });
snapshot.runId = "current-run";
snapshot.providerRecoveryPolicy = "same_session_only";
next.turnStartResponse = Promise.resolve({ turn: { id: "turn-2", status: "inProgress", items: [] } });
const recovered = await driver.recoverSession(snapshot);
expect(prepare).toHaveBeenCalledOnce();
expect(prepare).toHaveBeenCalledWith(expect.objectContaining({ threadId: "driver-1", model, cwd: WORKSPACE, approvalPolicy: "never", permissions: "paperclip-runner-workspace-only", dynamicTools: expect.any(Array) }));
expect(prepare.mock.calls[0]![0].baseInstructions).toContain(instructions.trim());
if (mode === "loaded") {
expect(recovered.recovered).toBe(true);
expect(next.calls.find(call => call.method === "thread/resume")!.params).toEqual(prepare.mock.calls[0]![0]);
expect((await recovered.session!.snapshot()).providerIdentity).toEqual(identity);
await recovered.session!.startTurn({ message: { role: "user", text: "Continue in the retained conversation." } });
expect(next.calls.filter(call => call.method === "turn/start")).toHaveLength(1);
await recovered.session!.close({ reason: "current per-turn lifetime retired" });
} else {
expect(recovered.recovered).toBe(false);
expect(next.calls.some(call => call.method === "turn/start" || call.method === "thread/start")).toBe(false);
if (mode === "missing-conversation") expect(next.calls.some(call => call.method === "thread/read")).toBe(false);
}
});
+4
View File
@@ -56,6 +56,9 @@ export { runnerBinaryTarget } from "./live/runner-binary.js";
export * from "./drivers/opencode/opencode-server-driver.js";
export * from "./drivers/opencode/mcp-bridge.js";
export * from "./drivers/acpx/qualified-profiles.js";
export { parseAcpxCandidateAdmission, type AcpxCandidateAdmission } from "./drivers/acpx/candidate-admission.js";
export { startAntigravityBrowserLogin, verifyAntigravityCredential, sanitizeAntigravityCredential } from "./drivers/acpx/antigravity-auth.js";
export { AntigravityModelDiscoveryCleanupError, discoverAntigravityModels, type AntigravityModelCatalog } from "./drivers/acpx/antigravity-models.js";
export { acpxRuntimeSessionDirectoryName, resolveAcpxRuntimeRoot } from "./drivers/acpx/recovery-identity.js";
export {
probeQualifiedAcpxEnvironment,
@@ -69,6 +72,7 @@ export * from "./drivers/runner-tool-bridge.js";
export {
createRunnerdCodexTransport,
probeNativeRunnerEnvironment,
NativeEnvironmentProbeCleanupError,
runnerCodexDynamicToolsFit,
defaultCapabilityRunnerdBinary,
readRunnerdArtifactBinding,
@@ -0,0 +1,102 @@
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it, vi } from "vitest";
import { spawnRunner, type RunnerProcessLaunchSpec } from "../control-plane/durable-prp-control-plane.js";
import { bindAcpxCandidateAdmission, type AcpxCandidateAdmission } from "../drivers/acpx/candidate-admission.js";
import { ANTIGRAVITY_DISTRIBUTION_PINS } from "../drivers/acpx/generated-profiles.js";
import { resolveQualifiedAcpxProfile } from "../drivers/acpx/qualified-profiles.js";
import { createRunnerdCodexTransport, runnerdLaunchProfileInternals } from "./runnerd-codex-transport.js";
const identity = { runnerInstanceId: "runner-candidate", environmentLeaseId: "lease-candidate", runId: "run-candidate", normalizedSessionId: "session-candidate", turnId: "turn-candidate", itemId: "item-candidate" };
const candidate = (): AcpxCandidateAdmission => ({ schema: "paperclip.acpx-candidate-admission.v1", companyId: "company-candidate", agent: "antigravity", model: "gemini-3.8-flash-low",
profileDigest: resolveQualifiedAcpxProfile("antigravity", "gemini-3.8-flash-low").commandDigest,
nativeClosureDigest: `sha256:${ANTIGRAVITY_DISTRIBUTION_PINS["darwin-arm64"].closureSha256}`,
platform: "darwin", architecture: "arm64", expiresAt: Date.now() + 60_000 });
const supported = process.platform === "darwin" && process.arch === "arm64";
describe("Antigravity candidate trusted startup", () => {
it("bounds argv and structured launches by the absolute candidate expiry across replacement", () => {
let now = Date.now();
const clock = vi.spyOn(Date, "now").mockImplementation(() => now);
try {
const admission = bindAcpxCandidateAdmission({ ...candidate(), expiresAt: now + 2_000 }, identity);
const launches: RunnerProcessLaunchSpec[] = [];
const handle = spawnRunner({ connectUrl: "ws://127.0.0.1:43127", stateDirectory: "/unused/candidate", identity,
ticket: "inert-ticket", maxOutboxBytes: 262144, p0ReserveBytes: 65536, maxRuntimeMs: 0, maxLifetimeMs: 10_000,
runnerVersion: "fixture", runnerDigest: `sha256:${"f".repeat(64)}`,
acpxLaunchProfile: { candidateAdmission: admission, authorityDigest: `sha256:${"d".repeat(64)}`, command: "/owned/node", commandSha256: `sha256:${"b".repeat(64)}`,
sidecarScript: "/owned/sidecar.cjs", sidecarScriptSha256: `sha256:${"c".repeat(64)}` },
processLauncher: spec => { launches.push(spec); return { child: { pid: 42, exitCode: 0, signalCode: null, kill: () => true },
completion: Promise.resolve({ code: 0, signal: null, stdout: "", stderr: "" }) }; },
});
expect(launches[0]!.args).not.toContain("--max-lifetime-ms");
expect(launches[0]!.args.slice(-2)).toEqual(["--max-runtime-ms", "2000"]);
expect(launches[0]!.nativeRunner).toMatchObject({ maxRuntimeMs: 2000, maxLifetimeMs: 2000 });
now += 1_500;
handle.restart!("replacement-ticket");
expect(launches[1]!.args.slice(-2)).toEqual(["--max-runtime-ms", "500"]);
expect(launches[1]!.nativeRunner).toMatchObject({ maxRuntimeMs: 500, maxLifetimeMs: 500 });
now += 501;
expect(() => handle.restart!("expired-ticket")).toThrow("runner_process_lifetime_expired");
expect(launches).toHaveLength(2);
} finally { clock.mockRestore(); }
});
it.skipIf(!supported)("allows an explicit cold local owner while rejecting absent and broadened authority", async () => {
const root = await mkdtemp(join(tmpdir(), "antigravity-candidate-startup-"));
try {
const options = { provider: "acpx" as const, acpxAgent: "antigravity" as const, stateDirectory: root, acpxCandidateAdmission: candidate() };
expect(() => createRunnerdCodexTransport({ ...options, acpxCandidateAdmission: undefined })).toThrow("evaluation opt-in");
const owner = createRunnerdCodexTransport(options);
await owner.transport.close();
for (const change of [{ lifecyclePolicy: { mode: "warm" as const, idleTimeoutMs: 1000 } }, { runnerFilesystemRoot: "/guest" },
{ homeMode: "preconfigured" as const }, { resumeProviderSession: { driverSessionId: "old-session" } },
{ provider: "codex" as const },
{ acpxCandidateAdmission: { ...candidate(), nativeClosureDigest: `sha256:${"a".repeat(64)}` } }]) {
expect(() => createRunnerdCodexTransport({ ...options, ...change })).toThrow();
}
} finally { await rm(root, { recursive: true, force: true }); }
});
it.skipIf(!supported)("loads a settled native conversation only through a fresh current-run owner", async () => {
const root = await mkdtemp(join(tmpdir(), "antigravity-candidate-continuation-"));
const admission = candidate();
const retained = { driverSessionId: "driver-retained", providerSessionId: "native-retained", providerIdentity: {
kind: "acpx", normalizedSessionId: identity.normalizedSessionId,
// The recovery binding digest is distinct from the release command digest.
profileDigest: `sha256:${"a".repeat(64)}`, requestedModel: admission.model, effectiveModel: admission.model,
} };
try {
const options = { provider: "acpx" as const, acpxAgent: "antigravity" as const, stateDirectory: root,
acpxCandidateAdmission: admission, prpIdentity: identity, resumeWorkingDirectory: root,
resumeProviderSession: retained, providerRecoveryPolicy: "allow_replacement_after_governed_wait" as const };
const owner = createRunnerdCodexTransport(options);
expect(owner.transport.prepareConversationResume).toBeTypeOf("function");
await expect(owner.transport.prepareConversationResume!({ threadId: retained.driverSessionId, model: "wrong-model", cwd: root })).rejects.toThrow("exact current conversation");
await expect(owner.transport.prepareConversationResume!({ threadId: retained.driverSessionId, model: admission.model, cwd: "/wrong-workspace" })).rejects.toThrow("exact current conversation");
await owner.transport.close();
expect(() => createRunnerdCodexTransport({ ...options, resumeActiveTurnId: "stale-active-turn" })).toThrow("fresh local isolated");
expect(() => createRunnerdCodexTransport({ ...options, resumeProviderSession: { ...retained,
providerIdentity: { ...retained.providerIdentity, effectiveModel: "wrong-model" } } })).toThrow("exact retained native conversation");
} finally { await rm(root, { recursive: true, force: true }); }
});
it("binds the startup authority to the actual Runner identity and keeps it out of provider configuration", () => {
const admission = bindAcpxCandidateAdmission(candidate(), identity);
const launches: RunnerProcessLaunchSpec[] = [];
spawnRunner({ connection: { mode: "connect", connectUrl: "ws://127.0.0.1:43127" }, stateDirectory: "/unused/candidate", identity,
ticket: "inert-ticket", maxOutboxBytes: 262144, p0ReserveBytes: 65536, runnerVersion: "fixture", runnerDigest: `sha256:${"f".repeat(64)}`,
acpxLaunchProfile: { candidateAdmission: admission, authorityDigest: `sha256:${"d".repeat(64)}`, command: "/owned/node", commandSha256: `sha256:${"b".repeat(64)}`, sidecarScript: "/owned/sidecar.cjs", sidecarScriptSha256: `sha256:${"c".repeat(64)}` },
environment: {}, processLauncher: spec => { launches.push(spec); return { child: { pid: 42, exitCode: 0, signalCode: null, kill: () => true }, completion: Promise.resolve({ code: 0, signal: null, stdout: "", stderr: "" }) }; },
});
expect(launches).toHaveLength(1);
const launch = launches[0]!;
const flag = launch.args.indexOf("--acpx-candidate-admission-json");
expect(flag).toBeGreaterThan(0);
expect(JSON.parse(launch.args[flag + 1]!)).toEqual({ ...candidate(), ...admission });
expect(launch.nativeRunner?.acpxLaunchProfile?.candidateAdmission).toEqual(admission);
expect(JSON.stringify(launch.environment)).not.toContain(admission.schema);
expect(runnerdLaunchProfileInternals.openedThreadModelProvider("acpx", "antigravity", admission.model)).toBe("google");
});
});
@@ -1,3 +1,4 @@
import { parseAcpxCandidateAdmission } from "../drivers/acpx/candidate-admission.js";
import { ACPX_CAPABILITY_PROFILES } from "../drivers/acpx/capability-profiles.js";
import { resolvePiThinkingLevel } from "../drivers/acpx/pi-thinking.js";
import { normalizeProviderNotice } from "../drivers/provider-notices.js";
@@ -907,9 +908,13 @@ export class CapabilityLiveSessionService {
async create(input: CreateCapabilityLiveSessionInput = {}): Promise<CapabilityLiveSession> {
resolvePiThinkingLevel(input.provider === "acpx" ? input.acpxAgent ?? "codex" : "", input.piThinkingLevel);
if (this.#transportOptions.acpxCandidateAdmission) {
parseAcpxCandidateAdmission(this.#transportOptions.acpxCandidateAdmission, { companyId: input.companyId ?? "", agent: input.acpxAgent ?? "", model: input.requestedModel ?? "" });
}
if (input.provider === "acpx" && input.acpxAgent !== undefined
&& ACPX_CAPABILITY_PROFILES[input.acpxAgent].qualification === "pending"
&& this.#transportOptions.acpxCandidateProfile !== input.acpxAgent) {
&& this.#transportOptions.acpxCandidateProfile !== input.acpxAgent
&& !(input.acpxAgent === "antigravity" && this.#transportOptions.acpxCandidateAdmission)) {
throw new Error("The candidate ACPX profile requires explicit evaluation opt-in");
}
if (input.provider === "claude_managed" && !input.managedProfile) {
@@ -0,0 +1,83 @@
import { spawn } from "node:child_process";
import { mkdir, mkdtemp, rm, stat } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { expect, it, vi } from "vitest";
import { NativeEnvironmentProbeCleanupError, probeNativeRunnerEnvironment } from "./runnerd-codex-transport.js";
it.each(["eventual-exit", "bounded-retry", "windows-unconfirmed"] as const)("retains a timed-out probe home until physical proof and runs cleanup once (%s)", async mode => {
const root = await mkdtemp(join(tmpdir(), "native-probe-retained-"));
const home = join(root, "selected-home");
await mkdir(home, { mode: 0o700 });
let started!: () => void;
const launched = new Promise<void>(resolve => { started = resolve; });
let child: ReturnType<typeof spawn> | undefined;
let exited!: Promise<void>;
const copyBack = vi.fn(async () => {
expect(child!.exitCode !== null || child!.signalCode !== null).toBe(true);
expect((await stat(home)).isDirectory()).toBe(true);
});
const cleanup = vi.fn(async () => {
expect(child!.exitCode !== null || child!.signalCode !== null).toBe(true);
await rm(home, { recursive: true, force: true });
});
const probe = probeNativeRunnerEnvironment({ runtimeDirectory: root, provider: "codex", model: "fixture-model", admissionOnly: true,
environment: {}, timeoutMs: 1000, ...(mode !== "eventual-exit" ? { cleanupTimeoutMs: 100 } : {}), onCleanupConfirmed: cleanup, onCodexCredentialRefresh: copyBack,
transportOptions: { sourceCodexHome: "", runnerBinary: resolve("runner/target/debug/paperclip-runnerd"), codexCommand: resolve("runner/target/debug/fake-codex-app-server"),
runnerProcessLauncher: () => {
child = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { detached: true, stdio: "ignore", env: { PATH: "/usr/bin:/bin" } });
exited = new Promise<void>(resolve => child!.once("exit", () => resolve()));
started();
return {
// A controlled process owner reports an unresolved signal dispatch.
// The real child remains alive until this test's original owner stops it.
child: { pid: child.pid, exitCode: null, signalCode: null, kill: () => true },
processGroupId: null,
isExitConfirmed: () => child!.exitCode !== null || child!.signalCode !== null,
completion: exited.then(() => ({ code: child!.exitCode, signal: child!.signalCode, stdout: "", stderr: "" })),
};
},
},
}).catch(error => error);
try {
await launched;
const failure = await probe;
expect(failure).toBeInstanceOf(NativeEnvironmentProbeCleanupError);
expect(failure.operationError).toBeInstanceOf(Error);
expect(failure.cleanupErrors.length).toBeGreaterThan(0);
let retired = false, retirementError: unknown;
void failure.retired.then(() => { retired = true; }, (error: unknown) => { retirementError = error; });
await new Promise(resolve => setTimeout(resolve, 20));
if (mode !== "eventual-exit") {
await vi.waitFor(() => expect(retirementError).toBeInstanceOf(Error), { timeout: 500 });
expect(failure.retryCleanup).toBeTypeOf("function");
}
expect(retired).toBe(false);
expect(cleanup).not.toHaveBeenCalled();
expect(copyBack).not.toHaveBeenCalled();
expect((await stat(home)).isDirectory()).toBe(true);
expect(child!.exitCode).toBeNull();
child!.kill("SIGKILL");
await exited;
if (mode === "windows-unconfirmed") {
const platform = Object.getOwnPropertyDescriptor(process, "platform")!;
Object.defineProperty(process, "platform", { ...platform, value: "win32" });
try {
await expect(failure.retryCleanup()).rejects.toThrow("platform process retirement remains unconfirmed");
expect(cleanup).not.toHaveBeenCalled();
expect(copyBack).not.toHaveBeenCalled();
expect((await stat(home)).isDirectory()).toBe(true);
} finally { Object.defineProperty(process, "platform", platform); }
}
if (mode !== "eventual-exit") await failure.retryCleanup();
else { await failure.retired; expect(retired).toBe(true); }
expect(cleanup).toHaveBeenCalledTimes(1);
expect(copyBack).toHaveBeenCalledTimes(1);
await expect(stat(home)).rejects.toMatchObject({ code: "ENOENT" });
} finally {
child?.kill("SIGKILL");
await exited;
await probe;
await rm(root, { recursive: true, force: true });
}
}, 40_000);
@@ -77,6 +77,7 @@ import { RUNNERD_CANONICAL_ITEM } from "../drivers/codex/codex-driver-values.js"
import {
probeNativeRunnerEnvironment,
NativeEnvironmentProbeCleanupError,
parseAcpxTurnControlCapabilities,
authorizedToolSetForProvider,
createCapabilityRunnerdCodexTransport,
@@ -123,6 +124,7 @@ it("attributes opened candidate sessions to the harness biller instead of the mo
expect(provider("acpx", "pi", "openrouter/deepseek/deepseek-v4-flash-0731")).toBe("openrouter");
expect(provider("acpx", "claude", "claude-sonnet-5")).toBe("anthropic");
expect(provider("acpx", "codex", "gpt-5.6-sol")).toBe("openai");
expect(provider("acpx", "antigravity", "gemini-3.8-flash-low")).toBe("google");
expect(provider("opencode", "codex", "openrouter/openai/gpt-6-astra")).toBe("openrouter");
expect(provider("claude_managed", "codex", "claude-sonnet-5")).toBe("anthropic");
expect(provider("aws_agentcore", "codex", "global.anthropic.claude-sonnet-4-6")).toBe("aws");
@@ -1996,14 +1998,16 @@ it.each([false, true])("binds a vendored source server to its pnpm dependency ro
expect(runnerdLaunchProfileInternals.acpxProviderPackageAuthority(
join(server, "dist/vendor/paperclip-runner/cli/acpx-runtime-sidecar.cjs"),
)).toEqual({ root: selfRooted ? server : workspace, manifest: join(server, "package.json") });
expect(runnerdLaunchProfileInternals.acpxProviderPackageAuthority(
join(server, "dist/vendor/paperclip-runner/cli/acpx-runtime-sidecar.cjs"), workspace, "pi",
)).toEqual({ root: server, manifest: join(server, "package.json") });
for (const agent of ["pi", "antigravity"] as const) {
expect(runnerdLaunchProfileInternals.acpxProviderPackageAuthority(
join(server, "dist/vendor/paperclip-runner/cli/acpx-runtime-sidecar.cjs"), workspace, agent,
)).toEqual({ root: server, manifest: join(server, "package.json") });
}
} finally {
await rm(workspace, { recursive: true, force: true });
}
});
it.each((["copilot", "pi"] as const).flatMap(agent =>
it.each((["copilot", "pi", "antigravity"] as const).flatMap(agent =>
["source", "scoped-npm", "deployment"].map(layout => ({ agent, layout }))))(
"admits the controller-selected $agent package authority in a $layout layout",
async ({ agent, layout }) => {
@@ -2597,8 +2601,8 @@ it.each(["reuse", "large-instructions", "rotated-recovery", "removed-context", "
const checkpoint = vi.fn();
const releaseRoute = vi.fn();
let currentRun = () => "";
const create = (closeGraceMs = 5_000, runId = "run-prepared") => createCapabilityRunnerdCodexTransport({ provider: "codex", runnerArtifact: artifact,
closeGraceMs,
const create = (closeGraceMs?: number, runId = "run-prepared") => createCapabilityRunnerdCodexTransport({ provider: "codex", runnerArtifact: artifact,
...(closeGraceMs === undefined ? {} : { closeGraceMs }),
stateDirectory: root, runtimeContext: runId === "run-prepared" ? context
: mode === "removed-context" ? null : mode === "omitted-context" ? undefined : context,
...(mode === "rotated-recovery" || mode === "removed-context" || mode === "omitted-context" ? {
@@ -3489,9 +3493,10 @@ it.each(["stuck", "late-release", "late-release-error", "stuck-release", "cooper
let finishRelease!: () => void;
const releasing = new Promise<void>(resolve => { finishRelease = resolve; });
const releaseError = new Error("setup registration release failed");
let releaseFails = mode === "late-release-error";
const release = vi.fn(async () => {
if (mode === "stuck-release") await releasing;
if (mode === "late-release-error") throw releaseError;
if (releaseFails) throw releaseError;
});
const registered = {
connection: { mode: "listen" as const, listenAddress: "127.0.0.1", listenPort: 43127, listenPath: "/setup-probe" },
@@ -3554,9 +3559,24 @@ it.each(["stuck", "late-release", "late-release-error", "stuck-release", "cooper
finishRelease();
await vi.advanceTimersByTimeAsync(0);
expect(release).toHaveBeenCalledOnce();
expect(cleanup).not.toHaveBeenCalled();
expect(failure).toBeInstanceOf(NativeEnvironmentProbeCleanupError);
if (mode === "late-release-error") {
await expect((failure as NativeEnvironmentProbeCleanupError).retired).rejects.toBe(releaseError);
expect(cleanup).not.toHaveBeenCalled();
expect((await stat(runtimeDirectory)).isDirectory()).toBe(true);
releaseFails = false;
await (failure as NativeEnvironmentProbeCleanupError).retryCleanup!();
expect(release).toHaveBeenCalledTimes(2);
expect(cleanup).toHaveBeenCalledOnce();
await expect(stat(runtimeDirectory)).rejects.toThrow();
} else {
await (failure as NativeEnvironmentProbeCleanupError).retired;
expect(cleanup).toHaveBeenCalledOnce();
await expect(stat(runtimeDirectory)).rejects.toThrow();
}
} else {
expect(release).toHaveBeenCalledOnce();
if (failure instanceof NativeEnvironmentProbeCleanupError) await failure.retired;
expect(cleanup).toHaveBeenCalledOnce();
await expect(stat(runtimeDirectory)).rejects.toThrow();
}
@@ -3725,7 +3745,7 @@ it("does not certify probe cleanup while owned credential preparation is unconfi
}
}, 15_000);
it.each(["complete", "api-key", "default-model", "mismatched-model", "model-refresh-error", "model-cleanup-error", "missing-model", "missing-runtime", "missing-completion", "late-bootstrap", "remote-complete", "remote-mismatched-model", "late-remote-preparation", "late-remote-release-error"] as const)("native Codex setup requires its daemon/app-server hello (%s)", async mode => {
it.each(["admission-only", "admission-only-mismatched-model", "complete", "api-key", "default-model", "mismatched-model", "model-refresh-error", "model-cleanup-error", "missing-model", "missing-runtime", "missing-completion", "late-bootstrap", "remote-complete", "remote-mismatched-model", "late-remote-preparation", "late-remote-release-error"] as const)("native Codex setup requires its daemon/app-server hello (%s)", async mode => {
const root = await mkdtemp(join(tmpdir(), "runnerd-native-setup-"));
const runtimeDirectory = join(root, "native-state");
await mkdir(runtimeDirectory, { mode: 0o700 });
@@ -3744,6 +3764,7 @@ it.each(["complete", "api-key", "default-model", "mismatched-model", "model-refr
try {
const probe = probeNativeRunnerEnvironment({
runtimeDirectory, provider: "codex", model: mode === "default-model" ? null : "gpt-6.1-sol", reasoningEffort: "low",
...(mode.startsWith("admission-only") ? { admissionOnly: true } : {}),
environment: { PATH: process.env.PATH, OPENAI_API_KEY: mode === "api-key" ? "selected-account" : "", HOME: "/unrelated/home" },
timeoutMs: mode.startsWith("late-remote-") ? 800 : mode === "late-bootstrap" ? 100 : mode === "missing-completion" ? 1_000 : 10_000,
...(remote ? { workingDirectory: remoteWorkspace } : {}),
@@ -3764,7 +3785,7 @@ it.each(["complete", "api-key", "default-model", "mismatched-model", "model-refr
runnerBinary: mode === "missing-runtime" ? join(root, "missing-runner") : resolve("runner/target/debug/paperclip-runnerd"),
codexCommand: fakeCodex, codexArgs: fakeCodexArgs(root, "--request-log", calls,
...(mode === "missing-completion" ? ["--hold-turn"] : []),
...(mode === "mismatched-model" || mode === "remote-mismatched-model" || mode === "model-refresh-error" || mode === "model-cleanup-error" ? ["--returned-model", "another-model"] : []),
...(mode === "admission-only-mismatched-model" || mode === "mismatched-model" || mode === "remote-mismatched-model" || mode === "model-refresh-error" || mode === "model-cleanup-error" ? ["--returned-model", "another-model"] : []),
...(mode === "missing-model" ? ["--returned-model", ""] : []),
...(mode === "late-bootstrap" ? ["--thread-start-delay-ms", "1000"] : [])),
sourceCodexHome: sourceHome,
@@ -3787,18 +3808,19 @@ it.each(["complete", "api-key", "default-model", "mismatched-model", "model-refr
await rm(runtimeDirectory, { recursive: true, force: true });
},
});
if (mode === "complete" || mode === "api-key" || mode === "default-model" || mode === "remote-complete") {
if (mode === "admission-only" || mode === "complete" || mode === "api-key" || mode === "default-model" || mode === "remote-complete") {
const observedModel = mode === "default-model" ? "gpt-test" : "gpt-6.1-sol";
await expect(probe).resolves.toEqual({ provider: "codex", providerDriver: "codex_app_server", effectiveModel: observedModel, helloProbePassed: true });
await expect(probe).resolves.toEqual({ provider: "codex", providerDriver: "codex_app_server", effectiveModel: observedModel, ...(mode === "admission-only" ? { helloProbePassed: false, admissionOnly: true, sessionAdmissionPassed: true } : { helloProbePassed: true }) });
expect(refreshed).toBe(true);
if (remote) expect(registrationReleased).toBe(true);
expect(cleanupConfirmed).toBe(true);
await expect(stat(runtimeDirectory)).rejects.toThrow();
const requests = (await readFile(calls, "utf8")).trim().split("\n").map(line => JSON.parse(line));
expect(requests.find(value => value.method === "thread/start").params).toMatchObject({ ...(remote ? { cwd: remoteWorkspace } : {}), permissions: "paperclip-runner-workspace-read-only", dynamicTools: [] });
expect(requests.find(value => value.method === "turn/start").params).toMatchObject({ effort: "low", permissions: "paperclip-runner-workspace-read-only" });
if (mode === "admission-only") expect(requests.some(value => value.method === "turn/start")).toBe(false);
else expect(requests.find(value => value.method === "turn/start").params).toMatchObject({ effort: "low", permissions: "paperclip-runner-workspace-read-only" });
} else {
await expect(probe).rejects.toThrow(mode === "missing-runtime" ? /ENOENT|unavailable|cleanup/i : mode === "mismatched-model" || mode === "remote-mismatched-model" || mode === "model-refresh-error" || mode === "model-cleanup-error" || mode === "missing-model" ? /observed model|mismatched provider model/i : /timed out|cleanup|suspension/i);
await expect(probe).rejects.toThrow(mode === "missing-runtime" ? /ENOENT|unavailable|cleanup/i : mode === "admission-only-mismatched-model" || mode === "mismatched-model" || mode === "remote-mismatched-model" || mode === "model-refresh-error" || mode === "model-cleanup-error" || mode === "missing-model" ? /observed model|mismatched provider model/i : /timed out|cleanup|suspension/i);
if (mode === "late-remote-release-error") {
expect(registrationReleased).toBe(true); expect(cleanupConfirmed).toBe(false);
expect((await stat(runtimeDirectory)).isDirectory()).toBe(true);
@@ -3812,11 +3834,11 @@ it.each(["complete", "api-key", "default-model", "mismatched-model", "model-refr
expect(cleanupConfirmed).toBe(true);
expect((await stat(runtimeDirectory)).isDirectory()).toBe(true);
}
if (mode === "mismatched-model" || mode === "remote-mismatched-model" || mode === "missing-model") {
if (mode === "admission-only-mismatched-model" || mode === "mismatched-model" || mode === "remote-mismatched-model" || mode === "missing-model") {
expect(cleanupConfirmed).toBe(true);
await expect(stat(runtimeDirectory)).rejects.toThrow();
}
if (mode === "mismatched-model" || mode === "remote-mismatched-model" || mode === "model-refresh-error" || mode === "model-cleanup-error" || mode === "missing-model" || mode === "late-bootstrap" || mode.startsWith("late-remote-")) {
if (mode === "admission-only-mismatched-model" || mode === "mismatched-model" || mode === "remote-mismatched-model" || mode === "model-refresh-error" || mode === "model-cleanup-error" || mode === "missing-model" || mode === "late-bootstrap" || mode.startsWith("late-remote-")) {
const requests = (await readFile(calls, "utf8").catch(() => "")).trim().split("\n").filter(Boolean).map(line => JSON.parse(line));
expect(requests.some(value => value.method === "turn/start")).toBe(false);
}
@@ -9418,6 +9440,25 @@ describe("cold process admission budget", () => {
});
});
it("retains the authenticated command failure without implicitly logging its structured result", async () => {
const result = { commandId: "command-2", commandType: "session.open", status: "failed",
result: { code: "command_execution_failed", message: "closed runtime failure" } };
let failure: unknown;
try {
await waitForRunnerCommand({ type: "session.open", deadline: Date.now() + 1000, abortOnClose: true,
isClosed: () => false, throwIfFailed: () => {}, command: () => ({ status: "failed", result }),
runnerHasExited: async () => false, failureCode: () => "startup_failed" });
} catch (error) { failure = error; }
expect(failure).toBeInstanceOf(Error);
expect(failure).toHaveProperty("message", `PRP command session.open failed: ${JSON.stringify(result)}`);
const structured = (failure as Error & { runnerCommandResult: unknown }).runnerCommandResult;
expect(structured).toEqual(result);
expect(structured).not.toBe(result);
expect(JSON.stringify(failure)).not.toContain("command_execution_failed");
result.result.message = "mutated after receipt";
expect(structured).toHaveProperty("result.message", "closed runtime failure");
});
it("shares Pi's absolute cold deadline across recovery barriers and rejects a late ACK", async () => {
vi.useFakeTimers();
@@ -1,3 +1,4 @@
import { confirmAntigravityProbeNativeRetirement } from "../drivers/acpx/antigravity-probe-retirement.js";
import { resolveGrokDisableWebSearch, admittedGrokWebSearchPolicy } from "../drivers/acpx/grok-web-search.js";
import { PRP_COMPLETION_TOOL_NAME, PRP_COMPLETION_RESULT_BRIDGE_INPUT_SCHEMA } from "../contracts/completion-result.js";
import { RunnerLaunchPreparationStore } from "./runner-launch-preparation-store.js";
@@ -7,6 +8,7 @@ export { createRunnerdCodexAppServerArgs, resolveRunnerdAcpxPermissionMode, trus
import { applyProviderObservabilityEnvironment, redactObservabilityDiagnostic } from "../drivers/agent-observability.js";
import type { RunnerExecutionDriver } from "../control-plane/runner-execution-driver.js";
import { resolvePinnedOpenCodeCommand } from "../drivers/opencode/opencode-server-driver.js";
import { bindAcpxCandidateAdmission, parseAcpxCandidateAdmission, type AcpxCandidateAdmission, type AcpxCandidateStartupAdmission } from "../drivers/acpx/candidate-admission.js";
import { ACPX_CAPABILITY_PROFILES } from "../drivers/acpx/capability-profiles.js";
import { resolveClaudeEffort, admittedClaudeEffort } from "../drivers/acpx/claude-effort.js";
import { resolveGrokEffort, admittedGrokEffort } from "../drivers/acpx/grok-effort.js";
@@ -1243,6 +1245,8 @@ export interface CapabilityRunnerdCodexTransportOptions extends RunnerExecutionD
acpxAgent?: QualifiedAcpxAgent;
/** Explicit evaluation-only candidate selection, never derived from persisted session input. */
acpxCandidateProfile?: "pi" | "cursor" | "copilot";
/** Server/operator authority; never derived from persisted agent or provider input. */
acpxCandidateAdmission?: AcpxCandidateAdmission;
acpxPermissionMode?: NativeAcpxPermissionMode;
acpxMode?: string;
piThinkingLevel?: "off" | "low" | "high" | "max";
@@ -1390,6 +1394,8 @@ export interface CapabilityRunnerdCodexTransport {
evidence(): Readonly<CapabilityRunnerdProcessEvidence>;
/** Relinquish controller authority without stopping the durable runner. */
detachControllerForRestart(): Promise<void>;
/** Setup-only retained cleanup; joins original work and proves physical retirement. */
retireProbeResources?(timeoutMs?: number): Promise<void>;
}
export type RunnerdCodexTransport = CapabilityRunnerdCodexTransport;
@@ -3061,7 +3067,7 @@ function acpxProviderPackageAuthority(
const workspaceRoot = dirname(serverRoot);
// Source/Docker builds share the workspace's pnpm store. Published npm
// bundles and self-rooted pnpm deployments retain their server authority.
if (agent !== "pi" && basename(serverRoot) === "server" &&
if (agent !== "pi" && agent !== "antigravity" && basename(serverRoot) === "server" &&
!existsSync(resolve(serverRoot, "node_modules/.pnpm")) &&
existsSync(resolve(workspaceRoot, "node_modules/.pnpm")) &&
existsSync(resolve(workspaceRoot, "pnpm-workspace.yaml")) &&
@@ -3084,10 +3090,10 @@ function acpxProviderPackageAuthority(
);
}
const sidecarPackageRoot = resolve(cliDirectory, "../..");
// Native Pi and Copilot closures belong to the runner package itself. Their asset
// Native provider closures belong to the runner package itself. Their asset
// guard requires the bound manifest directly inside that same package root;
// the workspace dependency root used by JS providers is a different owner.
if (agent === "copilot" || agent === "pi") {
if (agent === "copilot" || agent === "pi" || agent === "antigravity") {
return {
root: sidecarPackageRoot,
manifest: resolve(sidecarPackageRoot, "package.json"),
@@ -3123,7 +3129,9 @@ function acpxRunnerLaunchProfile(
options: CapabilityRunnerdCodexTransportOptions,
command: string,
sidecarScript: string,
identity?: DurableRecoveryIdentity,
): {
candidateAdmission?: AcpxCandidateStartupAdmission;
authorityDigest: string;
command: string;
commandSha256: string;
@@ -3132,6 +3140,11 @@ function acpxRunnerLaunchProfile(
} {
const localDigest = (path: string) =>
`sha256:${createHash("sha256").update(readFileSync(path)).digest("hex")}`;
let candidateAdmission: AcpxCandidateStartupAdmission | undefined;
if (options.acpxCandidateAdmission) {
if (!identity || options.runnerFilesystemRoot) throw new Error("ACPX candidate requires an actual local startup identity");
candidateAdmission = bindAcpxCandidateAdmission(options.acpxCandidateAdmission, identity);
}
if (!options.runnerFilesystemRoot) {
const buildCommand = process.execPath;
const buildSidecarCandidates = buildOwnedCliArtifactCandidates(
@@ -3162,6 +3175,7 @@ function acpxRunnerLaunchProfile(
const commandSha256 = localDigest(buildCommand);
const sidecarScriptSha256 = localDigest(buildSidecar);
return {
...(candidateAdmission ? { candidateAdmission } : {}),
authorityDigest: commandDigest({
schema: "paperclip.runner.local-acpx-authority.v1",
commandSha256,
@@ -3364,6 +3378,8 @@ function createCapabilityRunnerdProviderEnvironmentBase(input: {
...(credentialBinding === undefined ? {} : { [ACPX_CREDENTIAL_BINDING_ENV]: credentialBinding }),
...(input.options.acpxAgent === "grok" && input.options.environment?.PAPERCLIP_ACPX_GROK_AUTH_JSON_SECRET
? { PAPERCLIP_ACPX_GROK_AUTH_JSON_SECRET: input.options.environment.PAPERCLIP_ACPX_GROK_AUTH_JSON_SECRET } : {}),
...(input.options.acpxAgent === "antigravity" && input.options.environment?.PAPERCLIP_ACPX_ANTIGRAVITY_AUTH_JSON_SECRET
? { PAPERCLIP_ACPX_ANTIGRAVITY_AUTH_JSON_SECRET: input.options.environment.PAPERCLIP_ACPX_ANTIGRAVITY_AUTH_JSON_SECRET } : {}),
...commonIdentity,
// The verified sidecar bundle cannot use import.meta.url while Node
// executes it through /proc/self/fd. Anchor its closed provider package
@@ -3372,7 +3388,7 @@ function createCapabilityRunnerdProviderEnvironmentBase(input: {
PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT: providerPackageAuthority.root,
PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST:
providerPackageAuthority.manifest,
...(input.options.providerRecoveryPolicy ===
...(!input.options.acpxCandidateAdmission && input.options.providerRecoveryPolicy ===
"allow_replacement_after_governed_wait"
? {
PAPERCLIP_ACPX_PROVIDER_RECOVERY_POLICY:
@@ -3600,6 +3616,8 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
readonly #closedSignal: Promise<void>;
#resolveClosedSignal!: () => void;
#closePromise: Promise<void> | null = null;
#probeRetirement: Promise<void> | null = null;
readonly prepareConversationResume?: CodexAppServerTransport["prepareConversationResume"];
#detachPromise: Promise<void> | null = null;
#startupOperation: Promise<unknown> | null = null;
#attachmentOperation: Promise<void> | null = null;
@@ -3661,9 +3679,33 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
if (options.adoptExistingRunner && !options.stateDirectory?.trim()) {
throw new Error("native_adopted_runner_state_directory_required");
}
if (options.acpxCandidateAdmission !== undefined) {
const candidate = options.acpxCandidateAdmission;
const admitted = parseAcpxCandidateAdmission(candidate, { companyId: candidate.companyId, agent: options.acpxAgent ?? "", model: candidate.model });
this.options = { ...this.options, acpxCandidateAdmission: Object.freeze(admitted), providerRecoveryPolicy: "same_session_only" };
this.prepareConversationResume = params => this.#prepareCandidateConversationResume(params);
if (options.provider !== "acpx" || process.platform !== "darwin" || process.arch !== "arm64"
|| options.runnerFilesystemRoot || options.prepareLaunch || options.runnerProcessLauncher
|| options.homeMode === "preconfigured" || options.lifecyclePolicy?.mode === "warm"
|| options.adoptExistingRunner || options.resumeActiveTurnId
) {
throw new Error("ACPX candidate admission supports only a fresh local isolated per-turn execution");
}
}
if (options.acpxCandidateAdmission && options.resumeProviderSession) {
const prior = options.resumeProviderSession;
const identity = record(prior.providerIdentity);
if (!prior.driverSessionId?.trim() || !prior.providerSessionId?.trim()
|| identity.kind !== "acpx" || identity.normalizedSessionId !== options.prpIdentity?.normalizedSessionId
|| identity.requestedModel !== options.acpxCandidateAdmission.model || identity.effectiveModel !== options.acpxCandidateAdmission.model
|| typeof identity.profileDigest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(identity.profileDigest)) {
throw new Error("ACPX candidate continuation requires the exact retained native conversation identity");
}
}
if (options.provider === "acpx" && options.acpxAgent !== undefined
&& ACPX_CAPABILITY_PROFILES[options.acpxAgent].qualification === "pending"
&& options.acpxCandidateProfile !== options.acpxAgent) {
&& options.acpxCandidateProfile !== options.acpxAgent
&& !(options.acpxAgent === "antigravity" && options.acpxCandidateAdmission)) {
throw new Error("The candidate ACPX profile requires explicit evaluation opt-in");
}
this.#failureSignal = new Promise<never>((_resolve, reject) => {
@@ -4522,6 +4564,42 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
return this.#closePromise;
}
retireProbeResources(timeoutMs?: number): Promise<void> {
if (this.#probeRetirement) return this.#probeRetirement;
const operation = (async () => {
const deadline = Date.now() + probeCleanupBudget(timeoutMs);
// Cancellation/timeouts revoke admission but cannot cancel an in-flight
// filesystem request, registration, or child spawn. Join those originals.
await awaitProbeCleanup(Promise.allSettled([this.#startupOperation, this.#attachmentOperation]), deadline, "joining the original launch");
if (this.#closePromise) await awaitProbeCleanup(this.#closePromise.catch(() => undefined), deadline, "joining the original close");
const handle = this.#handle;
if (handle) {
// Close already exercised the original owner's signal authority. Proof
// must never dispatch another signal using a possibly reused PID/group.
try { await awaitProbeCleanup(handle.completion, deadline, "waiting for the original child exit"); }
catch (error) { if (handle.child.pid !== undefined) throw error; }
await awaitProbeProof(() => handle.isExitConfirmed?.() !== false, deadline, "confirming the original owner exit");
}
// A remote owner must explicitly prove its own process retirement. An
// absent local handle or a dispatched remote signal is not that proof.
if (this.options.prepareLaunch || this.options.runnerFilesystemRoot || this.options.runnerProcessLauncher || this.options.adoptExistingRunner) {
if (handle?.isExitConfirmed?.() !== true && this.#evidence.runnerPid !== null) throw new Error("Native probe remote retirement remains unconfirmed");
}
this.#pumpEventsSafely();
const pids = [...new Set([this.#evidence.runnerPid, this.#evidence.sidecarPid, this.#evidence.agentPid, this.#evidence.providerPid])].filter((pid): pid is number => pid !== null);
if (pids.length && process.platform === "win32") throw new Error("Native probe platform process retirement remains unconfirmed");
await awaitProbeProof(() => pids.every(maintenanceProcessAbsent), deadline, "confirming recorded process absence");
if (this.options.provider === "acpx" && this.options.acpxAgent === "antigravity") {
const sessionId = this.#eventIdentity?.normalizedSessionId ?? this.options.prpIdentity?.normalizedSessionId;
if (sessionId) await awaitProbeCleanup(confirmAntigravityProbeNativeRetirement(this.options.acpxRuntimeDirectory ?? resolve(this.#root, "acpx"), sessionId), deadline, "confirming the original Google owner retirement");
else if (handle?.child.pid !== undefined) throw new Error("Native probe credential lifetime is unidentified");
}
})();
this.#probeRetirement = operation;
void operation.catch(() => { if (this.#probeRetirement === operation) this.#probeRetirement = null; });
return operation;
}
detachControllerForRestart(): Promise<void> {
this.#detachPromise ??= this.#detachControllerOnce();
return this.#detachPromise;
@@ -4898,6 +4976,10 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
params: Record<string, unknown>, identity: DurableRecoveryIdentity,
dynamicTools: Record<string, unknown>[],
) {
if (this.options.acpxCandidateAdmission) {
const candidate = this.options.acpxCandidateAdmission;
parseAcpxCandidateAdmission(candidate, { companyId: candidate.companyId, agent: this.options.acpxAgent ?? "", model: String(params.model ?? "") });
}
if (this.options.prepareLaunch) {
const provider = this.options.provider ?? "codex";
this.#authorizedTools = authorizedToolSetForProvider(provider, dynamicTools);
@@ -4991,6 +5073,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
this.options,
providerNodeCommand,
acpxSidecarPath,
identity,
)
: undefined;
const runnerOpenCodeLaunchProfile =
@@ -5184,6 +5267,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
maxOutboxBytes: RUNNERD_MAX_OUTBOX_BYTES,
p0ReserveBytes: RUNNERD_P0_RESERVE_BYTES,
maxRuntimeMs: 0,
...(this.options.acpxCandidateAdmission ? { maxLifetimeMs: Math.max(1, this.options.acpxCandidateAdmission.expiresAt - Date.now()) } : {}),
reconnectGraceMs: runnerProcessReconnectGraceMs(this.options),
lifecyclePolicy: this.options.lifecyclePolicy,
runnerBinaryPath,
@@ -5240,7 +5324,27 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
};
}
async #prepareCandidateConversationResume(params: Record<string, unknown>): Promise<void> {
const candidate = this.options.acpxCandidateAdmission;
const prior = this.options.resumeProviderSession;
if (!candidate || !prior || this.#core || this.#startupOperation
|| params.threadId !== prior.driverSessionId || params.model !== candidate.model
|| typeof params.cwd !== "string" || params.cwd !== this.options.resumeWorkingDirectory
|| existsSync(resolve(this.#root, "control-plane", "control-plane-state.json"))
|| existsSync(resolve(this.#root, "runner", "runner-state.json"))) {
throw new Error("ACPX candidate continuation requires a fresh daemon and the exact current conversation selection");
}
parseAcpxCandidateAdmission(candidate, { companyId: candidate.companyId, agent: "antigravity", model: String(params.model) });
this.#checkpointProviderIdentityExpectation = {
driverSessionId: prior.driverSessionId, providerSessionId: prior.providerSessionId!,
providerIdentity: structuredClone(record(prior.providerIdentity)),
};
await this.#trackStartup(() => this.#start(params));
if (!this.#checkpointProviderIdentityConfirmed) throw new Error("ACPX candidate continuation has no verified native conversation receipt");
}
async #resume(): Promise<void> {
if (this.options.acpxCandidateAdmission) throw new Error("ACPX candidate durable runner recovery is not admitted");
const desiredIdentity = this.options.prpIdentity;
if (desiredIdentity === undefined) {
throw new Error("PRP provider resume state is unavailable");
@@ -5531,6 +5635,7 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
this.options,
providerNodeCommand,
acpxSidecarPath,
identity,
)
: undefined;
const runnerOpenCodeLaunchProfile =
@@ -6875,7 +6980,8 @@ class DurablePrpCodexTransport implements CodexAppServerTransport {
descriptor.agent === "claude" ||
descriptor.agent === "codex" ||
descriptor.agent === "cursor" ||
descriptor.agent === "copilot"
descriptor.agent === "copilot" ||
descriptor.agent === "antigravity"
)
this.#evidence.acpxAgent = descriptor.agent;
if (typeof descriptor.agentServerVersion === "string")
@@ -7344,19 +7450,76 @@ export function createCapabilityRunnerdCodexTransport(
transport,
evidence: () => transport.evidence(),
detachControllerForRestart: () => transport.detachControllerForRestart(),
retireProbeResources: timeoutMs => transport.retireProbeResources(timeoutMs),
};
}
export const createRunnerdCodexTransport =
createCapabilityRunnerdCodexTransport;
const retainedNativeProbeCleanups = new Set<{ run: () => Promise<void> }>();
function probeCleanupBudget(timeoutMs = 10_000): number {
return Number.isFinite(timeoutMs) && timeoutMs > 0 ? Math.min(timeoutMs, 10_000) : 10_000;
}
async function awaitProbeCleanup<T>(pending: Promise<T>, deadline: number, detail: string): Promise<T> {
let timeout: ReturnType<typeof setTimeout> | undefined;
try {
return await Promise.race([pending, new Promise<never>((_resolve, reject) => {
timeout = setTimeout(() => reject(new Error(`Native probe retirement timed out while ${detail}; recovery state retained.`)), Math.max(0, deadline - Date.now()));
})]);
} finally { clearTimeout(timeout); }
}
async function awaitProbeProof(proven: () => boolean, deadline: number, detail: string): Promise<void> {
while (!proven()) {
if (Date.now() >= deadline) throw new Error(`Native probe retirement remains unconfirmed while ${detail}; recovery state retained.`);
await new Promise(resolve => setTimeout(resolve, Math.min(100, Math.max(1, deadline - Date.now()))));
}
}
export class NativeEnvironmentProbeCleanupError extends AggregateError {
readonly code = "NATIVE_ENVIRONMENT_PROBE_CLEANUP_PENDING";
constructor(
readonly retired: Promise<void>,
readonly operationError: unknown,
readonly cleanupErrors: readonly unknown[],
readonly retryCleanup?: () => Promise<void>,
) {
const errors = [...(operationError === undefined ? [] : [operationError]), ...cleanupErrors];
super(errors, errors.map(error => error instanceof Error ? error.message : "Native probe cleanup failed.").join("; "), { cause: operationError ?? cleanupErrors[0] });
this.name = "NativeEnvironmentProbeCleanupError";
}
}
function retainNativeProbeCleanup(cleanup: (retry: boolean) => Promise<void>) {
let operation: Promise<void> | undefined;
let attempts = 0;
const owner = { run: (): Promise<void> => {
if (operation) return operation;
const retry = attempts++ > 0;
operation = Promise.resolve().then(() => cleanup(retry)).then(() => {
retainedNativeProbeCleanups.delete(owner);
}, error => { operation = undefined; throw error; });
// The HTTP caller may retain only the error, without awaiting retirement.
// Keep awaited failures observable while preventing detached rejections.
void operation.catch(() => undefined);
return operation;
} };
retainedNativeProbeCleanups.add(owner);
return { retired: owner.run(), retryCleanup: owner.run };
}
export async function probeNativeRunnerEnvironment(options: {
runtimeDirectory: string;
provider: "codex" | "opencode" | "acpx";
model: string | null;
reasoningEffort?: string;
/** Verify native session identity/model and cleanup without a billable turn. */
admissionOnly?: boolean;
environment: NodeJS.ProcessEnv;
timeoutMs?: number;
/** A finite cleanup budget, capped at the ordinary ten-second boundary. */
cleanupTimeoutMs?: number;
workingDirectory?: string;
transportOptions?: CapabilityRunnerdCodexTransportOptions;
onCodexCredentialRefresh?: (filename: string) => Promise<void>;
@@ -7365,7 +7528,9 @@ export async function probeNativeRunnerEnvironment(options: {
provider: "codex" | "opencode" | "acpx";
providerDriver: string;
effectiveModel: string;
helloProbePassed: true;
helloProbePassed: boolean;
admissionOnly?: true;
sessionAdmissionPassed?: true;
}> {
const environment = { ...options.environment };
const remoteFilesystemRoot = options.transportOptions?.runnerFilesystemRoot;
@@ -7376,7 +7541,21 @@ export async function probeNativeRunnerEnvironment(options: {
const controller = new AbortController();
let registrationTeardownError: unknown;
let registrationOperation: Promise<unknown> | undefined;
let registrationReleaseOperation: Promise<void> | undefined;
let registrationReleaseFailed = false;
let releaseRegistration: (() => Promise<void>) | undefined;
let cleanupCallbackOperation: Promise<void> | undefined;
let cleanupCallbackFailed = false;
let credentialRefreshOperation: Promise<void> | undefined;
let credentialRefreshFailed = false;
const preserveCredentialRefresh = (retry = false) => {
if (retry && credentialRefreshFailed) { credentialRefreshOperation = undefined; credentialRefreshFailed = false; }
return credentialRefreshOperation ??= Promise.resolve()
.then(() => options.onCodexCredentialRefresh?.(resolve(remoteFilesystemRoot ?? options.runtimeDirectory, "codex-home", "auth.json")))
.catch(error => { credentialRefreshFailed = true; throw error; });
};
let registrationCleanupConfirmed = true;
const cleanupBudget = probeCleanupBudget(options.cleanupTimeoutMs);
const sourceCodexHome = ownsPreparation ? undefined : options.transportOptions?.sourceCodexHome ?? resolveSourceCodexHome(environment);
delete environment.PAPERCLIP_RUNNER_EXTERNAL_SANDBOX;
if (!ownsPreparation) {
@@ -7392,13 +7571,14 @@ export async function probeNativeRunnerEnvironment(options: {
controller.signal.throwIfAborted();
registrationCleanupConfirmed = false;
const registration = await options.transportOptions!.controlPlaneRegistration!(authority, identity, controller.signal);
let releaseOperation: Promise<void> | undefined;
const release = () => releaseOperation ??= Promise.resolve()
const release = () => registrationReleaseOperation ??= Promise.resolve()
.then(() => registration.release())
.then(() => { registrationCleanupConfirmed = true; }, error => {
registrationReleaseFailed = true;
registrationTeardownError = error;
throw error;
});
releaseRegistration = release;
if (controller.signal.aborted) {
await release();
controller.signal.throwIfAborted();
@@ -7419,7 +7599,7 @@ export async function probeNativeRunnerEnvironment(options: {
externallySandboxed: options.transportOptions?.externallySandboxed === true,
disableCodexProjectInstructions: true,
disableCodexWebSearch: options.provider === "codex",
closeGraceMs: 10_000,
closeGraceMs: cleanupBudget,
lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null },
});
const driver = new CodexAppServerDriver({
@@ -7453,7 +7633,7 @@ export async function probeNativeRunnerEnvironment(options: {
let effectiveModel: string | undefined;
let failure: unknown;
const operation = (async () => {
session = await driver.openSession({ runId: "environment-probe", normalizedSessionId: "environment-probe",
session = await driver.openSession({ runId: options.transportOptions?.prpIdentity?.runId ?? "environment-probe", normalizedSessionId: options.transportOptions?.prpIdentity?.normalizedSessionId ?? "environment-probe",
workingDirectory, signal: controller.signal });
controller.signal.throwIfAborted();
const ids = session.ids();
@@ -7472,6 +7652,7 @@ export async function probeNativeRunnerEnvironment(options: {
if (!effectiveModel || (options.model !== null && effectiveModel !== options.model)) throw new Error("The selected native runtime returned a missing or mismatched provider model.");
}
controller.signal.throwIfAborted();
if (options.admissionOnly) { completed = true; return; }
const { turnId } = await session.startTurn({ message: { role: "user", text: "Respond only with hello. Do not use tools or inspect files." } });
let responseObserved = false;
while (true) {
@@ -7503,7 +7684,7 @@ export async function probeNativeRunnerEnvironment(options: {
clearTimeout(timer);
controller.abort(failure ?? new Error("Native provider hello probe complete."));
const teardownErrors: unknown[] = [];
const teardownDeadline = Date.now() + 10_000;
const teardownDeadline = Date.now() + cleanupBudget;
const awaitTeardown = async (pending: Promise<unknown>, detail: string): Promise<void> => {
let timeout: ReturnType<typeof setTimeout> | undefined;
try {
@@ -7515,8 +7696,9 @@ export async function probeNativeRunnerEnvironment(options: {
]);
} finally { clearTimeout(timeout); }
};
const closingOperation = session ? session.close({ reason: "environment probe complete" }) : bundle.transport.close("environment probe complete");
try {
await awaitTeardown(session ? session.close({ reason: "environment probe complete" }) : bundle.transport.close("environment probe complete"), "closing the selected runtime");
await awaitTeardown(closingOperation, "closing the selected runtime");
if (bundle.evidence().runnerPid !== null && !bundle.evidence().runnerExited) throw new Error("Native setup probe cleanup is incomplete.");
} catch (error) { teardownErrors.push(error); }
try { await awaitTeardown(operation.catch(() => undefined), "draining native setup"); }
@@ -7528,21 +7710,41 @@ export async function probeNativeRunnerEnvironment(options: {
if (registrationTeardownError !== undefined && !teardownErrors.includes(registrationTeardownError)) teardownErrors.push(registrationTeardownError);
if (!registrationCleanupConfirmed) teardownErrors.push(new Error("Native setup probe authority release is unconfirmed; recovery state retained."));
if (bundle.evidence().runnerPid !== null && bundle.evidence().runnerExited && options.provider === "codex" && options.onCodexCredentialRefresh) {
try { await options.onCodexCredentialRefresh(resolve(remoteFilesystemRoot ?? options.runtimeDirectory, "codex-home", "auth.json")); }
try {
await awaitTeardown(preserveCredentialRefresh(), "preserving credential refresh");
}
catch (error) { teardownErrors.push(error); }
}
const confirmCleanup = (retry = false) => {
if (retry && cleanupCallbackFailed) { cleanupCallbackOperation = undefined; cleanupCallbackFailed = false; }
return cleanupCallbackOperation ??= Promise.resolve().then(() => options.onCleanupConfirmed?.())
.catch(error => { cleanupCallbackFailed = true; throw error; });
};
if (!teardownErrors.length && options.onCleanupConfirmed) {
try { await options.onCleanupConfirmed(); }
try { await awaitTeardown(confirmCleanup(), "confirming credential handoff"); }
catch (error) { teardownErrors.push(error); }
}
if (teardownErrors.length) {
const errors = [...(failure === undefined ? [] : [failure]), ...teardownErrors];
throw new AggregateError(errors, errors.map(error => error instanceof Error ? error.message : "Native probe cleanup failed.").join("; "), { cause: failure ?? teardownErrors[0] });
const retained = retainNativeProbeCleanup(async retry => {
const deadline = Date.now() + cleanupBudget;
await awaitProbeCleanup(Promise.allSettled([operation, closingOperation, registrationOperation]), deadline, "joining original probe operations");
if (!bundle.retireProbeResources) throw new Error("Native probe physical retirement proof is unavailable");
await awaitProbeCleanup(bundle.retireProbeResources(Math.max(1, deadline - Date.now())), deadline, "confirming physical retirement");
if (retry && registrationReleaseFailed) { registrationReleaseOperation = undefined; registrationReleaseFailed = false; }
if (!registrationCleanupConfirmed && releaseRegistration) await awaitProbeCleanup(releaseRegistration(), deadline, "releasing original probe authority");
if (bundle.evidence().runnerPid !== null && options.provider === "codex" && options.onCodexCredentialRefresh) {
await awaitProbeCleanup(preserveCredentialRefresh(retry), deadline, "preserving credential refresh");
}
if (!registrationCleanupConfirmed) throw new Error("Native probe authority release remains unconfirmed");
await awaitProbeCleanup(confirmCleanup(retry), deadline, "confirming credential handoff");
});
throw new NativeEnvironmentProbeCleanupError(retained.retired, failure, teardownErrors, retained.retryCleanup);
}
}
if (failure !== undefined) throw failure;
if (!completed) throw new Error("The native provider hello probe did not complete.");
return Object.freeze({ provider: options.provider, providerDriver: bundle.evidence().providerDriver!, effectiveModel: effectiveModel!, helloProbePassed: true });
return Object.freeze({ provider: options.provider, providerDriver: bundle.evidence().providerDriver!, effectiveModel: effectiveModel!,
...(options.admissionOnly ? { helloProbePassed: false, admissionOnly: true as const, sessionAdmissionPassed: true as const } : { helloProbePassed: true }) });
}
/** The transport provider identifies the biller, independently of model family. */
@@ -7560,6 +7762,7 @@ function openedThreadModelProvider(
if (acpxAgent === "claude") return "anthropic";
if (acpxAgent === "cursor") return "cursor";
if (acpxAgent === "copilot") return "github";
if (acpxAgent === "antigravity") return "google";
}
return "openai";
}
@@ -7631,6 +7834,11 @@ export function coldAdmissionTimeoutMs(provider: string | undefined, agent: stri
return coldProcess && provider === "acpx" && agent === "pi" ? 60_000 : 30_000;
}
export interface RunnerCommandFailure extends Error {
/** Private diagnostic value copied from the authenticated command result. */
readonly runnerCommandResult: unknown;
}
/** Shared polling path keeps startup cancellation separate from owned cleanup commands. */
export async function waitForRunnerCommand(input: {
type: string;
@@ -7650,7 +7858,9 @@ export async function waitForRunnerCommand(input: {
const command = input.command();
if (command?.status === "completed") return;
if (command !== undefined && command.status !== "pending") {
throw new Error(`PRP command ${input.type} ${command.status}: ${JSON.stringify(command.result)}`);
const error = new Error(`PRP command ${input.type} ${command.status}: ${JSON.stringify(command.result)}`) as RunnerCommandFailure;
Object.defineProperty(error, "runnerCommandResult", { value: structuredClone(command.result), enumerable: false });
throw error;
}
if (await input.runnerHasExited()) {
throw new Error(`runnerd exited while waiting for ${input.type}`);
@@ -1,4 +1,4 @@
// Grok admission source identity: sha256:79188d089f3f6b575726af77fe055a58449144a373a0745dcad4be414cba8cd0
// Grok admission source identity: sha256:9d778ec8c5987e304d3121787806f546c7c9f545cf5e0628ce0035c59a559907
// This launcher is admitted by digest and receives a verified native executable.
// Never resolve an executable through PATH or accept command-line overrides.
const { isAbsolute, join } = require("node:path");
@@ -0,0 +1,52 @@
{
"commandDigest": "sha256:9473de6ef4baf2bd82fe8b6757239bdc160960b0a78ae6f47fa9e0996544226c",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "pi",
"agentProfileVersion": 35,
"acpxVersion": "0.13.1",
"agentServerVersion": "0.0.33",
"agentRuntimeVersion": "1.0.0",
"nodeVersion": "24.21.0",
"closure": {
"darwin-arm64": "282022db10150c6632b3444df421342e7d534bdf5d5fb1097a2e79d0625a2bcf",
"darwin-x64": "64e251e19009f755c0b04f73ce2138246faab71a961b0f13d75ebfcc34bef12e",
"linux-x64": "713b1fdff42fb56a1518bdc084f181d70bee8ebadc3e4b1d76321ed9108c8410"
},
"wrapperSha256": "9d129b3d38772e93e97080aa6c4e574ac5df4e47ce5bc331a484a9fbf8188b36",
"helperSha256": "41e0490b617da0d60e0c8ec58ef311236f945129d99f816cff6897f78da7f91d",
"extensionSha256": "f5818573a355702388072b28926897db546a5cf346b5171f646582ce5f9102a4",
"agentFilesBinding": "registered-runtime-context-v1",
"nativeQuestions": "select-confirm-input-editor-v4-root-field-types",
"pathPolicy": "sdk-normalization-v1",
"mcpToolNames": "provider-safe-exact-source-v1",
"acpxPatchSha256": "1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2",
"nativeAssistantMessages": "pi1-rpc-deltas-v3-bound-message-and-tool-receipts",
"nativeNotices": "session-extension-v2-bounded-runtime-failure",
"messageProjectionSha256": "17d1e91b948dfa6a56e1905645add52b502041ad843c8d50a13b07956f22aadd",
"noticeProjectionSha256": "014485adc690998e395d334ea3ae67f72be6060a2fc78a1185df7b8d1da28fbf",
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
"nativeInputDisposition": "queued-only-v1",
"cacheWarming": "disabled-owned-decision-v1",
"thinkingLevel": {
"supported": [
"off",
"low",
"high",
"max"
],
"newConfigurationDefault": "low",
"admission": "explicit-native-effective-before-prompt-v1",
"recovery": "mode-bound-v1"
},
"modelSelection": "caller-selected-native-verified-v1",
"providerConfigurationSourceSha256": "75a9a13f13f224dc092bd8ebf49e8747162172943761e44476c8eb18d80c52d4",
"credentialEnvironmentSourceSha256": "2ed68b9aed9756fdd682721833eb2aeaa58f30f1dc8825c88886cc8e7ee92abe",
"runtimeSandboxSourceSha256": "b9f05e522f92b6bade51c7a17f8d28bf31ab9db26510b8d5ddce8519d563a875",
"recoveryIdentitySourceSha256": "f74d061cc07b0c7ca04df62ca991d940a733d9ef6594b3362451769fc10030b8",
"nativeCancellation": "acknowledged-cancelled-terminal-preserves-usage-v1",
"dependencySecurityPatchSha256": "5273a195b952f233336122842faaf650193c04f36b9359d20d4d63079b0ce814",
"openRouterAttributionSourceSha256": "1211191529d6073b733b311c3d69cada4973eede0f3e540c46e261f78e0b90f9",
"preparedHarnessHomeSourceSha256": "bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e"
}
}
@@ -47,6 +47,25 @@ test("the ACPX sidecar schema accepts each versioned message family", () => {
}
});
test("candidate startup and sidecar admission share one strict identity-bound JSON record", () => {
const candidateAdmission = { schema: "paperclip.acpx-candidate-admission.v1", companyId: "company-1", agent: "antigravity", model: "gemini-3.8-flash-low",
profileDigest: `sha256:${"a".repeat(64)}`, nativeClosureDigest: `sha256:${"b".repeat(64)}`, platform: "darwin", architecture: "arm64",
expiresAt: 1800000060000, runId: "run-1", normalizedSessionId: "session-1" };
const startup = new Ajv2020({ allErrors: true, strict: true }).compile({ ...schema, oneOf: undefined, $ref: "#/$defs/candidateAdmission" });
assert.equal(startup(candidateAdmission), true, JSON.stringify(startup.errors));
for (const command of ["initialize", "session.open"]) {
const request = { protocolVersion, id: 1, command, params: { agent: "antigravity", model: candidateAdmission.model, candidateAdmission } };
assert.equal(validate(request), true, JSON.stringify(validate.errors));
for (const change of [{ unknown: true }, { runId: undefined }, { normalizedSessionId: "wrong session" }, { platform: "linux" }, { expiresAt: "1800000060000" }]) {
const altered = JSON.parse(JSON.stringify({ ...request, params: { ...request.params, candidateAdmission: { ...candidateAdmission, ...change } } }));
assert.equal(validate(altered), false);
assert.equal(startup(altered.params.candidateAdmission), false);
}
assert.equal(validate({ ...request, params: { ...request.params, agent: "codex" } }), false);
}
assert.equal(validate({ protocolVersion, id: 1, command: "turn.start", params: { candidateAdmission } }), false);
});
test("Pi session admission requires an explicit supported thinking level including recovery", () => {
const open = (params) => ({ protocolVersion, id: 1, command: "session.open", params });
for (const piThinkingLevel of ["off", "low", "high", "max"]) {
@@ -0,0 +1,88 @@
{
"commandDigest": "sha256:99d3a7f3853bc3f518c1579cbe165f38fd4764c020a688814d3adfd7e3d7b1a8",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 82,
"acpxVersion": "0.13.1",
"agentServerVersion": "1.0.88",
"protocolPolicyRevision": "copilot-agent-manual-v1",
"acpxPatchSha256": "1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2",
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
"agentFilesBinding": "registered-runtime-context-v1",
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
"messageIdentityContract": "copilot-native-message-id-v1",
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
"permissionContextContract": "copilot-edit-permission-context-v1",
"permissionContextSourceSha256": "de2d38d702b3c574161af0c0c04790fb6f6347916eb3916ce7a131a8c229121b",
"permissionAdapterSourceSha256": "ee5929ed2ee647ad0369465dee9d51a00b2dd28e1ff735b43e969ddc9c164668",
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
"semanticBridgeSourceSha256": "8764b8c9032fdc7d1415b01e75a9fc57c550e7a5e11da4bed7d9100082872ae5",
"toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706",
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
"semanticSidecarSourceSha256": "abe3e6c718e8e0fed154526affc94e34b46d96f7bdaed5d88e2b00a21a5b84db",
"semanticDirectDriverSourceSha256": "4ec65aa1d4e905a1e732f2d043bbd39de1ab92f380276ede37d6a72efc19c64d",
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
"semanticCompletionContractSourceSha256": "f49398b682a5623812f5d3c33d6bb9a749008e47cc0ed969072041615f66df59",
"semanticValidatorsSourceSha256": "b20ec8ee6e7165117233ce4421a975bb9d3f276220ce31655c0b9a717cb2d9cb",
"semanticSchemaBundleSourceSha256": "81ef98ca7b34896a0f76390ac7af3dd8ef7609fe6f38d54bd6a69373d50fe40b",
"semanticSidecarProtocolSourceSha256": "26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
"displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877",
"displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e",
"configuredEnvironmentSourceSha256": "ecef0ada320a762e0a3d91c38c72d4e19e2c8e8be8c060938a47e7f1a029058a",
"configuredEnvironmentRustSourceSha256": "1671cb2735b3fa119d32f08d4a12b21b9f78a0e4d7285eb63c35df79ce17bd3c",
"profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282",
"profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571",
"profileInstallationSourceSha256": "58841b2d3b47ab9956db20608802edcd0e55cf5856002dc7c51e05e75bdcb8f6",
"attachmentPolicySourceSha256": "4f7638b7cc64e18f6a4ba33fd46de2b3d49225e32aeab41f5c695b268fa4f8e2",
"singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f",
"nativeSessionRuntimeSourceSha256": "1fe6fa9e6b96d84d70432b85a57c473f833606d4a521563d5022b4d3c41576ce",
"nativeRuntimeContextSourceSha256": "71607f80dbb8dc5cc6c123d0b8f3995d6c5407277695a8c82cbedc90fb8f4842",
"nativeRuntimePromptsSourceSha256": "fff5755dba9b7dce8a5f10ff6ac6d5ed06b92ed079ce1e0a44ac72f2ce7869c1",
"nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90",
"harnessSessionContractSourceSha256": "baba1a210b863518f01e0c875606ed37f9467c433222251438578bd999bc43b5",
"harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249",
"harnessSessionSourceSha256": "81361733e86b56dd5a66415aefc6f72e69be24acc8d3417370154ebb7fef6e94",
"transportContractSourceSha256": "06f06c53ecae304f72034ffb2f659ff087c82a70ed4994daf21a9d2edaf56e4e",
"nativeBackendSourceSha256": "7495d9898d22dca1ebf6336361d332bafefc4498f6e49f1135025fad846c8586",
"nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44",
"nativeInstructionsSourceSha256": "82ac6e5f7275b4ef79fb166c7330efc504e80cab3d7e9081e4e40a8b570bc50b",
"providerLaunchSourceSha256": "c744235b3067ea75973523f78f1e13399337bc155f1775af694e34782d5c9bef",
"launchPreparationStoreSourceSha256": "ff6025568bbc0aa6ce545772d73dfca2d3da226740bd3a7b386b069e76cff296",
"runtimeRequestBridgeSourceSha256": "8442088daf04fc7f09bca2fbb32bf5e9b550be6a4606f8c2ff3a23a268fa0564",
"runtimeRequestFacadeSourceSha256": "d0ed28e0c5ddad34db8f437e704c8a755eb0b47f27e08e44185f66d1d2205295",
"runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211",
"guardedInstallationSourceSha256": "64b137e3ef695e7d03013ea2b6c8830171e0a73e0bf2e73d55e9ced57a47bcf6",
"nativeExecutionSourceSha256": "116c348e232848a80c5556494c109314baafb20fb5c97133d9656c7732ee54be",
"profileCompatibilitySourceSha256": "d3bd56d7c2d924c0ed623e4b92aea9b858020a3e4a6245e20c91476079e1bbf8",
"installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d",
"assetOwnershipSourceSha256": "e5bb6bc58b3f0e957a64b2cd99c710337ffcd9c0e1fadb341f48e033b64b99f5",
"assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f",
"credentialBindingSourceSha256": "2ed68b9aed9756fdd682721833eb2aeaa58f30f1dc8825c88886cc8e7ee92abe",
"admissionDiagnosticsSourceSha256": "df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af",
"grokAdmissionSourceSha256": "f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777",
"sidecarErrorSourceSha256": "17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c",
"profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456",
"metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727",
"permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"semanticContinuationSourceSha256": "c9cc0fd516b3df92a6655f56ee9bef37615a889c371e2e137d8ac5c1176de811",
"preparedHarnessHomeSourceSha256": "bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e",
"runtimeHostSourceSha256": "26b2e115b3f4ae7c20e9d963d117e0286c958af81386deade83397f12abf9662",
"runtimeAdapterSourceSha256": "fab1e278e0f2aed55b35e57d14329eb431b79a08040fa079aa2403842d16085e",
"runtimeSandboxSourceSha256": "b9f05e522f92b6bade51c7a17f8d28bf31ab9db26510b8d5ddce8519d563a875",
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
"nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b"
}
}
@@ -0,0 +1,88 @@
{
"commandDigest": "sha256:56361a790c38c59633ad4a5adad2d3b4a9098974f15264e4b506d69807902bea",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 83,
"acpxVersion": "0.13.1",
"agentServerVersion": "1.0.88",
"protocolPolicyRevision": "copilot-agent-manual-v1",
"acpxPatchSha256": "1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2",
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
"agentFilesBinding": "registered-runtime-context-v1",
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
"messageIdentityContract": "copilot-native-message-id-v1",
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
"permissionContextContract": "copilot-edit-permission-context-v1",
"permissionContextSourceSha256": "de2d38d702b3c574161af0c0c04790fb6f6347916eb3916ce7a131a8c229121b",
"permissionAdapterSourceSha256": "ee5929ed2ee647ad0369465dee9d51a00b2dd28e1ff735b43e969ddc9c164668",
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
"semanticBridgeSourceSha256": "8764b8c9032fdc7d1415b01e75a9fc57c550e7a5e11da4bed7d9100082872ae5",
"toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706",
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
"semanticSidecarSourceSha256": "abe3e6c718e8e0fed154526affc94e34b46d96f7bdaed5d88e2b00a21a5b84db",
"semanticDirectDriverSourceSha256": "4ec65aa1d4e905a1e732f2d043bbd39de1ab92f380276ede37d6a72efc19c64d",
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
"semanticCompletionContractSourceSha256": "f49398b682a5623812f5d3c33d6bb9a749008e47cc0ed969072041615f66df59",
"semanticValidatorsSourceSha256": "b20ec8ee6e7165117233ce4421a975bb9d3f276220ce31655c0b9a717cb2d9cb",
"semanticSchemaBundleSourceSha256": "81ef98ca7b34896a0f76390ac7af3dd8ef7609fe6f38d54bd6a69373d50fe40b",
"semanticSidecarProtocolSourceSha256": "26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
"displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877",
"displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e",
"configuredEnvironmentSourceSha256": "ecef0ada320a762e0a3d91c38c72d4e19e2c8e8be8c060938a47e7f1a029058a",
"configuredEnvironmentRustSourceSha256": "1671cb2735b3fa119d32f08d4a12b21b9f78a0e4d7285eb63c35df79ce17bd3c",
"profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282",
"profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571",
"profileInstallationSourceSha256": "58841b2d3b47ab9956db20608802edcd0e55cf5856002dc7c51e05e75bdcb8f6",
"attachmentPolicySourceSha256": "4f7638b7cc64e18f6a4ba33fd46de2b3d49225e32aeab41f5c695b268fa4f8e2",
"singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f",
"nativeSessionRuntimeSourceSha256": "1fe6fa9e6b96d84d70432b85a57c473f833606d4a521563d5022b4d3c41576ce",
"nativeRuntimeContextSourceSha256": "71607f80dbb8dc5cc6c123d0b8f3995d6c5407277695a8c82cbedc90fb8f4842",
"nativeRuntimePromptsSourceSha256": "fff5755dba9b7dce8a5f10ff6ac6d5ed06b92ed079ce1e0a44ac72f2ce7869c1",
"nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90",
"harnessSessionContractSourceSha256": "baba1a210b863518f01e0c875606ed37f9467c433222251438578bd999bc43b5",
"harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249",
"harnessSessionSourceSha256": "81361733e86b56dd5a66415aefc6f72e69be24acc8d3417370154ebb7fef6e94",
"transportContractSourceSha256": "06f06c53ecae304f72034ffb2f659ff087c82a70ed4994daf21a9d2edaf56e4e",
"nativeBackendSourceSha256": "7495d9898d22dca1ebf6336361d332bafefc4498f6e49f1135025fad846c8586",
"nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44",
"nativeInstructionsSourceSha256": "82ac6e5f7275b4ef79fb166c7330efc504e80cab3d7e9081e4e40a8b570bc50b",
"providerLaunchSourceSha256": "c744235b3067ea75973523f78f1e13399337bc155f1775af694e34782d5c9bef",
"launchPreparationStoreSourceSha256": "ff6025568bbc0aa6ce545772d73dfca2d3da226740bd3a7b386b069e76cff296",
"runtimeRequestBridgeSourceSha256": "f9b5a400a449f36de123bb511b21c4cb4564b50081f9958b92e6b7f0c6891d47",
"runtimeRequestFacadeSourceSha256": "d0ed28e0c5ddad34db8f437e704c8a755eb0b47f27e08e44185f66d1d2205295",
"runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211",
"guardedInstallationSourceSha256": "64b137e3ef695e7d03013ea2b6c8830171e0a73e0bf2e73d55e9ced57a47bcf6",
"nativeExecutionSourceSha256": "116c348e232848a80c5556494c109314baafb20fb5c97133d9656c7732ee54be",
"profileCompatibilitySourceSha256": "d3bd56d7c2d924c0ed623e4b92aea9b858020a3e4a6245e20c91476079e1bbf8",
"installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d",
"assetOwnershipSourceSha256": "e5bb6bc58b3f0e957a64b2cd99c710337ffcd9c0e1fadb341f48e033b64b99f5",
"assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f",
"credentialBindingSourceSha256": "2ed68b9aed9756fdd682721833eb2aeaa58f30f1dc8825c88886cc8e7ee92abe",
"admissionDiagnosticsSourceSha256": "df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af",
"grokAdmissionSourceSha256": "f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777",
"sidecarErrorSourceSha256": "17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c",
"profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456",
"metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727",
"permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"semanticContinuationSourceSha256": "c9cc0fd516b3df92a6655f56ee9bef37615a889c371e2e137d8ac5c1176de811",
"preparedHarnessHomeSourceSha256": "bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e",
"runtimeHostSourceSha256": "26b2e115b3f4ae7c20e9d963d117e0286c958af81386deade83397f12abf9662",
"runtimeAdapterSourceSha256": "fab1e278e0f2aed55b35e57d14329eb431b79a08040fa079aa2403842d16085e",
"runtimeSandboxSourceSha256": "b9f05e522f92b6bade51c7a17f8d28bf31ab9db26510b8d5ddce8519d563a875",
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
"nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b"
}
}
@@ -0,0 +1,88 @@
{
"commandDigest": "sha256:a95f1d8e4d0a18d0c2cbc3bffdf998a2c5b8a582bdf51a72085b01d948d0a04e",
"declaration": {
"schema": "paperclip.acpx_profile_declaration.v1",
"agent": "copilot",
"agentProfileVersion": 84,
"acpxVersion": "0.13.1",
"agentServerVersion": "1.0.88",
"protocolPolicyRevision": "copilot-agent-manual-v1",
"acpxPatchSha256": "1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2",
"policySha256": "a4be69be43db84052560420a3ca546a665c9165573c24d543eb33d144ba138a3",
"distributionSourceSha256": "c0a65e247fc47b263bd369a3d5ab505c4b323d4e6493be8a9ecbeb0e9dbb9107",
"agentFilesBinding": "registered-runtime-context-v1",
"systemInstructionDelivery": "COPILOT_HOME/copilot-instructions.md:replace-under-lifetime-lease-before-launch:v1",
"sharedRuntimeContract": "paperclip.acpx-runtime-contract.v1",
"innerDistributionSourceSha256": "9bb08347eed01f1ab44e065c8b06ead9be3a25973d6e6329dc4c0a099096279a",
"upstreamAppSha256": "7b48282a19b5b0814a0c96ad5e3a125173d792cc55f9525b2effea962f6063c0",
"patchedAppSha256": "61f8eebe0c30cc03b311c6818b2acb158a6d16ca58c8bfac5ebd42db095c2679",
"messageIdentityContract": "copilot-native-message-id-v1",
"ownedDistributionDelivery": "COPILOT_CLI_DIST_DIR:lease-owned-guarded-inner-distribution:v1",
"permissionContextContract": "copilot-edit-permission-context-v1",
"permissionContextSourceSha256": "de2d38d702b3c574161af0c0c04790fb6f6347916eb3916ce7a131a8c229121b",
"permissionAdapterSourceSha256": "ee5929ed2ee647ad0369465dee9d51a00b2dd28e1ff735b43e969ddc9c164668",
"permissionLocationsSourceSha256": "f39a81da395b6e23aa2a1a45572a28846e5e81bfd3d764d9a456f74a79c752e9",
"permissionRedactionSourceSha256": "ad6770c69269087951790e2e87e22ac088e3d432769adf17ebc865ed512e6c4f",
"permissionClassifierSourceSha256": "1161eabd82e26133e3b3db3ab567f65d52808b03f176e93497e02c8330349fb4",
"semanticToolReceiptContract": "paperclip.semantic_tool_receipt.v2",
"semanticReceiptSourceSha256": "b2795726b2234a663fec1a6ca11f0551ae69882fe9e82aa39686e952d036e791",
"semanticBridgeSourceSha256": "8764b8c9032fdc7d1415b01e75a9fc57c550e7a5e11da4bed7d9100082872ae5",
"toolEvidenceSourceSha256": "6eef5039afc75bb851eaf9cd82a6f099e4bd398ca2deb59dd66f5b2a81a7a706",
"semanticNormalizedInputContract": "validated-forwarded-input-commit-v1",
"semanticSidecarSourceSha256": "abe3e6c718e8e0fed154526affc94e34b46d96f7bdaed5d88e2b00a21a5b84db",
"semanticDirectDriverSourceSha256": "4ec65aa1d4e905a1e732f2d043bbd39de1ab92f380276ede37d6a72efc19c64d",
"semanticValidationSourceSha256": "02fbb92681c78c5de5d2f1aae25aabc25a0e5df6b0ff4baccac652192a8da975",
"semanticNormalizationSourceSha256": "8b628b82e8eacc91cdcc6fdca2210ba4cf282f01e9c6f74e95ff271655b5047e",
"semanticCompletionContractSourceSha256": "f49398b682a5623812f5d3c33d6bb9a749008e47cc0ed969072041615f66df59",
"semanticValidatorsSourceSha256": "b20ec8ee6e7165117233ce4421a975bb9d3f276220ce31655c0b9a717cb2d9cb",
"semanticSchemaBundleSourceSha256": "81ef98ca7b34896a0f76390ac7af3dd8ef7609fe6f38d54bd6a69373d50fe40b",
"semanticSidecarProtocolSourceSha256": "26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab",
"semanticSidecarReceiptCommitContract": "correlated-tool-resolve-v1",
"displayEventSourceSha256": "d72f9a6035c247bc6a42c067c05753076c54322c4b58984e93922f3325dc6877",
"displayProjectionSourceSha256": "75952606b26dd93bbc582c1f0a4424487377272ea745fb3a27ceae031fd5dc7e",
"configuredEnvironmentSourceSha256": "ecef0ada320a762e0a3d91c38c72d4e19e2c8e8be8c060938a47e7f1a029058a",
"configuredEnvironmentRustSourceSha256": "1671cb2735b3fa119d32f08d4a12b21b9f78a0e4d7285eb63c35df79ce17bd3c",
"profileActivitySourceSha256": "4fdb6b0f9125fd8372ba6f27945fa5c8a3d3b2ec891df544092e3dc7a75d5282",
"profileExtensionsSourceSha256": "a80ddc95eb4505095bfdbbf85c758199949ab20be9c84b34c77f762eb7180571",
"profileInstallationSourceSha256": "58841b2d3b47ab9956db20608802edcd0e55cf5856002dc7c51e05e75bdcb8f6",
"attachmentPolicySourceSha256": "4f7638b7cc64e18f6a4ba33fd46de2b3d49225e32aeab41f5c695b268fa4f8e2",
"singleReadEvidenceSourceSha256": "4069393405089bde613cecec04f5123c426594811da4c668bd96f0e7e283550f",
"nativeSessionRuntimeSourceSha256": "1fe6fa9e6b96d84d70432b85a57c473f833606d4a521563d5022b4d3c41576ce",
"nativeRuntimeContextSourceSha256": "71607f80dbb8dc5cc6c123d0b8f3995d6c5407277695a8c82cbedc90fb8f4842",
"nativeRuntimePromptsSourceSha256": "fff5755dba9b7dce8a5f10ff6ac6d5ed06b92ed079ce1e0a44ac72f2ce7869c1",
"nativeSessionContractSourceSha256": "22a8c96ce41ba4f03e976fc37f0cc7c5d05cee99fa789941da31ee1dab440b90",
"harnessSessionContractSourceSha256": "baba1a210b863518f01e0c875606ed37f9467c433222251438578bd999bc43b5",
"harnessBackendSourceSha256": "dc827e281ae6731545b166ee4d709a4f67f594a556dcbe63c70fe8f1aca26249",
"harnessSessionSourceSha256": "81361733e86b56dd5a66415aefc6f72e69be24acc8d3417370154ebb7fef6e94",
"transportContractSourceSha256": "06f06c53ecae304f72034ffb2f659ff087c82a70ed4994daf21a9d2edaf56e4e",
"nativeBackendSourceSha256": "7495d9898d22dca1ebf6336361d332bafefc4498f6e49f1135025fad846c8586",
"nativeBackendFactorySourceSha256": "7e431c926b76d53c4c08e3a4105353dcd34bd940f6588a1a68f79631f6feeb44",
"nativeInstructionsSourceSha256": "82ac6e5f7275b4ef79fb166c7330efc504e80cab3d7e9081e4e40a8b570bc50b",
"providerLaunchSourceSha256": "c744235b3067ea75973523f78f1e13399337bc155f1775af694e34782d5c9bef",
"launchPreparationStoreSourceSha256": "ff6025568bbc0aa6ce545772d73dfca2d3da226740bd3a7b386b069e76cff296",
"runtimeRequestBridgeSourceSha256": "f9b5a400a449f36de123bb511b21c4cb4564b50081f9958b92e6b7f0c6891d47",
"runtimeRequestFacadeSourceSha256": "d0ed28e0c5ddad34db8f437e704c8a755eb0b47f27e08e44185f66d1d2205295",
"runtimeRequestNotificationSourceSha256": "6196c6175f11c69295dc00fc773201f4fdeca26c500653870253c43b81e65211",
"guardedInstallationSourceSha256": "64b137e3ef695e7d03013ea2b6c8830171e0a73e0bf2e73d55e9ced57a47bcf6",
"nativeExecutionSourceSha256": "116c348e232848a80c5556494c109314baafb20fb5c97133d9656c7732ee54be",
"profileCompatibilitySourceSha256": "eecf2c4e42b451df811e08c1ea25d37619eaf67ad255c5dd742e9232aada35ac",
"installationSourceSha256": "a1c7c6f103b898216973e77ffbac0ef7572e250aa574d1a4065270e9dfa0dc2d",
"assetOwnershipSourceSha256": "e5bb6bc58b3f0e957a64b2cd99c710337ffcd9c0e1fadb341f48e033b64b99f5",
"assetInstallerSourceSha256": "d3bd9514e5920d0cc3567aa50487fb534c57a6f66b8da0d3f22c5f6e499c841f",
"credentialBindingSourceSha256": "2ed68b9aed9756fdd682721833eb2aeaa58f30f1dc8825c88886cc8e7ee92abe",
"admissionDiagnosticsSourceSha256": "df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af",
"grokAdmissionSourceSha256": "f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777",
"sidecarErrorSourceSha256": "17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c",
"profileSourceSha256": "f5538b3dbefa7634a332b87d5ededcefb56aa7e5790cede1c7a0d948a548a456",
"metadataProbeSourceSha256": "be4c8416edd27061109bc1f33d0c26071b4facbb3f651beaae6b3c0275cce727",
"permissionToolIdentitySourceSha256": "770b47305e571a81f344883057d0e4a482a1cd59ac77d045836593a8e739deb1",
"semanticContinuationSourceSha256": "c9cc0fd516b3df92a6655f56ee9bef37615a889c371e2e137d8ac5c1176de811",
"preparedHarnessHomeSourceSha256": "bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e",
"runtimeHostSourceSha256": "26b2e115b3f4ae7c20e9d963d117e0286c958af81386deade83397f12abf9662",
"runtimeAdapterSourceSha256": "fab1e278e0f2aed55b35e57d14329eb431b79a08040fa079aa2403842d16085e",
"runtimeSandboxSourceSha256": "b9f05e522f92b6bade51c7a17f8d28bf31ab9db26510b8d5ddce8519d563a875",
"cancellationSourceSha256": "a07d1a9e982dec0ae130c1ad1efa5bc75c6a39e013b730ba3ee45256f09ceeca",
"agentFilesSourceSha256": "fb32f6badc13dfa22f231479509b631b2f0eba0c99b1ab9f3cd5c0c601aa03ba",
"nativeDistributionSourceSha256": "aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b"
}
}
@@ -0,0 +1,35 @@
{
"commandDigest": "sha256:e311f8111dd12f0c49bf12a80345cfe5f31a1222e0189209adeabf2f9bd0e1b0",
"sourceDigest": "sha256:9d778ec8c5987e304d3121787806f546c7c9f545cf5e0628ce0035c59a559907",
"declaration": {
"schema": "paperclip.grok-profile-identity.v1",
"agent": "grok",
"agentProfileVersion": 13,
"agentServerPackage": "builtin:grok-acp",
"agentServerVersion": "1",
"agentRuntimePackage": "native:grok",
"agentRuntimeVersion": "1.0.13",
"requiresProviderPolicy": false,
"acpxVersion": "0.13.1",
"sources": {
"patches/acpx@0.13.1.patch": "sha256:1e7dae8c83bf8620f53f2bea13c175eeff95e7e955783f6f529bb1dcbdcdb6d2",
"packages/paperclip-runner/src/cli/acpx-runtime-sidecar.ts": "sha256:abe3e6c718e8e0fed154526affc94e34b46d96f7bdaed5d88e2b00a21a5b84db",
"packages/paperclip-runner/src/cli/acpx-sidecar-input.ts": "sha256:17948ad74350dc487ab20d24a0fb89daf0b426b98396454fff0e3856e922d82c",
"packages/paperclip-runner/src/drivers/acpx/admission-diagnostics.ts": "sha256:df8f1aa4f2d1bc63b2635bdd09cb8165c8b7681ddcc2618435c7c08ca90475af",
"packages/paperclip-runner/src/drivers/acpx/grok-effort.ts": "sha256:f1e46cf2e1e36b7704bd797b678e3db8db2003f97bdb9d827cfa3d7662698777",
"packages/paperclip-runner/src/drivers/acpx/grok-web-search.ts": "sha256:754b5ca2345c045421f6007f34999508d996e1cea035f0f1952b3d5f00072b9c",
"packages/paperclip-runner/src/drivers/acpx/grok-credentials.ts": "sha256:1c4ff0be6da798b6fcc604d20a9198dcc8523a3806d563b5b5345b7260549f73",
"packages/paperclip-runner/src/drivers/acpx/codex-runtime-adapter.ts": "sha256:fab1e278e0f2aed55b35e57d14329eb431b79a08040fa079aa2403842d16085e",
"packages/paperclip-runner/src/drivers/acpx/runtime-host.ts": "sha256:26b2e115b3f4ae7c20e9d963d117e0286c958af81386deade83397f12abf9662",
"packages/paperclip-runner/src/drivers/preconfigured-home.ts": "sha256:bb286f7ddc6e9597c56d46bf7ff6e3b556c5b680d7e147fd46518724432b083e",
"packages/paperclip-runner/src/drivers/acpx/runtime-sandbox.ts": "sha256:b9f05e522f92b6bade51c7a17f8d28bf31ab9db26510b8d5ddce8519d563a875",
"packages/paperclip-runner/src/drivers/acpx/sidecar-protocol.ts": "sha256:26b6b21dad582810d00f46e947e682a9615ee9761c487f6315970d6d014c05ab",
"packages/paperclip-runner/src/drivers/acpx/native-distribution-integrity.ts": "sha256:aed327c389e957483a503cb1bb8b247ee7cd7ff4b528fb4567c39bcb4577690b",
"packages/paperclip-runner/src/providers/grok/launcher.cjs": "sha256:f0b698395a3704ed2ffaf84ea19bdb20c36c8a0a70b7c629c7b6ffe144e59e55",
"packages/paperclip-runner/src/providers/grok/platforms.json": "sha256:931eed3abbf2bf041e6c72e6e9486d976f4352613cf069b216ddf010c9a8e704",
"packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_backend.rs": "sha256:a1002d0d7fa8ce6b8287ad0397f7b9d93fe123aecc3dcbe5981b4efb41685736",
"packages/paperclip-runner/runner/crates/runner-core/src/acpx_provider_session.rs": "sha256:09be94877a11ff526f4351b36471ed9685828a3ee4f8e6d329031e88c713afcb",
"packages/paperclip-runner/runner/crates/runner-core/src/acpx_sidecar_transport.rs": "sha256:839cb1d2ac19462f2bcef94d014e528e56beb7a1a1cbf6c3b10906d0ab7eb10c"
}
}
}
+12 -3
View File
@@ -84,7 +84,10 @@ export const AI_CONNECTION_CAPABILITIES: Record<
name: "GitHub Copilot",
methods: { api_key: { adapters: ["copilot_runtime"], envKey: "COPILOT_GITHUB_TOKEN" } },
},
google: { name: "Google", methods: { api_key: { adapters: ["gemini_local", "pi_local"], envKey: "GEMINI_API_KEY" } } },
google: { name: "Google", methods: {
api_key: { adapters: ["gemini_local", "pi_local", "antigravity_runtime"], envKey: "GEMINI_API_KEY" },
subscription: { adapters: ["antigravity_runtime"], envKey: "GEMINI_HOME" },
} },
anthropic: {
name: "Claude",
methods: {
@@ -124,6 +127,11 @@ export function isAiConnectionCompatible(
acpxAgent?: unknown,
): boolean {
adapterType = aiRoutingHarness(adapterType, runnerProvider, acpxAgent);
// Antigravity uses its own Google runtime sign-in or a Gemini API key.
// Tool OAuth and API-compatible endpoints cannot authenticate this harness.
if (adapterType === "antigravity_runtime") return requirement.provider === "google"
&& (requirement.method === "api_key" || requirement.method === "subscription")
&& !("routing" in requirement && requirement.routing);
if ("routing" in requirement && requirement.routing) return requirement.method === "api_key" && isAiRoutingCompatible(requirement.routing, adapterType);
// A fixed binding contains identity only. The service checks authoritative
// connection metadata before resolving credentials or running the harness.
@@ -229,7 +237,7 @@ export type CreateAiConnection = z.infer<typeof createAiConnectionSchema>;
export const aiConnectionLoginIntentSchema = z
.object({
provider: z.enum(["anthropic", "openai", "xai"]),
provider: z.enum(["anthropic", "openai", "xai", "google"]),
method: z.literal("subscription"),
name: z.string().trim().min(1).max(160),
ownership: z.enum(["personal", "shared"]),
@@ -251,6 +259,7 @@ export interface LocalAiLoginStatus {
authorizationUrl?: string;
code?: string;
error?: string;
accountLabel?: string;
}
export interface LocalAiLoginAttempt {
sessionId: string;
@@ -263,6 +272,6 @@ export interface LocalAiLoginAttempt {
export function aiSubscriptionNeedsIsolatedLogin(config: Record<string, unknown> | undefined): boolean {
const metadata = aiConnectionMetadataSchema.safeParse(config?.ai);
return metadata.success && metadata.data.method === "subscription" &&
(metadata.data.provider === "openai" || metadata.data.provider === "xai") &&
(["openai", "xai", "google"].includes(metadata.data.provider)) &&
config?.aiIsolatedSubscription !== true;
}
@@ -82,6 +82,7 @@ export function aiRoutingHarness(
): string {
if (adapter !== "paperclip_runner") return adapter;
if (provider === "acpx" && acpxAgent === "copilot") return "copilot_runtime";
if (provider === "acpx" && acpxAgent === "antigravity") return "antigravity_runtime";
const runner =
provider === "acpx" && ["claude", "grok", "pi"].includes(String(acpxAgent))
? acpxAgent
@@ -80,6 +80,7 @@ test("the script's path list and key inputs cover what the release build of the
assert.deepEqual(HASHED_INPUTS, [
`${RUNNER_PACKAGE}/rust-toolchain.toml`,
`${RUNNER_PACKAGE}/acpx-profiles.json`,
`${RUNNER_PACKAGE}/antigravity-distributions.json`,
`${RUNNER_PACKAGE}/runner/Cargo.toml`,
`${RUNNER_PACKAGE}/runner/Cargo.lock`,
`${RUNNER_PACKAGE}/runner/crates/**`,
@@ -280,6 +281,51 @@ test("every file the crate includes by path sits under a hashed input, so an edi
assert.deepEqual([...forms].sort(), ["concat-manifest-dir", "concat-metavariable", "literal"]);
});
test("isolated Docker Rust stages preserve the layout of embedded schema and identity inputs", () => {
const crateRoot = path.join(repoRoot, RUNNER_PACKAGE, "runner/crates");
const sources = [];
(function walk(dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walk(full);
else if (entry.name.endsWith(".rs")) sources.push(full);
}
})(crateRoot);
for (const [dockerfile, stage] of [["Dockerfile", "runner-build"], ["docker/daytona-runner/Dockerfile", "runnerd-build"]]) {
const docker = readFileSync(path.join(repoRoot, dockerfile), "utf8");
const body = docker.split(/^FROM .+ AS /m).find(part => part.startsWith(`${stage}\n`));
assert.ok(body, `${dockerfile}: missing ${stage}`);
const workdir = /^WORKDIR (.+)$/m.exec(body)?.[1];
assert.ok(workdir?.startsWith("/"), `${dockerfile}: an absolute stage working directory is required`);
const copies = [...body.matchAll(/^COPY (?!-)(.+)$/gm)].flatMap(([, instruction]) => {
const args = instruction.trim().split(/\s+/);
const destination = path.posix.resolve(workdir, args.pop());
return args.map(source => {
const absolute = path.join(repoRoot, source);
return { source: absolute, destination: statSync(absolute).isDirectory() ? destination : path.posix.join(destination, path.basename(source)) };
});
});
const copiedPath = source => {
const copy = copies.find(copy => source === copy.source || source.startsWith(`${copy.source}${path.sep}`));
return copy ? path.posix.join(copy.destination, path.relative(copy.source, source)) : null;
};
let checked = 0;
for (const source of sources) {
for (const { resolved } of crateIncludes(source, readFileSync(source, "utf8"))) {
// Cargo test-only fixture trees are intentionally absent from production
// Docker stages. Their cache coverage is checked by the preceding test.
if (/\/(?:test-fixtures|test\/fixtures|tests\/runner-e2e\/fixtures)(?:\/|$)/.test(resolved)) continue;
const stagedSource = copiedPath(source);
assert.ok(stagedSource, `${dockerfile}: crate source was not copied: ${path.relative(repoRoot, source)}`);
const expected = path.posix.resolve(path.posix.dirname(stagedSource), path.relative(path.dirname(source), resolved));
assert.equal(copiedPath(resolved), expected, `${dockerfile}: include input ${path.relative(repoRoot, resolved)} must retain its crate-relative path`);
checked++;
}
}
assert.ok(checked > 0, `${dockerfile}: the guard must see real Rust include paths`);
}
});
test("every PR reader restores the crate after the dependency cache and marks it before building", () => {
for (const [name, buildCommand] of READERS) {
const body = job(prWorkflow, name);
+3
View File
@@ -74,6 +74,9 @@ export function paperclipRunnerBinaryNeedsBuild({
const runnerRoot = path.join(packageRoot, "runner");
const buildInputs = [
path.join(packageRoot, "antigravity-distributions.json"),
path.join(packageRoot, "rust-toolchain.toml"),
path.join(packageRoot, "protocol"),
path.join(runnerRoot, "Cargo.toml"),
path.join(runnerRoot, "Cargo.lock"),
path.join(runnerRoot, ".cargo"),
+2 -2
View File
@@ -52,8 +52,8 @@ assert.equal(JSON.parse(await readFile(join(server, 'package.json'), 'utf8')).na
const { resolvePaperclipRunnerBinary } = await import(pathToFileURL(join(server, 'dist/services/native-runtime/native-codex-runner.js')));
const { createCapabilityRunnerdCodexTransport } = await import(pathToFileURL(join(server, 'dist/vendor/paperclip-runner/live/runnerd-codex-transport.js')));
const { QUALIFIED_ACPX_PROFILES } = await import(pathToFileURL(join(server, 'dist/vendor/paperclip-runner/drivers/acpx/qualified-profiles.js')));
assert.equal(QUALIFIED_ACPX_PROFILES.pi.agentProfileVersion, 34);
assert.equal(QUALIFIED_ACPX_PROFILES.pi.commandDigest, 'sha256:ed4b32cc8fbf36309d38b3bd378f41f8e7b9e96a2405d275cc55e89042d94c7f');
assert.equal(QUALIFIED_ACPX_PROFILES.pi.agentProfileVersion, 35);
assert.equal(QUALIFIED_ACPX_PROFILES.pi.commandDigest, 'sha256:9473de6ef4baf2bd82fe8b6757239bdc160960b0a78ae6f47fa9e0996544226c');
assert.equal(Object.hasOwn(QUALIFIED_ACPX_PROFILES.pi, 'qualificationModel'), false);
assert.equal(Object.hasOwn(QUALIFIED_ACPX_PROFILES.pi, 'reportedModelId'), false);
const daemon = resolvePaperclipRunnerBinary();
+1
View File
@@ -54,6 +54,7 @@ export const CACHED_PATHS = [
export const HASHED_INPUTS = [
`${RUNNER_PACKAGE}/rust-toolchain.toml`,
`${RUNNER_PACKAGE}/acpx-profiles.json`,
`${RUNNER_PACKAGE}/antigravity-distributions.json`,
`${RUNNER_PACKAGE}/runner/Cargo.toml`,
`${RUNNER_PACKAGE}/runner/Cargo.lock`,
`${RUNNER_PACKAGE}/runner/crates/**`,
@@ -138,6 +138,23 @@ describe("paperclip runner native dev prerequisite", () => {
).toBe(false);
});
it.each([
"antigravity-distributions.json", "rust-toolchain.toml", "protocol/provider-schemas/acpx-sidecar.schema.json",
])("rebuilds an existing native binary after changing embedded input %s", input => {
const checkout = createRunnerCheckout();
const dependency = path.join(checkout.root, "packages/paperclip-runner", input);
fs.mkdirSync(path.dirname(dependency), { recursive: true });
fs.writeFileSync(dependency, "fixture compile input\n");
const now = Date.now(), old = new Date(now - 2_000), built = new Date(now + 2_000), changed = new Date(now + 4_000);
fs.utimesSync(dependency, old, old);
fs.utimesSync(checkout.binary, built, built);
const options = { repoRoot: checkout.root, nativeRunnerRequired: true };
expect(paperclipRunnerBinaryNeedsBuild(options)).toBe(false);
fs.utimesSync(dependency, changed, changed);
expect(paperclipRunnerBinaryNeedsBuild(options)).toBe(true);
expect(paperclipRunnerBinaryNeedsBuild({ ...options, nativeRunnerRequired: false })).toBe(false);
});
it.each(["arm64", "x64"])("uses the staged Mac %s target before the flat build output", architecture => {
const checkout = createRunnerCheckout();
const target = path.join(path.dirname(checkout.binary), `darwin-${architecture}`, "paperclip-runnerd");
@@ -1,4 +1,5 @@
import { describe, expect, it } from "vitest";
import { aiConnectionMetadataSchema, aiRoutingHarness, isAiConnectionCompatible } from "@paperclipai/shared";
import { aiBindingForAuthRecovery, isAiAuthenticationFailure } from "./ai-auth-failure.js";
describe("AI authentication failure recovery", () => {
@@ -26,3 +27,30 @@ describe("AI authentication failure recovery", () => {
expect(aiBindingForAuthRecovery("gemini_local", {})).toEqual({ provider: "google", method: "api_key", mode: "responsible_user" });
});
});
describe("native Antigravity account compatibility", () => {
const compatible = (connection: Parameters<typeof isAiConnectionCompatible>[0]) =>
isAiConnectionCompatible(connection, "paperclip_runner", "gemini-3.8-flash-low", "acpx", "antigravity");
it.each(["api_key", "subscription"] as const)("accepts the selected Google %s account", method => {
expect(aiRoutingHarness("paperclip_runner", "acpx", "antigravity")).toBe("antigravity_runtime");
expect(compatible({ provider: "google", method })).toBe(true);
expect(compatible({ provider: "google", method, mode: "responsible_user" })).toBe(true);
expect(compatible({ provider: "google", method, mode: "delegated",
connectionId: "11111111-1111-4111-8111-111111111111", grantId: "22222222-2222-4222-8222-222222222222" })).toBe(true);
});
it("rejects other providers, tool OAuth and API-compatible endpoints", () => {
for (const provider of ["openai", "anthropic", "openrouter", "github", "xai"] as const) {
expect(compatible({ provider, method: "api_key" })).toBe(false);
expect(compatible({ provider, method: "api_key", mode: "shared",
connectionId: "11111111-1111-4111-8111-111111111111", grantId: "22222222-2222-4222-8222-222222222222" })).toBe(false);
}
expect(aiConnectionMetadataSchema.safeParse({ provider: "google", method: "oauth" }).success).toBe(false);
expect(compatible({ provider: "google", method: "api_key", routing: {
kind: "openrouter", protocol: "chat", auth: "bearer", models: [],
} })).toBe(false);
expect(isAiConnectionCompatible({ provider: "google", method: "subscription" }, "gemini_local")).toBe(false);
});
});
+8 -2
View File
@@ -65,8 +65,14 @@ export function aiBindingForAuthRecovery(
const missingProvider = failure ? missingPersonalAiCredentialProvider(failure) : undefined;
for (const provider of AI_PROVIDERS) {
if (missingProvider && missingProvider !== provider) continue;
const binding = { provider, method: AI_CONNECTION_CAPABILITIES[provider].methods.subscription ? "subscription" : "api_key", mode: "responsible_user" } as const;
if (isAiConnectionCompatible(binding, adapterType, config.model, config.provider, config.acpxAgent)) return binding;
for (const method of ["subscription", "api_key"] as const) {
if (!AI_CONNECTION_CAPABILITIES[provider].methods[method]) continue;
// Recovery presents a concrete sign-in method. A responsible-user binding
// normally follows any compatible default, so check exact metadata first.
if (isAiConnectionCompatible({ provider, method }, adapterType, config.model, config.provider, config.acpxAgent)) {
return { provider, method, mode: "responsible_user" };
}
}
}
return undefined;
}