fix(agentgate): collapse repeated approval prompts into one card - #169
Merged
Conversation
A single logical command can trap the gate many times with byte-identical argv. The common case is a PATH search: anything launched through a wrapper such as timeout(1), env(1) or nohup(1) is resolved with execvp(3), which issues one execve(2) per PATH entry until one succeeds. Since "Allow once" persisted nothing, every probe prompted again — one wrapped `git push` could stack ten approval cards, each needing its own click. Manager now memoises once-scoped decisions (both "Allow once" and "Deny") for a short TTL, so an immediately-repeated identical request reuses the answer instead of re-prompting. Session decisions still populate the permanent cache and additionally drop any stale memo for the same key. The window only has to span the burst itself (probes arrive ~1ms apart), so it is deliberately measured in seconds: a genuinely new invocation later still prompts, and "Allow for session" remains the only way to stop being asked. Burst hits short-circuit before the rate-limit check, like session cache hits, so a PATH walk no longer consumes the per-minute prompt budget either. File and connect approvals route through the same Manager, so they benefit too.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
A single logical command can trap the gate many times with byte-identical argv, and each trap produced its own approval card.
The common case is a PATH search. Anything launched through a wrapper —
timeout(1),env(1),nohup(1)— is resolved withexecvp(3), which issues oneexecve(2)per PATH entry until one succeeds. Every probe carries identical argv, so every probe matched the same rule and prompted again. A baregit pushtraps once, because the shell stats its way to the absolute path first.Observed in a gate audit log: one wrapped push, one pid, ten identical
execvetraps milliseconds apart, three of them answered by hand before the user reached for "Allow for session":The reason each probe re-prompted is that
applyResolutionpersisted to the dedup cache only forDecisionSession/DecisionDenySession— "Allow once" cached nothing at all.Change
Managergains a short-lived burst memo alongside the session cache:Requestresolves aCacheKeythrough a newlookupCached: session cache first (Reason: "cache-hit"), then the memo (Reason: "burst-hit"). Expired entries are dropped on read.applyResolutionmemoises once-scoped decisions — both "Allow once" and "Deny" — forburstCacheTTL. Denying a wrapped command would otherwise stack the same ten cards.Burst hits short-circuit before
checkLimits, exactly like session cache hits, so a PATH walk no longer consumes ten slots of the per-minute prompt budget either.fileandconnectapprovals route through the sameManagerwith aCacheKey, so they get the same collapse.Net effect: the first probe prompts, the remaining nine resolve silently. One card instead of ten, with no session grant.
Trade-off
Documented at the constant: a script re-running the identical command inside the window rides the first decision. That is why the TTL is measured in seconds rather than minutes, and why "Allow for session" remains the only way to stop being asked at all.
Tests
Seven new cases in
approval_test.go:burstTTL == 0disables the memo entirelyCacheKeymemoises nothinglookupCached,rememberBurstLockedandapplyResolutionare all at 100%; the package's single uncovered line is the pre-existingDeadlineExceededbranch inRequest.go test -race ./internal/...passes andmake lintis clean.Not included
The audit also shows counts that vary across containers (2, 3, 4, 5, 7, 8, 10), which a pure PATH walk does not fully explain. One suspect is
server_linux.gotreatingENOENTfromSECCOMP_IOCTL_NOTIF_SENDas "tracee died": the kernel also returnsENOENTwhen a notification was invalidated because the tracee's syscall was signal-interrupted, and that tracee is alive and will re-trap. Left for a follow-up — this change collapses the duplicate cards either way.