fix(approval): make "always" auto-approve work for credentialed HTTP requests#1257
fix(approval): make "always" auto-approve work for credentialed HTTP requests#1257nearfamiliarcow wants to merge 1 commit intonearai:stagingfrom
Conversation
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request resolves a bug where the 'always' auto-approve feature was ineffective for HTTP requests containing credentials. Previously, these requests were incorrectly classified as requiring explicit approval every time, bypassing the auto-approval mechanism. The changes reclassify credentialed HTTP requests to allow for auto-approval, making the 'always' option functional. Additionally, the system now intelligently suppresses the 'always' approval button in user interfaces for tools that truly necessitate per-invocation approval, enhancing the user experience and security posture. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request effectively addresses the issue where the 'always' auto-approve option didn't work for credentialed HTTP requests. The root cause is correctly identified and fixed by changing the approval requirement from Always to UnlessAutoApproved.
The changes are comprehensive, propagating the allow_always flag through various layers of the application to ensure the UI correctly hides the 'always' option for destructive commands. The addition of new regression tests and updates to existing ones are well done and improve the robustness of the codebase.
I have one minor suggestion to improve the clarity of a new test case, but overall, this is an excellent and well-executed fix.
1cefb13 to
96f8332
Compare
…requests The HTTP tool returned `ApprovalRequirement::Always` for requests with credentials, but `Always` is hardcoded to ignore the session auto-approve set. This meant users who clicked "always" were re-prompted on every subsequent HTTP call — the UI offered "always" but the backend ignored it. Two fixes: 1. HTTP credentialed requests now return `UnlessAutoApproved` instead of `Always`, so the session auto-approve set is respected. 2. `StatusUpdate::ApprovalNeeded` now carries `allow_always: bool`. All channel UIs (Telegram, Slack, Signal, REPL, Web) conditionally hide the "always" option when a tool truly requires per-invocation approval (`ApprovalRequirement::Always`, e.g. destructive shell commands). Also boxes `PendingApproval` in `AgenticLoopResult::NeedApproval` to fix a pre-existing clippy `large_enum_variant` warning. Regression tests included (test_credentialed_requests_respect_auto_approve, test_allow_always_matches_approval_requirement) but CI heuristic cannot detect them in cross-fork PR diffs. [skip-regression-check]
96f8332 to
05c731e
Compare
zmanian
left a comment
There was a problem hiding this comment.
Review: APPROVE
Excellent PR -- correct fix direction, well-tested, great PR description explaining the three-tier approval model.
What's good
- Preserves the
Alwaysinvariant (destructive ops still require per-invocation approval) - Downgrades credentialed HTTP from
AlwaystoUnlessAutoApproved-- correct risk classification - UI fix: hides "always" button when
ApprovalRequirement::Alwaysis in effect - Backward compatible:
#[serde(default = "default_true")]onPendingApproval.allow_always - Boxing
PendingApprovalinNeedApprovaladdresses pre-existing clippy warning - Good regression tests
Minor requests (non-blocking)
1. Relay channel discards allow_always -- destructured as allow_always: _ in relay/channel.rs. Slack buttons would still show "always" for ApprovalRequirement::Always tools. Add a comment explaining why relay is exempt, or thread the field through.
2. Duplicated allow_always computation -- !matches!(requirement, ApprovalRequirement::Always) appears in both dispatcher.rs and thread_ops.rs. Consider centralizing as ApprovalRequirement::allows_always() method.
3. Consider documenting the approval tier model on ApprovalRequirement itself -- the PR description explains it beautifully but this knowledge shouldn't live only in PR history.
Security analysis: positive. No privilege escalation, Always invariant preserved, session-scoped, allow_always computed server-side and not client-manipulable.
CI all green.
Problem
When IronClaw makes HTTP calls to APIs with stored credentials (GitHub, Attio, etc.), the user is prompted to approve each call. The prompt offers "yes", "no", and "always". Clicking "always" should auto-approve all future calls to that tool for the session — but it doesn't work. The user gets re-prompted on every single HTTP call, indefinitely.
Real-world example (Telegram)
Root cause
IronClaw has a three-tier approval system (
ApprovalRequirement):NeverUnlessAutoApprovedAlwaystrueThe HTTP tool (
http.rs:839) returnedApprovalRequirement::Alwaysfor any request where the credential registry has a mapping for that host. Since virtually every useful API call has credentials (GitHub token viagithub.capabilities.json, Attio keys, etc.), every HTTP call hit theAlwayspath.In the dispatcher (
dispatcher.rs:558), theAlwaysarm is:So even though clicking "always" correctly saves
"http"tosession.auto_approved_tools(inthread_ops.rs:909), the next call ignores it becauseAlwaysnever looks at that set.Meanwhile, the UI always offered "always" as an option regardless of tier — so the user sees and clicks a button that the backend silently ignores.
Fix
Two changes addressing both the backend bug and the UI lie:
1. HTTP credentialed requests:
Always→UnlessAutoApprovedCredentialed HTTP requests are not in the same risk class as
rm -rf /orgit push --force. TheAlwaystier is reserved for genuinely destructive operations (destructive shell commands,tool_remove,skill_remove). API calls with credentials should beUnlessAutoApproved— prompt on first use, then respect "always".2. Hide "always" in the UI when it won't work
A new
allow_always: boolfield flows from the dispatcher throughPendingApproval→StatusUpdate::ApprovalNeeded→ every channel UI. When a tool returnsApprovalRequirement::Always(e.g. destructive shell), the "always" button/text is hidden. When it returnsUnlessAutoApproved, the option is shown.Why not the other direction (make
Alwaysrespect auto-approve)?That would defeat the purpose of
Always. The existing testtest_always_approval_requirement_bypasses_session_auto_approveexplicitly verifies thatAlwaysignores the auto-approve set — that's correct behavior fortool_remove, destructive shell commands, etc. Our fix preserves that invariant.After this fix, the example conversation becomes:
api.github.com→UnlessAutoApproved→ prompted"http"added tosession.auto_approved_toolsUnlessAutoApproved→ checks set → found → no promptBackward compatibility
PendingApproval.allow_alwaysuses#[serde(default = "default_true")]— old serialized values deserialize with "always" shown (safe default)app.jsusesdata.allow_always !== false— missing field defaults to showing the buttonPendingApprovalinAgenticLoopResult::NeedApprovalto fix a pre-existing clippylarge_enum_variantwarningReview Track: C
Touches
src/agent/(dispatcher, thread_ops, session).Rollback plan
Revert the single commit. Serde defaults ensure no deserialization breakage.
Test plan
--all-features)cargo fmtcleantest_credentialed_requests_respect_auto_approve— asserts HTTP with credentials returnsUnlessAutoApproved, notAlwaystest_allow_always_matches_approval_requirement— verifiesallow_alwayscomputation for all threeApprovalRequirementvariantsUnlessAutoApprovedrm -rf), verify "always" option is NOT shownTest gaps
Always-tier tool has no effect on future calls (covered by existingtest_always_approval_requirement_bypasses_session_auto_approveat the dispatcher logic level, but not end-to-end).Files changed (13)
tools/builtin/http.rsAlways→UnlessAutoApprovedfor credentialsagent/session.rsallow_always: boolonPendingApprovalagent/submission.rsallow_alwaysonSubmissionResult::NeedApprovalagent/dispatcher.rsallow_always, threads through, boxesPendingApprovalagent/thread_ops.rsallow_alwaysthrough 3 approval emission siteschannels/channel.rsallow_alwaysonStatusUpdate::ApprovalNeededchannels/repl.rschannels/signal.rschannels/wasm/wrapper.rschannels/relay/channel.rschannels/web/mod.rsallow_alwaysto SSE eventchannels/web/types.rsallow_alwaysonSseEvent::ApprovalNeededchannels/web/static/app.js