fix(approvals): show purpose, effect, and risk in approval prompts - #75795
fix(approvals): show purpose, effect, and risk in approval prompts#75795troyechung wants to merge 2 commits into
Conversation
Add optional purpose/effect/risk context fields to terminal tool calls and thread them through command approval requests. Gateway approval prompts keep the existing request message intact and send a follow-up context message when explanation data is available.
Enhance approval prompts with model-supplied Purpose/Effect/Risk context co-located in a single message — no standalone follow-up. - tools/approval.py: _sanitize_explanation, _build_enhanced_description_with_context, enhanced_desc replaces combined_desc on all user-facing surfaces - gateway/run.py: extract _deliver_approval_message (module-level, testable); DeliveryError for fail-closed delivery; button success returns, button explicit failure falls through to text, unknown/None/timeout raises DeliveryError - prompt_dangerous_approval: CLI displays unverified-context annotation - tests: production-delivery, redaction, fail-closed, E2E, AST wiring
Related to #22363: both make dangerous-command approvals more understandable, but the current diff uses one sanitized/redacted approval message and fail-closed delivery cleanup rather than #22363's separate follow-up context message. This is a design choice for maintainers, not a duplicate. |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for rebuilding the approval-context idea around the current single-message gateway path. Current main still sends only the command and detector reason (gateway/run.py:4817-4889), so the feature has a real target.
Problems
- Blocking —
tools/approval.py:3428-3434: the sanitizer removes every C0 control character before normalizing/splitting newlines. A value such asnormal text\n/approve sessionbecomes one line, so the anchored_FORGE_REmisses the forged approval instruction. The new test attests/gateway/test_approval_context_message.py:385covers only a forged first line. - Suggestion —
tools/approval.py:3917-3921:enhanced_descalready embeds the context, thenprompt_dangerous_approval()receives the same explanation separately. The direct renderer prints both (tools/approval.py:2502-2513), duplicating context in its no-callback fallback.
Suggested changes
- Normalize CRLF first, retain LF while stripping other controls, then filter forged lines; add embedded-line and CRLF regression cases.
- Render the context through one of the two prompt inputs, not both.
Automated hermes-sweeper review.
| if not value: | ||
| continue | ||
| value = redact_sensitive_text(value, force=True) | ||
| value = re.sub(r"[\x00-\x1f\x7f]", "", value) |
There was a problem hiding this comment.
Blocking: this removes LF/CR before the normalization and line-based _FORGE_RE pass below. A value like normal text\n/approve session becomes one non-matching line, leaving a forged approval instruction in the user-visible context. Normalize line endings first and preserve LF while stripping the remaining controls; add a regression case with a benign first line.
| allow_permanent=has_permanent_capable and not smart_denied_for_owner, | ||
| smart_denied=smart_denied_for_owner, | ||
| approval_callback=approval_callback, | ||
| explanation=approval_explanation, |
There was a problem hiding this comment.
enhanced_desc already embeds the model context, while the direct prompt renderer also prints explanation separately. In the no-callback fallback this duplicates the purpose/effect/risk block; pass only one representation to that renderer.
|
This PR would also help with another approval-clarity case I just ran into: approvals generated by the pending write-approval helpers for skill/memory writes. Example shape: python - <<'PY'
from hermes_cli.write_approval_commands import handle_pending_subcommand
from tools import write_approval as wa
pid='...'
rec=wa.get_pending(wa.SKILLS, pid)
print('found', bool(rec), rec.get('summary') if rec else '')
if rec:
print(handle_pending_subcommand(wa.SKILLS, ['approve', pid]))
print('remaining', wa.get_pending(wa.SKILLS, pid))
PYToday the approval reason is technically correct (
It would be great if the Purpose/Effect/Risk surface could either:
That would keep the useful one-approval-per-staged-operation flow, but make the approval card human-readable: users approve the actual pending skill/memory change, not an opaque Python heredoc. |
What does this PR do?
Rebuilds the intent of #22363 against the current gateway approval architecture.
Dangerous-command approval prompts currently expose the command and a guard reason, but not enough context for users to understand why the command is needed, what it changes, or what could go wrong. This PR surfaces model-provided Purpose / Effect / Risk in the same actionable approval prompt while keeping the system-generated risk assessment separate.
Model-provided context is explicitly labeled as unverified, length-limited, sanitized, and credential-redacted. The gateway also redacts again at the outbound delivery boundary. Delivery failures with unknown status fail closed and remove the pending approval entry.
Related Issue
Refs #22363
Type of Change
Changes Made
How to Test
Run the related approval and gateway suites:
Result: 249 passed.
Additional checks performed:
git diff --checkpy_compilefortools/approval.pyandgateway/run.pyChecklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests pass (targeted related suites pass: 249 tests)Documentation & Housekeeping
cli-config.yaml.example— N/A, no config keys changedCONTRIBUTING.md/AGENTS.md— N/A, no contributor workflow changedScreenshots / Logs
The targeted test suites pass with 249 tests. No UI screenshots are included because the behavior is covered at the shared CLI and gateway delivery boundaries.