Skip to content

Add shared editor and agent adapter protocol#458

Open
Darkroom4364 wants to merge 1 commit into
mainfrom
issue-451-adapter-protocol
Open

Add shared editor and agent adapter protocol#458
Darkroom4364 wants to merge 1 commit into
mainfrom
issue-451-adapter-protocol

Conversation

@Darkroom4364
Copy link
Copy Markdown
Collaborator

@Darkroom4364 Darkroom4364 commented May 27, 2026

Summary

  • add a versioned JSON adapter protocol covering scan-file, scan-workspace, diff, secrets, pqc, explain, and suppress workflows
  • expose the protocol through foxguard::adapter and a foxguard-adapter stdin/stdout reference binary
  • document request/response shapes, VS Code and Claude Code mappings, and fail-open vs fail-closed host policy

Verification

  • 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

Closes #451

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced a JSON-based adapter protocol enabling external editors and tools to integrate with foxguard via stdin/stdout, supporting file scanning, workspace scanning, diffing, secrets detection, and suppression suggestions.
  • Documentation

    • Formally documented the shared adapter integration contract and command mappings for agent and editor host integrations.

Review Change Stack

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
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 27, 2026

📝 Walkthrough

Walkthrough

This PR introduces a stable, versioned JSON adapter protocol that standardizes how editor and agent integrations invoke foxguard operations. A new foxguard-adapter CLI binary reads JSON requests from stdin, dispatches them to handlers for scan, diff, secrets, and suppression workflows, and emits JSON responses to stdout with exit codes for policy enforcement.

Changes

Shared Adapter Protocol Implementation

Layer / File(s) Summary
Adapter Protocol Contract & Module Export
src/adapter.rs (lines 1–173), src/lib.rs, Cargo.toml
Defines seven command types (scan-file, scan-workspace, diff, secrets, pqc, explain, suppress) and request/response payload structs with kebab-case serde naming, versioned schema, and optional fields controlling JSON output shape.
Request Dispatch & Entry Point
src/bin/foxguard_adapter.rs, src/adapter.rs (lines 175–242, 415–481)
CLI binary reads JSON from stdin, deserializes to AdapterRequest, executes via execute_adapter_request, and serializes response to stdout; dispatcher routes commands to handlers and shared utilities construct CLI args and standardize responses.
Scan, Analysis & Suppression Handlers
src/adapter.rs (lines 244–413)
Command handlers for explain (forced explain mode), secrets (with baseline/exclusion filters), diff (path resolution and optional severity filtering), and suppress (validation and suggestion generation).
Supporting Utilities & Tests
src/adapter.rs (lines 482–738)
Summary aggregation by severity, finding filtering (rule/file/line/column), suppression rendering (inline/config/baseline), exit code mapping, path/workspace resolution, and unit tests validating serde naming, response fields, and snippet formatting.
Adapter Protocol Specification & Integration Mapping
docs/agent-editor-integration.md, docs/claude-code-integration.md
Documents the Adapter Protocol contract (JSON stdin/stdout, exit behavior, required/optional fields), defines command-to-request mapping, expands Existing Host Mapping, and updates editor example to construct requests via jq and parse summary.findings_total.

Sequence Diagram

sequenceDiagram
  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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • peaktwilight

🐰 A protocol born to share,
JSON dancing through the air,
Adapters bridging near and far,
Editors and agents, a unified star!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add shared editor and agent adapter protocol' directly and clearly describes the main change: introduction of a new adapter protocol for editor and agent integrations.
Linked Issues check ✅ Passed All acceptance criteria from #451 are met: protocol defines request/response shapes for all required workflows [scan-file, scan-workspace, diff, secrets, pqc, explain, suppress], existing VS Code and Claude Code surfaces are mapped in documentation, a reference adapter binary (foxguard-adapter) and library API (foxguard::adapter) are provided, integration docs include fail-open/fail-closed guidance, and tests cover serialization and command behavior.
Out of Scope Changes check ✅ Passed All changes are directly aligned with PR #451 objectives: Cargo.toml adds the binary target, documentation updates map existing integrations and formalize the protocol, src/adapter.rs implements the protocol schema and handlers, src/bin/foxguard_adapter.rs provides the reference binary, and src/lib.rs exports the new module.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-451-adapter-protocol

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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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

📥 Commits

Reviewing files that changed from the base of the PR and between 84f6ae5 and 8694eae.

📒 Files selected for processing (6)
  • Cargo.toml
  • docs/agent-editor-integration.md
  • docs/claude-code-integration.md
  • src/adapter.rs
  • src/bin/foxguard_adapter.rs
  • src/lib.rs

Comment thread src/adapter.rs
Comment on lines +218 to +223
fn scan_workspace_response(request: &AdapterRequest) -> AdapterResponse {
let path = request
.path
.as_deref()
.or(request.workspace_root.as_deref())
.unwrap_or(".");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment thread src/adapter.rs
Comment on lines +516 to +519
.file
.as_ref()
.is_none_or(|file| finding.file == *file || finding.file.ends_with(file))
&& selector.line.is_none_or(|line| finding.line == line)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

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.

Suggested change
.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))).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Create shared editor and agent adapter protocol for non-VS Code integrations

1 participant