Skip to content

feat(proxy): wire interactive approval into proxy and proxy-http#183

Merged
dgenio merged 4 commits into
mainfrom
claude/github-issues-triage-wqfk36
Jun 14, 2026
Merged

feat(proxy): wire interactive approval into proxy and proxy-http#183
dgenio merged 4 commits into
mainfrom
claude/github-issues-triage-wqfk36

Conversation

@dgenio

@dgenio dgenio commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Summary

Makes ask decisions usable in both live proxies instead of the hardwired deny-all behavior, and unifies the approver contract the three call sites share. This delivers a coherent group of four related issues that all live in the approval/proxy code path.

What changed:

Related issue

Closes #126
Closes #127
Closes #129
Closes #137

How verified

  • make ci (fmt-check, go vet ./..., go test -race ./...) — all packages pass, race-clean, total coverage 80.1%.
  • New/updated tests: internal/proxy (TestProcessAgentLineAskApproved/Denied/Errors/Timeout/NonInteractive/NotificationDropped) and internal/httpproxy (TestHTTPAskApprovedForwards/ApproverErrorBlocks/Timeout + reason assertion on the existing deny test) — each fails without the fix (deny-all) and passes with it.
  • Smoke: agentfence proxy --help lists --approval-timeout; an invalid --approval-timeout value is rejected with a parse error.

Tradeoffs / risks

  • Behavior change (intentional, Mode B / no back-compat constraint): the proxies previously audited the unresolved ask; they now audit the resolved allow/deny. The agent-facing blocked message now uses canonical reasons (denied interactively, approval timeout, approval I/O error) instead of "... (denied via ask)" / "approval error: <raw>", and the raw approver error text is no longer exposed to the agent. Documented in the CHANGELOG.
  • The stdio proxy's interactive prompt reads /dev/tty; where that is unavailable (e.g. Windows/CI) interactive approval isn't meaningful — use --no-interactive there. The CLI's resolveAsk reason text is unchanged (an existing test pins non-interactive: ask auto-denied).

Checklist

  • Tests added or updated
  • Documentation updated if needed
  • CI passes
  • Issue number included

https://claude.ai/code/session_01K8hWMifivsN6RMKqzXV3gX


Generated by Claude Code

Make `ask` decisions usable in both live proxies instead of the hardwired
deny-all behavior, and unify the approver contract the three call sites share.

- #137: collapse the duplicate proxy.Approver / httpproxy.Approver interfaces
  and their DenyAllApprover structs into type aliases for the single
  approval.Approver / approval.DenyAllApprover (no import cycle).
- #126: connect approval.TTYApprover and a new --approval-timeout to runProxy
  and runProxyHTTP; --no-interactive keeps the fail-closed DenyAllApprover.
  Add approval.Resolve as the shared ask->decision mapping (check's resolveAsk
  now delegates to it). The proxies audit the resolved decision with the
  approval outcome appended to the engine reason (preserving taint-escalation
  context) and return the canonical reason to the agent without leaking
  internal approver error text.
- #129: table-driven regression tests across both proxies for approve, deny,
  timeout, I/O error, non-interactive, and ask-on-notification.
- #127: correct docs/integration-guide.md and docs/architecture.md that still
  described proxy-http and the approver as roadmap/unimplemented and referenced
  closed issues #29/#30.

https://claude.ai/code/session_01K8hWMifivsN6RMKqzXV3gX
Copilot AI review requested due to automatic review settings June 14, 2026 17:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR wires interactive ask approval into both live proxies (stdio and HTTP) by unifying the Approver contract across packages and centralizing ask→decision reason mapping in internal/approval. It also updates docs/changelog and adds regression coverage to lock the behavior in.

Changes:

  • Aliases proxy.Approver/httpproxy.Approver (and deny-all implementations) to the shared approval.Approver contract.
  • Adds approval.Resolve and uses it in check, proxy, and proxy-http to apply approval timeout + consistent canonical reasons.
  • Expands proxy/httpproxy tests and updates documentation to reflect shipped interactive approval behavior.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
internal/proxy/proxy.go Uses shared approver + resolves ask before auditing/forwarding; adds timeout/non-interactive options.
internal/proxy/proxy_test.go Adds/updates regression tests for approve/deny/error/timeout/non-interactive/notification behavior.
internal/httpproxy/httpproxy.go Mirrors stdio proxy: resolves ask via shared resolver before audit/response/forward.
internal/httpproxy/httpproxy_test.go Adds approval-path tests for allow/deny/error/timeout and audit assertions.
internal/approval/approval.go Introduces Outcome + Resolve as canonical ask→reason mapping used by all call sites.
cmd/agentfence/main.go Wires TTY approver + --approval-timeout into proxy and proxy-http; delegates check ask resolution to approval.Resolve.
docs/integration-guide.md Updates approval guidance and removes stale “unimplemented/roadmap” statements.
docs/architecture.md Updates architecture description to match interactive approval + timeout behavior.
CHANGELOG.md Documents the new proxy approval behavior, unified approver contract, and reason/audit behavior changes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread cmd/agentfence/main.go
Comment thread docs/integration-guide.md Outdated
Addresses PR review: newProxyApprover used approval.NewTTYApprover(), which
silently falls back to os.Stdin when /dev/tty is unavailable. In the stdio
proxy, stdin is the agent's JSON-RPC channel, so that fallback could block on
protocol traffic or interleave prompt reads with frames.

- Add approval.NewTTYApproverStrict() that opens /dev/tty and errors instead of
  falling back to stdin; NewTTYApprover() (used by check) keeps the fallback.
- newProxyApprover now uses the strict constructor and, when no terminal is
  present, fails with a message pointing the operator to --no-interactive.
- Document the strict behavior in the integration guide.
- Add cmd tests covering the deny-all and require-TTY paths.

https://claude.ai/code/session_01K8hWMifivsN6RMKqzXV3gX

dgenio commented Jun 14, 2026

Copy link
Copy Markdown
Owner Author

Good catch — fixed in e5918b4. Both comments are the same root cause: the proxies must not fall back to stdin for prompts.

  • Added approval.NewTTYApproverStrict() which opens /dev/tty and returns an error instead of falling back to os.Stdin.
  • newProxyApprover now uses it; with no controlling terminal the proxy exits with a message to re-run with --no-interactive rather than silently reading approvals off the JSON-RPC channel. (check keeps the stdin fallback, since its stdin is the user terminal and calls come from --call.)
  • Updated the integration-guide paragraph to state the "no /dev/tty → use --no-interactive" behavior, and added tests for the deny-all and require-TTY paths.

Generated by Claude Code

claude added 2 commits June 14, 2026 18:40
…triage-wqfk36

# Conflicts:
#	cmd/agentfence/main.go
#	cmd/agentfence/main_test.go
#	internal/httpproxy/httpproxy.go
When an ask prompt times out or is cancelled, TTYApprover.Request returned
while its read goroutine stayed parked on /dev/tty. With --approval-timeout now
wired into the long-running proxies, repeated timeouts could leave multiple
goroutines racing on the same fd, letting a stale reader steal or drop the
operator's keystroke. Keep a single persistent bufio.Reader and reuse the
in-flight read across calls so exactly one reader exists and a late keystroke is
delivered to the next prompt. Also guard the audit-reason annotation against an
empty engine reason to avoid a leading-space '(reason)' string.

Adds a regression test covering the timeout-then-late-response path.
@dgenio dgenio merged commit a2d8db5 into main Jun 14, 2026
4 checks passed
@dgenio dgenio deleted the claude/github-issues-triage-wqfk36 branch June 14, 2026 19:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants