Skip to content

feat(approval): gate external-effect tool calls until user approves (#1339)#2149

Merged
senamakel merged 19 commits into
tinyhumansai:mainfrom
oxoxDev:feat/1339-triggers-approval-gate
May 20, 2026
Merged

feat(approval): gate external-effect tool calls until user approves (#1339)#2149
senamakel merged 19 commits into
tinyhumansai:mainfrom
oxoxDev:feat/1339-triggers-approval-gate

Conversation

@oxoxDev
Copy link
Copy Markdown
Contributor

@oxoxDev oxoxDev commented May 18, 2026

Summary

  • New ApprovalGate middleware that parks any tool call whose Tool::external_effect() returns true (composio, pushover, gmail_unsubscribe, proactive external-channel sends, triage React/Escalate dispatch) until the user decides.
  • Pending requests persist in a new pending_approvals SQLite table so they survive a core restart; the gate exposes approval_list_pending + approval_decide over JSON-RPC and emits DomainEvent::ApprovalRequested / ApprovalDecided on the event bus.
  • Action summaries + JSON args are routed through a redactor that strips message bodies, contact names, tokens, and home paths before persistence or broadcast.
  • Opt-in at boot via OPENHUMAN_APPROVAL_GATE=1. When unset, ApprovalGate::try_global() returns None and the agent harness behaves exactly as before — safe to merge ahead of the React UI.
  • v1 ships the backend only; React toast, settings panel, and SSE (depends on Security: SSE EventSource in useWebhooks.ts has no authentication #1922) are deferred follow-ups.

Problem

Per #1339, proactive triggers (src/openhuman/channels/proactive.rs), agent side-effect tools (composio, pushover, gmail_unsubscribe), and triage React/Escalate dispatch all fire side effects without an explicit user-consent gate. PermissionLevel filters tools but does not gate execution through an approval queue, so an autonomous agent can publish to Slack, send email, or create calendar events without surfacing the action to the user first.

Solution

  • Tool trait — adds fn external_effect(&self) -> bool { false } to src/openhuman/tools/traits.rs. Composio, pushover, gmail_unsubscribe override to true. Voice reply_speech is documented as internal (default false) so a future Tool-wrap doesn't accidentally flip it (Polish mascot-driven TTS and STT voice experience #1206 coordination).
  • Gate (src/openhuman/approval/gate.rs)ApprovalGate::intercept(tool, summary, redacted_args) checks the session-scoped allowlist (set by prior ApproveAlwaysForTool decisions), then persists a row, emits DomainEvent::ApprovalRequested, and parks the future on a oneshot::Sender keyed by request_id. Default TTL = 10 minutes; timeout → Deny.
  • Store (src/openhuman/approval/store.rs) — synchronous rusqlite with the same with_connection pattern as notifications/store.rs. list_pending returns every undecided row regardless of session so the UI can show orphans after restart per the acceptance criterion; decisions on orphan rows update the DB but no live waiter resumes, so no side effect can fire across processes.
  • Redaction (src/openhuman/approval/redact.rs) — case-insensitive sensitive-key match (body, message, token, recipient, name, …) plus home-path scrub; arrays/objects collapse to <redacted: array (N items)> markers. Per feedback_redact_paths_and_ids_in_public.md and feedback_pr_no_chat_content.md.
  • RPC (src/openhuman/approval/rpc.rs + schemas.rs)openhuman.approval_list_pending + openhuman.approval_decide wired into the controller registry via src/core/all.rs. Decision values: approve_once, approve_always_for_tool, deny.
  • Harness hookstool_loop.rs and subagent_runner/ops.rs route external-effect tools through the gate; channels/proactive.rs gates external-channel sends (web exempt); agent/triage/escalation.rs gates React/Escalate dispatch at the entry point so the user can decline the whole escalation up-front. Defense in depth — the session allowlist short-circuits subsequent prompts after the first approval.
  • Bootstrap (src/core/jsonrpc.rs) — gate is installed only when OPENHUMAN_APPROVAL_GATE=1 and keyed on OPENHUMAN_CORE_TOKEN for session binding. Default off until the UI lands.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy — 46 new unit tests in openhuman::approval covering gate allow/deny/timeout/always-allowlist/unknown-id, store CRUD + cross-handle survival + purge_session, redactor sensitive-key match + array-count + home-path scrub, types decision serde, and RPC schemas
  • Diff coverage ≥ 80% — changed lines (Vitest + cargo-llvm-cov merged via diff-cover) meet the gate enforced by .github/workflows/coverage.yml. Local cargo test --lib openhuman::approval covers gate + store + redactor + types + schemas (46/46 pass); CI will confirm cross-file diff coverage on the merged lcov.
  • N/A: backend-only RPC surface — no new user-facing feature row in docs/TEST-COVERAGE-MATRIX.md. The follow-up UI PR will add the matrix entry when the toast + settings panel land.
  • N/A: backend-only RPC surface — no feature IDs from the matrix apply to this change. See the matrix N/A note above.
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • N/A: gate is opt-in via env var and the default-unset path is no behavior change — no docs/RELEASE-MANUAL-SMOKE.md update needed for this PR. Follow-up UI PR will add the smoke checklist entry.
  • Linked issue closed via Closes #NNN in the ## Related section

Impact

  • Runtime: desktop core only. Default OPENHUMAN_APPROVAL_GATE unset → ApprovalGate::try_global() returns None → zero behavior change in production. Enabling the env var without a UI deployed will park tool calls until the 10-minute TTL elapses (Deny) — intentional safety default; documented in the bootstrap log line and the gate module header.
  • Security: pending rows bound to the per-launch OPENHUMAN_CORE_TOKEN (session_id field) for audit; decisions on orphan rows from prior launches update the DB but cannot resume a parked future across processes, so no side effect can replay. Redactor strips secrets before any data leaves the gate.
  • Performance: SQLite insert + emit per gated call; allowlist hit short-circuits with a single lock acquisition. No impact when gate is uninstalled.
  • Migration: schema is idempotent (CREATE TABLE IF NOT EXISTS), applied per-connection like notifications/store.rs — no separate migration step.

Live smoke (backend only — no app required)

# Acceptance criterion Status
1 external_effect tool parks + emits event before execute live — two real Gmail triage escalations parked
2 UI toast with redacted summary UI deferred (RPC-level proven; list_pending returns redacted shape)
3 approve resumes parked future live — decision="approve_once" → escalation dispatched to orchestrator
4 deny aborts with explicit reason live — decision="deny"triage::escalation log: approval gate denied dispatch ... reason=User denied 'triage.escalate' execution.
5 pending rows survive restart live — orphan row re-listed after kill+relaunch with original request_id, created_at, session_id
6 PII/secrets redacted live — args_redacted returns only action / external_id / label / prompt_chars / target_agent; no email body, subject, sender, recipient
7 reply_speech not gated trait default + doc comment
8 shell sandboxed branch deferred to follow-up; shell stays default-false

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: feat/1339-triggers-approval-gate
  • Commit SHA: caeccc9cb63c165eda55cf550a4c27e94f235eb0

Validation Run

  • N/A: backend-only change, no app/ files touched
  • N/A: backend-only change, no app/ files touched
  • Focused tests: cargo test --lib openhuman::approval → 46 passed
  • Rust fmt/check (if changed): cargo fmt --check + cargo check both clean
  • N/A: no app/src-tauri/ files touched

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: when OPENHUMAN_APPROVAL_GATE=1, agent tool calls with external_effect()==true park until approval_decide resolves; default-off path is unchanged.
  • User-visible effect: none in this PR (no UI). The follow-up UI PR will surface a toast + settings panel that consume approval_list_pending + approval_decide.

Parity Contract

  • Legacy behavior preserved: ApprovalGate::try_global() == None returns the pre-Keep triggers read-only until users approve writes #1339 Allow short-circuit at every hook site; the existing CLI ApprovalManager path in tool_loop is untouched.
  • Guard/fallback/dispatch parity checks: harness/channels/triage hooks all wrap try_global() in Option so absence of the gate is a no-op; reply_speech documented as internal (default false) so the gate never fires on TTS.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none known
  • Canonical PR: this PR
  • Resolution: N/A

Summary by CodeRabbit

  • New Features

    • Approval gate to pause external-effect tool calls for user approve/deny decisions
    • Persistent pending-approval storage and session-aware handling
    • RPC endpoints to list pending approvals and apply decisions (approve once / approve-always / deny)
    • Argument redaction and concise action summaries in approval prompts to protect sensitive data
  • Documentation

    • Clarified approval classification for reply-speech and guidance for tool behavior

Review Change Stack

oxoxDev added 11 commits May 19, 2026 01:50
Default-false hook that classifies tool calls producing outbound
side effects (Slack/Telegram/email/calendar/webhook writes, etc.)
distinct from local writes. Used by the agent harness to route
gated calls through the approval gate before execute() runs.

Refs tinyhumansai#1339.
composio, gmail_unsubscribe, pushover all fan out to outbound
services on the user's behalf. Returning true here routes the
call through the approval gate before execute() runs.

composio is unconditional in v1; per-action write metadata
mapping is tracked as tinyhumansai#1339-v2 in an inline comment.

Refs tinyhumansai#1339.
…umansai#1339)

Coordinates with tinyhumansai#1206. reply_speech is the user's own assistant
speaking through the user's own speakers — no outbound side
effect. Code default already false; doc comment locks the
classification so a future Tool-wrap doesn't accidentally flip it.

Refs tinyhumansai#1339.
…nyhumansai#1339)

ApprovalRequested fires when the gate parks a tool-call future
(carries redacted args + session_id). ApprovalDecided fires from
the RPC handler after the parked future resolves. Both belong to
the new 'approval' domain.

Refs tinyhumansai#1339.
…inyhumansai#1339)

Async middleware that parks tool-call futures whose Tool returns
external_effect()==true. Pending rows persist in
pending_approvals so they survive core restart. Decisions land
via approval_list_pending / approval_decide RPC; the gate
resumes the parked oneshot and the agent continues. Session
allowlist (approve_always_for_tool) short-circuits subsequent
calls to the same tool. Redactor scrubs PII / chat content from
action_summary and args_redacted before persistence or broadcast.

51 unit tests cover gate (allow/deny/timeout/always-allowlist/
unknown id), store (insert/list/decide/purge_session/cross-handle
survival), redactor (sensitive-key match/array-count/home-path
scrub), types (decision serde), and RPC schemas.

Refs tinyhumansai#1339.
Wires approval_list_pending + approval_decide into the global
controller registry so they're discoverable via 'openhuman
approval --help' and reachable over JSON-RPC.

Refs tinyhumansai#1339.
…#1339)

tool_loop + subagent_runner both check tool.external_effect()
and route through ApprovalGate::intercept before tool.execute().
Deny is translated into a denied tool_result the agent can
reason about. When the gate is not installed (try_global() ==
None), behavior matches the pre-tinyhumansai#1339 path exactly.

Refs tinyhumansai#1339.
)

Proactive sends to Telegram/Discord/Slack/etc. route through
ApprovalGate before handing off to the channel implementation.
Web delivery (step 1 of the subscriber) is internal and exempt.

Refs tinyhumansai#1339.
Before invoking trigger_reactor / orchestrator on a React or
Escalate triage decision, ApprovalGate intercepts so the user
can decline the whole escalation up-front instead of one tool
call at a time. Defense in depth alongside the per-tool gate —
the session allowlist short-circuits subsequent prompts after
the first approval.

Denied dispatches publish TriggerEscalationFailed with the gate
reason and abort the trigger cleanly.

Refs tinyhumansai#1339.
…tinyhumansai#1339)

Opt-in at boot via env var. When unset (default) the gate is
never installed, try_global() returns None everywhere, and the
agent harness behaves exactly as before — safe to merge ahead
of the React UI that will dispatch approval_decide. When set,
the gate is keyed on OPENHUMAN_CORE_TOKEN for session binding.

Refs tinyhumansai#1339.

Closes tinyhumansai#1339.
@oxoxDev oxoxDev requested a review from a team May 18, 2026 20:25
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 166f7018-6ae1-49b0-a6ca-a6051a33b120

📥 Commits

Reviewing files that changed from the base of the PR and between 6d33ca2 and 41e8265.

📒 Files selected for processing (2)
  • src/openhuman/approval/redact.rs
  • src/openhuman/tools/impl/system/pushover.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/openhuman/approval/redact.rs

📝 Walkthrough

Walkthrough

Adds a persistent ApprovalGate with redaction, SQLite-backed pending approvals, RPC/controllers for list/decide, controller schemas/events, tool external-effect classification, and integration that blocks or short-circuits external-effect actions in harness, subagent, triage, and proactive delivery.

Changes

Approval gate system and integration

Layer / File(s) Summary
Approval data model
src/openhuman/approval/types.rs
Defines PendingApproval, ApprovalDecision, and GateOutcome used across gate, store, and RPC layers.
SQLite persistence store
src/openhuman/approval/store.rs
pending_approvals schema, with_connection, insert_pending, list_pending, decide, purge_session, decoding helpers, and tests.
Gate coordinator and lifecycle
src/openhuman/approval/gate.rs
ApprovalGate singleton with init/try, intercept flow (allowlist, request creation, persistence, event publish), oneshot waiter parking, TTL handling, decide/list_pending APIs, and tests.
Redaction, RPC, and schemas
src/openhuman/approval/redact.rs, src/openhuman/approval/rpc.rs, src/openhuman/approval/schemas.rs
Redaction of sensitive fields and home paths, summarize_action, RPC handlers approval_list_pending/approval_decide, controller schemas and registration, param helpers, and tests.
Tool external-effect capability & implementations
src/openhuman/tools/traits.rs, src/openhuman/tools/impl/network/composio.rs, src/openhuman/tools/impl/network/gmail_unsubscribe.rs, src/openhuman/tools/impl/system/pushover.rs, src/openhuman/voice/reply_speech.rs
Adds Tool::external_effect and external_effect_with_args (default false). Composio declares arg-dependent external effects; Gmail unsubscribe and Pushover remain intentionally unmarked; reply_speech docs clarified.
Approval gate integration in execution paths
src/openhuman/agent/harness/tool_loop.rs, src/openhuman/agent/harness/subagent_runner/ops.rs, src/openhuman/agent/triage/escalation.rs, src/openhuman/channels/proactive.rs
Interposes gate before executing external-effect tool calls or sending external deliveries; denials short-circuit execution or emit failed completions; behavior unchanged when gate absent.
Core bootstrap, events, and registry
src/core/event_bus/events.rs, src/core/jsonrpc.rs, src/core/all.rs, src/openhuman/approval/mod.rs
Adds ApprovalRequested/ApprovalDecided DomainEvent variants, bootstraps global ApprovalGate when OPENHUMAN_APPROVAL_GATE is enabled (session id handling), and registers approval controllers/schemas; expands approval module exports.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • tinyhumansai/openhuman#1657 — Related to the Gmail unsubscribe approval flow; this PR adjusts gmail_unsubscribe to avoid double prompting while still producing pending_approval UI payloads.
  • tinyhumansai/openhuman#2003 — Also extends core controller registry and schema catalog registration patterns similar to the approval namespace additions here.

Poem

🐰 I guard the hops where actions bound,
I hide the seeds that secrets found,
I wait for taps from human hands,
Approve, deny — I keep the lands,
A tiny rabbit holding ground.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main feature: gating external-effect tool calls behind user approval, with clear reference to the linked issue #1339.
Linked Issues check ✅ Passed The PR comprehensively implements all core coding requirements from issue #1339: ApprovalGate middleware for external-effect tools, persistent SQLite store with TTL/timeout handling, redaction of sensitive data, RPC methods (approval_list_pending, approval_decide), DomainEvent emission, harness integration hooks, session-scoped allowlisting, and test coverage across gate, store, redaction, types, and RPC modules.
Out of Scope Changes check ✅ Passed All code changes directly support the approval gating feature: new approval modules (gate, store, rpc, schemas, redact, types), Tool trait extensions, harness integration, domain events, boot-time configuration, and documentation updates. No unrelated refactoring or unscoped modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot added the working A PR that is being worked on by the team. label May 18, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/jsonrpc.rs`:
- Around line 1239-1242: The fallback to a random session_id when
OPENHUMAN_CORE_TOKEN is unset causes pending approvals to be hidden across
restarts; update the code around the session_id initialization (the variable
session_id where OPENHUMAN_CORE_TOKEN is read) to log the generated UUID at
debug or trace level when the env var is absent, e.g., detect when
unwrap_or_else branch runs and emit a debug/trace via the existing logger with
the resulting session_id; mention session_id and approval_list_pending in the
log context so operators can correlate restarts to stored approvals.
- Line 1243: The call to
crate::openhuman::approval::ApprovalGate::init_global(cfg.clone(), session_id)
currently discards its Result, hiding initialization failures; change this to
capture the Result, check for Err, and emit a clear error log (e.g., using the
module's logger or tracing::error) that includes the error details and context
(cfg/session_id) before continuing or returning; specifically locate the
init_global invocation and replace the ignored-let with proper error handling
that logs the error if init_global() returns Err so failures aren't silently
swallowed.

In `@src/openhuman/agent/harness/subagent_runner/ops.rs`:
- Around line 1429-1470: The gate denial branch currently returns the raw denial
reason (gate_denial) which gets treated as a success later via the call_success
check, so change the denial return to a consistent error-formatted string (e.g.,
prefix with "Error: ") or alternatively return a tuple/struct with an explicit
success flag; specifically update the code around the gate_denial handling (the
variable gate_denial and the if let Some(reason) = gate_denial block that
currently returns reason) to return a string like format!("Error: {reason}") (or
propagate a success boolean alongside the result) so downstream logic (which
checks result_text.starts_with("Error")) will mark denied approvals as failures.

In `@src/openhuman/approval/gate.rs`:
- Around line 123-140: The code persists the pending row with
store::insert_pending before registering the oneshot waiter (tx) in
self.waiters, allowing a race where approval_decide marks the request approved
before a waiter exists; fix by creating the oneshot channel and inserting tx
into self.waiters (using request_id and waiters.lock()) before calling
store::insert_pending, and if insert_pending subsequently fails remove the tx
from waiters and return the GateOutcome::Deny as before; apply the same change
to the other occurrence (the block around lines 208-221) so both code paths
register the waiter prior to persisting.
- Around line 18-21: The module-level doc comment for the gate says
prior-session rows are purged on init but the init_global function's docs state
the opposite; pick the actual restart behavior and make both docs consistent:
either change the module header (the top comment) to state that prior-session
rows are preserved across restarts, or update the init_global doc to state that
init purges prior-session rows, ensuring both the module doc and the init_global
function comment refer to the same behavior and include the same wording about
per-launch session_id and stale-decision purging.

In `@src/openhuman/approval/rpc.rs`:
- Around line 18-25: Add structured tracing/logging to approval_list_pending: on
entry emit a debug/trace with stable prefix (e.g.
"rpc:approval_list_pending:entry") including a generated or passed request_id
and method="approval_list_pending"; when ApprovalGate::try_global() returns None
emit a debug log ("rpc:approval_list_pending:gate_missing") with request_id and
decision="empty" before returning; when calling gate.list_pending() wrap the
call to capture errors and on error emit an error/trace log
("rpc:approval_list_pending:error") with request_id, method, and the error; on
success emit an exit debug/trace ("rpc:approval_list_pending:exit") with
request_id, method, decision="ok", and rows.len() before returning
RpcOutcome::single_log(rows, log). Use the existing symbols
ApprovalGate::try_global, gate.list_pending, and RpcOutcome::single_log to
locate spots to insert logs.

In `@src/openhuman/tools/impl/network/composio.rs`:
- Around line 557-571: The current Composio implementation always returns true
from external_effect(), which forces approval even for read-only actions like
action=="list" or action=="connect"; update the gating so only write actions
require approval — either change the harness to perform args-aware gating for
the Composio tool or adapt the tool API (e.g., add an args-aware predicate or
replace external_effect() with external_effect_with_args(action: &str) that
returns false for read-only actions such as "list"/"connect" and true for write
actions (send/post/create)); ensure the unique symbol external_effect (or its
args-aware replacement) and the action values "list"/"connect" are used to
determine read vs write behavior so read-only flows bypass the approval prompt.

In `@src/openhuman/tools/impl/network/gmail_unsubscribe.rs`:
- Around line 37-40: The legacy UI-confirmation flow is still returning the
frontend pending_approval payload from execute(), so marking external_effect()
as true allows the gate to be cleared while the real unsubscribe still runs via
the old approval path (causing duplicate prompts); fix by either (A) keep this
tool non-external for now by changing external_effect() -> false in the
GmailUnsubscribe implementation so the legacy UI continues to control the
outbound effect, or (B) move the actual unsubscribe side-effect into execute()
(so execute() performs the outbound HTTP/mailto request only after approval and
clears the gate first), and ensure the gate-clearing happens before performing
the external request; update references to external_effect() and execute()
accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 548e9142-5b7c-4e07-b9e8-a8d6f15048b1

📥 Commits

Reviewing files that changed from the base of the PR and between 0b053c5 and caeccc9.

📒 Files selected for processing (19)
  • src/core/all.rs
  • src/core/event_bus/events.rs
  • src/core/jsonrpc.rs
  • src/openhuman/agent/harness/subagent_runner/ops.rs
  • src/openhuman/agent/harness/tool_loop.rs
  • src/openhuman/agent/triage/escalation.rs
  • src/openhuman/approval/gate.rs
  • src/openhuman/approval/mod.rs
  • src/openhuman/approval/redact.rs
  • src/openhuman/approval/rpc.rs
  • src/openhuman/approval/schemas.rs
  • src/openhuman/approval/store.rs
  • src/openhuman/approval/types.rs
  • src/openhuman/channels/proactive.rs
  • src/openhuman/tools/impl/network/composio.rs
  • src/openhuman/tools/impl/network/gmail_unsubscribe.rs
  • src/openhuman/tools/impl/system/pushover.rs
  • src/openhuman/tools/traits.rs
  • src/openhuman/voice/reply_speech.rs

Comment thread src/core/jsonrpc.rs Outdated
Comment thread src/core/jsonrpc.rs Outdated
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| format!("session-{}", uuid::Uuid::new_v4()));
let _ = crate::openhuman::approval::ApprovalGate::init_global(cfg.clone(), session_id);
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot May 18, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Log initialization errors before swallowing them.

The init_global result is silently dropped. If initialization fails (e.g., SQLite schema migration error, permission issue, or lock contention), the operator will see the "approval gate installed" log but the gate won't actually be functional, leading to confusing behavior where external-effect tools run unsupervised.

🛡️ Proposed fix
-        let _ = crate::openhuman::approval::ApprovalGate::init_global(cfg.clone(), session_id);
+        if let Err(e) = crate::openhuman::approval::ApprovalGate::init_global(cfg.clone(), session_id.clone()) {
+            log::warn!(
+                "[runtime] approval gate init_global failed: {e} — external-effect tools will run unsupervised"
+            );
+            return;
+        }
         log::info!(
-            "[runtime] approval gate installed (OPENHUMAN_APPROVAL_GATE=1) — \
+            "[runtime] approval gate installed (OPENHUMAN_APPROVAL_GATE=1, session_id={session_id}) — \
              external-effect tool calls will block until approval_decide"
         );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/jsonrpc.rs` at line 1243, The call to
crate::openhuman::approval::ApprovalGate::init_global(cfg.clone(), session_id)
currently discards its Result, hiding initialization failures; change this to
capture the Result, check for Err, and emit a clear error log (e.g., using the
module's logger or tracing::error) that includes the error details and context
(cfg/session_id) before continuing or returning; specifically locate the
init_global invocation and replace the ignored-let with proper error handling
that logs the error if init_global() returns Err so failures aren't silently
swallowed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Skipped — ApprovalGate::init_global returns Arc, not Result; lazy SQLite errors surface inside with_connection on first use, not at install. Addressed the spirit in 6d33ca2 by surfacing session_id on the bootstrap log line so operators can grep across restarts.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@oxoxDev, you're right — I was mistaken. Looking at the file content, init_global infallibly returns Arc<ApprovalGate> (no Result), so the Err handling in my suggestion was invalid. The lazy with_connection design also makes sense: SQLite errors at first use will surface in the relevant operation's error path rather than blocking boot.

The updated code in the file already addresses the observability concern well — the (session_id, ephemeral) tuple tracks whether the session was derived from OPENHUMAN_CORE_TOKEN or generated ephemerally, the debug log on the ephemeral path explains the cross-restart correlation caveat, and the info log on success includes session_id for grep-ability. That's a cleaner approach than what I originally suggested. Apologies for the noise.


🧠 Learnings used
Learnt from: sanil-23
Repo: tinyhumansai/openhuman PR: 416
File: src/openhuman/memory/relex.rs:441-464
Timestamp: 2026-04-07T15:49:51.275Z
Learning: When using the `ort` Rust crate v2.x with the `load-dynamic` feature enabled, don’t require individual execution-provider feature flags (e.g., `directml`, `coreml`, `cuda`) alongside `load-dynamic` to get EP registration code. The `ort` crate already compiles EP registration via `#[cfg(any(feature = "load-dynamic", feature = "<ep_name>"))]` guards, and adding per-EP feature flags can pull in static-linking dependencies that conflict with the dynamic loading approach. At runtime, EP availability is determined by what the dynamically loaded ONNX Runtime library (`onnxruntime.dll`/`.so`/`.dylib`) supports; ort docs indicate providers like `directml`/`xnnpack`/`coreml` are available in builds when the platform supports them.

Learnt from: sanil-23
Repo: tinyhumansai/openhuman PR: 416
File: src/openhuman/memory/relex.rs:441-464
Timestamp: 2026-04-07T15:49:51.275Z
Learning: When integrating the `ort` Rust crate v2.x with the `load-dynamic` feature enabled, do NOT also require/enable individual provider EP Cargo features like `directml`, `coreml`, or `cuda`. In `ort` v2.x, EP registration for providers (e.g., DirectML, CoreML, CUDA, etc.) is already compiled in under source-level `#[cfg(any(feature = "load-dynamic", feature = "<provider>"))]` guards, such as in `ep/directml.rs`. Adding provider feature flags alongside `load-dynamic` can pull in static-linking dependencies that conflict with the dynamic-loading approach. Provider availability should be treated as runtime-determined by what the loaded `onnxruntime` library (`onnxruntime.dll`/`libonnxruntime.so`/`libonnxruntime.dylib`) actually supports.

Learnt from: oxoxDev
Repo: tinyhumansai/openhuman PR: 571
File: src/openhuman/local_ai/service/whisper_engine.rs:69-80
Timestamp: 2026-04-14T19:59:04.826Z
Learning: When reviewing Rust code in this repo that uses the upstream `whisper-rs` crate (v0.16.0), do not report `WhisperContextParameters::use_gpu(...)` or `WhisperContextParameters::flash_attn(...)` as missing/invalid APIs. These builder-style methods exist upstream and return `&mut Self`; they are not limited to `WhisperVadContextParams`.

Learnt from: senamakel
Repo: tinyhumansai/openhuman PR: 1173
File: tests/agent_memory_loader_public.rs:88-88
Timestamp: 2026-05-04T06:50:47.877Z
Learning: In this repository, the general camelCase naming guideline should not be applied to Rust source files. For all .rs files, Rust function (and related) names should use snake_case, and snake_case Rust function names should not be flagged—even for async test functions annotated with attributes like #[tokio::test]. This is consistent with Rust’s non_snake_case lint behavior.

Comment thread src/openhuman/agent/harness/subagent_runner/ops.rs
Comment thread src/openhuman/approval/gate.rs Outdated
Comment thread src/openhuman/approval/gate.rs Outdated
Comment thread src/openhuman/approval/rpc.rs
Comment thread src/openhuman/tools/impl/network/composio.rs
Comment thread src/openhuman/tools/impl/network/gmail_unsubscribe.rs Outdated
oxoxDev added 6 commits May 19, 2026 02:05
…ai#1339)

Two CodeRabbit findings on PR tinyhumansai#2149:

- Race: insert_pending ran before the oneshot waiter was registered,
  so a fast approval_decide could mark the row approved while no
  waiter existed; the parked future then timed out and returned
  Deny. Register the waiter first, evict on persist failure.
- Module header doc claimed prior-session rows are purged on init,
  contradicting init_global's own doc. Restart-survival is the
  intended contract — header now matches init_global.
Per CodeRabbit on PR tinyhumansai#2149: subagent path returned the raw denial
reason, but downstream call_success uses
!result_text.starts_with("Error"), so denied approvals showed up
as successful tool executions in progress events and tool_result
blocks. Prefix the denial string with "Error: " so the success
computation classifies it correctly.
…nsai#1339)

Per CodeRabbit on PR tinyhumansai#2149 and CLAUDE.md debug-logging rules.
Adds debug entry/exit logs to approval_list_pending and
approval_decide with stable [rpc:approval_*] prefixes plus
correlation fields (request_id, decision, tool, row count).
Failures get warn/error logs at the matching site so a missing
gate / unknown request_id is debuggable without re-running.
…ansai#1339)

Per CodeRabbit on PR tinyhumansai#2149: forcing the entire composio tool
through the approval gate prompted users for action="list"
(read-only discovery) and action="connect" (returns an OAuth
URL the user manually visits — no outbound side effect of its
own), conflicting with the goal to keep read flows approval-free.

- Tool trait gains external_effect_with_args(&self, args) which
  defaults to external_effect() so existing static overrides keep
  working with no churn.
- Composio overrides the args-aware variant to skip the gate when
  action == "list" or "connect"; everything else (action ==
  "execute" or missing) still gates as a write.
- Harness call sites (tool_loop, subagent_runner) now use the
  args-aware variant so per-call gating works.
…humansai#1339)

Per CodeRabbit on PR tinyhumansai#2149: gmail_unsubscribe.execute() still
returns a pending_approval JSON payload that the React UI
intercepts with its own legacy confirmation flow — it does not
perform the unsubscribe itself. Marking the tool external_effect
= true would gate the call AND then immediately fall back into
the old UI prompt, prompting the user twice while the real side
effect remained outside core enforcement.

Keep the tool at the trait default (false) for v1 and document
the follow-up (move the actual unsubscribe into execute() so the
gate is the single approval surface) in an inline comment.
…i#1339)

Per CodeRabbit on PR tinyhumansai#2149: when OPENHUMAN_CORE_TOKEN is unset
the bootstrap fell back to a fresh UUID with no diagnostic; an
operator restarting without a stable token had no way to
correlate pending approvals across launches.

Now logs both the ephemeral fallback (at debug) and the active
session_id on the bootstrap info line. Note: approval_list_pending
is session-agnostic so pending rows from prior launches stay
visible — this log is purely for audit-trail correlation.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/core/jsonrpc.rs (1)

1254-1259: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Log initialization failures before declaring success.

If init_global fails (e.g., SQLite schema error, permission issue), the result is silently discarded but the success log at line 1256 still fires. This misleads operators into thinking the gate is active when external-effect tools may run unsupervised.

🛡️ Proposed fix
-        let _ =
-            crate::openhuman::approval::ApprovalGate::init_global(cfg.clone(), session_id.clone());
-        log::info!(
+        if let Err(e) = crate::openhuman::approval::ApprovalGate::init_global(cfg.clone(), session_id.clone()) {
+            log::warn!(
+                "[runtime] approval gate init_global failed: {e} — external-effect tools will run unsupervised"
+            );
+        } else {
+            log::info!(
-            "[runtime] approval gate installed (OPENHUMAN_APPROVAL_GATE=1, session_id={session_id}) — \
-             external-effect tool calls will block until approval_decide"
-        );
+                "[runtime] approval gate installed (OPENHUMAN_APPROVAL_GATE=1, session_id={session_id}) — \
+                 external-effect tool calls will block until approval_decide"
+            );
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/jsonrpc.rs` around lines 1254 - 1259, The call to
ApprovalGate::init_global currently discards its Result but still logs success;
change it to handle the Result from
crate::openhuman::approval::ApprovalGate::init_global(cfg.clone(),
session_id.clone()) — if Err, log the error with details (include the error
value) and avoid emitting the success log (or propagate/return the error),
otherwise on Ok emit the existing log referencing session_id; ensure you update
the code paths around ApprovalGate::init_global and the log::info call so
initialization failures are recorded before any success message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/core/jsonrpc.rs`:
- Around line 1254-1259: The call to ApprovalGate::init_global currently
discards its Result but still logs success; change it to handle the Result from
crate::openhuman::approval::ApprovalGate::init_global(cfg.clone(),
session_id.clone()) — if Err, log the error with details (include the error
value) and avoid emitting the success log (or propagate/return the error),
otherwise on Ok emit the existing log referencing session_id; ensure you update
the code paths around ApprovalGate::init_global and the log::info call so
initialization failures are recorded before any success message.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a9143f06-05d3-475f-bd22-e53ee9f324f7

📥 Commits

Reviewing files that changed from the base of the PR and between caeccc9 and 6d33ca2.

📒 Files selected for processing (8)
  • src/core/jsonrpc.rs
  • src/openhuman/agent/harness/subagent_runner/ops.rs
  • src/openhuman/agent/harness/tool_loop.rs
  • src/openhuman/approval/gate.rs
  • src/openhuman/approval/rpc.rs
  • src/openhuman/tools/impl/network/composio.rs
  • src/openhuman/tools/impl/network/gmail_unsubscribe.rs
  • src/openhuman/tools/traits.rs
✅ Files skipped from review due to trivial changes (1)
  • src/openhuman/tools/impl/network/gmail_unsubscribe.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/openhuman/approval/rpc.rs
  • src/openhuman/agent/harness/tool_loop.rs
  • src/openhuman/tools/traits.rs
  • src/openhuman/approval/gate.rs

@oxoxDev
Copy link
Copy Markdown
Contributor Author

oxoxDev commented May 19, 2026

@coderabbitai review

All 8 inline findings addressed in commits ae871b0..6d33ca2 (push c2caeccc..6d33ca2). Per-comment summary in the inline replies. CI: 22/22 green including Coverage Gate ≥ 80% and PR Submission Checklist. Requesting fresh review to clear changes_requested.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

@oxoxDev Thanks for the detailed update — glad to hear all 8 findings are addressed and CI is fully green across the board. I'll kick off a fresh review now to reflect the latest state of the PR.

✅ Actions performed

Full review triggered.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/openhuman/agent/triage/escalation.rs (1)

88-129: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

This approval key won't suppress the later per-tool prompts.

This gate records approval against triage.react / triage.escalate, but the actual external writes later run under different tool keys (pushover, channels.proactive_send, Composio action tools, etc.). So an approval here does not get consumed by the downstream execution paths, and users can still see one prompt for the escalation plus another prompt for the first external-effect tool it invokes. If the intent is an up-front “approve this escalation” decision, that approval needs to flow through as a decision/token the later tool gates honor; otherwise this layer is just an extra prompt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/agent/triage/escalation.rs` around lines 88 - 129, The current
approval uses tool_key = format!("triage.{}", run.decision.action.as_str()) so
ApprovalGate::intercept only records approval for "triage.*" but downstream tool
gates look up keys like "pushover" or "channels.proactive_send", so the triage
allow isn't consumed; change the flow so when ApprovalGate::intercept returns
GateOutcome::Allow you create and attach a transferable approval token/allowlist
entry (e.g., generate approval_key or approval_id) into the run or envelope
state (e.g., envelope.metadata or run.session.allowlist) and ensure downstream
tool invocation code/gates check and consume that token (or map triage approval
to the actual tool keys) so approvals recorded in ApprovalGate::intercept are
honored by the per-tool gates for "pushover", "channels.proactive_send",
Composio tools, etc.
🧹 Nitpick comments (1)
src/openhuman/approval/store.rs (1)

212-216: 💤 Low value

Silent fallback to Utc::now() may mask data corruption.

If the stored timestamp is malformed, substituting Utc::now() silently could produce misleading created_at/expires_at values. Consider adding a tracing::warn! on parse failure so corrupted rows are observable.

🔧 Optional: add a warning log
 fn parse_rfc3339(input: &str) -> DateTime<Utc> {
     DateTime::parse_from_rfc3339(input)
         .map(|t| t.with_timezone(&Utc))
-        .unwrap_or_else(|_| Utc::now())
+        .unwrap_or_else(|err| {
+            tracing::warn!(input = input, error = %err, "[approval::store] invalid RFC3339 timestamp, using now");
+            Utc::now()
+        })
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/approval/store.rs` around lines 212 - 216, The parse_rfc3339
function currently swallows parse errors and returns Utc::now(), which can hide
corrupted timestamps; modify parse_rfc3339 to log parse failures using
tracing::warn! (include the input string and the parse error) before falling
back to Utc::now(), so malformed rows are observable; keep the existing return
behavior but ensure the warning contains the function name or context, the
offending input, and the error message to aid debugging.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhuman/approval/redact.rs`:
- Around line 112-137: scrub_paths currently only matches "{sep}Users{sep}"
(using needle), so Windows paths like "C:\Users\name\..." are missed; update
scrub_paths to locate "Users" occurrences rather than relying on a leading
MAIN_SEPARATOR and treat a match valid if it is preceded by a path separator or
by a Windows drive pattern (a letter + ':' immediately before "Users"); then
when you find such a "Users" match (use rest.find("Users") or similar), replace
the whole "Users/<username>" or "Users\<username>" segment with "<HOME>" the
same way you do now (use MAIN_SEPARATOR to find the end of the username and
advance rest accordingly). Use the existing symbols scrub_paths, rest, needle
(remove or replace its use), MAIN_SEPARATOR, and after to implement this
cross-platform matching and skipping logic.

In `@src/openhuman/tools/impl/system/pushover.rs`:
- Around line 114-118: The external_effect() implementation should not
unconditionally return true; update external_effect() to mirror the pre-checks
used by execute() so it returns false when the send is impossible (e.g., when
the action is read-only or rate-limited). Locate external_effect() in
pushover.rs and have it call the same helper or inline the same conditions used
by execute() (or a can_send()/is_read_only()/is_rate_limited() helper) so the
gate will not prompt for approval for requests that execute() will immediately
reject.

---

Duplicate comments:
In `@src/openhuman/agent/triage/escalation.rs`:
- Around line 88-129: The current approval uses tool_key = format!("triage.{}",
run.decision.action.as_str()) so ApprovalGate::intercept only records approval
for "triage.*" but downstream tool gates look up keys like "pushover" or
"channels.proactive_send", so the triage allow isn't consumed; change the flow
so when ApprovalGate::intercept returns GateOutcome::Allow you create and attach
a transferable approval token/allowlist entry (e.g., generate approval_key or
approval_id) into the run or envelope state (e.g., envelope.metadata or
run.session.allowlist) and ensure downstream tool invocation code/gates check
and consume that token (or map triage approval to the actual tool keys) so
approvals recorded in ApprovalGate::intercept are honored by the per-tool gates
for "pushover", "channels.proactive_send", Composio tools, etc.

---

Nitpick comments:
In `@src/openhuman/approval/store.rs`:
- Around line 212-216: The parse_rfc3339 function currently swallows parse
errors and returns Utc::now(), which can hide corrupted timestamps; modify
parse_rfc3339 to log parse failures using tracing::warn! (include the input
string and the parse error) before falling back to Utc::now(), so malformed rows
are observable; keep the existing return behavior but ensure the warning
contains the function name or context, the offending input, and the error
message to aid debugging.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a88a0bc5-1494-4571-a674-1d498db1046d

📥 Commits

Reviewing files that changed from the base of the PR and between 0b053c5 and 6d33ca2.

📒 Files selected for processing (19)
  • src/core/all.rs
  • src/core/event_bus/events.rs
  • src/core/jsonrpc.rs
  • src/openhuman/agent/harness/subagent_runner/ops.rs
  • src/openhuman/agent/harness/tool_loop.rs
  • src/openhuman/agent/triage/escalation.rs
  • src/openhuman/approval/gate.rs
  • src/openhuman/approval/mod.rs
  • src/openhuman/approval/redact.rs
  • src/openhuman/approval/rpc.rs
  • src/openhuman/approval/schemas.rs
  • src/openhuman/approval/store.rs
  • src/openhuman/approval/types.rs
  • src/openhuman/channels/proactive.rs
  • src/openhuman/tools/impl/network/composio.rs
  • src/openhuman/tools/impl/network/gmail_unsubscribe.rs
  • src/openhuman/tools/impl/system/pushover.rs
  • src/openhuman/tools/traits.rs
  • src/openhuman/voice/reply_speech.rs

Comment thread src/openhuman/approval/redact.rs
Comment thread src/openhuman/tools/impl/system/pushover.rs Outdated
oxoxDev added 2 commits May 19, 2026 10:04
…nsai#1339)

Per CodeRabbit on PR tinyhumansai#2149: scrub_paths only matched
{MAIN_SEPARATOR}Users{MAIN_SEPARATOR}, so Windows paths like
C:\Users\<name>\... slipped through on Unix-built artifacts
reading cross-platform log payloads. Rewrote to recognise the
three real shapes:

- /Users/<name>/...   (macOS)
- /home/<name>/...    (Linux)
- C:\Users\<name>\...  (Windows, any drive letter, case-insensitive)

Multiple home paths in the same string are all scrubbed (was a
single-shot loop bug). Three new tests cover Windows, Linux,
and multi-path strings.
…1339)

Per CodeRabbit on PR tinyhumansai#2149: gate runs BEFORE execute(), so
marking pushover external_effect=true raised approvals even for
calls that execute() would immediately reject via
security.can_act() (read-only autonomy) or
security.record_action() (rate limit) — a dead-end approval
flow for actions that can never run.

Keep this tool at the trait default (false) until the gate can
consult SecurityPolicy before parking. Inline comment notes the
follow-up tinyhumansai#1339-v3.
Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

Review — graycyrus

Solid implementation of the approval gate backend for #1339. The architecture is well-thought-out: defense in depth at four interception points (tool_loop, subagent_runner, proactive channels, triage escalation), fail-closed on every error path, and the OnceLock + Option pattern means the default-off path is truly zero-cost. The 46 unit tests cover the critical flows, and all CI checks pass cleanly.

CodeRabbit caught the significant issues (waiter-before-persist race, denial error formatting, docs alignment, composio read-only gating, cross-platform path scrubbing) — all addressed. Nice work responding to those.

Change summary

File Change Description
approval/gate.rs New Core ApprovalGate — intercept, decide, session allowlist, timeout
approval/store.rs New SQLite persistence for pending approvals
approval/redact.rs New PII/path redaction for approval summaries
approval/types.rs New PendingApproval, ApprovalDecision, GateOutcome
approval/rpc.rs New approval_list_pending + approval_decide RPC ops
approval/schemas.rs New Controller registry wiring
approval/mod.rs Modified Module re-exports for new submodules
core/all.rs Modified Register approval controllers + schemas
core/event_bus/events.rs Modified ApprovalRequested + ApprovalDecided domain events
core/jsonrpc.rs Modified Bootstrap gate on OPENHUMAN_APPROVAL_GATE=1
tools/traits.rs Modified external_effect() + external_effect_with_args() trait methods
tools/impl/network/composio.rs Modified Args-aware gating (list/connect exempt)
tools/impl/network/gmail_unsubscribe.rs Modified Documented as intentionally non-gated (legacy UI)
tools/impl/system/pushover.rs Modified Documented as intentionally non-gated (policy pre-check)
agent/harness/tool_loop.rs Modified Gate check before execute() in main agent loop
agent/harness/subagent_runner/ops.rs Modified Gate check in subagent tool execution
agent/triage/escalation.rs Modified Gate at escalation dispatch entry point
channels/proactive.rs Modified Gate before external-channel delivery
voice/reply_speech.rs Modified Doc comment: TTS is internal, never gated

Two minor observations below — neither is blocking.

/// or unknown id).
pub fn decide(
config: &Config,
request_id: &str,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[minor] list_pending returns all rows with decided_at IS NULL but doesn't filter by expires_at. If the process crashes between insert_pending and the timeout handler marking the row decided, orphan rows with a past expires_at will appear as still-pending indefinitely.

The PR docs say orphans are intentionally preserved for audit — that's fine. But the query could add AND (expires_at IS NULL OR expires_at > datetime('now')) so the UI doesn't have to implement its own expiry filter. Alternatively, document that the UI must filter client-side.

Non-blocking — the follow-up React UI can handle this either way.

.context("[approval::store] serialize args_redacted")?;
let created = pending.created_at.to_rfc3339();
let expires = pending.expires_at.map(|t| t.to_rfc3339());
conn.execute(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[minor] The store uses the default SQLite journal mode. Since intercept and list_pending / decide can be called concurrently from async tasks (each opening their own connection via with_connection), WAL mode would reduce SQLITE_BUSY contention under load.

One line in the schema block:

PRAGMA journal_mode = WAL;

Non-blocking — the per-call connection pattern limits concurrency pressure in practice, and this matches notifications/store.rs's current approach.

Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

Continuation review — prior minor findings (expires_at filter in list_pending, WAL mode for the approval store) remain non-blocking and are documented as follow-up-safe. No new commits since last review, all CI green, all CodeRabbit issues addressed. Clean from a blocking-findings perspective.

Moving to approval queue.

@senamakel senamakel merged commit 34bcf34 into tinyhumansai:main May 20, 2026
27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keep triggers read-only until users approve writes

3 participants