antigravity-tui-supervisor.py: partial terminal redraw during a permission prompt can trip the fail-closed guard and halt agy delivery
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 28/100
Research direction
Start in scripts/drivers/types/antigravity/antigravity-tui-supervisor.py at permission_input_rejection_reason() / permission_input_ready() (around line 315), which judge only the current self.screen snapshot. Reproduce a mid-redraw permission modal (incomplete footer/nav/body tokens) and confirm it trips fail() / durableAttention. Any fix must stay fail-closed: do not search raw PTY text, and do not keep a stale screen snapshot across later inputs. Done means a legitimate permission prompt still authorizes one stdin event while incomplete frames no longer halt delivery.
Written by the indexing model from the issue text.
Description
Summary
On our fork (joelmitz/agmsg) we hit a case where a real agy (Antigravity) session
stopped mid-delivery with a fail-closed halt, even though the child process was
legitimately showing a permission-approval modal (from whoami.sh). What we
confirmed: the captured frame was incomplete while the permission flow was
active, so permission_input_rejection_reason() returned a rejection reason
(e.g. permission-nav-missing / permission-body-incomplete) for that frame.
The supervisor then treated the input as an unexpected human interruption and
called fail(), which sets durableAttention and halts automatic delivery.
We believe this is not fork-specific: we diffed scripts/drivers/types/antigravity/antigravity-tui-supervisor.py
at the commit where we last merged upstream (e26815a8, which took your main
as-is for this file) against current origin/main and they are byte-identical.
permission_input_rejection_reason() only ever looks at the current screen
snapshot — there is no tolerance for a screen caught mid-redraw. So we'd expect
the same halt to be reproducible upstream under the right timing, even though we
haven't reproduced it there directly (our repro is fork-only, see below).
Root cause
permission_input_rejection_reason() (currently around line 315) makes a
purely structural judgement of self.screen at the instant it's called:
footer text, nav row position, and the three required tokens
(Requesting permission for:, Do you want to proceed?, > 1. Yes) all in
the current frame. There is no tolerance for a frame caught mid-redraw — if the
frame is incomplete at read time, permission_input_ready() returns False
for that read cycle even while the permission flow is genuinely active. (A
plausible mechanism for how a frame ends up incomplete is a redraw split
across two reads — e.g. footer visible but nav row not yet redrawn, or vice
versa — but we have not instrumented this to confirm it; what we observed
directly is the incomplete-frame rejection and the resulting halt, not the
underlying terminal I/O timing.)
What we changed (fork-only so far, not proposed as-is)
We went through three iterations before landing on something we're relatively
confident isn't a regression from a security standpoint (each iteration got an
independent adversarial review before merging):
-
First attempt (
5ca18bf2) — added a fallback that searched the raw
accumulated PTY transcript (permission_raw_window, up to 64KB of
unfiltered child output) for the three required tokens in order, regardless
of screen structure. This was rejected in review: the accumulated
transcript can contain agmsg message bodies echoed by the child CLI (agy
surfaces the received message text on screen), so if a delivered message
body happened to contain the right phrases in the right order, the
fallback would authorize input even though no real permission modal was
ever on screen. Structural position (footer → nav → body, within a bounded
window) is what makes the existing check resistant to this; a flat
substring search over raw output throws that away. -
Second attempt (
1b21beb1) — replaced the raw-transcript search with a
copy.deepcopy(self.screen)snapshot, captured only at the instant the
structural check (permission_input_ready()) actually passes. Falling
back to this snapshot when the live screen's structural check fails lets a
momentarily-redrawing screen still authorize input, without trusting raw
text. Also rejected: the snapshot was only cleared ininject(), so
once captured it stayed valid for the rest of the turn — meaning if a
second, unrelated human interruption happened after the first legitimate
permission prompt, it would be silently authorized via the stale snapshot
instead of trippingfail(). -
Final (
875785129+8403e766) — the snapshot is consumed exactly
once per legitimate use (permission_snapshot_fallback_usedflag, checked
inpermission_input_ready_with_snapshot()), and re-armed only when a
new structurally-valid permission screen is captured (not on any other
condition). So: the fallback authorizes at most one subsequent stdin event
after a structurally-valid snapshot was captured — with no time limit or
poll-cycle bound on how long that one use may be deferred — and is
re-armed only by capturing a new structurally-valid snapshot; every input
outside of that one use still goes through the normal fail-closed path.
Relevant commits (fork, joelmitz/agmsg main, all already pushed):
5ca18bf2e7b3b368f002a7d2c1a380096fb1275c(superseded, kept for history)1b21beb19c9d1ac05fa6d66121f2dc3842cb6f7b(superseded, kept for history)875785129eb2202f916e9106f7323ae6d40438ac8403e76631fe63592b829cb5020fecb69792c16d(current state)
Net diff against e26815a8 (the last point where this file matched upstream)
is small — one new field pair, one new method, and a few lines wiring it into
the existing stdin-handling branch. Happy to open this as a PR against
origin/main if that's useful, or to just leave this as a heads-up in case
you've seen (or want to guard against) the same class of halt.
Why we're not just opening a PR outright
Our fix is tied to our fork's specific snapshot/consume/re-arm design, and we
haven't tried to reproduce the original halt against origin/main directly
(only against our fork, post-1.3.0-merge, where the file was identical to
yours at merge time). Wanted to flag the underlying issue and our approach
first in case you have a different preference for how to harden this check,
or already have something in flight.
- Dominant language
- Shell
- Stars
- 1.5k
- Forks
- 156
- Avg merge
- 16h 23m
- Merged PRs (30d)
- 197
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from fujibee/agmsg
-
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
Maintainers usually reply within 1 day
-
Difficulty 1/5 Under an hour Newbie friendliness 88/100
Maintainers usually reply within 1 day
-
[P2] app: peer-message previews pass C0/DEL control bytes to pty_injectPossibly taken A pull request linked to this issue is open or already merged. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
[Bug] adb backup script reports "Done" & exits 0 when the backup failsPossibly taken @bhattmanthan claimed this today. Openrequest-issue-assignment
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
OWASP/mastg#4034 · 5 comments ·
Maintainers usually reply within 1 day
-
S3: friction & debt ws:tooling
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
MedakaLang/medaka#4865 ·
Maintainers usually reply within 1 day
-
severity: low
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
luainkernel/lunatik#2318 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 62/100