Skip to content

Antigravity CLI provider: a Google subscription for every LLM feature - #14

Open
volkotyk wants to merge 3 commits into
vesaias:mainfrom
volkotyk:feat/antigravity-cli-provider
Open

volkotyk wants to merge 3 commits into
vesaias:mainfrom
volkotyk:feat/antigravity-cli-provider

Conversation

@volkotyk

Copy link
Copy Markdown
Contributor

Adds Antigravity CLI as a fifth LLM provider, beside Claude API, Claude Code, Codex CLI, OpenAI, OpenRouter and Ollama. A Google Antigravity subscription now drives scoring, tailoring, cover letters, autofill and email classification. Cost counts as $0; token usage is still logged.

One extra: the catalog this subscription grants is not only Google. agy models serves Gemini, claude-sonnet-4-6, claude-opus-4-6-thinking and gpt-oss-120b-medium through the same login.

Setup

docker compose exec -it backend agy

The container has no browser, so the CLI prints a URL and asks for the code the browser shows. Then pick Antigravity CLI (Google Subscription) in Settings › AI. No API key.

Three decisions worth reviewing

The prompt travels on stdin, not argv. -p reads argv, and argv caps near 128 KB — a résumé plus a job description can reach that. The adapter sends one NDJSON line to --input-format stream-json. -p also swallows the next token, so it goes last as -p=.

A deny list holds the agent to text. agy carries 57 tools, among them run_command and write_file, and it reads files without asking. Its --sandbox flag restricts the terminal only, never the file tools. The image ships backend/antigravity-settings.json, and compose binds the same file read-only over the volume — the volume would otherwise hide the image's copy, and a stale deny list is a silent hole.

The per-call transcript is deleted. agy writes the whole prompt to brain/<id>/transcript_full.jsonl. That is a copy of the user's résumé on disk that nothing ever reads back.

Auth

The CLI skips the OS keyring when no D-Bus session bus is present and writes an OAuth token to a file instead. So the container needs one volume, exactly like codex_auth, and no keyring daemon. No login pre-check is needed either: unlike codex, agy answers an unauthenticated call in about a second, with a readable error field. A missing login or a plan limit raises NonRetryableLLMError, so call_llm goes to the fallback instead of retrying four times.

Known cost

Every call carries about 13 200 input tokens of the agent's own preamble — its 57 tool definitions. That is free in money and not free in subscription quota. A CLI agent is a heavyweight way to run one completion, and I found no lever to shrink the tool set.

The spike

The first commit adds spike/antigravity/ — the isolated rig that answered these questions. It touches no application file and can be dropped if you would rather not carry it. probe.sh runs eight probes; models.sh prints the live catalog as a ready block for llm_models_list.

Verification

Against a live subscription, from inside the backend container:

plain call : 'OK'  {'input_tokens': 13239, 'output_tokens': 27, ...}
file call  : I cannot read /app/backend/seed.py because access is denied by a user-configured deny rule
conversations before/after: 0 / 0

Backend suite: 1037 passed. (test_r4_live_contract.py::test_protected_routes_require_a_key fails on my machine before and after this branch — it needs an API key configured on the running instance.)

🤖 Generated with Claude Code

@vesaias vesaias left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review from Claude Code

Verified hands-on: PR built into the backend image, agy 1.2.3 installed, adapter flags checked, unauthenticated call exercised, deny list inspected, full suite on the merged tree 2705 passed. The code is clean and the tests cover the contract. Two things to fix before merge:

  1. The :ro bind of the deny list rides an error path. agy rewrites settings.json on every start; over the read-only bind that fails (failed to write validated settings: atomic rename … device or resource busy) and it logs failed to load cli settings, using defaults. Today the defaults still carry the deny list you shipped, but that is a closed binary's fallback behaviour — a version bump that really falls back to defaults would silently hand the agent run_command and write_file back. Suggest copying the file into the volume from an entrypoint (or the adapter) before each call instead of binding it read-only, and/or asserting after startup that the loaded permissions contain the deny list (the log line CLI settings initialized: permissions=&{… Deny:[command(*) …]} is greppable).

  2. A timeout keeps the transcript. _run_cli raises on the 300 s wall clock before _agy_discard runs, so the brain/<id> transcript holding the résumé survives exactly in the case where the call hung. A try/finally around the discard (with the conversation id read from whatever stdout arrived) closes it.

Smaller, not blocking: the cli log under log/ grows ~16 KB per call and conversation_summaries.db is not in the discard list, so the volume grows without bound; and spike/antigravity/README.md is Ukrainian in an English repo — happy to keep the spike, but an English README or dropping the directory would be better.

Verified for the record: installer resolves to /root/.local/bin/agy 1.2.3 (+270 MB image); --input-format/--output-format stream-json, --print-timeout, --model, -p= all accepted; unauthenticated result event is status: ERROR, error: "authentication failed or timed out" in 0.6 s and matches your auth regex; the PR's "1037 passed" is a partial run, the merged tree gives 2705.

volkotyk and others added 3 commits September 15, 2026 18:00
…oogle subscription works from Docker

The provider question was one blocker and six unknowns, and none of them
could be settled by reading the docs. This is the rig that settled them.

It touches no application file. Two services share one Dockerfile: `plain`
has no keyring, `keyring` carries D-Bus and gnome-keyring. probe.sh runs
eight probes and prints one line each; models.sh emits the live catalog as
a ready block for llm_models_list.

What it found:
- The CLI skips the OS keyring when no D-Bus session bus is present and
  writes an OAuth token to a file instead, so one volume is enough.
  The keyring variant of the image is unnecessary.
- `-p` reads argv and swallows the next token. A prompt of 63 KB survives
  argv, but the stdin route is one NDJSON line and has no such ceiling.
- `agy models` needs a login, and every slug carries its reasoning effort.
- `--sandbox` restricts the terminal only. The agent still reads files
  unasked; a deny list in settings.json is the lever that stops it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ure, and the agent stays inside plain text

Scoring, tailoring, letters, autofill and email can now run on a Google
Antigravity subscription through `agy`, beside the Claude Code and Codex
CLI providers. Sign in once with `docker compose exec -it backend agy`;
the OAuth token lands in a file under the antigravity_auth volume,
because the CLI skips the OS keyring when no D-Bus session bus is
present. Cost counts as $0 and token usage is still logged.

Three decisions worth knowing:

The prompt travels as one NDJSON line on stdin. `-p` reads argv and
argv caps near 128 KB, which a resume plus a job description can reach.
`-p` also swallows the next token, so it goes last as `-p=`.

A deny list holds the agent to text. `agy` carries 57 tools, among them
run_command and write_file, and it reads files without asking. Its
`--sandbox` flag restricts the terminal only, never the file tools. The
image ships backend/antigravity-settings.json, and compose binds the
same file read-only over the volume: the volume would otherwise hide the
image's copy, and a stale deny list is a silent hole.

The per-call transcript is deleted. agy writes the whole prompt to
brain/<id>/transcript_full.jsonl, which means a copy of the user's
resume on disk that nothing ever reads back.

Errors map onto the existing contract: a missing login or a plan limit
raises NonRetryableLLMError, so call_llm goes straight to the fallback
instead of retrying four times. No login pre-check is needed - unlike
codex, agy answers an unauthenticated call in about a second.

Verified against a live subscription from inside the backend container:
a plain call answers, a file read is refused by the deny rule, and the
transcript count does not grow. 1037 backend tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…h, and a timed-out call still drops the transcript

Both blocking points from the review, plus the two smaller ones.

The read-only bind is gone. agy rewrites settings.json as it starts, so a
read-only copy there made that write fail and put the CLI on the error
path that logs "failed to load cli settings, using defaults". The
shipped copy now lives at /opt/antigravity-settings.json, outside the
volume, and the adapter installs it into the state directory before each
call, where agy is free to rewrite it. Verified: the two error lines are
gone from the log.

That fallback is still a closed binary deciding what its defaults are,
so the adapter no longer takes its word for it. After each call it reads
"CLI settings initialized: permissions=..." from agy's own log and
refuses the call unless every shipped deny rule is in that line. A
missing log line, a missing rule or a missing settings file all raise
NonRetryableLLMError: an unconstrained agent is not something to fall
back to quietly.

A timeout no longer keeps the transcript. _run_cli shields the read task,
so after the kill it still returns what the process printed; the adapter
takes the conversation id from the init event and discards in a finally.
The hung call was the one case where the resume stayed on disk, and it is
the case that matters.

The per-call log goes to the temp working directory via --log-file. That
is the ~16 KB per call the review measured, and the same file is what the
permissions check reads, so it earns its place twice.

The spike README is English now.

Verified live from inside the backend container: a plain call answers, a
file read is refused by the deny rule, and neither the log directory nor
the conversation directory in the volume grows. Full suite: 2775 passed.
(test_r4_live_contract.py::test_protected_routes_require_a_key fails on
this machine on main as well; it wants an API key on the running
instance.)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@volkotyk
volkotyk force-pushed the feat/antigravity-cli-provider branch from 6af8a6c to 6da58a3 Compare September 16, 2026 01:11
@volkotyk

Copy link
Copy Markdown
Contributor Author

Rebased onto main and pushed. Both blocking points are fixed, and the two smaller ones with them.

1. The read-only bind is gone. You were right about the mechanism: agy rewrites settings.json as it starts, the :ro copy made that write fail, and the CLI went down the failed to load cli settings, using defaults path. The shipped copy now lives at /opt/antigravity-settings.json, outside the volume, and the adapter installs it into the state directory before each call where agy can rewrite it freely. The two error lines are gone from the log; CLI settings initialized: permissions=&{Allow:[] Deny:[command(*) read_file(/) write_file(/) read_url(*) execute_url(*)] Ask:[]} is now reached the ordinary way.

I took the assertion as well, since a closed binary deciding its own defaults is not something to rely on. After each call the adapter reads that line out of agy's log and refuses the call unless every shipped deny rule appears in it. A missing line, a missing rule, or a missing settings file all raise NonRetryableLLMError, so a degraded state fails over to the fallback provider instead of quietly running an unconstrained agent.

2. The timeout keeps nothing now. _run_cli shields the read task, so after the kill it still hands back what the process printed. The adapter reads the conversation id from the init event and discards in a finally. The hung call was exactly the case where the résumé stayed on disk.

3. The log. --log-file points at the per-call temp directory, so the ~16 KB you measured dies with it. That is also the file the permissions check reads, so it pays for itself twice. conversation_summaries.db I left alone deliberately — deleting it mid-flight can pull a sqlite file out from under a concurrent call, and it grows slowly (28 KB after 49 conversations). Happy to prune it inside the semaphore if you would rather have the bound.

4. The spike README is English now.

Verified live from inside the backend container: a plain call answers, a file read is refused by the deny rule, and neither log/ nor conversations/ in the volume grows across calls.

On the test count — you are right, mine was a -k filtered run and I should have said so. The merged tree here gives 2775 passed, 1 failed: test_r4_live_contract.py::test_protected_routes_require_a_key, which fails on main on this machine too (it wants an API key configured on the running instance).

One number worth putting on the record, since it is a cost you inherit: every call carries about 13 200 input tokens of agy's own preamble, its 57 tool definitions. Free in money, not free in subscription quota, and I found no lever to shrink the tool set.

🤖 Generated with Claude Code

@volkotyk
volkotyk requested a review from vesaias September 16, 2026 01:23
@vesaias

vesaias commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Generated by Claude Code

Thanks, all four points check out. I built the PR's backend image and ran the adapter against agy 1.2.3: the deny list installs to the state dir, --log-file lands in the per-call temp dir, the log carries CLI settings initialized: permissions=&{Allow:[] Deny:[command(*) read_file(/) write_file(/) read_url(*) execute_url(*)] Ask:[]} and _agy_assert_denied passes on it. Full suite on the PR tree: 2711 passed. The head already sits on current main, so it merges clean.

One last ask before merge: please drop spike/antigravity/. The app never touches it, and the findings that matter (flags, auth shape, deny list) are already in the provider's comments and in this PR's description, which stays as the record. Once that commit is up I'll merge.

The 13k-token preamble is good to know; I'll note it in the changelog line so people on a quota are not surprised.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants