diff --git a/CHANGELOG.md b/CHANGELOG.md index a078d18a7..7db52c72c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added +- feat(acp): bump `agent-client-protocol` 0.10.2→0.10.3, `agent-client-protocol-schema` 0.11.2→0.11.3; add `unstable-logout` feature with no-op logout handler and `auth.logout` capability advertisement; add `unstable-elicitation` feature gate (exposes schema types; SDK methods not yet available upstream); fix discovery endpoint `protocol_version` to use `ProtocolVersion::LATEST`; fix double-feature-activation antipattern in `zeph-acp` feature flags (#2411) - feat(skills): add `category` field to SKILL.md frontmatter — optional grouping for skill library organisation; all 26 bundled skills annotated with categories (`web`, `data`, `dev`, `system`) (#2268) - feat(skills): `/skills` command now groups output by category when skills have `category` set; uncategorised skills appear under `[other]` (#2268) - feat(skills): `/skills confusability` — new command showing all skill pairs whose cosine similarity exceeds `[skills] confusability_threshold`; identifies disambiguation risk before library phase transition (#2268) diff --git a/Cargo.lock b/Cargo.lock index eb7b4556f..95b9beb2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,9 +83,9 @@ dependencies = [ [[package]] name = "agent-client-protocol" -version = "0.10.2" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c56a59cf6315e99f874d2c1f96c69d2da5ffe0087d211297fc4a41f849770a2" +checksum = "0e5bbb6e9d91077607468af8b5bb0f75d10d0b8ac0db49a0c46542cba8ff9f88" dependencies = [ "agent-client-protocol-schema", "anyhow", @@ -100,9 +100,9 @@ dependencies = [ [[package]] name = "agent-client-protocol-schema" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0497b9a95a404e35799904835c57c6f8c69b9d08ccfd3cb5b7d746425cd6789" +checksum = "96daddd0d00f2eab88f8099d38190881bf8d6c5e46b6fa21751f482775d0dba7" dependencies = [ "anyhow", "derive_more 2.1.1", diff --git a/Cargo.toml b/Cargo.toml index 65b1b72b9..8fcea5ee5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ publish = true [workspace.dependencies] age = { version = "0.11.2", default-features = false } -agent-client-protocol = "0.10.2" +agent-client-protocol = "0.10.3" anyhow = "1.0" async-stream = "0.3" async-trait = "0.1" @@ -179,7 +179,7 @@ a2a = ["dep:zeph-a2a", "zeph-a2a?/server"] compression-guidelines = ["zeph-core/compression-guidelines", "zeph-memory/compression-guidelines"] acp = ["dep:zeph-acp"] acp-http = ["acp", "dep:axum", "zeph-acp/acp-http"] -acp-unstable = ["acp", "zeph-acp/unstable-session-fork", "zeph-acp/unstable-session-resume"] +acp-unstable = ["acp", "zeph-acp/unstable-session-fork", "zeph-acp/unstable-session-resume", "zeph-acp/unstable-elicitation", "zeph-acp/unstable-logout"] candle = ["zeph-llm/candle", "zeph-core/candle"] classifiers = ["candle", "zeph-llm/classifiers", "zeph-sanitizer/classifiers", "zeph-core/classifiers"] metal = ["candle", "zeph-llm/metal", "zeph-core/metal"] diff --git a/crates/zeph-acp/Cargo.toml b/crates/zeph-acp/Cargo.toml index 76fcda9bc..cdefd12e8 100644 --- a/crates/zeph-acp/Cargo.toml +++ b/crates/zeph-acp/Cargo.toml @@ -19,20 +19,20 @@ default = [ "unstable-session-resume", "unstable-session-usage", "unstable-session-model", + "unstable-elicitation", + "unstable-logout", ] acp-http = ["dep:axum", "dep:blake3", "dep:dashmap", "dep:async-stream", "dep:tower-http", "dep:subtle", "dep:tower"] unstable-session-fork = ["agent-client-protocol/unstable_session_fork"] unstable-session-resume = ["agent-client-protocol/unstable_session_resume"] unstable-session-usage = ["agent-client-protocol/unstable_session_usage"] unstable-session-model = ["agent-client-protocol/unstable_session_model"] +# Exposes schema types (ElicitationRequest, etc.); SDK methods not yet wired in agent-client-protocol 0.10.3. +unstable-elicitation = ["agent-client-protocol/unstable_elicitation"] +unstable-logout = ["agent-client-protocol/unstable_logout"] [dependencies] -agent-client-protocol = { workspace = true, features = [ - "unstable_session_fork", - "unstable_session_resume", - "unstable_session_usage", - "unstable_session_model", -] } +agent-client-protocol = { workspace = true } async-stream = { workspace = true, optional = true } async-trait.workspace = true axum = { workspace = true, features = ["ws"], optional = true } @@ -68,7 +68,7 @@ zeph-tools.workspace = true tempfile.workspace = true tower = { workspace = true, features = ["util"] } axum = { workspace = true, features = ["ws"] } -tokio-tungstenite.workspace = true +tokio-tungstenite = { workspace = true, features = ["connect"] } [lints] workspace = true diff --git a/crates/zeph-acp/README.md b/crates/zeph-acp/README.md index 7972de726..1e61ce998 100644 --- a/crates/zeph-acp/README.md +++ b/crates/zeph-acp/README.md @@ -360,12 +360,13 @@ The `initialize` response includes an `auth_hint` key in its metadata map. For s | Feature | Status | Description | |---------|--------|-------------| | `acp-http` | stable | Enables the HTTP+SSE and WebSocket transports (axum-based). Required for `post_handler`, `get_handler`, `ws_upgrade_handler`, and `router`. | -| `unstable-session-list` | unstable | Enables the `list_sessions` ACP method. See below. | | `unstable-session-fork` | unstable | Enables the `fork_session` ACP method. See below. | | `unstable-session-resume` | unstable | Enables the `resume_session` ACP method. See below. | | `unstable-session-usage` | unstable | Enables `UsageUpdate` events — token counts (input, output, cache) sent to the IDE after each turn. See below. | | `unstable-session-model` | unstable | Enables `SetSessionModel` — IDE-driven model switching via a native picker without `session/configure`. See below. | | `unstable-session-info-update` | unstable | Enables `SessionInfoUpdate` — agent-generated session title emitted to the IDE after the first turn. See below. | +| `unstable-elicitation` | unstable | Exposes elicitation schema types (`ElicitationRequest`, etc.) for future agent-loop integration. SDK methods not yet available in 0.10.3. | +| `unstable-logout` | unstable | Enables the `logout` ACP method and advertises `auth.logout` capability. Zeph logout is a no-op (vault-based auth). | > [!WARNING] > All `unstable-*` features have wire protocol that is not yet finalized. Expect breaking changes before these features graduate to stable. @@ -375,22 +376,23 @@ To opt in, add the desired features in your `Cargo.toml`: ```toml [dependencies] zeph-acp = { version = "*", features = [ - "unstable-session-list", "unstable-session-fork", "unstable-session-resume", "unstable-session-usage", "unstable-session-model", "unstable-session-info-update", + "unstable-elicitation", + "unstable-logout", ] } ``` All flags are independent and can be combined freely. -### `unstable-session-list` +### `session/list` (stable) -Enables the `list_sessions` method on `ZephAcpAgent`. Returns a snapshot of all active in-memory sessions as `SessionInfo` records (session ID, working directory, last-updated timestamp). Supports an optional `cwd` filter — when provided, only sessions whose working directory matches the given path are returned. +The `list_sessions` method on `ZephAcpAgent` is stable as of `agent-client-protocol` 0.10.3 — no feature flag required. Returns a snapshot of all active in-memory sessions as `SessionInfo` records (session ID, working directory, last-updated timestamp). Supports an optional `cwd` filter — when provided, only sessions whose working directory matches the given path are returned. -When this feature is active, `initialize` advertises `SessionListCapabilities` in the `session` capabilities block, signalling to the IDE that the server supports session enumeration. +The `initialize` response always advertises `SessionListCapabilities` in the `session` capabilities block, signalling to the IDE that the server supports session enumeration. ### `unstable-session-fork` diff --git a/crates/zeph-acp/src/agent/mod.rs b/crates/zeph-acp/src/agent/mod.rs index a92613a60..019e29835 100644 --- a/crates/zeph-acp/src/agent/mod.rs +++ b/crates/zeph-acp/src/agent/mod.rs @@ -743,6 +743,10 @@ impl acp::Agent for ZephAcpAgent { caps.session_capabilities(session_caps) }; + #[cfg(feature = "unstable-logout")] + let caps = caps + .auth(acp::AgentAuthCapabilities::default().logout(acp::LogoutCapabilities::default())); + Ok(acp::InitializeResponse::new(acp::ProtocolVersion::LATEST) .agent_info( acp::Implementation::new(&self.agent_name, &self.agent_version).title(title), @@ -782,6 +786,14 @@ impl acp::Agent for ZephAcpAgent { Ok(acp::AuthenticateResponse::default()) } + #[cfg(feature = "unstable-logout")] + async fn logout(&self, _args: acp::LogoutRequest) -> acp::Result { + // Zeph uses vault-based authentication, not session-based auth. + // Logout is a no-op but we advertise the capability for protocol compliance. + tracing::debug!("ACP logout (no-op: vault-based auth)"); + Ok(acp::LogoutResponse::default()) + } + async fn new_session( &self, args: acp::NewSessionRequest, diff --git a/crates/zeph-acp/src/transport/discovery.rs b/crates/zeph-acp/src/transport/discovery.rs index 2a0281ba2..234715ef0 100644 --- a/crates/zeph-acp/src/transport/discovery.rs +++ b/crates/zeph-acp/src/transport/discovery.rs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2026 Andrei G // SPDX-License-Identifier: MIT OR Apache-2.0 +use agent_client_protocol as acp; use axum::Json; use axum::extract::State; use axum::response::IntoResponse; @@ -23,7 +24,7 @@ pub async fn discovery_handler(State(state): State) -> impl IntoRe "name": state.server_config.agent_name, "version": state.server_config.agent_version, "protocol": "acp", - "protocol_version": "0.9", + "protocol_version": acp::ProtocolVersion::LATEST, "transports": { "http_sse": { "url": "/acp" }, "websocket": { "url": "/acp/ws" }, diff --git a/crates/zeph-acp/src/transport/tests.rs b/crates/zeph-acp/src/transport/tests.rs index 5996c241c..503817f63 100644 --- a/crates/zeph-acp/src/transport/tests.rs +++ b/crates/zeph-acp/src/transport/tests.rs @@ -7,6 +7,7 @@ use std::pin::Pin; use std::sync::Arc; use std::sync::atomic::AtomicU64; +use agent_client_protocol; use axum::body::Body; use axum::http::{Request, StatusCode}; use tower::ServiceExt as _; @@ -557,6 +558,11 @@ async fn discovery_returns_expected_json_fields() { ); assert_eq!(json["readiness"]["stdio_notification"], "zeph/ready"); assert_eq!(json["readiness"]["http_health_endpoint"], "/health"); + // protocol_version must be the integer value of ProtocolVersion::LATEST (1). + assert_eq!( + json["protocol_version"], + serde_json::json!(agent_client_protocol::ProtocolVersion::LATEST) + ); } #[tokio::test] diff --git a/crates/zeph-acp/tests/integration.rs b/crates/zeph-acp/tests/integration.rs index 51d9abf73..835f29417 100644 --- a/crates/zeph-acp/tests/integration.rs +++ b/crates/zeph-acp/tests/integration.rs @@ -613,3 +613,63 @@ async fn e2e_set_session_mode_rejects_unknown_session() { }) .await; } + +#[cfg(feature = "unstable-logout")] +#[tokio::test] +async fn initialize_advertises_logout_capability() { + let (client_stream, server_stream) = duplex(65536); + let (client_read, client_write) = tokio::io::split(client_stream); + let (server_read, server_write) = tokio::io::split(server_stream); + + let server_fut = serve_connection( + make_echo_spawner(), + make_server_config(), + server_write.compat_write(), + server_read.compat(), + ); + + let client_fut = async { + let local = tokio::task::LocalSet::new(); + local + .run_until(async { + let (client_conn, io_fut) = acp::ClientSideConnection::new( + NoopClient, + client_write.compat_write(), + client_read.compat(), + |fut| { + tokio::task::spawn_local(fut); + }, + ); + tokio::task::spawn_local(async move { + let _ = io_fut.await; + }); + + let resp = client_conn + .initialize(acp::InitializeRequest::new(acp::ProtocolVersion::LATEST)) + .await + .expect("initialize failed"); + + assert!( + resp.agent_capabilities.auth.logout.is_some(), + "logout capability must be advertised" + ); + }) + .await; + }; + + tokio::select! { + res = server_fut => { let _ = res; } + () = client_fut => {} + } +} + +#[cfg(feature = "unstable-logout")] +#[tokio::test] +async fn logout_returns_ok() { + with_initialized_client(|conn| async move { + conn.logout(acp::LogoutRequest::default()) + .await + .expect("logout must return Ok"); + }) + .await; +}