Skip to content

feat(chat_with_data): NL dataset querying + anomaly detection - #4148

Closed
Sathvik-1007 wants to merge 2 commits into
tinyhumansai:mainfrom
Sathvik-1007:feat/vc-chat-with-data
Closed

Sathvik-1007 wants to merge 2 commits into
tinyhumansai:mainfrom
Sathvik-1007:feat/vc-chat-with-data

Conversation

@Sathvik-1007

@Sathvik-1007 Sathvik-1007 commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Self-contained domain module — split from #2261 (PR 7/7). Depends on #4142.

9 files: types, engine, sql_gen, db_connector, anomaly, webhooks, rpc, schemas, mod.
E2E: register → query → list → insight → scan_anomalies.

This completes the split of #2261 into 7 focused PRs.

Summary by CodeRabbit

  • New Features

    • Added a new data-analysis experience that lets users register datasets, ask natural-language questions, view insights, and scan for anomalies.
    • Added support for live webhook notifications when new insights or anomalies are detected.
    • Expanded app capabilities to include voice, captions, operator inbox, guided recommendations, and data analysis foundations.
  • Bug Fixes

    • Improved query safety and validation for data-related requests.
    • Added stronger protections for outgoing webhook destinations.

- Cargo.toml: sqlparser, whatlang, nnnoiseless, strsim, lettre async
- util.rs: uuid_v4(), now_epoch() shared helpers
- about_app: UserContent privacy variant, 6 capability catalog entries
- docs: capability plan + full API reference
9-file domain: types, engine (in-memory execution), sql_gen (NL->SQL
+ sqlparser AST safety), db_connector (SQLite read-only), anomaly
(z-score detection), webhooks (async notifications), rpc, schemas.

E2E: register_dataset -> query -> list -> generate_insight -> scan_anomalies.
@Sathvik-1007
Sathvik-1007 requested a review from a team June 26, 2026 05:57
@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds the openhuman::chat_with_data domain, including dataset registration, SQL generation, anomaly detection, webhook notifications, JSON-RPC handlers and schemas, catalog/docs updates, and an end-to-end lifecycle test.

Changes

Chat-with-data capability

Layer / File(s) Summary
Shared surface and dependencies
Cargo.toml, src/openhuman/mod.rs, src/openhuman/util.rs, src/openhuman/chat_with_data/mod.rs, src/openhuman/chat_with_data/types.rs, src/core/all.rs
The new domain is exported, shared types and helpers are added, the controller registry is extended, and Cargo gains the new dependencies.
Capability catalog and docs
src/openhuman/about_app/types.rs, src/openhuman/about_app/catalog_data.rs, docs/boost-vc-capability-plan.md, docs/VC_CAPABILITIES.md
The capability catalog entries, privacy enum, and VC documentation pages are updated together.
SQL generation and SQLite access
src/openhuman/chat_with_data/sql_gen.rs, src/openhuman/chat_with_data/db_connector.rs
SQL generation, SQL safety checks, and read-only SQLite query/schema helpers are added with tests.
Anomaly, engine, and webhooks
src/openhuman/chat_with_data/anomaly.rs, src/openhuman/chat_with_data/engine.rs, src/openhuman/chat_with_data/webhooks.rs
Anomaly detection types and detectors, dataset lifecycle and query orchestration, and insight webhook notifications are added with tests.
RPC schemas and e2e coverage
src/openhuman/chat_with_data/rpc.rs, src/openhuman/chat_with_data/schemas.rs, tests/json_rpc_e2e.rs
The chat_with_data RPC handlers and controller schemas expose dataset registration, querying, insights, listing, ingestion, anomaly scanning, and deletion, and the JSON-RPC e2e test drives that lifecycle.

Sequence Diagram(s)

sequenceDiagram
  participant Engine as chat_with_data::engine
  participant SQLGen as chat_with_data::sql_gen
  participant DB as chat_with_data::db_connector
  Engine->>SQLGen: generate_sql_for_question(table_name, columns, question)
  SQLGen-->>Engine: GeneratedSql
  Engine->>SQLGen: is_safe_query(sql)
  alt SQLite dataset
    Engine->>DB: execute_sqlite_query(db_path, sql)
    DB-->>Engine: rows
  else In-memory dataset
    Engine->>Engine: execute_in_memory(dataset_id, sql)
  end
  Engine-->>Engine: QueryResult
Loading
sequenceDiagram
  participant Engine as chat_with_data::engine
  participant Anomaly as chat_with_data::anomaly
  participant Webhooks as chat_with_data::webhooks
  Engine->>Anomaly: detect_combined(values, z_threshold, iqr_k)
  Anomaly-->>Engine: AnomalyReport
  Engine->>Webhooks: notify_insight(insight, AnomalyDetected)
  Webhooks-->>Webhooks: POST matching webhook URLs
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

feature, rust-core, agent

Suggested reviewers

  • graycyrus
  • M3gA-Mind

Poem

A rabbit hopped through logs at dawn,
With SQL carrots neatly drawn.
🐇 Insights blinked, webhooks rang,
And dataset bells went ding-a-lang.
The chat-with-data moon now gleams.

🚥 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 summarizes the main change: chat_with_data natural-language querying plus anomaly detection.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

❤️ Share

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

@coderabbitai coderabbitai Bot added agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. feature Net-new user-facing capability or product behavior. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. labels Jun 26, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b7c6caecdd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tests/json_rpc_e2e.rs
Comment on lines +13391 to +13392
.await;
let draft_r = assert_no_jsonrpc_error(&draft, "operator_inbox_generate_draft");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remove the orphaned top-level await

The newly added fragment starts after the previous test has already closed, so this .await and the following draft assertions are at module scope. Any Rust test/CI run that includes tests/json_rpc_e2e.rs will fail to parse this integration test target before reaching the new chat-with-data test.

Useful? React with 👍 / 👎.

Comment on lines +1460 to +1461
how_to: "Start a voice session via RPC: openhuman.voice_assistant_start_session.",
status: CapabilityStatus::Beta,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not advertise unregistered RPC capabilities

This new Beta catalog entry, and the similar guided_flows/live_captions/voice_actions/operator_inbox entries below it, points users at RPC methods that are not registered anywhere in the controller registry. In the current tree, invoking openhuman.voice_assistant_start_session and the other advertised methods returns unknown-method errors, so the about_app catalog now exposes broken user-facing how-to steps; these should be marked coming soon or backed by registered controllers before being listed as Beta.

Useful? React with 👍 / 👎.

columns: Vec<String>,
row_count: u64,
) -> Result<DatasetMeta, String> {
let id = format!("ds-{}", name.to_lowercase().replace(' ', "_"));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve SQLite connection metadata in dataset IDs

When callers register a dataset with source: "sqlite", this always creates an id like ds-sales, but query_dataset only attempts real SQLite execution when the id starts with sqlite:. As a result, every SQLite dataset registered through the new RPC path skips db_connector and falls back to an empty in-memory store, so database-backed queries never execute against the SQLite file.

Useful? React with 👍 / 👎.

arg_list.args.iter().find_map(|a| match a {
FunctionArg::Unnamed(FunctionArgExpr::Expr(
Expr::Identifier(ident),
)) => Some(ident.value.to_lowercase()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve column casing when reading ingested rows

For in-memory aggregates, the SQL identifier is lowercased before looking it up in rows that ingest_rows stored with the caller's original keys. Any dataset with mixed-case headers such as Amount will generate AVG("Amount") but then look for amount, returning NULL (no matching column data) despite valid ingested values.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 18

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (6)
src/openhuman/chat_with_data/rpc.rs-11-16 (1)

11-16: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject unknown source values instead of coercing them to CSV.

A typo currently registers the dataset as DataSource::Csv and still returns success. Since the schema advertises a closed set (csv|json|sqlite|api), invalid values should fail fast.

🤖 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/chat_with_data/rpc.rs` around lines 11 - 16, The
`get("source")` handling in `rpc.rs` is too permissive because unknown values
currently fall through to `DataSource::Csv`, which hides typos and returns
success. Update the source parsing logic in the `source` match to treat only the
advertised closed set (`csv`, `json`, `sqlite`, `api`) as valid and return an
error for anything else, so the RPC fails fast instead of coercing invalid
input. Keep the fix localized to the `source` mapping used during dataset
registration.
src/openhuman/chat_with_data/engine.rs-132-134 (1)

132-134: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Fix the execution caveat for SQLite results.

Successful SQLite execution currently reports "Executed against in-memory dataset", which misleads RPC/API consumers.

🤖 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/chat_with_data/engine.rs` around lines 132 - 134, The execution
caveat in the chat_with_data result path is using the in-memory dataset message
even when SQLite execution succeeds. Update the caveat selection logic in the
engine.rs result assembly around execution_result and generated.is_valid so
SQLite-backed successful runs emit a SQLite-specific caveat instead of "Executed
against in-memory dataset", and keep the in-memory message only for the
in-memory execution branch.
src/openhuman/chat_with_data/db_connector.rs-39-42 (1)

39-42: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Avoid logging full local database paths.

db_path can expose usernames or sensitive filesystem layout in logs. Log a redacted basename/hash instead.

🤖 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/chat_with_data/db_connector.rs` around lines 39 - 42, The debug
log in the sqlite query path is exposing the full db_path, which can leak
sensitive local filesystem details. Update the logging in the sqlite query
execution flow to redact the path before emitting it, using a basename or stable
hash instead of the raw value. Keep the change localized to the db connector’s
query logging near the debug! call so the SQL length context remains while the
path is sanitized.
src/openhuman/chat_with_data/webhooks.rs-191-199 (1)

191-199: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Serialize severity as a number, not a debug string.

Webhook consumers will receive "0.7" instead of 0.7, which breaks numeric comparisons and schema expectations.

Proposed fix
-        "severity": format!("{:?}", insight.severity),
+        "severity": insight.severity,
🤖 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/chat_with_data/webhooks.rs` around lines 191 - 199, The webhook
payload in the `payload` निर्माण inside `webhooks.rs` is serializing
`insight.severity` with debug formatting, which turns the numeric value into a
string. Update the `payload` construction to emit `severity` as a numeric JSON
value instead of a formatted debug string, using the existing `insight.severity`
field directly so consumers receive a number and not text.
docs/boost-vc-capability-plan.md-56-65 (1)

56-65: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clarify whether this file is aspirational or current-state documentation.

These non-goals say v1 has no speaker diarization, no real email transport, no real SQL execution, and no persistence, but docs/VC_CAPABILITIES.md in the same PR describes those exact capabilities as implemented. One of the two docs is stale.

🤖 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 `@docs/boost-vc-capability-plan.md` around lines 56 - 65, Clarify the intent of
docs/boost-vc-capability-plan.md by updating the Explicit Non-Goals section to
state whether it is aspirational v1 planning or current-state behavior, and make
it consistent with docs/VC_CAPABILITIES.md. Use the section header and bullets
under Explicit Non-Goals to either reframe them as future targets or
remove/adjust any items that contradict implemented capabilities such as
diarization, email transport, SQL execution, and persistence.
docs/VC_CAPABILITIES.md-286-287 (1)

286-287: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Replace the hard-coded src/core/all.rs line references.

This section points to lines 252–265, but in the supplied branch the new chat_with_data wiring is at Lines 308-310 and 473-474. Referencing build_registered_controllers / build_declared_controller_schemas instead will stay accurate as the file shifts.

🤖 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 `@docs/VC_CAPABILITIES.md` around lines 286 - 287, Update the documentation
reference that still hard-codes `src/core/all.rs` line numbers, since those
positions have shifted with the new `chat_with_data` wiring. Replace the
line-specific mention with symbol-based references to
`build_registered_controllers` and `build_declared_controller_schemas` in
`src/core/all.rs` so the capability docs stay accurate as the file changes.
🧹 Nitpick comments (7)
tests/json_rpc_e2e.rs (1)

13428-13518: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Strengthen the lifecycle assertions and close the loop with deletion.

Right now this only proves each RPC returned ok: true. It will not catch regressions where list_datasets omits the registered dataset, generate_insight references the wrong dataset, or scan_anomalies returns an unexpected payload shape. Please assert the propagated dataset_id shows up downstream, and finish the lifecycle with delete_dataset so that path is covered too.

🤖 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 `@tests/json_rpc_e2e.rs` around lines 13428 - 13518, The chat-with-data
lifecycle test only checks ok:true responses and misses propagation/cleanup
validation. In the test flow around post_json_rpc, assert the registered
dataset_id is present in the downstream list_datasets result, verify
generate_insight and scan_anomalies return payloads tied to that same dataset_id
and expected shape, and add a delete_dataset call at the end so the full
register-query-list-insight-scan-delete lifecycle is covered.
src/openhuman/chat_with_data/schemas.rs (1)

304-329: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Delegate schema handlers to ops.rs, not rpc.rs.

This wiring keeps the transport layer as the domain boundary instead of the canonical ops layer the repo expects for new controller migrations. As per coding guidelines, "src/openhuman/*/{mod,schemas}.rs: Controller migration requires ... define schemas with handlers in schemas.rs delegating to ops.rs" and "src/openhuman/*/: Canonical Rust module shape ... ops.rs (business logic)."

🤖 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/chat_with_data/schemas.rs` around lines 304 - 329, The schema
handler wiring is pointing to the wrong layer: the handler functions in
schemas.rs are delegating into rpc.rs instead of the canonical ops.rs boundary.
Update the h_register, h_query, h_insight, h_list_ds, h_list_ins, h_get,
h_ingest, h_scan, and h_delete adapters to call the corresponding ops.rs
functions so schemas.rs only routes transport requests to business logic through
ops.rs.

Source: Coding guidelines

src/openhuman/chat_with_data/rpc.rs (1)

95-125: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Don't swallow LLM/config/provider failures without debug logs.

Both helpers convert config load, provider creation, and model-call failures into None via .ok()?, so the fallback path becomes opaque in production. As per coding guidelines, "Add debug logging to entry/exit, branches, external calls, retries/timeouts, state transitions, and errors using log/tracing at debug/trace level in Rust."

Also applies to: 154-170

🤖 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/chat_with_data/rpc.rs` around lines 95 - 125, The
`chat_with_data` flow is swallowing failures from `load_config_with_timeout`,
`create_chat_provider`, and `provider.chat_with_system` by converting them to
`None` with `.ok()?`, which makes production fallback behavior opaque. Update
the relevant helper(s) in `rpc.rs` to emit `debug`/`trace` logs around each
external call and failure branch, using the existing `chat_with_data` context
and the `provider/chat_with_system` path, so config-load, provider-creation, and
LLM-call errors are visible before returning `None`. Apply the same logging
pattern to the other affected helper mentioned in the review so both entry/exit
and error paths are observable without changing the return type.

Source: Coding guidelines

src/openhuman/chat_with_data/db_connector.rs (1)

88-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add debug logs around schema and table-list DB calls.

get_sqlite_schema and list_sqlite_tables perform external SQLite calls without entry/exit/error branch logging. As per coding guidelines, Rust code under src/openhuman/**/*.rs should add debug logging for entry/exit, external calls, state transitions, and errors.

🤖 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/chat_with_data/db_connector.rs` around lines 88 - 136, Add
debug logging to the SQLite helper functions so their database interactions are
traceable. In get_sqlite_schema and list_sqlite_tables, log entry before opening
the connection, log before and after each external SQLite call
(Connection::open_with_flags, prepare, query_map/collection), and log error
branches with the existing LOG_PREFIX context. Use the function names
get_sqlite_schema and list_sqlite_tables to place the logs so the
entry/exit/state transitions are covered without changing behavior.

Source: Coding guidelines

src/openhuman/chat_with_data/sql_gen.rs (1)

462-579: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move tests out to keep the module under the size limit.

This file is 579 lines; moving the test module to sql_gen_tests.rs behind #[cfg(test)] mod sql_gen_tests; keeps production logic focused and satisfies the repository limit. As per coding guidelines, “Rust modules must be ≤ ~500 lines in size.”

🤖 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/chat_with_data/sql_gen.rs` around lines 462 - 579, The test
module at the bottom of sql_gen.rs is pushing the module over the size limit.
Move the entire #[cfg(test)] mod tests block into a separate sql_gen_tests.rs
module and keep sql_gen.rs focused on production code. Update the module wiring
by replacing the inline tests with a #[cfg(test)] mod sql_gen_tests;
declaration, preserving all existing test functions that reference validate_sql,
generate_sql_for_question, is_safe_query, and extract_time_filter.

Source: Coding guidelines

src/openhuman/chat_with_data/engine.rs (1)

418-572: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move tests out to keep the module under the size limit.

This file is 572 lines; moving tests to engine_tests.rs behind #[cfg(test)] mod engine_tests; brings the module back under the repository limit. As per coding guidelines, “Rust modules must be ≤ ~500 lines in size.”

🤖 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/chat_with_data/engine.rs` around lines 418 - 572, The tests
block in the `tests` module is pushing `engine.rs` over the module size limit.
Move these test cases into a separate `engine_tests.rs` test module and expose
them with `#[cfg(test)] mod engine_tests;` from the `engine` module so the main
module stays under the ~500 line guideline. Keep the existing test functions and
references like `get_dataset`, `register_dataset`, `query_dataset`,
`generate_insight`, `delete_dataset`, and `scan_all_datasets_for_anomalies` in
the new test file.

Source: Coding guidelines

docs/boost-vc-capability-plan.md (1)

69-77: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align this architecture section with the repo’s canonical module layout.

This documents engine.rs/rpc.rs and points readers to about_app/catalog.rs, but the repo standard for src/openhuman/*/ is export-only mod.rs plus ops.rs/schemas.rs/tools.rs/bus.rs, and the capability list in this PR is in src/openhuman/about_app/catalog_data.rs. Leaving the old structure here will send follow-up work to the wrong files.

As per coding guidelines, src/openhuman/*/: "Canonical Rust module shape: mod.rs (export-focused only), types.rs (Serde types), store.rs (persistence), ops.rs (business logic), schemas.rs (RPC schemas), tools.rs (agent tools), bus.rs (event subscribers)"; and src/openhuman/*/mod.rs: "Rust modules must have export-focused mod.rs with no business logic; business logic goes in ops.rs."

🤖 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 `@docs/boost-vc-capability-plan.md` around lines 69 - 77, Update the
architecture section to match the repo’s canonical `src/openhuman/*/` layout:
replace the outdated `engine.rs`/`rpc.rs`/`about_app/catalog.rs` references with
the export-only `mod.rs` pattern and the expected split into `types.rs`,
`store.rs`, `ops.rs`, `schemas.rs`, `tools.rs`, and `bus.rs`. Use the capability
list source from `src/openhuman/about_app/catalog_data.rs` when describing where
the catalog data lives, and make sure the text reflects that `mod.rs` is
export-focused only while business logic belongs in `ops.rs`.

Source: Coding guidelines

🤖 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/about_app/catalog_data.rs`:
- Around line 1452-1538: The Beta capability catalog currently advertises
unimplemented features, so either remove the `Capability` entries for
`voice_assistant.session`, `guided_flows.recommendation`,
`live_captions.transcript`, `voice_actions.intent`, and `operator_inbox.triage`
from `catalog_data.rs` or fully implement and register their Rust modules first.
Make sure each advertised capability has a matching source directory, RPC
handler, and `src/core/all.rs` registration before it remains in the catalog;
`chat_with_data.query` can stay as-is since it is already wired up.

In `@src/openhuman/chat_with_data/anomaly.rs`:
- Around line 54-63: Non-finite inputs are being fed into the anomaly stats
pipeline, which can poison mean/stddev/percentile calculations and suppress
detection. Update the data filtering in the anomaly flow around detect_zscore
and the other affected report/stat helpers (the sections referenced at the later
ranges) to reject or skip NaN/inf values before any aggregation, and ensure the
returned anomaly/report stats are computed only from finite samples.

In `@src/openhuman/chat_with_data/db_connector.rs`:
- Around line 33-37: The public SQLite connector validation in
execute_sqlite_query is too shallow because it only checks for a SELECT prefix,
allowing unsafe queries like UNIONs or subqueries to slip past. Reuse the
existing SQL safety validation used by is_safe_query in db_connector.rs before
executing the query, and keep the current read-only check as a defense-in-depth
layer. Make the fix in the execute_sqlite_query path so direct callers are
subject to the same syntax and safety rules as other entry points.
- Around line 52-81: The SQLite query result cap is only applied after execution
via the query_map iterator, so expensive statements can still run unbounded
inside SQLite. Update the query path in db_connector.rs to enforce a hard limit
at execution time by adding a SQL LIMIT clause before prepare/query, and
consider using conn.get_interrupt_handle() with a timeout to abort long-running
queries. Keep the existing row decoding logic and the 1000-row cap, but move the
protection to the SQL/connection level in the same query execution flow.

In `@src/openhuman/chat_with_data/engine.rs`:
- Around line 262-274: The anomaly insight generation in generate_insight is
using synthetic sample_values with an injected spike at index 42, which can
produce false alerts. Replace this test data logic with values derived from the
actual dataset rows in ds so detect_combined analyzes real data only, and remove
the hardcoded outlier injection while keeping the existing
anomaly::detect_combined call intact.
- Around line 63-85: The Sqlite path currently uses ds.id for SQL generation,
which includes the full encoded dataset string instead of the real table name,
and the execution failure is only logged then ignored. Update
chat_with_data::engine to extract and pass the actual SQLite table name
separately to sql_gen::generate_sql_for_question, and in the
execute_sqlite_query branch surface the error back to the caller instead of
falling through to the generated-SQL answer when execution fails.
- Around line 332-337: The anomaly scan in the `ROW_STORE` lookup is picking a
metric with `values().next()`, which is nondeterministic because `HashMap`
iteration order is unstable. Update the logic in the `engine.rs` data scan path
to avoid choosing an arbitrary first value; instead use a deterministic
configured metric column or iterate all numeric columns explicitly in the
relevant `ROW_STORE`/scan code.
- Around line 58-80: The dataset registry lock is held for too long in
`chat_with_data` while SQL generation and SQLite execution run, which can block
other dataset operations. In `engine.rs`, clone the needed `DatasetMeta` (or the
specific fields used by `generate_sql_for_question` and the execution branch)
from `DATASETS.lock()` and then drop the lock before calling
`sql_gen::generate_sql_for_question` and `db_connector::execute_sqlite_query`;
keep the lock only around the `get(dataset_id)` access.
- Around line 196-207: The in-memory aggregation path in engine.rs is
normalizing parsed identifiers to lowercase before looking them up, but row keys
are stored with their original casing, so matching fails for columns like
Amount. Update the column resolution logic in the aggregation code near the
Expr::Identifier handling to preserve the parsed column name casing when
performing r.get lookups, or otherwise resolve against the original identifier
value consistently. Keep the fix localized to the aggregation lookup that builds
values from rows so identifiers and stored keys use the same casing.

In `@src/openhuman/chat_with_data/mod.rs`:
- Around line 8-15: The chat_with_data domain is using a custom root-module
layout instead of the canonical openhuman/* structure. Update the module
organization around mod.rs, engine, db_connector, rpc, and webhooks so the
domain follows the required mod.rs + types.rs + store.rs + ops.rs + schemas.rs +
tools.rs + bus.rs pattern, with mod.rs kept export-focused only. Move
controller/handler definitions into schemas.rs and delegate their logic into
ops.rs, and adjust or rename the existing engine/db_connector/rpc/webhooks
modules to fit the canonical roles or exports expected by the openhuman module
layout.

In `@src/openhuman/chat_with_data/rpc.rs`:
- Around line 61-76: The LLM fallback branch in chat_with_data/rpc.rs is
returning the generated SQL text instead of running it, so successful matches
can short-circuit without actual results. Update the SqlGenMethod::Fallback flow
to pass the validated llm_sql into the same read-only SQLite execution path used
elsewhere, then build the response from the query output rather than formatting
the SQL string; keep the existing validation/safety checks in place before
execution.
- Around line 204-220: The row parser in ingest_rows is dropping any non-numeric
cell by only keeping as_f64 values, which breaks the expected {col: value}
contract and can mark partially empty rows as ingested. Update the parsing in
rpc.rs around rows_val/engine::ingest_rows so it either preserves arbitrary JSON
values for each column or explicitly rejects payloads containing non-numeric
fields, and make the schema in schemas.rs match that decision. Ensure the
ingest_rows flow does not silently succeed on rows that lose data.

In `@src/openhuman/chat_with_data/schemas.rs`:
- Around line 111-124: The ControllerSchema outputs are missing fields that the
matching RPC handlers in rpc.rs actually return, so the published contract is
incomplete. Update each affected schema definition in schemas.rs to match the
payloads from the corresponding functions in rpc.rs: register_dataset, query,
generate_insight, get_dataset, scan_anomalies, and the other referenced
handlers. Make sure the outputs in the relevant ControllerSchema entries include
every returned field with the correct names and types so the registry published
from core/all.rs stays in sync.

In `@src/openhuman/chat_with_data/sql_gen.rs`:
- Around line 82-89: Escape all SQL identifiers before interpolating them into
query strings: the current string formatting in sql_gen.rs directly wraps
table/column names in quotes, which can still break on embedded quote
characters. Introduce a shared identifier-quoting helper and use it in the
SQL-building paths around the SELECT construction and the other affected
formatting sites, so table_name and column names are always safely escaped
before being inserted into SQL.
- Around line 281-289: The time-window conversion in sql_gen::parse_time_filter
can overflow when multiplying the parsed count by the suffix multiplier. Update
the logic around the `num_str.parse::<u32>()` branch to use `checked_mul` on `n`
with the computed multiplier instead of პირდაპირ `n * multiplier`, and return
`None` (or otherwise reject the filter) when the multiplication would overflow.
Keep the fix localized to the time-filter parsing path so very large inputs like
“last 4294967295 months” are safely rejected.
- Around line 431-459: `is_safe_query` currently relies on brittle string
matching and can miss non-SELECT statements that bypass the keyword list.
Replace the logic in `sql_gen::is_safe_query` with `sqlparser` parsing via
`Parser::parse_sql`, require exactly one `Statement`, accept only
`Statement::Query`, and ensure the query body is `SetExpr::Select` while
rejecting `SetExpr::SetOperation` and any non-empty `cte_tables`. If strict
select-only behavior is needed, also walk the `FROM` clause to reject derived
tables/subqueries so the validator enforces the contract structurally instead of
by keywords.

In `@src/openhuman/chat_with_data/webhooks.rs`:
- Around line 208-225: `notify_insight` currently uses `tokio::spawn`, which can
panic when called without an active runtime. Update the spawn path to use a
captured `tokio::runtime::Handle` and call `handle.spawn(...)` instead, or store
the handle in the shared state used by `webhooks.rs`. Make sure the async block
that does `resolve_and_check`, the POST request, and the logging remains
unchanged, but the task is scheduled through the runtime handle rather than the
thread-local `tokio::spawn`.
- Around line 104-124: The current pre-flight DNS check in resolve_and_check is
vulnerable to DNS rebinding because reqwest::Client::send will resolve the host
again later. Move the IP filtering into a custom reqwest::dns::Resolve
implementation that rejects private, loopback, and link-local addresses at
connection time, and wire it into the ClientBuilder in the webhook client setup.
Also ensure the reqwest client is built with no_proxy() and strict redirect
limits so the protection applies during actual requests.

---

Minor comments:
In `@docs/boost-vc-capability-plan.md`:
- Around line 56-65: Clarify the intent of docs/boost-vc-capability-plan.md by
updating the Explicit Non-Goals section to state whether it is aspirational v1
planning or current-state behavior, and make it consistent with
docs/VC_CAPABILITIES.md. Use the section header and bullets under Explicit
Non-Goals to either reframe them as future targets or remove/adjust any items
that contradict implemented capabilities such as diarization, email transport,
SQL execution, and persistence.

In `@docs/VC_CAPABILITIES.md`:
- Around line 286-287: Update the documentation reference that still hard-codes
`src/core/all.rs` line numbers, since those positions have shifted with the new
`chat_with_data` wiring. Replace the line-specific mention with symbol-based
references to `build_registered_controllers` and
`build_declared_controller_schemas` in `src/core/all.rs` so the capability docs
stay accurate as the file changes.

In `@src/openhuman/chat_with_data/db_connector.rs`:
- Around line 39-42: The debug log in the sqlite query path is exposing the full
db_path, which can leak sensitive local filesystem details. Update the logging
in the sqlite query execution flow to redact the path before emitting it, using
a basename or stable hash instead of the raw value. Keep the change localized to
the db connector’s query logging near the debug! call so the SQL length context
remains while the path is sanitized.

In `@src/openhuman/chat_with_data/engine.rs`:
- Around line 132-134: The execution caveat in the chat_with_data result path is
using the in-memory dataset message even when SQLite execution succeeds. Update
the caveat selection logic in the engine.rs result assembly around
execution_result and generated.is_valid so SQLite-backed successful runs emit a
SQLite-specific caveat instead of "Executed against in-memory dataset", and keep
the in-memory message only for the in-memory execution branch.

In `@src/openhuman/chat_with_data/rpc.rs`:
- Around line 11-16: The `get("source")` handling in `rpc.rs` is too permissive
because unknown values currently fall through to `DataSource::Csv`, which hides
typos and returns success. Update the source parsing logic in the `source` match
to treat only the advertised closed set (`csv`, `json`, `sqlite`, `api`) as
valid and return an error for anything else, so the RPC fails fast instead of
coercing invalid input. Keep the fix localized to the `source` mapping used
during dataset registration.

In `@src/openhuman/chat_with_data/webhooks.rs`:
- Around line 191-199: The webhook payload in the `payload` निर्माण inside
`webhooks.rs` is serializing `insight.severity` with debug formatting, which
turns the numeric value into a string. Update the `payload` construction to emit
`severity` as a numeric JSON value instead of a formatted debug string, using
the existing `insight.severity` field directly so consumers receive a number and
not text.

---

Nitpick comments:
In `@docs/boost-vc-capability-plan.md`:
- Around line 69-77: Update the architecture section to match the repo’s
canonical `src/openhuman/*/` layout: replace the outdated
`engine.rs`/`rpc.rs`/`about_app/catalog.rs` references with the export-only
`mod.rs` pattern and the expected split into `types.rs`, `store.rs`, `ops.rs`,
`schemas.rs`, `tools.rs`, and `bus.rs`. Use the capability list source from
`src/openhuman/about_app/catalog_data.rs` when describing where the catalog data
lives, and make sure the text reflects that `mod.rs` is export-focused only
while business logic belongs in `ops.rs`.

In `@src/openhuman/chat_with_data/db_connector.rs`:
- Around line 88-136: Add debug logging to the SQLite helper functions so their
database interactions are traceable. In get_sqlite_schema and
list_sqlite_tables, log entry before opening the connection, log before and
after each external SQLite call (Connection::open_with_flags, prepare,
query_map/collection), and log error branches with the existing LOG_PREFIX
context. Use the function names get_sqlite_schema and list_sqlite_tables to
place the logs so the entry/exit/state transitions are covered without changing
behavior.

In `@src/openhuman/chat_with_data/engine.rs`:
- Around line 418-572: The tests block in the `tests` module is pushing
`engine.rs` over the module size limit. Move these test cases into a separate
`engine_tests.rs` test module and expose them with `#[cfg(test)] mod
engine_tests;` from the `engine` module so the main module stays under the ~500
line guideline. Keep the existing test functions and references like
`get_dataset`, `register_dataset`, `query_dataset`, `generate_insight`,
`delete_dataset`, and `scan_all_datasets_for_anomalies` in the new test file.

In `@src/openhuman/chat_with_data/rpc.rs`:
- Around line 95-125: The `chat_with_data` flow is swallowing failures from
`load_config_with_timeout`, `create_chat_provider`, and
`provider.chat_with_system` by converting them to `None` with `.ok()?`, which
makes production fallback behavior opaque. Update the relevant helper(s) in
`rpc.rs` to emit `debug`/`trace` logs around each external call and failure
branch, using the existing `chat_with_data` context and the
`provider/chat_with_system` path, so config-load, provider-creation, and
LLM-call errors are visible before returning `None`. Apply the same logging
pattern to the other affected helper mentioned in the review so both entry/exit
and error paths are observable without changing the return type.

In `@src/openhuman/chat_with_data/schemas.rs`:
- Around line 304-329: The schema handler wiring is pointing to the wrong layer:
the handler functions in schemas.rs are delegating into rpc.rs instead of the
canonical ops.rs boundary. Update the h_register, h_query, h_insight, h_list_ds,
h_list_ins, h_get, h_ingest, h_scan, and h_delete adapters to call the
corresponding ops.rs functions so schemas.rs only routes transport requests to
business logic through ops.rs.

In `@src/openhuman/chat_with_data/sql_gen.rs`:
- Around line 462-579: The test module at the bottom of sql_gen.rs is pushing
the module over the size limit. Move the entire #[cfg(test)] mod tests block
into a separate sql_gen_tests.rs module and keep sql_gen.rs focused on
production code. Update the module wiring by replacing the inline tests with a
#[cfg(test)] mod sql_gen_tests; declaration, preserving all existing test
functions that reference validate_sql, generate_sql_for_question, is_safe_query,
and extract_time_filter.

In `@tests/json_rpc_e2e.rs`:
- Around line 13428-13518: The chat-with-data lifecycle test only checks ok:true
responses and misses propagation/cleanup validation. In the test flow around
post_json_rpc, assert the registered dataset_id is present in the downstream
list_datasets result, verify generate_insight and scan_anomalies return payloads
tied to that same dataset_id and expected shape, and add a delete_dataset call
at the end so the full register-query-list-insight-scan-delete lifecycle is
covered.
🪄 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: 3e9f1081-9907-49c6-ab50-cff07c42d9c9

📥 Commits

Reviewing files that changed from the base of the PR and between 2652db7 and b7c6cae.

📒 Files selected for processing (18)
  • Cargo.toml
  • docs/VC_CAPABILITIES.md
  • docs/boost-vc-capability-plan.md
  • src/core/all.rs
  • src/openhuman/about_app/catalog_data.rs
  • src/openhuman/about_app/types.rs
  • src/openhuman/chat_with_data/anomaly.rs
  • src/openhuman/chat_with_data/db_connector.rs
  • src/openhuman/chat_with_data/engine.rs
  • src/openhuman/chat_with_data/mod.rs
  • src/openhuman/chat_with_data/rpc.rs
  • src/openhuman/chat_with_data/schemas.rs
  • src/openhuman/chat_with_data/sql_gen.rs
  • src/openhuman/chat_with_data/types.rs
  • src/openhuman/chat_with_data/webhooks.rs
  • src/openhuman/mod.rs
  • src/openhuman/util.rs
  • tests/json_rpc_e2e.rs

Comment on lines +1452 to +1538
// ── Boost VC capability foundations ─────────────────────────────────────
Capability {
id: "voice_assistant.session",
name: "Voice Assistant Sessions",
domain: "voice_assistant",
category: CapabilityCategory::Automation,
description: "Core/RPC voice session foundation with open STT/TTS adapters, transcript \
handoff, and session status controls.",
how_to: "Start a voice session via RPC: openhuman.voice_assistant_start_session.",
status: CapabilityStatus::Beta,
privacy: Some(CapabilityPrivacy {
leaves_device: true,
data_kind: PrivacyDataKind::UserContent,
destinations: &["OpenHuman backend", "TinyHumans Neocortex"],
}),
},
Capability {
id: "guided_flows.recommendation",
name: "Guided Recommendation Flows",
domain: "guided_flows",
category: CapabilityCategory::Automation,
description: "Core/RPC quiz and recommendation flow engine with structured answers, \
reusable flow definitions, and recommendation output.",
how_to: "Start a flow via RPC: openhuman.guided_flows_start_flow with flow_id.",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "live_captions.transcript",
name: "Live Caption Transcripts",
domain: "live_captions",
category: CapabilityCategory::Automation,
description: "Core/RPC transcript pipeline for caption segments, saved transcripts, \
summaries, and meeting-note handoff.",
how_to: "Start via RPC: openhuman.live_captions_start_transcript with source.",
status: CapabilityStatus::Beta,
privacy: Some(CapabilityPrivacy {
leaves_device: true,
data_kind: PrivacyDataKind::UserContent,
destinations: &["OpenHuman backend", "TinyHumans Neocortex"],
}),
},
Capability {
id: "voice_actions.intent",
name: "Voice Action Recognition",
domain: "voice_actions",
category: CapabilityCategory::Automation,
description: "Core/RPC voice-command recognizer that maps utterances to controller-backed \
or skill-backed actions with visible status metadata for app callers.",
how_to: "Recognize via RPC: openhuman.voice_actions_recognize with utterance.",
status: CapabilityStatus::Beta,
privacy: Some(CapabilityPrivacy {
leaves_device: true,
data_kind: PrivacyDataKind::UserContent,
destinations: &["OpenHuman backend", "TinyHumans Neocortex"],
}),
},
Capability {
id: "operator_inbox.triage",
name: "Operator Inbox Triage",
domain: "operator_inbox",
category: CapabilityCategory::Automation,
description: "Core/RPC operator inbox foundation with IMAP fetching, SMTP reply sending, \
message triage, contextual draft replies, and follow-up scheduling.",
how_to: "Triage via RPC: openhuman.operator_inbox_triage_message with sender/subject/body; fetch via openhuman.operator_inbox_fetch_inbox.",
status: CapabilityStatus::Beta,
privacy: Some(CapabilityPrivacy {
leaves_device: true,
data_kind: PrivacyDataKind::UserContent,
destinations: &["OpenHuman backend", "TinyHumans Neocortex"],
}),
},
Capability {
id: "chat_with_data.query",
name: "Chat-with-Data Analytics",
domain: "chat_with_data",
category: CapabilityCategory::Automation,
description: "Core/RPC natural-language analytics over registered datasets with \
SQL validation, anomaly detection, trend analysis, and summaries.",
how_to: "Query via RPC: openhuman.chat_with_data_query with dataset_id and question.",
status: CapabilityStatus::Beta,
privacy: Some(CapabilityPrivacy {
leaves_device: true,
data_kind: PrivacyDataKind::UserContent,
destinations: &["OpenHuman backend", "TinyHumans Neocortex"],
}),
},

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.

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

domains='voice_assistant|guided_flows|live_captions|voice_actions|operator_inbox|chat_with_data'

echo "== Domain directories =="
fd -td 'voice_assistant|guided_flows|live_captions|voice_actions|operator_inbox|chat_with_data' src/openhuman

echo
echo "== Controller/schema registration symbols =="
rg -nP "all_(${domains})_(registered_controllers|controller_schemas)" src/openhuman src/core/all.rs

echo
echo "== Namespace/RPC references =="
rg -nP "\"(${domains})\"\\s*=>|openhuman\\.(${domains})_" src/core/all.rs src/openhuman tests

Repository: tinyhumansai/openhuman

Length of output: 2061


Remove unimplemented capability entries or implement the required Rust modules.

The catalog exposes five Beta capabilities (voice_assistant, guided_flows, live_captions, voice_actions, operator_inbox) that lack corresponding Rust implementation directories and RPC controller registration in src/core/all.rs.

Only chat_with_data is fully wired. The others have no source directories (e.g., src/openhuman/voice_assistant/) and their RPC endpoints (e.g., openhuman.voice_assistant_start_session) will fail in the runtime.

This violates the required feature workflow: "Specify → prove in Rust → prove over RPC → surface in UI".

Options:

  • Remove these entries from src/openhuman/about_app/catalog_data.rs until the feature is fully implemented and registered.
  • Or, implement the missing modules and wire them in src/core/all.rs before surfacing them as Beta features.
- Capability {
-     id: "voice_assistant.session", ...
- },
- Capability {
-     id: "guided_flows.recommendation", ...
- },
- ... (all 5 unimplemented domains)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// ── Boost VC capability foundations ─────────────────────────────────────
Capability {
id: "voice_assistant.session",
name: "Voice Assistant Sessions",
domain: "voice_assistant",
category: CapabilityCategory::Automation,
description: "Core/RPC voice session foundation with open STT/TTS adapters, transcript \
handoff, and session status controls.",
how_to: "Start a voice session via RPC: openhuman.voice_assistant_start_session.",
status: CapabilityStatus::Beta,
privacy: Some(CapabilityPrivacy {
leaves_device: true,
data_kind: PrivacyDataKind::UserContent,
destinations: &["OpenHuman backend", "TinyHumans Neocortex"],
}),
},
Capability {
id: "guided_flows.recommendation",
name: "Guided Recommendation Flows",
domain: "guided_flows",
category: CapabilityCategory::Automation,
description: "Core/RPC quiz and recommendation flow engine with structured answers, \
reusable flow definitions, and recommendation output.",
how_to: "Start a flow via RPC: openhuman.guided_flows_start_flow with flow_id.",
status: CapabilityStatus::Beta,
privacy: None,
},
Capability {
id: "live_captions.transcript",
name: "Live Caption Transcripts",
domain: "live_captions",
category: CapabilityCategory::Automation,
description: "Core/RPC transcript pipeline for caption segments, saved transcripts, \
summaries, and meeting-note handoff.",
how_to: "Start via RPC: openhuman.live_captions_start_transcript with source.",
status: CapabilityStatus::Beta,
privacy: Some(CapabilityPrivacy {
leaves_device: true,
data_kind: PrivacyDataKind::UserContent,
destinations: &["OpenHuman backend", "TinyHumans Neocortex"],
}),
},
Capability {
id: "voice_actions.intent",
name: "Voice Action Recognition",
domain: "voice_actions",
category: CapabilityCategory::Automation,
description: "Core/RPC voice-command recognizer that maps utterances to controller-backed \
or skill-backed actions with visible status metadata for app callers.",
how_to: "Recognize via RPC: openhuman.voice_actions_recognize with utterance.",
status: CapabilityStatus::Beta,
privacy: Some(CapabilityPrivacy {
leaves_device: true,
data_kind: PrivacyDataKind::UserContent,
destinations: &["OpenHuman backend", "TinyHumans Neocortex"],
}),
},
Capability {
id: "operator_inbox.triage",
name: "Operator Inbox Triage",
domain: "operator_inbox",
category: CapabilityCategory::Automation,
description: "Core/RPC operator inbox foundation with IMAP fetching, SMTP reply sending, \
message triage, contextual draft replies, and follow-up scheduling.",
how_to: "Triage via RPC: openhuman.operator_inbox_triage_message with sender/subject/body; fetch via openhuman.operator_inbox_fetch_inbox.",
status: CapabilityStatus::Beta,
privacy: Some(CapabilityPrivacy {
leaves_device: true,
data_kind: PrivacyDataKind::UserContent,
destinations: &["OpenHuman backend", "TinyHumans Neocortex"],
}),
},
Capability {
id: "chat_with_data.query",
name: "Chat-with-Data Analytics",
domain: "chat_with_data",
category: CapabilityCategory::Automation,
description: "Core/RPC natural-language analytics over registered datasets with \
SQL validation, anomaly detection, trend analysis, and summaries.",
how_to: "Query via RPC: openhuman.chat_with_data_query with dataset_id and question.",
status: CapabilityStatus::Beta,
privacy: Some(CapabilityPrivacy {
leaves_device: true,
data_kind: PrivacyDataKind::UserContent,
destinations: &["OpenHuman backend", "TinyHumans Neocortex"],
}),
},
// ── Boost VC capability foundations ─────────────────────────────────────
Capability {
id: "chat_with_data.query",
name: "Chat-with-Data Analytics",
domain: "chat_with_data",
category: CapabilityCategory::Automation,
description: "Core/RPC natural-language analytics over registered datasets with \
SQL validation, anomaly detection, trend analysis, and summaries.",
how_to: "Query via RPC: openhuman.chat_with_data_query with dataset_id and question.",
status: CapabilityStatus::Beta,
privacy: Some(CapabilityPrivacy {
leaves_device: true,
data_kind: PrivacyDataKind::UserContent,
destinations: &["OpenHuman backend", "TinyHumans Neocortex"],
}),
},
🤖 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/about_app/catalog_data.rs` around lines 1452 - 1538, The Beta
capability catalog currently advertises unimplemented features, so either remove
the `Capability` entries for `voice_assistant.session`,
`guided_flows.recommendation`, `live_captions.transcript`,
`voice_actions.intent`, and `operator_inbox.triage` from `catalog_data.rs` or
fully implement and register their Rust modules first. Make sure each advertised
capability has a matching source directory, RPC handler, and `src/core/all.rs`
registration before it remains in the catalog; `chat_with_data.query` can stay
as-is since it is already wired up.

Source: Coding guidelines

Comment on lines +54 to +63
pub fn detect_zscore(data: &[f64], threshold: f64) -> Vec<Anomaly> {
if !threshold.is_finite() || threshold <= 0.0 {
return vec![];
}
if data.len() < 3 {
return vec![];
}

let mean = mean(data);
let std = std_dev(data, mean);

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Filter or reject non-finite values before computing stats.

A single NaN/inf value makes mean/stddev/percentiles unreliable and can suppress anomaly detection while surfacing invalid report stats over RPC.

Proposed fix
 pub fn detect_zscore(data: &[f64], threshold: f64) -> Vec<Anomaly> {
+    let data: Vec<f64> = data.iter().copied().filter(|v| v.is_finite()).collect();
     if !threshold.is_finite() || threshold <= 0.0 {
         return vec![];
     }
-    if data.len() < 3 {
+    if data.len() < 3 {
         return vec![];
     }

Also applies to: 107-112, 187-204

🤖 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/chat_with_data/anomaly.rs` around lines 54 - 63, Non-finite
inputs are being fed into the anomaly stats pipeline, which can poison
mean/stddev/percentile calculations and suppress detection. Update the data
filtering in the anomaly flow around detect_zscore and the other affected
report/stat helpers (the sections referenced at the later ranges) to reject or
skip NaN/inf values before any aggregation, and ensure the returned
anomaly/report stats are computed only from finite samples.

Comment on lines +33 to +37
// Validate read-only (defense in depth — sqlparser already validates).
let lower = sql.trim().to_lowercase();
if !lower.starts_with("select") {
return Err("only SELECT queries allowed on database connector".into());
}

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reuse the SQL safety validator in the public connector.

execute_sqlite_query accepts any string starting with SELECT, so direct callers can bypass is_safe_query restrictions for UNION and subqueries. Validate syntax and safety here too.

Proposed fix
     let lower = sql.trim().to_lowercase();
     if !lower.starts_with("select") {
         return Err("only SELECT queries allowed on database connector".into());
     }
+    super::sql_gen::validate_sql(sql)?;
+    super::sql_gen::is_safe_query(sql)?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Validate read-only (defense in depth — sqlparser already validates).
let lower = sql.trim().to_lowercase();
if !lower.starts_with("select") {
return Err("only SELECT queries allowed on database connector".into());
}
// Validate read-only (defense in depth — sqlparser already validates).
let lower = sql.trim().to_lowercase();
if !lower.starts_with("select") {
return Err("only SELECT queries allowed on database connector".into());
}
super::sql_gen::validate_sql(sql)?;
super::sql_gen::is_safe_query(sql)?;
🤖 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/chat_with_data/db_connector.rs` around lines 33 - 37, The
public SQLite connector validation in execute_sqlite_query is too shallow
because it only checks for a SELECT prefix, allowing unsafe queries like UNIONs
or subqueries to slip past. Reuse the existing SQL safety validation used by
is_safe_query in db_connector.rs before executing the query, and keep the
current read-only check as a defense-in-depth layer. Make the fix in the
execute_sqlite_query path so direct callers are subject to the same syntax and
safety rules as other entry points.

Comment on lines +52 to +81
let mut stmt = conn
.prepare(sql)
.map_err(|e| format!("{LOG_PREFIX} prepare: {e}"))?;
let col_count = stmt.column_count();
let col_names: Vec<String> = (0..col_count)
.map(|i| stmt.column_name(i).unwrap_or("?").to_string())
.collect();

let rows = stmt
.query_map([], |row| {
let mut map = HashMap::new();
for (i, name) in col_names.iter().enumerate() {
let val: String = row
.get::<_, rusqlite::types::Value>(i)
.map(|v| match v {
rusqlite::types::Value::Null => "NULL".into(),
rusqlite::types::Value::Integer(n) => n.to_string(),
rusqlite::types::Value::Real(f) => f.to_string(),
rusqlite::types::Value::Text(s) => s,
rusqlite::types::Value::Blob(_) => "<blob>".into(),
})
.unwrap_or_else(|_| "?".into());
map.insert(name.clone(), val);
}
Ok(map)
})
.map_err(|e| format!("{LOG_PREFIX} query: {e}"))?
.take(1000) // Limit rows returned.
.collect::<Result<Vec<_>, _>>()
.map_err(|e| format!("{LOG_PREFIX} row decode: {e}"))?;

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.

🩺 Stability & Availability | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In rusqlite 0.37.0, what is the recommended way to interrupt long-running SQLite queries using progress handlers or timeouts?

💡 Result:

In rusqlite 0.37.0, there are two primary mechanisms for interrupting long-running SQLite queries, both of which are designed to handle concurrency and long-running operations [1][2]. 1. Using InterruptHandle (Recommended for Cross-Thread Cancellation) The recommended way to stop a query from a different thread is to use the InterruptHandle [1][2]. - You obtain this handle by calling connection.get_interrupt_handle() on the database connection [2]. - The InterruptHandle can be shared across threads [3]. - When you need to abort the operation, call the .interrupt() method on the handle [3]. - This triggers the underlying sqlite3_interrupt behavior, which forces the running SQL statement to terminate as soon as possible, typically returning an SQLITE_INTERRUPT error [3][4]. 2. Using Progress Handlers (Recommended for In-Process Checks) If you need to interrupt a query based on its internal progress or a custom condition, you can use a progress handler [2][5]. - This functionality requires the hooks crate feature to be enabled [2][6]. - You register the handler using connection.progress_handler(num_ops, handler), where num_ops defines the number of SQLite virtual machine instructions executed between handler invocations [2]. - The handler is a closure that returns a bool [2]. If the closure returns true, the query is interrupted [2]. Neither method provides a built-in "timeout" mechanism directly; to implement a timeout, you would typically spawn a separate thread that waits for the desired duration and then calls .interrupt() on the InterruptHandle [3][4]. Top results: [1][3][4][2]

Citations:


Enforce query execution limits at the SQLite level

The current .take(1000) only limits rows after SQLite finishes execution. For potentially expensive queries (joins, full scans), add a SQL LIMIT clause directly to the query string and/or use conn.get_interrupt_handle() with a timeout to abort long-running operations.

// Append LIMIT if not present in original SQL
let sql = if !sql.to_uppercase().contains("LIMIT") {
    format!("{sql} LIMIT 1000")
} else {
    sql
};

let interrupt_handle = conn.get_interrupt_handle();
let timeout_handle = tokio::spawn({
    let handle = interrupt_handle.clone();
    async move {
        tokio::time::sleep(Duration::from_secs(30)).await;
        handle.interrupt();
    }
});

// Run query; SQLite stops early
let rows = stmt.query_map(...)...;

Relying solely on .take(1000) allows SQLite to process unbounded data before yielding, risking timeouts and instability.

🤖 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/chat_with_data/db_connector.rs` around lines 52 - 81, The
SQLite query result cap is only applied after execution via the query_map
iterator, so expensive statements can still run unbounded inside SQLite. Update
the query path in db_connector.rs to enforce a hard limit at execution time by
adding a SQL LIMIT clause before prepare/query, and consider using
conn.get_interrupt_handle() with a timeout to abort long-running queries. Keep
the existing row decoding logic and the 1000-row cap, but move the protection to
the SQL/connection level in the same query execution flow.

Comment on lines +58 to +80
let store = DATASETS.lock().map_err(|e| format!("lock poisoned: {e}"))?;
let ds = store
.get(dataset_id)
.ok_or_else(|| format!("dataset not found: {dataset_id}"))?;

// Generate real SQL using sqlparser-validated generation.
let generated = super::sql_gen::generate_sql_for_question(&ds.id, &ds.columns, question);

// Validate safety (no DROP/DELETE/etc).
if let Err(e) = super::sql_gen::is_safe_query(&generated.sql) {
return Err(format!("unsafe query rejected: {e}"));
}

// Execute against in-memory data if available, or SQLite if source is Sqlite.
let execution_result = if ds.source == DataSource::Sqlite {
// Try real SQLite execution via db_connector.
// Dataset ID encodes the path for Sqlite sources (convention: "sqlite:/path/to/db:table").
let db_path = ds
.id
.strip_prefix("sqlite:")
.and_then(|s| s.split(':').next());
if let Some(path) = db_path {
match super::db_connector::execute_sqlite_query(path, &generated.sql) {

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Drop the dataset lock before query execution.

store remains locked while SQL generation and SQLite execution run, so a slow DB query can block dataset registration/deletion/listing. Clone DatasetMeta inside the lock, then release it before I/O.

Proposed fix
-    let store = DATASETS.lock().map_err(|e| format!("lock poisoned: {e}"))?;
-    let ds = store
+    let ds = DATASETS
+        .lock()
+        .map_err(|e| format!("lock poisoned: {e}"))?
         .get(dataset_id)
-        .ok_or_else(|| format!("dataset not found: {dataset_id}"))?;
+        .cloned()
+        .ok_or_else(|| format!("dataset not found: {dataset_id}"))?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let store = DATASETS.lock().map_err(|e| format!("lock poisoned: {e}"))?;
let ds = store
.get(dataset_id)
.ok_or_else(|| format!("dataset not found: {dataset_id}"))?;
// Generate real SQL using sqlparser-validated generation.
let generated = super::sql_gen::generate_sql_for_question(&ds.id, &ds.columns, question);
// Validate safety (no DROP/DELETE/etc).
if let Err(e) = super::sql_gen::is_safe_query(&generated.sql) {
return Err(format!("unsafe query rejected: {e}"));
}
// Execute against in-memory data if available, or SQLite if source is Sqlite.
let execution_result = if ds.source == DataSource::Sqlite {
// Try real SQLite execution via db_connector.
// Dataset ID encodes the path for Sqlite sources (convention: "sqlite:/path/to/db:table").
let db_path = ds
.id
.strip_prefix("sqlite:")
.and_then(|s| s.split(':').next());
if let Some(path) = db_path {
match super::db_connector::execute_sqlite_query(path, &generated.sql) {
let ds = DATASETS
.lock()
.map_err(|e| format!("lock poisoned: {e}"))?
.get(dataset_id)
.cloned()
.ok_or_else(|| format!("dataset not found: {dataset_id}"))?;
// Generate real SQL using sqlparser-validated generation.
let generated = super::sql_gen::generate_sql_for_question(&ds.id, &ds.columns, question);
// Validate safety (no DROP/DELETE/etc).
if let Err(e) = super::sql_gen::is_safe_query(&generated.sql) {
return Err(format!("unsafe query rejected: {e}"));
}
// Execute against in-memory data if available, or SQLite if source is Sqlite.
let execution_result = if ds.source == DataSource::Sqlite {
// Try real SQLite execution via db_connector.
// Dataset ID encodes the path for Sqlite sources (convention: "sqlite:/path/to/db:table").
let db_path = ds
.id
.strip_prefix("sqlite:")
.and_then(|s| s.split(':').next());
if let Some(path) = db_path {
match super::db_connector::execute_sqlite_query(path, &generated.sql) {
🤖 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/chat_with_data/engine.rs` around lines 58 - 80, The dataset
registry lock is held for too long in `chat_with_data` while SQL generation and
SQLite execution run, which can block other dataset operations. In `engine.rs`,
clone the needed `DatasetMeta` (or the specific fields used by
`generate_sql_for_question` and the execution branch) from `DATASETS.lock()` and
then drop the lock before calling `sql_gen::generate_sql_for_question` and
`db_connector::execute_sqlite_query`; keep the lock only around the
`get(dataset_id)` access.

Comment on lines +82 to +89
columns
.iter()
.map(|c| format!("\"{}\"", c))
.collect::<Vec<_>>()
.join(", ")
};
(
format!("SELECT {cols} FROM \"{}\" LIMIT 100", table_name),

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Escape SQL identifiers before interpolation.

Column/table names containing " can break out of the quoted identifier and produce invalid or attacker-shaped SQL. Centralize identifier quoting and use it everywhere SQL is formatted.

Proposed fix
+fn quote_ident(ident: &str) -> String {
+    format!("\"{}\"", ident.replace('"', "\"\""))
+}
+
 ...
-                    .map(|c| format!("\"{}\"", c))
+                    .map(|c| quote_ident(c))
 ...
-                format!("SELECT {cols} FROM \"{}\" LIMIT 100", table_name),
+                format!("SELECT {cols} FROM {} LIMIT 100", quote_ident(table_name)),
 ...
-            "SELECT {}(\"{}\") AS result FROM \"{}\"",
-            agg_fn, target_col, table
+            "SELECT {}({}) AS result FROM {}",
+            agg_fn, quote_ident(&target_col), quote_ident(table)
 ...
-            "SELECT {cols} FROM \"{table}\" WHERE \"{date_col}\" >= datetime('now', '-{days} days') ORDER BY \"{date_col}\" DESC LIMIT 100"
+            "SELECT {cols} FROM {} WHERE {} >= datetime('now', '-{days} days') ORDER BY {} DESC LIMIT 100",
+            quote_ident(table),
+            quote_ident(&date_col),
+            quote_ident(&date_col)

Also applies to: 154-159, 194-195, 237-238

🤖 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/chat_with_data/sql_gen.rs` around lines 82 - 89, Escape all SQL
identifiers before interpolating them into query strings: the current string
formatting in sql_gen.rs directly wraps table/column names in quotes, which can
still break on embedded quote characters. Introduce a shared identifier-quoting
helper and use it in the SQL-building paths around the SELECT construction and
the other affected formatting sites, so table_name and column names are always
safely escaped before being inserted into SQL.

Comment on lines +281 to +289
if let Ok(n) = num_str.parse::<u32>() {
let multiplier = if suffix.contains("week") {
7
} else if suffix.contains("month") {
30
} else {
1
};
return Some(n * multiplier);

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Avoid overflow in time-filter multiplication.

A question like “last 4294967295 months” can overflow n * multiplier, causing debug panics or wrapped release values. Use checked_mul and reject overly large windows.

Proposed fix
-                    return Some(n * multiplier);
+                    return n.checked_mul(multiplier);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Ok(n) = num_str.parse::<u32>() {
let multiplier = if suffix.contains("week") {
7
} else if suffix.contains("month") {
30
} else {
1
};
return Some(n * multiplier);
if let Ok(n) = num_str.parse::<u32>() {
let multiplier = if suffix.contains("week") {
7
} else if suffix.contains("month") {
30
} else {
1
};
return n.checked_mul(multiplier);
🤖 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/chat_with_data/sql_gen.rs` around lines 281 - 289, The
time-window conversion in sql_gen::parse_time_filter can overflow when
multiplying the parsed count by the suffix multiplier. Update the logic around
the `num_str.parse::<u32>()` branch to use `checked_mul` on `n` with the
computed multiplier instead of პირდაპირ `n * multiplier`, and return `None` (or
otherwise reject the filter) when the multiplication would overflow. Keep the
fix localized to the time-filter parsing path so very large inputs like “last
4294967295 months” are safely rejected.

Comment on lines +431 to +459
pub fn is_safe_query(sql: &str) -> Result<(), String> {
let upper = sql.to_uppercase();

// Reject multiple statements (semicolons).
if sql.contains(';') {
return Err("Query contains multiple statements (semicolons not allowed)".into());
}

let dangerous = [
"DROP", "DELETE", "TRUNCATE", "ALTER", "INSERT", "UPDATE", "CREATE", "EXEC", "EXECUTE",
];
for keyword in &dangerous {
// Check it's a standalone keyword, not part of a column name.
if upper.split_whitespace().any(|w| w == *keyword) {
return Err(format!("Query contains dangerous operation: {keyword}"));
}
}

// Reject UNION-based injection attempts.
if upper.split_whitespace().any(|w| w == "UNION") {
return Err("Query contains UNION (not allowed for safety)".into());
}

// Reject subqueries (parenthesized SELECT).
if upper.contains("(SELECT") {
return Err("Query contains subquery (not allowed for safety)".into());
}

Ok(())

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.

🔒 Security & Privacy | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For sqlparser 0.62.0, which AST variants should be accepted to enforce a single plain SELECT query and reject PRAGMA, ATTACH, EXPLAIN, UNION, and subqueries?

💡 Result:

To enforce a single plain SELECT query using sqlparser 0.62.0 while rejecting other statement types (PRAGMA, ATTACH, EXPLAIN, etc.), you must inspect the variant of the Statement enum returned by the parser [1]. The sqlparser library does not provide a built-in "restrictive" mode to automatically reject specific SQL statement types at the parser level [2][3]. Instead, you must parse the input into an AST and programmatically validate that the resulting Statement matches only the Query variant containing a plain SELECT [2][1]. 1. Parse the SQL input into a Statement: Use Parser::parse_sql or Parser::parse_statement to obtain a Statement enum [1]. 2. Validate the AST: Check that the returned object is specifically the Statement::Query variant [2]. 3. Enforce "Plain SELECT" Constraints: Within the Query struct (the body of the Statement::Query), you must ensure: - The body is a SetExpr::Select [4]. - The Select object does not contain features you wish to reject (e.g., subqueries in the FROM clause, which are represented by TableFactor::Derived or similar structures within the TableWithJoins entries) [4]. - You can also verify that fields like CTEs (ctes field in Query) are empty to enforce a "plain" query [2]. To reject statements like PRAGMA, ATTACH, or EXPLAIN, you simply assert that the statement is a Statement::Query. If the parser returns any other variant (e.g., Statement::Pragma, Statement::Attach, Statement::Explain), you reject the input [1]. To reject UNION, verify that the Query body is a SetExpr::Select rather than a SetExpr::SetOperation (which handles UNION, EXCEPT, INTERSECT) [4]. In summary, the target structure to accept is: Statement::Query { ctes: [], body: SetExpr::Select(Box),... } Any other variant of Statement or non-Select body should be treated as unauthorized [4][1]. Citations: 1: https://docs.rs/sqlparser/latest/sqlparser/parser/struct.Parser.html 2: https://github.com/sqlparser-rs/sqlparser-rs 3: https://github.com/apache/datafusion-sqlparser-rs 4: https://docs.rs/sqlparser/latest/sqlparser/ast/struct.Select.html Refactor is_safe_query to use sqlparser AST validation instead of string matching. The current string-based checks are insufficient to enforce a strict "SELECT-only" contract. As confirmed by sqlparser 0.62.0 semantics, relying on keyword detection allows bypassing rules for statements like PRAGMA, ATTACH, EXPLAIN, or DETACH that do not contain the forbidden keywords but violate the contract. Replace the current logic with a parser-based validation that: Parses the input to a Statement using Parser::parse_sql. Verifies the statement is exactly Statement::Query. Ensures the query body is SetExpr::Select (rejecting SetExpr::SetOperation which covers UNION). Ensures cte_tables is empty and no derived tables (subqueries) exist in the FROM clause if strict select-only is required. This approach natively rejects Statement::Pragma, Statement::Attach, Statement::Explain, and other non-query variants without brittle string heuristics. Recommended AST shape check let mut statements = Parser::parse_sql(&dialect, sql)?; if statements.len() != 1 { return Err("Only a single statement is allowed".into()); } match statements.pop().unwrap() { Statement::Query(query) => { // Reject UNION/INTERSECT/EXCEPT if !matches!(query.body, SetExpr::Select(_)) || !query.cte_tables.is_empty() { return Err("Only plain SELECT queries are allowed".into()); } // Optional: Recursively check for subqueries in FROM clause if strictness is needed Ok(()) } _ => Err("Only SELECT queries are allowed".into()), } 🤖 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/chat_with_data/sql_gen.rs` around lines 431 - 459, `is_safe_query` currently relies on brittle string matching and can miss non-SELECT statements that bypass the keyword list. Replace the logic in `sql_gen::is_safe_query` with `sqlparser` parsing via `Parser::parse_sql`, require exactly one `Statement`, accept only `Statement::Query`, and ensure the query body is `SetExpr::Select` while rejecting `SetExpr::SetOperation` and any non-empty `cte_tables`. If strict select-only behavior is needed, also walk the `FROM` clause to reject derived tables/subqueries so the validator enforces the contract structurally instead of by keywords.

Comment on lines +104 to +124
async fn resolve_and_check(url: &str) -> Result<(), String> {
let parsed = reqwest::Url::parse(url).map_err(|e| format!("invalid URL: {e}"))?;
let host = unbracket_host(
parsed
.host_str()
.ok_or_else(|| "URL has no host".to_string())?,
)
.to_string();
// Literal IPs are already covered by validate_webhook_url, but lookup_host
// handles them uniformly and we re-check regardless.
let port = parsed.port_or_known_default().unwrap_or(443);
let mut resolved = false;
for addr in tokio::net::lookup_host((host.as_str(), port))
.await
.map_err(|e| format!("dns resolution failed: {e}"))?
{
resolved = true;
if is_blocked_ip(addr.ip()) {
return Err("resolved address is private/loopback/link-local".into());
}
}

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.

🔒 Security & Privacy | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

In reqwest 0.12, does Client::send perform DNS resolution independently of a prior tokio::net::lookup_host call, and what supported patterns pin or filter resolved IPs for SSRF-safe outbound webhooks?

💡 Result:

In reqwest 0.12, the Client::send method performs DNS resolution independently of any prior calls made elsewhere in your application, such as tokio::net::lookup_host [1]. When a request is executed, the client uses its internal connector (which incorporates the configured DNS resolver) to resolve the hostname at the moment the connection is established [2][1]. If you rely on a pre-flight DNS check (e.g., calling lookup_host to validate an IP before passing a URL to reqwest), you create a Time-of-Check to Time-of-Use (TOCTOU) vulnerability [1]. An attacker could provide a domain that resolves to a public, allowed IP during your pre-flight check but switches to a restricted, internal IP (like 169.254.169.254) by the time the reqwest client actually resolves it [3][1]. To implement SSRF-safe outbound webhooks, you should not rely on external pre-flight checks. Instead, implement the following patterns: 1. Custom DNS Resolver (Recommended): Implement the reqwest::dns::Resolve trait and inject it into the client using the dns_resolver method on ClientBuilder [4][5]. This allows you to inspect and filter resolved IP addresses synchronously at the exact point of connection [1][4]. By enforcing your allowlist or blocklist (e.g., denying private ranges like 127.0.0.0/8, 10.0.0.0/8, or 169.254.0.0/16) within this resolver, you close the TOCTOU window entirely [6][1]. 2. Disable Proxies: Always call.no_proxy on your ClientBuilder [1]. Reqwest's default behavior may respect system proxy environment variables (e.g., HTTP_PROXY), which can be exploited to bypass your local DNS or network restrictions [7][1]. 3. Manage Redirects: Be aware that the default RedirectPolicy (limited to 10 hops) may follow redirects to restricted IPs [3]. You may need to set a stricter policy (e.g., RedirectPolicy::none) or implement custom redirect handling that re-verifies the target URL/IP at each hop [3]. Several community patterns exist, such as the http-acl-reqwest crate, which uses a middleware and custom DNS resolver approach to enforce access control lists on outbound requests [8][9]. Building your own resolver, as seen in patterns like SsrfScreeningResolver, is considered the standard practice for high-security environments [1].

Citations:


Close the DNS rebinding TOCTOU by injecting a custom DNS resolver into the reqwest client.

reqwest::Client::send performs its own DNS resolution independent of prior tokio::net::lookup_host calls. The current pre-flight check in resolve_and_check creates a Time-of-Check to Time-of-Use (TOCTOU) vulnerability: an attacker-controlled hostname can resolve to a public IP during validation and a private/internal IP during the actual request.

Remediation steps:

  • Implement a custom reqwest::dns::Resolve that filters out private, loopback, and link-local ranges at connection time.
  • Inject this resolver into the reqwest::ClientBuilder via dns_resolver.
  • Ensure the client disables system proxies (no_proxy()) and restricts redirects.

This fix is required for:

  • src/openhuman/chat_with_data/webhooks.rs (lines 104–124 and 215–219)
🤖 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/chat_with_data/webhooks.rs` around lines 104 - 124, The current
pre-flight DNS check in resolve_and_check is vulnerable to DNS rebinding because
reqwest::Client::send will resolve the host again later. Move the IP filtering
into a custom reqwest::dns::Resolve implementation that rejects private,
loopback, and link-local addresses at connection time, and wire it into the
ClientBuilder in the webhook client setup. Also ensure the reqwest client is
built with no_proxy() and strict redirect limits so the protection applies
during actual requests.

Comment on lines +208 to +225
tokio::spawn(async move {
// Re-resolve and re-validate at send time (DNS rebinding guard).
if let Err(e) = resolve_and_check(&url).await {
warn!("{LOG_PREFIX} host={host} blocked: {e}");
return;
}
debug!("{LOG_PREFIX} firing to host={host}");
match reqwest::Client::new()
.post(&url)
.json(&payload)
.timeout(std::time::Duration::from_secs(10))
.send()
.await
{
Ok(resp) => debug!("{LOG_PREFIX} host={host} responded {}", resp.status()),
Err(e) => warn!("{LOG_PREFIX} host={host} failed: {e}"),
}
});

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.

🩺 Stability & Availability | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

For tokio 1.x, what happens when tokio::spawn is called outside an active runtime, and how should libraries safely spawn optional background tasks?

💡 Result:

In Tokio 1.x, calling tokio::spawn from outside an active runtime context will result in a panic [1][2][3]. This function requires that a Tokio runtime is currently active on the thread, which is typically ensured by running code inside a task spawned by the runtime or by entering a runtime context using methods like Runtime::enter or Handle::enter [4][5][6]. For libraries that need to spawn optional background tasks safely, the recommended approach is to accept a tokio::runtime::Handle as an argument or capture it from the environment rather than relying on the global context [6][7]. Here is how to safely manage background tasks in a library: 1. Accept a Handle: Instead of calling tokio::spawn directly, allow users to pass a tokio::runtime::Handle to your library types or functions. This allows your library to spawn tasks onto the user's existing runtime explicitly [6]. 2. Use Handle::spawn: Use the handle.spawn method instead of the global tokio::spawn. This method is tied to the specific runtime instance represented by the handle and will not panic if called outside the context, provided the handle is valid [6]. 3. Capture the Handle: If your library creates its own tasks and needs to spawn others, you can capture the handle at the moment the library is initialized (e.g., using tokio::runtime::Handle::current) and store it for later use [6][7]. By using Handle::spawn, your library avoids the dependency on a thread-local runtime context, making it more robust and easier to integrate into various application architectures, including those where tasks might be spawned from non-Tokio threads [6][7].

Citations:


Replace tokio::spawn with Handle::spawn to prevent panics outside a runtime

The current implementation of tokio::spawn inside notify_insight will panic if called from a synchronous context that lacks an active Tokio runtime (e.g., synchronous schedulers, initialization code, or certain test setups). This causes a hard crash rather than a graceful degradation.

To fix this safely:

  1. Capture tokio::runtime::Handle::current() at the entry of the async boundary or store a Handle in the shared state.
  2. Use handle.spawn(...) instead of tokio::spawn(...). Handle::spawn uses the stored runtime instance and avoids the thread-local runtime dependency that triggers the panic.

If notify_insight must remain synchronous and the caller might not be in a runtime, capture the Handle once during initialization or return an error if no handle is available. Do not rely on tokio::spawn in library code that may be invoked from non-async contexts.

Original code at src/openhuman/chat_with_data/webhooks.rs:208-225
        tokio::spawn(async move {
            // Re-resolve and re-validate at send time (DNS rebinding guard).
            if let Err(e) = resolve_and_check(&url).await {
                warn!("{LOG_PREFIX} host={host} blocked: {e}");
                return;
            }
            debug!("{LOG_PREFIX} firing to host={host}");
            match reqwest::Client::new()
                .post(&url)
                .json(&payload)
                .timeout(std::time::Duration::from_secs(10))
                .send()
                .await
            {
                Ok(resp) => debug!("{LOG_PREFIX} host={host} responded {}", resp.status()),
                Err(e) => warn!("{LOG_PREFIX} host={host} failed: {e}"),
            }
        });
🤖 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/chat_with_data/webhooks.rs` around lines 208 - 225,
`notify_insight` currently uses `tokio::spawn`, which can panic when called
without an active runtime. Update the spawn path to use a captured
`tokio::runtime::Handle` and call `handle.spawn(...)` instead, or store the
handle in the shared state used by `webhooks.rs`. Make sure the async block that
does `resolve_and_check`, the POST request, and the logging remains unchanged,
but the task is scheduled through the runtime handle rather than the
thread-local `tokio::spawn`.

@Sathvik-1007

Copy link
Copy Markdown
Contributor Author

Closing — these need to be restacked properly with CI passing. Will resubmit once #4142 (shared wiring) lands and each domain PR compiles cleanly against it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. feature Net-new user-facing capability or product behavior. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant