fix(safety): add inbound secret scanning to engine v2 path - #2494
Conversation
The v2 engine path (`handle_with_engine_inner` in `bridge/router.rs`) forwarded user messages directly to the conversation manager without any safety checks. This allowed secrets (API keys, Slack tokens, AWS credentials, etc.) pasted in chat to reach the LLM and be permanently stored in conversation history. Add the same three safety checks that the v1 path (`thread_ops.rs`) already enforces: `validate_input`, `check_policy`, and `scan_inbound_for_secrets`. Messages containing detected secrets are now rejected with a user-facing warning before reaching the engine. Includes a regression test exercising Slack bot tokens and OpenAI keys through the v2 code path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces safety validation, policy enforcement, and secret scanning for inbound messages in the engine v2 pipeline, mirroring protections from the v1 path. It also includes a regression test for blocking leaked secrets. Feedback identified a bug in the test case where the mock OpenAI key was too short to trigger the leak detector, and a code suggestion was provided to correct the key length and strengthen the test assertions.
| let result = handle_with_engine_inner(&agent, &sk_msg, &sk_msg.content, 0) | ||
| .await | ||
| .expect("should not error"); | ||
| assert!(result.is_some(), "OpenAI key should be blocked"); | ||
|
|
||
| // Clean message should pass through (will fail at conversation | ||
| // manager level since test state has no real engine, but it must | ||
| // NOT be rejected by the safety checks). |
There was a problem hiding this comment.
The OpenAI key used in this test, sk-abc123def456ghi789, has a 19-character payload, but the regex for OpenAI keys in leak_detector.rs requires at least 20 characters. As a result, this test case will not be caught by the secret scanner, and the test will not behave as expected.
I've updated the key to be 20 characters long and also made the assertion more specific, similar to the check for the Slack token.
| let result = handle_with_engine_inner(&agent, &sk_msg, &sk_msg.content, 0) | |
| .await | |
| .expect("should not error"); | |
| assert!(result.is_some(), "OpenAI key should be blocked"); | |
| // Clean message should pass through (will fail at conversation | |
| // manager level since test state has no real engine, but it must | |
| // NOT be rejected by the safety checks). | |
| let sk_msg = IncomingMessage::new("web", "alice", "my key is sk-abc123def456ghi789j"); | |
| let result = handle_with_engine_inner(&agent, &sk_msg, &sk_msg.content, 0) | |
| .await | |
| .expect("should not error"); | |
| let warning = result.expect("should return a warning, not None"); | |
| assert!( | |
| warning.contains("secret") || warning.contains("credential"), | |
| "expected secret-detection warning for OpenAI key, got: {warning}" | |
| ); |
References
- In tests, prefer using .expect() to provide clear error messages and fail explicitly rather than using silent fallbacks like .unwrap_or() that could mask logic errors.
The mock OpenAI key `sk-abc123def456ghi789` had only 19 chars after the prefix, but the leak detector regex requires 20+. Extended the key and added a specific assertion matching the Slack token check. Addresses gemini-code-assist review feedback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Addressed gemini-code-assist review: extended the mock OpenAI key from 19 to 21 chars (regex requires 20+) and added a specific assertion. Fixed in b57c380. |
748e815 to
4acf399
Compare
Wildcard name constraint bypass in rustls-webpki 0.102.8, pinned by the libsql transitive dependency chain. Same root cause as the already-ignored RUSTSEC-2026-0049. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
4acf399 to
9ee4af3
Compare
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
henrypark133
left a comment
There was a problem hiding this comment.
Review: Inbound secret scanning on engine v2
No verified findings in the current diff. The safety validation, policy check, and inbound secret scan are now applied on the engine-v2 ingress path before the message is handed to the conversation manager, which restores parity with the v1 pipeline.
…t isolation - Resolve merge conflicts in deny.toml (keep both RUSTSEC-2026-0098 and 0099) and src/bridge/router.rs (keep both secret-scan and persistence tests) - Add V24__llm_calls_created_at_index checksum to migrations/checksums.lock - Fix config test isolation: pass explicit empty TOML to avoid reading host config.toml which may override DB-seeded selected_model Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Resolve merge conflict in src/bridge/router.rs by keeping both the PR's inbound secret-scan regression test and staging's legacy user-id migration tests. Fix pre-existing test failure in e2e_attachments: update assertion to match the new image-attachment text from staging commit b2a725b. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove useless .into_iter() in catalog.rs and fix rustfmt style in e2e_attachments.rs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ecks The inbound safety scanning code was written against the old Option<String> return type, but handle_with_engine_inner now returns BridgeOutcome. Replace Ok(Some(...)) with Ok(BridgeOutcome::Respond(...)) and update tests to match on the enum variants. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(safety): add inbound secret scanning to engine v2 path (nearai#2491) The v2 engine path (`handle_with_engine_inner` in `bridge/router.rs`) forwarded user messages directly to the conversation manager without any safety checks. This allowed secrets (API keys, Slack tokens, AWS credentials, etc.) pasted in chat to reach the LLM and be permanently stored in conversation history. Add the same three safety checks that the v1 path (`thread_ops.rs`) already enforces: `validate_input`, `check_policy`, and `scan_inbound_for_secrets`. Messages containing detected secrets are now rejected with a user-facing warning before reaching the engine. Includes a regression test exercising Slack bot tokens and OpenAI keys through the v2 code path. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * style(safety): fix rustfmt formatting in secret scan test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(safety): fix OpenAI key test — payload too short for regex (nearai#2494) The mock OpenAI key `sk-abc123def456ghi789` had only 19 chars after the prefix, but the leak detector regex requires 20+. Extended the key and added a specific assertion matching the Slack token check. Addresses gemini-code-assist review feedback. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(deps): ignore RUSTSEC-2026-0099 webpki advisory Wildcard name constraint bypass in rustls-webpki 0.102.8, pinned by the libsql transitive dependency chain. Same root cause as the already-ignored RUSTSEC-2026-0049. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: minor comment tweak to retrigger CI Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(ci): resolve clippy and fmt errors Remove useless .into_iter() in catalog.rs and fix rustfmt style in e2e_attachments.rs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(bridge): use BridgeOutcome instead of Option<String> in safety checks The inbound safety scanning code was written against the old Option<String> return type, but handle_with_engine_inner now returns BridgeOutcome. Replace Ok(Some(...)) with Ok(BridgeOutcome::Respond(...)) and update tests to match on the enum variants. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Illia Polosukhin <ilblackdragon@gmail.com>
Summary
handle_with_engine_innerinbridge/router.rs) forwarded user messages to the conversation manager without any safety checks, allowing pasted secrets to reach the LLM and be permanently storedthread_ops.rs) already enforces:validate_input,check_policy, andscan_inbound_for_secretsCloses #2491
Test plan
cargo test --lib -- bridge::router::tests::handle_with_engine_blocks_inbound_secrets— new regression testcargo test --lib -- bridge::router::tests— all 24 router tests passcargo clippy --all --benches --tests --examples --all-features— zero warningsENGINE_V2=true, paste axoxb-...token in chat, verify it's rejected with a warning🤖 Generated with Claude Code