Add shared editor and agent adapter protocol#458
Conversation
Introduce a versioned JSON adapter contract for scan-file, scan-workspace, diff, secrets, pqc, explain, and suppress workflows. Add foxguard-adapter as a stdin/stdout reference binary and expose the same execution path from foxguard::adapter for embedded Rust integrations. Document the shared request/response shape, host mappings for VS Code and Claude Code, and fail-open versus fail-closed policy handling. Tested: cargo fmt --check; cargo test adapter::tests; cargo build --bin foxguard-adapter; cargo clippy --all-targets --all-features -- -D warnings; target/debug/foxguard --config /dev/null --severity medium src/adapter.rs -f json; target/debug/foxguard --config /dev/null --severity medium src/bin/foxguard_adapter.rs -f json; PATH=/bin:/usr/bin:/usr/local/bin:/opt/homebrew/bin:/Users/darkroom/.cargo/bin cargo test
📝 WalkthroughWalkthroughThis PR introduces a stable, versioned JSON adapter protocol that standardizes how editor and agent integrations invoke foxguard operations. A new ChangesShared Adapter Protocol Implementation
Sequence DiagramsequenceDiagram
participant Editor as Editor/Agent
participant Adapter as foxguard-adapter
participant Scanner as Scan Executor
Editor->>Adapter: JSON AdapterRequest (stdin)
activate Adapter
Adapter->>Adapter: Parse & validate
Adapter->>Scanner: execute_scan / execute_diff / etc.
activate Scanner
Scanner-->>Adapter: findings, summaries
deactivate Scanner
Adapter->>Adapter: Aggregate summary<br/>Map exit code<br/>Build response
deactivate Adapter
Adapter-->>Editor: JSON AdapterResponse (stdout)
Editor->>Editor: Parse findings_total<br/>Apply policy
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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/adapter.rs`:
- Around line 218-223: The code is using request.workspace_root as a fallback
for path before any resolution which can double-apply resolution (e.g., "repo"
-> "repo/repo"); update scan_workspace_response (and the other similar sites
around the file) to not fall back to workspace_root directly: use
request.path.as_deref().unwrap_or(".") for the raw path value and pass
request.workspace_root (or its as_deref()) separately into
resolve_workspace_path (or resolve_workspace_path should be called on
workspace_root first) so resolution is applied exactly once; refer to the
symbols scan_workspace_response, AdapterRequest.path,
AdapterRequest.workspace_root, and resolve_workspace_path to locate and adjust
the logic.
- Around line 516-519: The selector file-match currently uses string suffix
checking (finding.file.ends_with(file)) which can give false positives; change
it to path-component-aware matching by using
std::path::Path::new(&finding.file).ends_with(Path::new(file)) (or
Path::new(file) directly) in the same closure where
.file.as_ref().is_none_or(|file| ... ) is used so the check becomes
.is_none_or(|file| finding.file == *file ||
Path::new(&finding.file).ends_with(Path::new(file))).
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 047dd30f-c1a5-4946-bf97-8e6defef88a2
📒 Files selected for processing (6)
Cargo.tomldocs/agent-editor-integration.mddocs/claude-code-integration.mdsrc/adapter.rssrc/bin/foxguard_adapter.rssrc/lib.rs
| fn scan_workspace_response(request: &AdapterRequest) -> AdapterResponse { | ||
| let path = request | ||
| .path | ||
| .as_deref() | ||
| .or(request.workspace_root.as_deref()) | ||
| .unwrap_or("."); |
There was a problem hiding this comment.
Avoid using workspace_root as a fallback path before resolution.
When workspace_root is relative (for example repo), this fallback pattern causes resolve_workspace_path to produce repo/repo, so scans target the wrong directory.
💡 Proposed fix
fn scan_workspace_response(request: &AdapterRequest) -> AdapterResponse {
- let path = request
- .path
- .as_deref()
- .or(request.workspace_root.as_deref())
- .unwrap_or(".");
+ let path = request.path.as_deref().unwrap_or(".");
run_scan_response(
request,
resolve_workspace_path(path, request.workspace_root.as_deref()),
false,
)
}
fn pqc_response(request: &AdapterRequest) -> AdapterResponse {
- let path = request
- .path
- .as_deref()
- .or(request.workspace_root.as_deref())
- .unwrap_or(".");
+ let path = request.path.as_deref().unwrap_or(".");
run_scan_response(
request,
resolve_workspace_path(path, request.workspace_root.as_deref()),
true,
)
}
fn explain_response(request: &AdapterRequest) -> AdapterResponse {
- let Some(path) = request
- .path
- .as_deref()
- .or(request.workspace_root.as_deref())
- else {
+ if request.path.is_none() && request.workspace_root.is_none() {
return request_error(request, "explain requires path or workspace_root");
- };
+ }
+ let path = request.path.as_deref().unwrap_or(".");
let mut scan = scan_args(
request,
resolve_workspace_path(path, request.workspace_root.as_deref()),
false,
);
@@
fn secrets_response(request: &AdapterRequest) -> AdapterResponse {
- let path = request
- .path
- .as_deref()
- .or(request.workspace_root.as_deref())
- .unwrap_or(".");
+ let path = request.path.as_deref().unwrap_or(".");
@@
fn diff_response(request: &AdapterRequest) -> AdapterResponse {
- let path = request
- .path
- .as_deref()
- .or(request.workspace_root.as_deref())
- .unwrap_or(".");
+ let path = request.path.as_deref().unwrap_or(".");Also applies to: 232-236, 245-249, 297-301, 340-344
🤖 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/adapter.rs` around lines 218 - 223, The code is using
request.workspace_root as a fallback for path before any resolution which can
double-apply resolution (e.g., "repo" -> "repo/repo"); update
scan_workspace_response (and the other similar sites around the file) to not
fall back to workspace_root directly: use request.path.as_deref().unwrap_or(".")
for the raw path value and pass request.workspace_root (or its as_deref())
separately into resolve_workspace_path (or resolve_workspace_path should be
called on workspace_root first) so resolution is applied exactly once; refer to
the symbols scan_workspace_response, AdapterRequest.path,
AdapterRequest.workspace_root, and resolve_workspace_path to locate and adjust
the logic.
| .file | ||
| .as_ref() | ||
| .is_none_or(|file| finding.file == *file || finding.file.ends_with(file)) | ||
| && selector.line.is_none_or(|line| finding.line == line) |
There was a problem hiding this comment.
Use path-component-aware matching in finding selection.
finding.file.ends_with(file) can falsely match unrelated names (e.g., myapp.py for selector app.py). Use path-aware suffix matching instead.
💡 Proposed fix
&& selector
.file
.as_ref()
- .is_none_or(|file| finding.file == *file || finding.file.ends_with(file))
+ .is_none_or(|file| {
+ let finding_path = std::path::Path::new(&finding.file);
+ let selected_path = std::path::Path::new(file);
+ finding.file == *file || finding_path.ends_with(selected_path)
+ })📝 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.
| .file | |
| .as_ref() | |
| .is_none_or(|file| finding.file == *file || finding.file.ends_with(file)) | |
| && selector.line.is_none_or(|line| finding.line == line) | |
| .file | |
| .as_ref() | |
| .is_none_or(|file| { | |
| let finding_path = std::path::Path::new(&finding.file); | |
| let selected_path = std::path::Path::new(file); | |
| finding.file == *file || finding_path.ends_with(selected_path) | |
| }) | |
| && selector.line.is_none_or(|line| finding.line == line) |
🤖 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/adapter.rs` around lines 516 - 519, The selector file-match currently
uses string suffix checking (finding.file.ends_with(file)) which can give false
positives; change it to path-component-aware matching by using
std::path::Path::new(&finding.file).ends_with(Path::new(file)) (or
Path::new(file) directly) in the same closure where
.file.as_ref().is_none_or(|file| ... ) is used so the check becomes
.is_none_or(|file| finding.file == *file ||
Path::new(&finding.file).ends_with(Path::new(file))).
Summary
Verification
Closes #451
Summary by CodeRabbit
Release Notes
New Features
Documentation