[F005,F006] grok auth: directory mount + atomic-write fallback

F005: the single-file bind mount of auth.json pinned the inode, so the
orchestrator's atomic refresh (tmp+rename within ~/.grok) never reached a
running grok container — a long-lived container hung at the login prompt
when the original ~6h token expired. Mount the host ~/.grok DIRECTORY (ro)
at /home/agent/.grok-auth-ro; the entrypoint symlinks ~/.grok/auth.json at
that RO mount so grok + the --check backstop read the live credential (the
directory mount sees the host-side rename) while grok's writable state
(config.toml, sessions/) stays in the image's ~/.grok.

F006: a rotated refresh_token is single-use — xAI invalidates the old one
the instant it issues the new one. If the atomic write failed after the
rotation, the file kept the now-dead old refresh_token and the credential
was permanently lost on the next refresh. _atomic_write now falls back to a
direct write when tmp+replace fails, so the rotated token always lands on
disk (losing the write is catastrophic; losing atomicity is not).

TDD: RED tests watched fail, then GREEN. ruff+mypy clean; 32 grok tests
green, no regressions.
This commit is contained in:
Renn F
2026-06-28 10:10:17 +02:00
parent 812bf1195a
commit 51990d7cad
6 changed files with 109 additions and 26 deletions
+6 -2
View File
@@ -241,7 +241,11 @@ async def test_grok_spawn_mounts_auth_when_present(_isolate_grok_auth: Path) ->
) as exec_mock:
await provider.spawn(_config())
cmd = list(exec_mock.call_args.args)
expected = f"{_isolate_grok_auth / 'auth.json'}:/home/agent/.grok/auth.json:ro"
# F005: mount the host ~/.grok DIRECTORY (ro), not the single auth.json
# file — a single-file bind mount pins the inode, so the orchestrator's
# atomic auth.json refresh (rename) never reaches a running container.
# The entrypoint symlinks ~/.grok/auth.json at this RO dir mount.
expected = f"{_isolate_grok_auth}:/home/agent/.grok-auth-ro:ro"
assert expected in cmd
@@ -254,7 +258,7 @@ async def test_grok_spawn_omits_auth_mount_when_absent() -> None:
) as exec_mock:
await provider.spawn(_config())
cmd = list(exec_mock.call_args.args)
assert not any("/home/agent/.grok/auth.json" in c for c in cmd)
assert not any("/home/agent/.grok-auth-ro" in c for c in cmd)
async def test_grok_spawn_prompt_is_injection_safe() -> None: