diff --git a/Cargo.lock b/Cargo.lock index a809b9b..cffa803 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1748,7 +1748,6 @@ name = "signal-gateway-app-code" version = "0.1.0" dependencies = [ "async-trait", - "conf", "flate2", "regex", "reqwest", @@ -1775,12 +1774,14 @@ dependencies = [ "serde", "serde_json", "signal-gateway", + "signal-gateway-app-code", "syslog_rfc5424", "tokio", "tokio-util", "toml", "tracing", "tracing-subscriber", + "url", ] [[package]] diff --git a/signal-gateway-app-code/Cargo.toml b/signal-gateway-app-code/Cargo.toml index 12451c4..fb37b66 100644 --- a/signal-gateway-app-code/Cargo.toml +++ b/signal-gateway-app-code/Cargo.toml @@ -10,10 +10,9 @@ workspace = true signal-gateway = { path = "../signal-gateway", default-features = false } async-trait = { workspace = true } -conf = { workspace = true } flate2 = { workspace = true } regex = { workspace = true } -reqwest = { workspace = true } +reqwest = { workspace = true, features = ["rustls-tls"] } serde = { workspace = true } serde_json = { workspace = true } tar = { workspace = true } diff --git a/signal-gateway-app-code/src/lib.rs b/signal-gateway-app-code/src/lib.rs index 6d318c7..8477506 100644 --- a/signal-gateway-app-code/src/lib.rs +++ b/signal-gateway-app-code/src/lib.rs @@ -4,7 +4,6 @@ //! tarballs from GitHub and caching them in memory. use async_trait::async_trait; -use conf::Conf; use flate2::read::GzDecoder; use regex::Regex; use serde::Deserialize; @@ -18,7 +17,8 @@ use tokio::sync::{Mutex, MutexGuard}; use tracing::{error, info, warn}; /// A GitHub repository identifier (owner/repo). -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Deserialize)] +#[serde(try_from = "String")] pub struct GitHubRepo { /// The repository owner (user or organization). pub owner: String, @@ -44,18 +44,22 @@ impl FromStr for GitHubRepo { } } +impl TryFrom for GitHubRepo { + type Error = String; + + fn try_from(s: String) -> Result { + s.parse() + } +} + /// Configuration for an application's source code access. -#[derive(Clone, Conf, Debug)] -#[conf(serde)] +#[derive(Clone, Debug, Deserialize)] pub struct AppCodeConfig { /// Name of the application (used to identify it in tool calls). - #[conf(long, env)] pub name: String, /// GitHub repository in "owner/repo" format. - #[conf(long, env, serde(use_value_parser))] pub github: GitHubRepo, /// Path to file containing the GitHub personal access token. - #[conf(long, env)] pub token_file: PathBuf, } diff --git a/signal-gateway-bin/Cargo.toml b/signal-gateway-bin/Cargo.toml index 4e20c57..af74fb7 100644 --- a/signal-gateway-bin/Cargo.toml +++ b/signal-gateway-bin/Cargo.toml @@ -17,6 +17,7 @@ rustls-tls = ["signal-gateway/rustls-tls"] [dependencies] signal-gateway = { path = "../signal-gateway" } +signal-gateway-app-code = { path = "../signal-gateway-app-code" } async-trait = { workspace = true } chrono = { workspace = true } @@ -26,6 +27,7 @@ dotenvy = { workspace = true } hyper = { workspace = true } hyper-util = { workspace = true } reqwest = { workspace = true } +url = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } syslog_rfc5424 = { workspace = true } diff --git a/signal-gateway-bin/src/app_code.rs b/signal-gateway-bin/src/app_code.rs new file mode 100644 index 0000000..477e3bd --- /dev/null +++ b/signal-gateway-bin/src/app_code.rs @@ -0,0 +1,72 @@ +//! Extended configuration for application source code browsing. + +use serde::Deserialize; +use signal_gateway_app_code::{AppCode, AppCodeConfig, ShaCallback}; +use std::sync::Arc; +use tracing::warn; +use url::Url; + +/// Extended configuration for AppCode with HTTP-based SHA fetching. +#[derive(Clone, Debug, Deserialize)] +pub struct AppCodeConfigExt { + /// The base AppCode configuration. + #[serde(flatten)] + pub config: AppCodeConfig, + /// URL to GET the current deployed version SHA. + pub version_sha_http_get: Url, +} + +impl AppCodeConfigExt { + /// Convert to an AppCode instance with HTTP-based SHA callback. + pub fn into_app_code(self) -> Result { + let url = self.version_sha_http_get.clone(); + let client = reqwest::Client::new(); + + let sha_callback: ShaCallback = Arc::new(move || { + let url = url.clone(); + let client = client.clone(); + Box::pin(async move { + let response = client + .get(url.as_str()) + .send() + .await + .map_err(|e| -> Box { + Box::new(std::io::Error::other(format!( + "HTTP request to {url} failed: {e}" + ))) + })?; + + if !response.status().is_success() { + return Err(Box::new(std::io::Error::other(format!( + "HTTP request to {url} returned {}", + response.status() + ))) as Box); + } + + let mut sha = response + .text() + .await + .map_err(|e| -> Box { + Box::new(std::io::Error::other(format!( + "Failed to read response from {url}: {e}" + ))) + })? + .trim() + .to_string(); + + // Handle -dirty suffix + if let Some(clean_sha) = sha.strip_suffix("-dirty") { + warn!( + "Version SHA has -dirty suffix, using clean SHA: {}", + clean_sha + ); + sha = clean_sha.to_string(); + } + + Ok(sha) + }) + }); + + AppCode::new(self.config, sha_callback) + } +} diff --git a/signal-gateway-bin/src/main.rs b/signal-gateway-bin/src/main.rs index ae7441d..0ee7f10 100644 --- a/signal-gateway-bin/src/main.rs +++ b/signal-gateway-bin/src/main.rs @@ -6,6 +6,7 @@ use conf::Conf; use hyper::service::service_fn; use hyper_util::{rt::TokioIo, server::conn::auto}; use signal_gateway::{CommandRouter, Gateway, GatewayConfig, Handling}; +use signal_gateway_app_code::AppCodeTools; use std::{env, fs, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration}; use tokio::net::TcpListener; use tokio_util::sync::CancellationToken; @@ -15,6 +16,9 @@ use tracing_subscriber::EnvFilter; mod admin_http; use admin_http::AdminHttpConfig; +mod app_code; +use app_code::AppCodeConfigExt; + mod syslog; use syslog::SyslogConfig; @@ -43,6 +47,9 @@ pub struct Config { /// Optional HTTP endpoint for forwarding admin messages. #[conf(flatten, prefix)] admin_http: Option, + /// Application source code configurations for Claude tools. + #[conf(long, env, value_parser = serde_json::from_str)] + app_code: Vec, #[conf(flatten, serde(flatten))] gateway: GatewayConfig, } @@ -123,11 +130,34 @@ async fn main() -> Result<(), Box> { } let command_router = router_builder.build(); - let gateway = Gateway::builder(config.gateway) + + // Build AppCode tools if configured + let app_code_tools = if !config.app_code.is_empty() { + let mut apps = Vec::new(); + for app_config in config.app_code { + let name = app_config.config.name.clone(); + match app_config.into_app_code() { + Ok(app) => apps.push(app), + Err(e) => { + error!("Failed to initialize app code '{name}': {e}"); + return Err(e.into()); + } + } + } + Some(Arc::new(AppCodeTools::new(apps))) + } else { + None + }; + + let mut gateway_builder = Gateway::builder(config.gateway) .with_cancellation_token(token.clone()) - .with_command_router(command_router) - .build() - .await; + .with_command_router(command_router); + + if let Some(tools) = app_code_tools { + gateway_builder = gateway_builder.with_tools(tools); + } + + let gateway = gateway_builder.build().await; let listener = TcpListener::bind(config.http_listen_addr).await.unwrap(); info!("Listening for http on {}", config.http_listen_addr);