sak-runner runs agy accounts through agy and signs them in, in a home whose keychain holds the login #162

Merged
agra merged 16 commits from task/3841-sak-runner-runs-gemini-through-the-agy-h into master 2026-10-29 15:35:51 +02:00
Owner

Tracker: #3841

The runner speaks a fourth harness, agy: Google's Antigravity CLI, through which runs use the Gemini models. A runner on macOS with agy on its run PATH registers it with its version. An account of harness agy signs in through the runner, taking the code pasted in sak, and serves only the Gemini models. A run whose model an agy account serves runs through agy. Its shapes were read from agy 1.4.5 on s32.

agy is offered only on macOS. An agy run's home keeps its login in a macOS keychain (see below), so a runner on any other platform does not look for agy (config.unrunnable, runner/config.sx). It leaves agy out of its offers, at its start and in each rescan after an install. Its scan line reads ✗ agy: macOS only. The deps summary leaves agy out of what it offers, and the running view's header shows ✗ agy, as for any harness not offered. sak-runner-live now takes its platform as the runner does, so it still resolves agy on macOS.

The adapter (runner/agy.sx).

  • argv: agy -p= --input-format stream-json --output-format stream-json --model <model> --effort <effort>, plus --dangerously-skip-permissions under the new --agy-permission allow (the default).
    • Under reject, agy's headless mode (request-review) denies every tool call that needs a permission. With the flag, the mode reads always-proceed, and a file read went through.
    • A bare -p takes the next argument as its prompt, so the empty prompt is given inline.
  • stdin: the prompt and each handed message are one {"event":"user","message":{…}} line. agy runs each line as a turn of its own, once the turn before it ends.
  • stdout:
    • init's conversation_id is the session id.
    • Each user_input step after the prompt's is the replay of a handed message.
    • Each result ends a turn.
    • An ERROR result before any init (no login, unknown model) fails the run with session failed: <error>. An ERROR result inside a session ends only its turn.
    • The quota error ("You have exhausted your quota on this model.") limits the account for one hour.
    • agy reports no context window and no usage on its stream.

The run home (src/home.sx). agy keeps its login in the OS keyring: on macOS, the generic password with service gemini and account antigravity, stored as go-keyring-base64: followed by the login in base64.

  • The home's setup makes Library/Keychains/sak.keychain-db, with no password and no lock timeout, and sets it as the keychain that home searches and writes to. security keeps that setting in the home's Library/Preferences, which has to exist first.
  • The setup writes the login through security -i's stdin, so it never appears on a command line.
  • login prints the login back, including a renewal agy writes, without the newline security ends it with. teardown deletes the keychain, then removes the home.
  • Checked on s32: with a fake login in such a home, agy 1.4.5 loads the token and tries to refresh it.

Sign-in (runner/sign_in.sx). agy has no login command: its interface signs in first when its home holds no login, and stays up once signed in. The runner runs agy with no arguments on the sign-in's pseudo-terminal (50 × 1000), in a home whose keychain holds no login yet.

  • At Select login method it presses Enter, which takes the first entry, Google OAuth.
  • It reports the link. A hyperlink (OSC 8) wins, as before. On a terminal of no type, which is what a runner under launchd gives it, agy prints the link as a bare word, so the runner takes the first https:// word once paste the authorization code shows below it.
  • It types the code pasted in sak, then Enter. token exchange failed on the screen ends the sign-in code_refused.
  • Since agy stays up, the login is done once the home's login script prints a login. The runner runs that script each second after the paste, then ends agy.
  • The models come from agy models, one <id>\t<label> line each. Each id is a model and an effort joined by a dash (gemini-3.8-flash-low is gemini-3.8-flash at low), so ids group into a model with its efforts. The listing is whole once agy's output ends.
  • The account keeps only the Gemini models, whose ids begin gemini-. agy 1.4.7 also lists two Anthropic models at three efforts each, and gpt-oss-120b. Those ids are the ones other harnesses' accounts serve, and sak takes the first serving account by position whatever its harness, so runs meant for those accounts could otherwise be claimed on the agy account.
  • POST /api/sign-ins/:id/code takes a pasted code for an agy sign-in, as it does for the other harness whose login takes one.
  • agy reads no BROWSER: it opens its link with open on the runner's host as well as showing it.
  • Checked live on s32 against agy 1.4.5 with no TERM. The sign-in took Google OAuth and reported Google's whole OAuth link with no code. A wrong code ended it code_refused. Signing in with a real Google account was not run.

The web. The sign-in dialog offers agy beside the other harnesses. A waiting agy sign-in shows its link and the paste field, as any sign-in with a link and no code does.

The schema. Migration 54 rebuilds account, run_request and sign_in with agy in their harness checks. run_request keeps every column and index master's migrations 50–53 gave it (kept_head, resume_ref, resume_head, cancelled, place, run_request_place). The store accepts agy offers, accounts and sign-ins.

  • A database migrated fresh by the head and one migrated by master's sak and then by the head have the same schema (sqlite3 .schema, equal). Against master's, the three tables have the same columns, types, defaults and indexes; only their harness checks name agy.

Regressions, each failing before its fix.

  • Before the Gemini rule, an agy account served every model agy lists. tests/stub_sign_in.sh now lists two non-Gemini ids among Gemini ones. In a git archive arm of daeadc9b carrying only the new tests, tests/sign_in.sx printed SIGN IN FAIL (1 checks), the failing check being "the agy account serves only the Gemini models agy lists, at the efforts their ids end in". At 7f8f82f9 it prints SIGN IN OK.
  • Before the macOS rule, a runner off macOS offered agy. In the same arm, tests/runner_core.sx failed "a runner off macOS leaves agy out of its offers, at its start and in a rescan, and its scan line says why". At 7f8f82f9 that check passes.
    • Both arms also failed "a run that ignores the message ends failed with cause forbidden_words within a second of its next turn's end". That is a wall-clock bound, and they ran beside a full Playwright run at a load of 12–16. The diff touches no run or forbidden-word code, and the whole file passed in the full gate below and in the arm of the head.
  • Before this change an agy sign-in ended harness_cannot_sign_in. In a git archive arm of 201f9b2a carrying only the new tests, tests/sign_in.sx printed SIGN IN FAIL (11 checks), every agy check failing, from "the agy sign-in takes its login's first way to sign in…" to "and its login is ended and its home removed". At daeadc9b it printed SIGN IN OK.
  • Before the login fix, the first run of each agy account "renewed" its login to the same login plus a newline. In the same arm, tests/runner_core.sx printed RUNNER CORE FAIL (1 checks), the failing check being "a login agy left as the run's home was given it is no renewal". At daeadc9b it printed RUNNER CORE OK (508 ok).

Tests.

  • tests/runner_agy.sx drives the adapter with lines recorded from agy 1.4.5.
  • tests/stub_agy.sh stands in for agy in tests/runner_core.sx, which pins:
    • registration with agy's version, and the --agy-permission flag;
    • the argv, the run's environment, and a home holding its own keychain whose login agy reads;
    • the prompt line, the session, and the stream landing verbatim;
    • a message handed mid-turn, run as the next turn;
    • a refusal before the session begins;
    • a used-up quota, which limits the account for an hour and queues the request again;
    • a login agy left as it was, which is no renewal.
  • tests/runner_core.sx also registers a runner whose platform is linux-x86_64, and pins that agy is left out of its offers and named in its scan line as macOS only.
  • tests/stub_sign_in.sh stands in for agy's login and listing in tests/sign_in.sx. It pins the menu, the bare link, the pasted code, the login read from the home's keychain while the login stays up, the Gemini models grouped by effort with the others left out, a refused code, and the home removed.
  • The schema-head asserts move to 54.
  • web/e2e/runner/wizard.spec.ts gives the runner a stub agy, as it gives the other harnesses, and reads ✓ agy 1.4.5 in the scan line.

Goldens. The sign-in dialog's harness picker gains a fourth segment, so these goldens changed, each light and dark at 1280×800 and 390×844:

  • accounts-sign-in
  • accounts-pasted-code
  • accounts-sign-in-expired
  • accounts-no-runner

The merge of master 45028434 took master's goldens where master had re-recorded them (14 of the 16), and one full run of the main config at the merge (--update-snapshots=changed, 777 passed, 1 skipped) re-recorded exactly those 14 and no other golden. Against master's, each of the 16 changes only a 56×28 px box at the picker's right end, where agy's segment is.

Docs. runner/README.md (Harnesses, the harness scan, the flags table, the sign-in steps, the running view's sample, the tests) and docs/api.md (the harness lists, the sign-ins, the code route, the registration, the run-home table) name agy. Both say that an agy account serves only the Gemini models and that agy is offered only on macOS.

Not in this PR.

  • The web does not read an agy stream into the transcript.
  • agy has no entry in the dependency catalog.
  • agy's sign-in opens a browser tab on the runner's host as well as showing its link.
  • Only one harness takes the run's instructions; giving them to agy and the others is a task of its own.

Gates. full gate run 814 passed at the head, d3875b7d, on a clean tree (14:05–14:49 EET on 2026-10-29). The head holds master at 45028434. The gate ran:

  • the sx builds at sx af8455c3, npm run build and the libc grep;
  • the tests/*.sx loop;
  • release.sh, step9 and the deploy contract;
  • Playwright: the main config with 777 passed and 1 skipped, and the runner config with 10 passed.

Before the gate, the 69 tests/*.sx files the loop runs each printed OK from a git archive arm of the merge (runner_core, sign_in, runner_agy, deployed_upgrade and the roll-backs in flows, tools, utf8_text and file_keys among them).

Tracker: [#3841](https://sak.swipelab.com/tasks/3841) The runner speaks a fourth harness, `agy`: Google's Antigravity CLI, through which runs use the Gemini models. A runner on macOS with `agy` on its run PATH registers it with its version. An account of harness `agy` signs in through the runner, taking the code pasted in sak, and serves only the Gemini models. A run whose model an agy account serves runs through agy. Its shapes were read from agy 1.4.5 on s32. **agy is offered only on macOS.** An agy run's home keeps its login in a macOS keychain (see below), so a runner on any other platform does not look for agy (`config.unrunnable`, runner/config.sx). It leaves agy out of its offers, at its start and in each rescan after an install. Its scan line reads `✗ agy: macOS only`. The deps summary leaves agy out of what it offers, and the running view's header shows `✗ agy`, as for any harness not offered. `sak-runner-live` now takes its platform as the runner does, so it still resolves agy on macOS. **The adapter (`runner/agy.sx`).** - **argv:** `agy -p= --input-format stream-json --output-format stream-json --model <model> --effort <effort>`, plus `--dangerously-skip-permissions` under the new `--agy-permission allow` (the default). - Under `reject`, agy's headless mode (`request-review`) denies every tool call that needs a permission. With the flag, the mode reads `always-proceed`, and a file read went through. - A bare `-p` takes the next argument as its prompt, so the empty prompt is given inline. - **stdin:** the prompt and each handed message are one `{"event":"user","message":{…}}` line. agy runs each line as a turn of its own, once the turn before it ends. - **stdout:** - `init`'s `conversation_id` is the session id. - Each `user_input` step after the prompt's is the replay of a handed message. - Each `result` ends a turn. - An `ERROR` result before any `init` (no login, unknown model) fails the run with `session failed: <error>`. An `ERROR` result inside a session ends only its turn. - The quota error ("You have exhausted your quota on this model.") limits the account for one hour. - agy reports no context window and no usage on its stream. **The run home (`src/home.sx`).** agy keeps its login in the OS keyring: on macOS, the generic password with service `gemini` and account `antigravity`, stored as `go-keyring-base64:` followed by the login in base64. - The home's setup makes `Library/Keychains/sak.keychain-db`, with no password and no lock timeout, and sets it as the keychain that home searches and writes to. `security` keeps that setting in the home's `Library/Preferences`, which has to exist first. - The setup writes the login through `security -i`'s stdin, so it never appears on a command line. - `login` prints the login back, including a renewal agy writes, without the newline `security` ends it with. `teardown` deletes the keychain, then removes the home. - Checked on s32: with a fake login in such a home, agy 1.4.5 loads the token and tries to refresh it. **Sign-in (`runner/sign_in.sx`).** agy has no login command: its interface signs in first when its home holds no login, and stays up once signed in. The runner runs `agy` with no arguments on the sign-in's pseudo-terminal (50 × 1000), in a home whose keychain holds no login yet. - At `Select login method` it presses Enter, which takes the first entry, Google OAuth. - It reports the link. A hyperlink (OSC 8) wins, as before. On a terminal of no type, which is what a runner under launchd gives it, agy prints the link as a bare word, so the runner takes the first `https://` word once `paste the authorization code` shows below it. - It types the code pasted in sak, then Enter. `token exchange failed` on the screen ends the sign-in `code_refused`. - Since agy stays up, the login is done once the home's login script prints a login. The runner runs that script each second after the paste, then ends agy. - The models come from `agy models`, one `<id>\t<label>` line each. Each id is a model and an effort joined by a dash (`gemini-3.8-flash-low` is `gemini-3.8-flash` at `low`), so ids group into a model with its efforts. The listing is whole once agy's output ends. - The account keeps only the Gemini models, whose ids begin `gemini-`. agy 1.4.7 also lists two Anthropic models at three efforts each, and gpt-oss-120b. Those ids are the ones other harnesses' accounts serve, and sak takes the first serving account by position whatever its harness, so runs meant for those accounts could otherwise be claimed on the agy account. - `POST /api/sign-ins/:id/code` takes a pasted code for an agy sign-in, as it does for the other harness whose login takes one. - agy reads no `BROWSER`: it opens its link with `open` on the runner's host as well as showing it. - Checked live on s32 against agy 1.4.5 with no `TERM`. The sign-in took Google OAuth and reported Google's whole OAuth link with no code. A wrong code ended it `code_refused`. Signing in with a real Google account was not run. **The web.** The sign-in dialog offers agy beside the other harnesses. A waiting agy sign-in shows its link and the paste field, as any sign-in with a link and no code does. **The schema.** Migration 54 rebuilds `account`, `run_request` and `sign_in` with `agy` in their `harness` checks. `run_request` keeps every column and index master's migrations 50–53 gave it (`kept_head`, `resume_ref`, `resume_head`, `cancelled`, `place`, `run_request_place`). The store accepts agy offers, accounts and sign-ins. - A database migrated fresh by the head and one migrated by master's sak and then by the head have the same schema (`sqlite3 .schema`, equal). Against master's, the three tables have the same columns, types, defaults and indexes; only their `harness` checks name agy. **Regressions, each failing before its fix.** - Before the Gemini rule, an agy account served every model agy lists. `tests/stub_sign_in.sh` now lists two non-Gemini ids among Gemini ones. In a `git archive` arm of daeadc9b carrying only the new tests, `tests/sign_in.sx` printed `SIGN IN FAIL (1 checks)`, the failing check being "the agy account serves only the Gemini models agy lists, at the efforts their ids end in". At 7f8f82f9 it prints `SIGN IN OK`. - Before the macOS rule, a runner off macOS offered agy. In the same arm, `tests/runner_core.sx` failed "a runner off macOS leaves agy out of its offers, at its start and in a rescan, and its scan line says why". At 7f8f82f9 that check passes. - Both arms also failed "a run that ignores the message ends failed with cause forbidden_words within a second of its next turn's end". That is a wall-clock bound, and they ran beside a full Playwright run at a load of 12–16. The diff touches no run or forbidden-word code, and the whole file passed in the `full` gate below and in the arm of the head. - Before this change an agy sign-in ended `harness_cannot_sign_in`. In a `git archive` arm of 201f9b2a carrying only the new tests, `tests/sign_in.sx` printed `SIGN IN FAIL (11 checks)`, every agy check failing, from "the agy sign-in takes its login's first way to sign in…" to "and its login is ended and its home removed". At daeadc9b it printed `SIGN IN OK`. - Before the login fix, the first run of each agy account "renewed" its login to the same login plus a newline. In the same arm, `tests/runner_core.sx` printed `RUNNER CORE FAIL (1 checks)`, the failing check being "a login agy left as the run's home was given it is no renewal". At daeadc9b it printed `RUNNER CORE OK` (508 ok). **Tests.** - `tests/runner_agy.sx` drives the adapter with lines recorded from agy 1.4.5. - `tests/stub_agy.sh` stands in for agy in `tests/runner_core.sx`, which pins: - registration with agy's version, and the `--agy-permission` flag; - the argv, the run's environment, and a home holding its own keychain whose login agy reads; - the prompt line, the session, and the stream landing verbatim; - a message handed mid-turn, run as the next turn; - a refusal before the session begins; - a used-up quota, which limits the account for an hour and queues the request again; - a login agy left as it was, which is no renewal. - `tests/runner_core.sx` also registers a runner whose platform is `linux-x86_64`, and pins that agy is left out of its offers and named in its scan line as macOS only. - `tests/stub_sign_in.sh` stands in for agy's login and listing in `tests/sign_in.sx`. It pins the menu, the bare link, the pasted code, the login read from the home's keychain while the login stays up, the Gemini models grouped by effort with the others left out, a refused code, and the home removed. - The schema-head asserts move to 54. - `web/e2e/runner/wizard.spec.ts` gives the runner a stub agy, as it gives the other harnesses, and reads `✓ agy 1.4.5` in the scan line. **Goldens.** The sign-in dialog's harness picker gains a fourth segment, so these goldens changed, each light and dark at 1280×800 and 390×844: - `accounts-sign-in` - `accounts-pasted-code` - `accounts-sign-in-expired` - `accounts-no-runner` The merge of master 45028434 took master's goldens where master had re-recorded them (14 of the 16), and one full run of the main config at the merge (`--update-snapshots=changed`, 777 passed, 1 skipped) re-recorded exactly those 14 and no other golden. Against master's, each of the 16 changes only a 56×28 px box at the picker's right end, where agy's segment is. **Docs.** runner/README.md (Harnesses, the harness scan, the flags table, the sign-in steps, the running view's sample, the tests) and docs/api.md (the harness lists, the sign-ins, the code route, the registration, the run-home table) name agy. Both say that an agy account serves only the Gemini models and that agy is offered only on macOS. **Not in this PR.** - The web does not read an agy stream into the transcript. - agy has no entry in the dependency catalog. - agy's sign-in opens a browser tab on the runner's host as well as showing its link. - Only one harness takes the run's instructions; giving them to agy and the others is a task of its own. **Gates.** `full` gate run 814 passed at the head, d3875b7d, on a clean tree (14:05–14:49 EET on 2026-10-29). The head holds master at 45028434. The gate ran: - the sx builds at sx af8455c3, `npm run build` and the libc grep; - the tests/*.sx loop; - release.sh, step9 and the deploy contract; - Playwright: the main config with 777 passed and 1 skipped, and the runner config with 10 passed. Before the gate, the 69 tests/*.sx files the loop runs each printed OK from a `git archive` arm of the merge (`runner_core`, `sign_in`, `runner_agy`, `deployed_upgrade` and the roll-backs in `flows`, `tools`, `utf8_text` and `file_keys` among them).
agra changed title from The runner speaks agy's print-mode stream-json through one adapter to sak-runner runs agy accounts through agy, in a home whose keychain holds the login 2026-10-26 12:41:29 +02:00
agra changed title from sak-runner runs agy accounts through agy, in a home whose keychain holds the login to sak-runner runs agy accounts through agy and signs them in, in a home whose keychain holds the login 2026-10-26 14:56:52 +02:00
# Conflicts:
#	web/e2e/accounts.spec.ts-snapshots/accounts-no-runner-dark-chromium-darwin.png
#	web/e2e/accounts.spec.ts-snapshots/accounts-no-runner-light-chromium-darwin.png
#	web/e2e/accounts.spec.ts-snapshots/accounts-no-runner-phone-dark-chromium-darwin.png
#	web/e2e/accounts.spec.ts-snapshots/accounts-no-runner-phone-light-chromium-darwin.png
#	web/e2e/accounts.spec.ts-snapshots/accounts-pasted-code-dark-chromium-darwin.png
#	web/e2e/accounts.spec.ts-snapshots/accounts-pasted-code-light-chromium-darwin.png
#	web/e2e/accounts.spec.ts-snapshots/accounts-pasted-code-phone-dark-chromium-darwin.png
#	web/e2e/accounts.spec.ts-snapshots/accounts-pasted-code-phone-light-chromium-darwin.png
#	web/e2e/accounts.spec.ts-snapshots/accounts-sign-in-dark-chromium-darwin.png
#	web/e2e/accounts.spec.ts-snapshots/accounts-sign-in-expired-dark-chromium-darwin.png
#	web/e2e/accounts.spec.ts-snapshots/accounts-sign-in-expired-light-chromium-darwin.png
#	web/e2e/accounts.spec.ts-snapshots/accounts-sign-in-expired-phone-dark-chromium-darwin.png
#	web/e2e/accounts.spec.ts-snapshots/accounts-sign-in-expired-phone-light-chromium-darwin.png
#	web/e2e/accounts.spec.ts-snapshots/accounts-sign-in-light-chromium-darwin.png
#	web/e2e/accounts.spec.ts-snapshots/accounts-sign-in-phone-dark-chromium-darwin.png
#	web/e2e/accounts.spec.ts-snapshots/accounts-sign-in-phone-light-chromium-darwin.png
# Conflicts:
#	runner/README.md
agra merged commit bbc5f00a47 into master 2026-10-29 15:35:51 +02:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
sx/sak!162
No description provided.