Skip to content

Headless agy: a soft-denied read is a denied round, and the ADC token is probed before a gcp spawn (2.8.0) - #34

Merged
Shavvimal merged 10 commits into
mainfrom
fix/agy-soft-deny-adc-probe
Oct 27, 2026
Merged

Shavvimal merged 10 commits into
mainfrom
fix/agy-soft-deny-adc-probe

Conversation

@Shavvimal

@Shavvimal Shavvimal commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

The fix for the two dead rounds in docs/antigravity-soft-deny-postmortem.md, 27 October: a headless agy run on billing route gcp whose ADC refresh token Google had expired (invalid_rapt) failed in 2.4 s while cairn doctor said ok, and a plan-mode run that was soft-denied one read_file outside the workspace ended with no answer and reported status succeeded, exit 0.

Engine

  • A counted denial decides the outcome. agy shows no tool error for a soft deny; only the result's denied_actions names it, and cairn read that after the outcome was fixed. Replay.refusals() merges the stream's tool errors, the soft denials agy's log names and one entry per counted action type the others did not name. The live runner, adopt, cairn invoke tail (which now waits briefly for the runner's record) and status (a finished round's record is authoritative, last_message: null included) all reach denied; an adopted denied round exits 2 like a live one.
  • The refused read gets its path and a verdict. agy's log says which step it soft-denied (soft-denying tool confirmation "ReadFile" at step 2); the harness keeps a step-to-target map from the stream and joins the two. read_verdict() models agy's documented read rules (deny beats all, read_file(<dir>) covers its subtree, write_file grants read, a bare target matches the name, the workspace and every --extra --add-dir need no rule; the round records those roots), so the error and the resume note say now readable (inside <dir>), still denied by read_file(.env*) or still outside the workspace with no read_file rule.
  • The ADC token must mint before a gcp spawn. adc_token_check() runs gcloud auth application-default print-access-token with the run's environment and discards the token. A rejected credential refuses the spawn (exit 2) with the login command and is a warn line in cairn doctor; gcloud missing, a timeout or a transport failure is a billing: warning and doctor says token not verified. adcAuth: failed to get token from token source in agy's log (with no success after it) becomes the round's error, not retryable for a rejected credential.
  • The docstrings said the turn goes on after an auto-denied call; a soft deny can end it.

Skill and docs

skills/invoke-antigravity/SKILL.md: which credential each billing route needs, agy models can fail while a gcp run works, plan mode reads the --cd tree only (files outside go inside, through --extra --add-dir <dir> repeated on every resume, or into the prompt), and the denied and billing: cases. docs/antigravity-soft-deny-postmortem.md is the anonymised post-mortem with the resolution. Version 2.8.0.

Review

The plan was reviewed adversarially by Codex (gpt-6-astra, high) before the build. It found three readers that still said succeeded on a soft-denied round (tail, status, an adopted round's exit code), confirmed by a probe; mixed refusal sources dropped; a read verdict that could not see --add-dir and ignored deny-over-workspace; every ADC failure made non-retryable; doctor green on an unverified probe; the skill still demanding agy models. All fixed here. It cut a prompt scan for paths outside the workspace (heuristic) and rejected an automatic --add-dir <prompt-file dir>. Its suggestion to split denial handling and ADC readiness into two PRs was not taken: both faults come from one incident and one document, as 2.7.0 did.

Codex then reviewed the diff (thread 01a17d84-933c-70d3-9108-474b495d0e07, reject, eight findings), all verified and fixed in the last commit: ask rules were ignored (they deny headlessly); a relative rule target was compared against an absolute path, so read_file(secret/) in deny let the workspace default win; tail and status did not load the log's soft denials; the ADC error from agy's log replaced any failed round's error and flipped retryable; the bare word reauth in gcloud's stderr counted as a rejected credential; an observation classified denied before finalisation still showed a progress message as the answer; the skill and changelog claimed one denial shape when live runs show two, and said --extra is not recorded when it is recorded but not reapplied; the foundations test module's main guard sat before two new classes. A third round on the fix commit found four of the eight only partly fixed and showed that the two untaken tests hid real defects: wait built a result from an unfinished round while another reader was adopting it, and tail read the log before its grace wait. The last commit fixes both with tests, reads a slashless rule target also as a workspace-relative path, lets a root of / cover everything, and narrows the authentication-text filter so a token-limit error keeps its own text.

A fourth round confirmed the path and tail fixes and left two defects, fixed in the last commit: the authentication filter matched adc inside readconfig (now whole words), and wait still built a null result from an unfinished round once its hold expired (now status: busy, exit 75, bounded by the peer's quiet_after_s). Two race tests that status had short-circuited now exercise the races.

A fresh Codex thread then reviewed the whole branch and found one high defect: gcloud run without a terminal reports a rejected token as "Reauthentication failed. cannot prompt during non-interactive execution", with no OAuth code, so the probe would have warned and spawned on the very credential it exists to catch. Fixed with that text as a rejection marker and the stub emitting it. Also fixed from that round: tail now follows a live runner to its record instead of trusting the stream after a fixed grace, ask rules count in command verdicts and in the resume-note diff, and the error counts tool calls only when every entry has a target.

Its follow-up confirmed the probe fix and found the tail hold could still discard a record another reader finished in the last poll, the --no-follow snapshot reported the stream's status while the peer was alive, the remedy said "add an allow rule" for a refusal a deny or ask rule caused, and the counts still read as exact calls. All four fixed. Its last round (the thread's third) confirmed those and left two small items, fixed in the final commit: the --no-follow snapshot now reloads the record once it finds both processes gone, and a mixed set of refusals gets both remedies, derived from the verdicts. Codex also notes that the two race tests rely on timing (a held lock plus elapsed time, a poll before release) rather than a handshake; that limitation stands.

Verification

make check green (475 tests) on every commit. Live against agy 1.4.8 on route gcp: cairn doctor prints token ok; a plan-mode run told to read_file an invented file outside --cd ended status denied, exit 2, denied: [{tool: read_file, target: <that path>}], the error naming read_file(<its dir>) and --extra --add-dir, and cairn invoke tail printed TOOL read_file, DENIED read_file <path>, RESULT denied; the resume with --extra --add-dir <dir> got the note previously refused read_file <path>: now readable (inside <dir>), recorded the directory in permissions.roots, and answered with the file's first line (status succeeded, exit 0).

…y answer is a denied round

agy soft-denies a read_file outside the workspace in plan mode and ends the turn with no
answer, but its stream shows no tool error: only the result's denied_actions names it.
denied_outcome() read the stream's denied events alone, and the denied_actions fallback ran
after the outcome was fixed, so the round reported status succeeded, exit 0, last_message
null. Replay.refusals() now yields the stream's refusals or, failing those, one entry per
counted action, and both _finalise() and adopt() decide from it. The error for a refused
read_file names the read_file(<absolute dir>) rule and --extra --add-dir. The agy stub
gains STUB_SOFT_DENY with the live shape (step DONE, log line, empty response); the module
docstrings say a soft deny can end the turn.
…e stream, the log and the count

The Codex plan review found three readers that still said succeeded on a soft-denied
round: cairn invoke tail printed the stream's own status when the final event arrived
before the runner finalised, status classified from that status and showed an earlier
progress message as the answer, and an adopted denied round exited 1 with a null exit
code. stream_outcome() applies the runner's classification to a stream any reader sees,
EXIT_BY_OUTCOME gives adopt and tail the same exit codes as the runner, tail waits a
moment for the runner's record before falling back, and a finished round's record is
authoritative for status (last_message None included). Replay.refusals() merges the
stream's refusals, the soft denials observed in agy's log and one entry per counted
action type the others did not name, with agy's 'command' action mapped to run_command.
The adopt test is its own method: the earlier one picked the finished thread.
…spawn

A soft-denied read_file had no path: agy's log names it by step ('soft-denying tool
confirmation "ReadFile" at step 2'), so the agy harness keeps a step-to-target map from
the stream and joins the two (Antigravity.soft_denials); the live runner reads the log
before the outcome is decided and adopt reads it from either location. read_verdict()
models agy's documented read rules (deny beats all, read_file(<dir>) covers its subtree,
write_file grants read, a bare target matches the name, the workspace and every --add-dir
need no rule) so the error and the resume note say whether a refused path is now readable,
still denied, or still outside the workspace; the round records its roots.

The ADC file can exist with a refresh token Google no longer honours (invalid_rapt, the
Workspace session-length policy). adc_token_check() runs gcloud's print-access-token with
the run's environment: a rejected credential refuses the spawn (exit 2) and is a warn line
in cairn doctor with the login command; gcloud missing, a timeout or a transport failure
is a warning, never a refusal, and doctor says the token was not verified. observe_billing
reads 'adcAuth: failed to get token from token source' when no success follows, so a
failed round names the cause (not retryable for a rejected credential, retryable for a
transport failure). A gcloud stub joins the test fixture.
The invoke-antigravity skill says which credential each billing route needs, that agy
models can fail while a gcp run works, that plan mode reads the --cd tree only (files
outside it go inside, through --extra --add-dir, or into the prompt), and that a soft deny
can end the turn. The anonymised post-mortem of the two dead rounds lands in docs with the
resolution, and the changelog carries 2.8.0.
…n every reader, narrower ADC classification

From the Codex review of the diff (gpt-6-astra, high). read_verdict() now takes agy's ask
rules (headless agy cannot ask, so they deny), resolves a relative rule target and a
relative --add-dir against the workspace, normalises dot segments, matches a slash glob
against the whole path and treats a trailing slash as the same rule; the round records
permissions.ask. status and tail load the log's soft denials like the runner and adopt
do, and an observation classified denied shows no progress message as the answer. A
rejected credential is recognised by 'invalid_grant', 'invalid_rapt', 'reauth related
error' or 'does not have any valid credentials', no longer by the bare word 'reauth'; the
ADC error from agy's log replaces a round's error only when that error is about
authentication, and any later applyAuthResult counts as recovery. The probe's stdin is
/dev/null. The skill and changelog describe both denial shapes seen live, say the
--add-dir flag is recorded but not reapplied, name thread.json as where permissions live,
and give the probe's 20 s bound. The foundations test module's main guard sits last.
…the log after its grace, slashless read rules, a narrower auth filter

cairn invoke wait could build a result from an unfinished round while another reader held
the thread lock and was adopting it (status and exit code null); it now holds up to 30 s
for that reader's record. tail read the peer's log before its grace wait, so a denial the
log showed during the wait was missed; it reads the log after the wait. A slashless
read_file target now also reads as a workspace-relative path or directory (the union with
the name match, since the two readings have not been told apart live), and a root of '/'
covers everything. The ADC log error replaces a failed round's error only for text about
authentication (not 'input token count'). The changelog and post-mortem say plan mode
reads the --cd tree with no rule, not only; the skill says wait holds for an adopting
reader. Tests: the tail fallback after its grace, wait under a held lock, the new rules.
…round; whole-word auth filter

Past its hold (the peer's quiet_after_s), cairn invoke wait reports status busy (exit 75)
with the reason instead of a result with null status, outcome and exit code. The
authentication filter matches whole words, so 'readconfig' no longer counts as ADC. The two
race tests now exercise the race: the record is stripped again after status adopted it, the
log's denial appears during tail's grace wait, and the waiter is checked to be still holding
before the lock is released.
…tion; tail follows to the record; ask rules in command verdicts

gcloud with no terminal (the probe's case) reports a rejected refresh token as
'Reauthentication failed. cannot prompt during non-interactive execution', with no OAuth
code, so the probe warned and spawned on the very credential it exists to catch; that text
now refuses, and the gcloud stub emits it (the with-terminal shape is a second mode). tail
no longer trusts the stream after a fixed grace: it follows a live runner to its record
(the watchdog may still make the round a timeout), adopts the round when both processes
are gone, and holds for another reader's adoption up to the peer's quiet threshold before
classifying the stream plus the log itself. Command verdicts and the resume-note diff take
agy's ask rules into account (a refusal when headless; ask beats allow). The error counts
tool calls only when every entry has a target; a targetless entry is an action type agy
counted. Tests cover each; the tail test under a held lock exercises the hold.
…er; a snapshot of a live round is not a verdict; the remedy follows the verdict

In tail's adoption hold, observe() could land another reader's record (a cancellation,
say) in the last poll and the loop still fell back to the stream: the round is reloaded
right after each observation and a finished record always wins. With --no-follow, a round
whose runner or peer is still alive is reported running, not with the stream's own
status. The denied error says 'Review the deny and ask rules' when every refusal is
covered by such a rule (adding an allow rule would not help) and 'Add the rule' only for
an unmatched call. Refusals are counted as distinct refusal(s) everywhere: entries are
(tool, target) pairs or counted action types, never exact calls. The tail hold test
asserts the hold took its time.
…oth processes are gone; the remedy covers a mixed set

With --no-follow, tail read the record, saw the stream's SUCCESS, found both processes
gone and used the stale record while the runner had just saved a timeout; it now reloads
the round at that point and a finished record wins. The denied error derives its remedy
from the verdicts (command_verdict, read_verdict) rather than from rendered text: review
the deny and ask rules for a covered refusal, add the missing allow rule for an unmatched
one, both for a mixed set.
@Shavvimal
Shavvimal merged commit 0e432de into main Oct 27, 2026
5 checks passed
@Shavvimal
Shavvimal deleted the fix/agy-soft-deny-adc-probe branch October 27, 2026 14:35
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.

1 participant