fix(approval): route WebChat approval parks via origin fallback - #5514
Conversation
cron_add scheduled from a chat turn is delegated to scheduler_agent as an async sub-agent. That spawn propagates the WebChat turn origin but not the APPROVAL_CHAT_CONTEXT task-local, so the approval gate parked with thread_id/client_id = None. The web-channel surface only fires when both are set, so the ApprovalRequested event was silently dropped and the park TTL-denied — the scheduled reminder was never created. The WebChat origin already carries the same thread_id/client_id the approval context does (both built from the same values in web_chat::start_chat), so the gate now falls back to the origin's thread/client for routing when the task-local is absent. Only WebChat origins are affected; inline chat turns and every other origin are unchanged. Also reword the unlabelled-origin (Unknown) denial from a raw policy dump into an actionable message, and add a regression test covering a WebChat-origin cron_add park with no APPROVAL_CHAT_CONTEXT scoped. Closes tinyhumansai#5499
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughWebChat approvals now use ChangesApproval routing
Estimated code review effort: 2 (Simple) | ~10 minutes Mergeability Score: ⚪ Minimal · up to This localized change routes approval requests from WebChat turns to the originating conversation without changing approval requirements for other origins; no actionable merge-blocking risk remains beyond normal checks and review. Possibly related PRs
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
tinysweeper found nothing blocking. Approving.
$0.0124 · 16,837 in / 3,372 out · 13,458 cached (80%) · z-ai/glm-5.2
critique: $0.0030 · 3,967 in / 872 out · 3,348 cached (84%) · z-ai/glm-5.2
security: $0.0025 · 3,946 in / 653 out · 3,331 cached (84%) · z-ai/glm-5.2
tests: $0.0043 · 3,861 in / 1,367 out · 3,072 cached (80%) · z-ai/glm-5.2
description: $0.0027 · 5,063 in / 480 out · 3,707 cached (73%) · z-ai/glm-5.2
…work # Conflicts: # src/openhuman/security/approval/gate.rs
Review — comment only (no approval)Reviewed VerifiedNo cross-routing / authorization hole. "byte-for-byte unchanged" holds. Effective diff is one file, +94/−2. Non- No degenerate-origin case. The test is non-vacuous. With No merge-order dependency. #5465 (origin propagation, merged 08-09) and #5527 (merged today) are both on F1 — MEDIUM, not currently disclosed: the surfaced card is cleared by the next turn on the thread, and while invisible a bare "ok" approves the background effectThe body discloses the
But Scenario A — the card vanishes, the park TTL-denies anyway. User asks for the reminder; parent turn ends; child parks and the card surfaces (the fix working). User then types anything else in that thread — that turn's Scenario B — conversational "ok" captured as an approval. While that now-invisible park is open, Cheapest mitigation that keeps the fix: for the origin-fallback park only, publish F2 — process note: the new test is not covered by a green lane yet
Nits
Sound, narrow, well-documented change that does what it claims and does not weaken the gate. F1 is the only thing I'd want resolved or explicitly recorded before treating #5499's class of failure as closed. Approve/merge is the maintainer's call — deliberately not approving. |
Summary
cron_add/cron_updatefrom a chat turn silently failing: the approval park is now routable, so the approval card surfaces and the user can approve instead of the call TTL-denying.WebChatturn origin'sthread_id/client_idfor approval routing when theAPPROVAL_CHAT_CONTEXTtask-local is absent.Problem
cron_add(andcron_update) areexternal_effecttools, so the approval gate is consulted. When the orchestrator routes a "remind me…" request toscheduler_agent, that runs as an async delegated sub-agent (spawn_async_subagent). The spawn propagates theWebChatturn origin (so the gate no longer seesUnknown— that half is handled by the turn-origin-propagation work in #5527) but does not propagate theAPPROVAL_CHAT_CONTEXTtask-local.Result: the gate takes the
WebChatpark arm but readschat_thread_id/chat_client_idasNone.ApprovalRequestedis published withthread_id: None, the web-channelApprovalSurfaceSubscriberexplicitly does not surface it ("thread/client absent — NOT surfacing"), the park waits the full TTL, and the call is denied. The scheduled reminder is never created, with no card ever shown to the user.Solution
The
WebChatorigin already carries the samethread_id/client_idthatAPPROVAL_CHAT_CONTEXTdoes — both are built from the same values inweb_chat::start_chat. SoApprovalGate::interceptnow derives the chat routing from the origin when the task-local is absent. Scope is deliberately narrow: the fallback fires only forAgentTurnOrigin::WebChat; inline chat turns (task-local present) and every other origin are byte-for-byte unchanged.Design note: fixing this in the gate — rather than propagating a second task-local at the spawn site — is minimal and covers every path where a
WebChatturn reaches the gate without the approval context, not just async sub-agents.Relationship to #5499 / #5527
The linked issue #5499 (
agent turn has no origin label) was the pre-fix shape of this failure: the origin arrived asUnknownbefore propagation existed. #5527 landed the origin-propagation half and closed #5499 — it scopes the turn origin for the local-agent spawn and reworded theUnknown-origin denial into an actionable message. This PR closes the residual silent-park gap #5527 did not cover: even with a validWebChatorigin, an async-delegated sub-agent's approval never surfaces because the approval context task-local is absent. Rebased on currentmain, so the denial-message rewording that was in an earlier revision of this PR is dropped (already present from #5527) — the diff is now purely the routing fallback and its regression test.Submission Checklist
webchat_origin_routes_park_when_approval_chat_context_absentproves the park is routable via the origin whenAPPROVAL_CHAT_CONTEXTis absent (fails on old code)openhuman::security::approval+openhuman::agent::turn_origin(139 pass);cargo fmt --checkandcargo clippy --libclean.N/A: behaviour-only change (no feature row added/removed/renamed)## Related—N/A: behaviour-only changeN/A: does not touch a release-cut surface## RelatedsectionImpact
src/openhuman/security/approval/gate.rs). No frontend, no schema, no migration.WebChat-origin external-effect park with no approval context silently TTL-denied; it now surfaces its approval card in the originating thread. No origin other thanWebChatchanges behaviour, and the gate still parks (never auto-allows) — the user must still approve.thread_to_requestis one-park-per-thread; a parent turn and a concurrent async child parking on the same thread can overwrite the chat-reply route (card-click byrequest_idstill resolves either).Related
Summary by CodeRabbit