Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5a88b40
refactor(utils): Remove dead code and structs that are no longer used
jcarver989 Oct 12, 2026
aff36ac
refactor(mcp-servers): remove dead subagent, task and skills code
jcarver989 Oct 12, 2026
9be9681
refactor(mcp-servers): remove dead coding-tool and lsp code
jcarver989 Oct 12, 2026
7047ec2
refactor(aether-lspd): remove dead language catalog api
jcarver989 Oct 12, 2026
2057fe1
refactor(wisp): remove dead code and unreachable branches
jcarver989 Oct 12, 2026
813058f
refactor(aether-cli): remove no-op code
jcarver989 Oct 12, 2026
436b219
refactor: remove dead code in core, evals and auth
jcarver989 Oct 13, 2026
adbe19c
refactor: share one TestWorkspace fixture across crates
jcarver989 Oct 13, 2026
0d8d237
refactor: share git test helpers through utils::testing
jcarver989 Oct 13, 2026
6b910c3
refactor: share one git command wrapper through utils::git
jcarver989 Oct 13, 2026
30923eb
refactor: report MCP gateway status as McpServerStatusEntry
jcarver989 Oct 13, 2026
568bc95
refactor: share to_meta / from_meta through utils::meta
jcarver989 Oct 13, 2026
0798d8d
refactor: derive Default for AgentSpec
jcarver989 Oct 13, 2026
1223044
refactor: add AgentEvent tool_call / tool_result / tool_error constru…
jcarver989 Oct 13, 2026
7295767
refactor: dedupe workspace status and file logging across wisp and cli
jcarver989 Oct 13, 2026
d2990b9
refactor(llm): expand Mantle endpoint templates with utils::Vars
jcarver989 Oct 13, 2026
0b31c8b
refactor(llm): build the Codex OAuth request with the oauth2 builder
jcarver989 Oct 13, 2026
5fb2a81
refactor(llm): lean on http, thiserror and the Bedrock SDK builders
jcarver989 Oct 13, 2026
5bb14cb
refactor(aether-auth): encrypt credentials with age::encrypt / decrypt
jcarver989 Oct 13, 2026
9bdf8e7
refactor(aether-cli): run the init wizard on ratatui::try_init
jcarver989 Oct 13, 2026
36d2915
refactor(aether-lspd): frame language server stdio with an LspCodec
jcarver989 Oct 13, 2026
9b58d8a
refactor(aether-lspd): lock the daemon pid file with File::try_lock
jcarver989 Oct 13, 2026
c91e1a3
refactor(aether-telemetry): propagate trace context through a HashMap
jcarver989 Oct 13, 2026
a83f71a
refactor: derive path-or-object settings through PathOrObject
jcarver989 Oct 13, 2026
e110e08
refactor(wisp): build styles from colors and tuples
jcarver989 Oct 13, 2026
185ce5d
refactor: replace hand-rolled helpers with std and dependency one-liners
jcarver989 Oct 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions crates/acp-utils/src/config_meta.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
use serde::{Deserialize, Serialize};
use utils::{ReasoningEffort, is_false};
use utils::ReasoningEffort;

use crate::meta::{from_meta, to_meta};
use utils::meta::{from_meta, to_meta};

type Meta = serde_json::Map<String, serde_json::Value>;

/// Meta for a top-level `SessionConfigOption` (e.g. the "model" config).
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConfigOptionMeta {
#[serde(default, skip_serializing_if = "is_false")]
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub multi_select: bool,
}

Expand All @@ -17,9 +17,9 @@ pub struct ConfigOptionMeta {
pub struct SelectOptionMeta {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub reasoning_levels: Vec<ReasoningEffort>,
#[serde(default, skip_serializing_if = "is_false")]
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub supports_image: bool,
#[serde(default, skip_serializing_if = "is_false")]
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub supports_audio: bool,
}

Expand Down
1 change: 0 additions & 1 deletion crates/acp-utils/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ pub mod content;
pub mod conversation;
#[cfg(not(target_family = "wasm"))]
pub mod elicitation;
pub mod meta;
pub mod notifications;

#[cfg(feature = "websocket")]
Expand Down
26 changes: 0 additions & 26 deletions crates/acp-utils/src/meta.rs

This file was deleted.

2 changes: 1 addition & 1 deletion crates/acp-utils/src/notifications.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
pub use utils::display_meta::{ToolDisplayMeta, ToolResultMeta};
pub use utils::mcp_status::{McpServerAuthCapability, McpServerStatus, McpServerStatusEntry};

use crate::meta::{from_meta, to_meta};
use utils::meta::{from_meta, to_meta};

pub const AETHER_META_NAMESPACE: &str = "contextbridge/aether";

Expand Down
2 changes: 1 addition & 1 deletion crates/aether-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ OAuth credential storage and authorization flows for Aether. Provides a pluggabl
- **`OAuthCredentialStorage`** -- Trait for persisting OAuth credentials keyed by provider ID, MCP server ID, or another credential key.
- **`OAuthCredential`** -- Access token, refresh token, client ID, and expiry timestamp for a single OAuth identity.
- **`OAuthHandler`** -- Trait implemented by consuming applications to drive the OAuth UI/UX (open a browser, wait for the redirect).
- **`BrowserOAuthHandler`** -- Default `OAuthHandler` that opens the system browser and listens on a dynamic local port.
- **`BrowserOAuthHandler`** -- Default `OAuthHandler` that opens the system browser and listens on a local port.
- **`OsKeyringStore`** -- `OAuthCredentialStorage` backed by the OS keychain (macOS Keychain, Windows Credential Manager, Linux/FreeBSD Secret Service). Available under the `keyring` feature.
- **`FakeOAuthCredentialStore`** -- In-memory `OAuthCredentialStorage` for tests.
- **`McpCredentialStore`** -- Per-server adapter that binds an `OAuthCredentialStorage` to one MCP server ID and implements `rmcp::transport::auth::CredentialStore`. Available under the `mcp` feature.
Expand Down
18 changes: 2 additions & 16 deletions crates/aether-auth/src/browser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,13 @@ use tokio::net::TcpListener;
use tokio::time::timeout;

/// Default `OAuthHandler` that opens the system browser and listens
/// for the OAuth callback on a dynamically-assigned local port.
/// for the OAuth callback on a local port.
pub struct BrowserOAuthHandler {
listener: TcpListener,
redirect_uri: String,
}

impl BrowserOAuthHandler {
pub fn new() -> Result<Self, std::io::Error> {
let std_listener = std::net::TcpListener::bind("127.0.0.1:0")?;
let port = std_listener.local_addr()?.port();
std_listener.set_nonblocking(true)?;
let listener = TcpListener::from_std(std_listener)?;
Ok(Self { listener, redirect_uri: format!("http://127.0.0.1:{port}/oauth2callback") })
}

/// Create a handler bound to a specific port with a custom redirect URI.
pub fn with_redirect_uri(redirect_uri: impl Into<String>, port: u16) -> Result<Self, std::io::Error> {
let std_listener = std::net::TcpListener::bind(format!("127.0.0.1:{port}"))?;
Expand Down Expand Up @@ -72,14 +64,8 @@ pub async fn accept_oauth_callback(listener: &TcpListener) -> Result<String, OAu
}
}

/// Start a local callback server and return the OAuth callback URL.
pub async fn wait_for_callback(port: u16) -> Result<String, OAuthError> {
let listener = TcpListener::bind(format!("127.0.0.1:{port}")).await?;
accept_oauth_callback(&listener).await
}

/// Open a URL in the default browser.
pub fn open_browser(url: &str) -> Result<(), OAuthError> {
fn open_browser(url: &str) -> Result<(), OAuthError> {
#[cfg(target_os = "macos")]
{
Command::new("open").arg(url).spawn().map_err(std::io::Error::other)?;
Expand Down
38 changes: 7 additions & 31 deletions crates/aether-auth/src/encrypted_file.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
use crate::credential::OAuthCredentialStorage;
use crate::error::OAuthError;
use age::scrypt::{Identity, Recipient as ScryptRecipient};
use age::scrypt::{Identity, Recipient};
use age::secrecy::SecretString;
use age::{Decryptor, Encryptor};
use async_trait::async_trait;
use dirs::home_dir;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::env::var;
use std::io::{Read, Write};
use std::iter::once;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Mutex;

Expand Down Expand Up @@ -52,39 +50,17 @@ impl EncryptedFileOAuthCredentialStorage {
}

fn encrypt(plaintext: &[u8], passphrase: &str, work_factor: Option<u8>) -> Result<Vec<u8>, OAuthError> {
let fail = |e| OAuthError::CredentialStore(format!("Encryption failed: {e}"));

let mut recipient = ScryptRecipient::new(SecretString::from(passphrase));
let mut recipient = Recipient::new(SecretString::from(passphrase));
if let Some(log_n) = work_factor {
recipient.set_work_factor(log_n);
}

let encryptor = Encryptor::with_recipients(once(&recipient as &dyn age::Recipient))
.map_err(|e| OAuthError::CredentialStore(format!("Encryption failed: {e}")))?;

let mut ciphertext = Vec::new();
let mut writer = encryptor.wrap_output(&mut ciphertext).map_err(fail)?;
writer.write_all(plaintext).map_err(fail)?;
writer.finish().map_err(fail)?;

Ok(ciphertext)
age::encrypt(&recipient, plaintext).map_err(|e| OAuthError::CredentialStore(format!("Encryption failed: {e}")))
}

fn decrypt(ciphertext: &[u8], passphrase: &str) -> Result<Vec<u8>, OAuthError> {
let decryptor = Decryptor::new(ciphertext)
.map_err(|e| OAuthError::CredentialStore(format!("Invalid encrypted file: {e}")))?;

let mut reader =
decryptor.decrypt(once(&Identity::new(SecretString::from(passphrase)) as &dyn age::Identity)).map_err(
|e| OAuthError::CredentialStore(format!("Decryption failed — wrong passphrase or corrupted file: {e}")),
)?;

let mut plaintext = Vec::new();
reader
.read_to_end(&mut plaintext)
.map_err(|e| OAuthError::CredentialStore(format!("Decryption failed: {e}")))?;

Ok(plaintext)
age::decrypt(&Identity::new(SecretString::from(passphrase)), ciphertext).map_err(|e| {
OAuthError::CredentialStore(format!("Decryption failed — wrong passphrase or corrupted file: {e}"))
})
}

fn load_store(&self) -> Result<CredentialStore, OAuthError> {
Expand Down
2 changes: 1 addition & 1 deletion crates/aether-auth/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pub mod error;
mod fake;
mod handler;

pub use browser::{BrowserOAuthHandler, accept_oauth_callback, open_browser, wait_for_callback};
pub use browser::{BrowserOAuthHandler, accept_oauth_callback};
pub use credential::{OAuthCredential, OAuthCredentialStorage, oauth_http_client};
pub use encrypted_file::EncryptedFileOAuthCredentialStorage;
pub use error::OAuthError;
Expand Down
2 changes: 1 addition & 1 deletion crates/aether-auth/src/oauth.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ OAuth 2.0 authentication primitives for the Aether agent framework.
# Architecture

- [`OAuthHandler`] -- Trait implemented by consuming applications to handle OAuth UI/UX. The handler opens a browser and returns the absolute callback URL.
- [`BrowserOAuthHandler`] -- Default implementation that opens the system browser and listens on a dynamic local port.
- [`BrowserOAuthHandler`] -- Default implementation that opens the system browser and listens on a local port.
- [`OAuthCredentialStorage`] -- Trait for persisting OAuth credentials keyed by provider ID, MCP server ID, or another credential key.
- [`OsKeyringStore`] -- OS-keychain-backed [`OAuthCredentialStorage`] (macOS Keychain, Windows Credential Manager, Linux/FreeBSD Secret Service). Available under the `keyring` feature.
- [`EncryptedFileOAuthCredentialStorage`] -- File-backed [`OAuthCredentialStorage`] that encrypts the file with [`age`](https://docs.rs/age). The passphrase is read from an environment variable.
Expand Down
3 changes: 1 addition & 2 deletions crates/aether-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ name = "aether"
path = "src/main.rs"

[features]
testing = ["acp_utils/testing", "mcp_utils/testing", "aether-core/testing"]
testing = ["acp_utils/testing", "mcp_utils/testing", "aether-core/testing", "utils/testing"]

[dependencies]
aether-auth = { path = "../aether-auth", version = "0.1.17", features = ["keyring"] }
Expand Down Expand Up @@ -61,7 +61,6 @@ clap = { workspace = true }
schemars = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
tracing-appender = { workspace = true }
futures = { workspace = true }
dirs = { workspace = true }
uuid = { workspace = true }
Expand Down
42 changes: 4 additions & 38 deletions crates/aether-cli/src/acp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,10 @@ use llm::{ProviderConnectionOverride, ProviderConnectionOverrides, ReasoningEffo
use std::collections::BTreeMap;
use std::env::current_dir;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::{
fs::create_dir_all,
path::{Path, PathBuf},
};
use thiserror::Error;
use tracing::{info, warn};
use tracing_appender::rolling::daily;
use tracing_subscriber::EnvFilter;

use aether_auth::OAuthError;
use aether_project::SettingsError;
Expand Down Expand Up @@ -81,31 +76,16 @@ pub struct AcpArgs {
#[serde(rename_all = "camelCase", deny_unknown_fields)]
#[schemars(rename = "AetherAcpOptions")]
pub struct AcpOptions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub log_dir: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub providers: Option<BTreeMap<String, ProviderConnectionOverride>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub settings: Option<AetherSettings>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub settings_file: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<ReasoningEffort>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trace_context: Option<AgentTraceContext>,
}

/// Outcome of running the ACP server successfully.
#[derive(Debug)]
pub enum AcpRunOutcome {
/// The client disconnected cleanly (e.g. EOF on stdin).
CleanDisconnect,
}

/// Errors that terminate the ACP server run.
#[derive(Debug, Error)]
pub enum AcpRunError {
Expand Down Expand Up @@ -145,18 +125,14 @@ pub enum AcpOptionsJsonError {
UnsupportedReasoningEffort(#[from] ReasoningEffortError),
}

pub async fn run_acp(args: AcpArgs) -> Result<AcpRunOutcome, AcpRunError> {
pub async fn run_acp(args: AcpArgs) -> Result<(), AcpRunError> {
info!("Starting Aether ACP server");

let cwd = current_dir().unwrap_or_else(|_| PathBuf::from("."));
let state = Arc::new(create_acp_state(args, &cwd, SessionHooks::default())?);
let connect_result = state.serve(Stdio::new(), state.stop_token()).await;
state.shutdown_all().await;

match connect_result {
Ok(()) => Ok(AcpRunOutcome::CleanDisconnect),
Err(err) => Err(AcpRunError::Protocol(err)),
}
connect_result.map_err(AcpRunError::Protocol)
}

#[derive(Debug)]
Expand Down Expand Up @@ -222,7 +198,7 @@ impl AcpRunConfig {

fn create_acp_state(args: AcpArgs, cwd: &Path, hooks: SessionHooks) -> Result<AcpState, AcpRunError> {
let config = AcpRunConfig::from_args(args)?;
setup_logging(&config.log_dir);
wisp::init_file_logging(&config.log_dir, "aether-acp.log", "warn");

let initial_selection = if let Some(agent) = config.agent.clone() {
InitialSessionSelection::agent(agent)
Expand Down Expand Up @@ -256,16 +232,6 @@ fn create_acp_state(args: AcpArgs, cwd: &Path, hooks: SessionHooks) -> Result<Ac
}))
}

fn setup_logging(log_dir: &Path) {
create_dir_all(log_dir).ok();
tracing_subscriber::fmt()
.with_writer(daily(log_dir, "aether-acp.log"))
.with_ansi(false) // No ANSI colors in log files
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("warn")))
.pretty()
.init();
}

fn default_log_dir() -> PathBuf {
PathBuf::from("/tmp/aether-acp-logs")
}
Expand Down
9 changes: 1 addition & 8 deletions crates/aether-cli/src/acp/protocol/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,13 +165,6 @@ pub fn map_agent_event_to_notification(msg: &AgentEvent, mode: NotificationMode)
}
}

fn json_patch_value(value: serde_json::Value) -> MaybeUndefined<serde_json::Value> {
match value {
serde_json::Value::Null => MaybeUndefined::Null,
value => MaybeUndefined::Value(value),
}
}

fn task_status_to_acp(status: &str) -> ToolCallStatus {
match status {
"working" => ToolCallStatus::InProgress,
Expand Down Expand Up @@ -238,7 +231,7 @@ fn tool_call_upsert(id: &str, name: &str) -> ToolCallUpdate {
}

fn map_tool_call_to_notification(request: &ToolCallRequest) -> SessionUpdate {
let raw_input = serde_json::from_str(&request.arguments).map_or(MaybeUndefined::Undefined, json_patch_value);
let raw_input = MaybeUndefined::from(serde_json::from_str::<Option<serde_json::Value>>(&request.arguments).ok());
SessionUpdate::ToolCallUpdate(tool_call_upsert(&request.id, &request.name).raw_input(raw_input))
}

Expand Down
Loading
Loading