feat(a4): ACP stdio JSON-RPC dispatcher (Zed/JetBrains/Neovim agent server) - #16
Merged
Conversation
New module acp_stdio.rs targeting Zed's Agent Client Protocol v0.11+ (JSON-RPC 2.0 over stdio) — distinct from the existing acp.rs HTTP surface. Type signatures + 6 BDD scenarios; impl bodies todo!() so tests panic at runtime — TDD red. Methods scaffolded: - initialize — handshake, returns protocolVersion + capabilities - authenticate — no-auth advertisement - newSession — create + return session id - loadSession — resume existing or SESSION_NOT_FOUND - cancel — best-effort cancel hook (full hookup with prompt) Scenarios: 1. initialize handshake exposes protocolVersion + agentCapabilities 2. newSession returns unique session ids and grows session_count 3. loadSession on unknown id → SESSION_NOT_FOUND (-32002) 4. Unknown method → METHOD_NOT_FOUND (-32601) 5. Malformed JSON → PARSE_ERROR response with id=null 6. jsonrpc != "2.0" → INVALID_REQUEST (-32600) Phase 53 P0 framing: VibeCLI as an ACP server callable from Zed + JetBrains + Neovim. Scope of THIS slice is the dispatcher (parse + route + envelope). The prompt / sessionUpdate slice (full agent loop integration) is tracked separately so it can land alongside the agent_runtime work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes A4 from v13 fitgap §16.1 — VibeCLI as an ACP server speaking
Zed's Agent Client Protocol v0.11+ (JSON-RPC 2.0 over stdio). After
Phase 54, the daemon advertises an ACP entry that Zed + JetBrains +
Neovim can register against, joining the live registry alongside
Claude Code, Codex CLI, GitHub Copilot CLI, OpenCode, Gemini CLI.
Implementation:
- AcpServer with Arc<Mutex<...>> session state — cheap clone, no
lifetime gymnastics for handlers.
- dispatch — JSON-RPC 2.0 envelope handling. Notifications (no id)
run for side effects with no response; requests always return one.
HandlerError variants map to JSON-RPC error codes:
MethodNotFound → -32601
MethodNotImplemented → -32001 (ACP extension; partial impl honest)
SessionNotFound → -32002 (ACP extension)
InvalidParams → -32602
Internal → -32603
- handle_initialize — protocolVersion 0.11.0, agentCapabilities
(loadSession, promptCapabilities,
mcpCapabilities), serverInfo (name=vibecli +
CARGO_PKG_VERSION).
- handle_authenticate — { authenticated: true } stub.
- handle_new_session — monotonic counter → vibecli-acp-{:016x};
recorded in BTreeMap.
- handle_load_session — looks up id, SESSION_NOT_FOUND if missing.
- handle_cancel — { cancelled: true } stub.
prompt + setSessionMode are recognised and rejected with
METHOD_NOT_IMPLEMENTED rather than METHOD_NOT_FOUND so the host
surfaces the partial-impl state honestly.
parse_request — separate function so callers can write the parse
error envelope directly to stdout without re-translating.
Tests (6 BDD scenarios, all in the red commit):
1. initialize handshake
2. newSession unique ids
3. loadSession unknown → SESSION_NOT_FOUND
4. unknown method → METHOD_NOT_FOUND
5. malformed JSON → PARSE_ERROR with id=null
6. wrong jsonrpc version → INVALID_REQUEST
Out of scope (intentional, sized for one PR):
- prompt + sessionUpdate notifications — full agent loop integration
with provider, agent_runtime, etc. Tracked separately so it can
land alongside the agent_runtime work.
- Stdin/stdout plumbing (line-delimited JSON read/write) — a thin
shell over dispatch that lives in the CLI subcommand or a separate
binary. Trivial follow-up; isolated from the dispatcher logic.
- ACP Registry registration — manual external step once dispatcher
wiring lands.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
ravituringworks
marked this pull request as ready for review
May 11, 2026 05:28
# Conflicts: # vibecli/vibecli-cli/src/lib.rs # vibecli/vibecli-cli/src/main.rs
ravituringworks
added a commit
that referenced
this pull request
May 14, 2026
) What ships ---------- * New module `vibecli/vibecli-cli/src/redact.rs`: - `Redact<T>` newtype with `Serialize`/`Deserialize` (transparent — so configs round-trip unchanged on disk) and *redacted* `Debug` / `Display` impls — `format!("{cfg:?}")` on a struct containing `api_key: Redact<String>` prints `api_key: [redacted]` instead of leaking the key into the daemon log file. - No `Deref` / `DerefMut` on purpose: callers must explicitly opt-in to the plaintext via `.expose()` / `.into_inner()`. Auto-deref would defeat the point — `format!("{}", *r)` would still leak. - Constant-time `PartialEq` for the common `Redact<String>` and `Redact<Vec<u8>>` shapes (via `subtle::ConstantTimeEq`) so two redacted tokens don't reveal a byte-level timing channel when compared directly. Other `Redact<T>` users don't get `PartialEq` by default — derive it themselves if they need it. - 8 unit tests cover redacted Debug, redacted Display, struct inheritance, `Option<Redact<T>>`, serde round-trip, `.expose()` /`.into_inner()`, and the constant-time PartialEq correctness. * New SAST rule `.semgrep/credential-logging.yml`: - `tracing-format-leaks-credential` (ERROR) — catches `tracing::{info,warn,error,debug,trace}!("…{api_key}…", …)` and the same for `bearer` / `secret` / `password` / `refresh_token` / `access_token` / `client_secret`. Scope is daemon + Tauri + workspace crates; tests / BDD fixtures excluded. - `println-leaks-credential` (WARNING) — same name pattern in `println!` / `eprintln!`. `banner.rs` excluded because it echoes *names* of configured providers (booleans), never keys. Migration plan -------------- Existing `api_key: Option<String>` fields stay as-is for now — wrap them in `Redact<…>` when their owning struct is next touched. New code uses the newtype directly. The semgrep gate makes accidental regression visible at PR review time before the redacted newtype migration is complete. Why not migrate everything now: ~50 config structs hold credential fields. Each migration touches the field, its `Default` impl, its serde round-trip (transparent passes through but explicit map serializers may need to call `.expose()`), and any builder. That's a multi-day sweep with non-trivial conflict surface against ongoing feature work. Shipping the newtype + the gate first is the right shape: zero-risk infrastructure now, opportunistic migration later. Doc updates ----------- - `docs/security/threat-model.md` row #16 → ✅ - Stale rows #4 / #5 / #13 / #15 also reconciled to ✅ (those CI gates were already in `.github/workflows/security.yml` from the earlier Phase-3 batch — §8 just hadn't been updated). - Two new change-log entries. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ravituringworks
added a commit
that referenced
this pull request
May 19, 2026
Full sweep of `tracing::*!` / `eprintln!` interpolation sites across
daemon (`vibecli-cli/src/`), agent crate (`vibeui/crates/vibe-ai/src/`),
Tauri commands, and provider crates for log-line leakage of user
content / model output.
One real leak found and fixed: `vibe-ai/src/agent.rs::run` logged
`summary = %summary` at agent-task-complete time where `summary` is
the model's `task_complete` argument. If a user pasted a secret into
their task description ("ignore my password 'hunter2'"), the model
could echo it in its final summary and the daemon would land it in
a plaintext log file under `RUST_LOG=info`.
Fixed by replacing the interpolation with `summary_len = summary.len()`,
matching the `response_len = accumulated.len()` pattern already in
use at line 840 for the same reason. The full summary still flows
through the event channel to the UI — only the log line is redacted.
All other audited sites log only IDs / counts / error chains / static
policy strings; the Slice F `Tainted<T>` helpers (`log_fingerprint`,
`audit_id`, `audit_summary`) cover the typed sites. Threat-model row
#16 marked ✅.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Wave 5 / A4 of v13 fitgap §16.1 — VibeCLI as an Agent Client Protocol server speaking Zed's ACP v0.11+ (JSON-RPC 2.0 over stdio). Once Phase 54 ships and the daemon registers in the live ACP Registry (built into Zed + JetBrains in Q1 2026), Zed/JetBrains/Neovim users discover VibeCLI natively alongside Claude Code, Codex CLI, GitHub Copilot CLI, OpenCode, Gemini CLI.
This PR is distinct from the existing
acp.rsHTTP surface — that's an older HTTP/SSE shape. Zed's ACP is JSON-RPC over stdio, similar in spirit to LSP.Scope
In: the dispatcher half — JSON-RPC envelope handling + the five always-on methods that hosts call during initialization and session lifecycle.
Out (intentional, sized for one PR):
prompt+sessionUpdatenotifications — full agent loop integration withprovider/agent_runtime. The dispatcher recognisespromptand rejects it withMETHOD_NOT_IMPLEMENTED(-32001) rather thanMETHOD_NOT_FOUND, so hosts see partial-impl honestly.dispatchthat lives in the CLI subcommand or a separate binary. Isolated from the dispatcher logic; trivial follow-up.Implementation
AcpServerholds session state inArc<Mutex<...>>so handlers can be cloned across threads without lifetime gymnastics. Sessions are in-memory for this slice; persistence ties intocrate::session_storein a follow-up.JSON-RPC error code mapping
MethodNotFound-32601MethodNotImplemented-32001(ACP ext)SessionNotFound-32002(ACP ext)InvalidParams-32602Internal-32603ParseError-32700id: nullInvalidRequest-32600jsonrpcfield !="2.0"The two ACP extensions (
-32001,-32002) are in the JSON-RPC implementation-defined range (-32000to-32099) and don't conflict with any standard JSON-RPC error.Tests (red → green)
6 BDD scenarios in
acp_stdio::tests, all committed before any impl in the red commit:initializereturnsprotocolVersion+agentCapabilities+serverInfo.name = "vibecli"newSessioncalls produce unique ids;session_countgrowsloadSessionon unknown id →SESSION_NOT_FOUND(-32002)METHOD_NOT_FOUND(-32601)parse_request→ response withcode: -32700,id: nulljsonrpc: "1.0"→INVALID_REQUEST(-32600)Tests run in milliseconds — no Rust workspace cold-compile required.
Test plan
cargo test -p vibecli --lib acp_stdio::tests— all 6 scenarios passcargo check -p vibeclicleaninitializerequest to the eventualvibecli acp servesubcommand, observe the v0.11.0 handshake responsePatent-distance posture
A4 implements the public Zed Agent Client Protocol specification (open, on agent-client-protocol.org). No UX surface, no overlap with Cursor / Copilot patents. JSON-RPC 2.0 itself is decades old and unencumbered.
Cross-cutting invariants
pub mod acp_stdio;inlib.rs,mod acp_stdio;inmain.rsper CLAUDE.md.serde+serde_json+anyhoware already pulled.acp.rs— that's the HTTP/SSE ACP surface; this is the stdio JSON-RPC one. Both names start withacp_to keep them adjacent in the module list.🤖 Generated with Claude Code