mirror of
https://github.com/block/goose.git
synced 2026-07-09 16:09:22 +00:00
Support TLS for ACP serve (#10088)
Co-authored-by: Douwe M Osinga <douwe@sidewalklabs.com> Co-authored-by: Lifei Zhou <lifei@squareup.com>
This commit is contained in:
parent
2f32070975
commit
c8971fe5fb
30 changed files with 423 additions and 244 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -4875,10 +4875,12 @@ dependencies = [
|
|||
"async-stream",
|
||||
"async-trait",
|
||||
"aws-config",
|
||||
"aws-lc-rs",
|
||||
"aws-sdk-bedrockruntime",
|
||||
"aws-sdk-sagemakerruntime",
|
||||
"aws-smithy-types",
|
||||
"axum",
|
||||
"axum-server",
|
||||
"base64 0.22.1",
|
||||
"blake3",
|
||||
"byteorder",
|
||||
|
|
@ -4931,6 +4933,7 @@ dependencies = [
|
|||
"nostr-sdk",
|
||||
"oauth2",
|
||||
"once_cell",
|
||||
"openssl",
|
||||
"opentelemetry 0.32.0",
|
||||
"opentelemetry-appender-tracing",
|
||||
"opentelemetry-otlp 0.32.0",
|
||||
|
|
@ -4938,10 +4941,12 @@ dependencies = [
|
|||
"opentelemetry_sdk 0.32.1",
|
||||
"pastey",
|
||||
"pctx_code_mode",
|
||||
"pem",
|
||||
"process-wrap",
|
||||
"pulldown-cmark",
|
||||
"rand 0.10.1",
|
||||
"rayon",
|
||||
"rcgen",
|
||||
"regex",
|
||||
"reqwest 0.13.4",
|
||||
"rmcp",
|
||||
|
|
@ -5017,6 +5022,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"async-trait",
|
||||
"axum",
|
||||
"axum-server",
|
||||
"base64 0.22.1",
|
||||
"bat",
|
||||
"bzip2",
|
||||
|
|
@ -5158,7 +5164,6 @@ name = "goose-server"
|
|||
version = "1.40.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"aws-lc-rs",
|
||||
"axum",
|
||||
"axum-server",
|
||||
"base64 0.22.1",
|
||||
|
|
@ -5172,10 +5177,7 @@ dependencies = [
|
|||
"goose-providers",
|
||||
"hex",
|
||||
"http 1.4.2",
|
||||
"openssl",
|
||||
"pem",
|
||||
"rand 0.10.1",
|
||||
"rcgen",
|
||||
"reqwest 0.13.4",
|
||||
"rmcp",
|
||||
"rustls",
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ comfy-table = { version = "7", default-features = false }
|
|||
sha2 = { workspace = true }
|
||||
sigstore-verify = { version = "0.9", default-features = false, optional = true }
|
||||
axum.workspace = true
|
||||
axum-server = { version = "0.8", default-features = false, optional = true }
|
||||
clap_complete_nushell = { version = "4", default-features = false }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
|
|
@ -99,14 +100,18 @@ portable-default = ["rustls-tls", "aws-providers", "telemetry", "otel", "tui"]
|
|||
# disables the update command
|
||||
disable-update = []
|
||||
rustls-tls = [
|
||||
"dep:axum-server",
|
||||
"reqwest/rustls",
|
||||
"axum-server/tls-rustls",
|
||||
"sigstore-verify?/rustls",
|
||||
"goose/rustls-tls",
|
||||
"goose-mcp/rustls-tls",
|
||||
"goose-providers/rustls-tls",
|
||||
]
|
||||
native-tls = [
|
||||
"dep:axum-server",
|
||||
"reqwest/native-tls",
|
||||
"axum-server/tls-openssl",
|
||||
"sigstore-verify?/native-tls",
|
||||
"goose/native-tls",
|
||||
"goose-mcp/native-tls",
|
||||
|
|
|
|||
|
|
@ -831,6 +831,15 @@ enum Command {
|
|||
#[arg(long, default_value = "3284")]
|
||||
port: u16,
|
||||
|
||||
#[arg(long, help = "Serve ACP over TLS")]
|
||||
tls: bool,
|
||||
|
||||
#[arg(long = "tls-cert-path", value_name = "PATH")]
|
||||
tls_cert_path: Option<String>,
|
||||
|
||||
#[arg(long = "tls-key-path", value_name = "PATH")]
|
||||
tls_key_path: Option<String>,
|
||||
|
||||
#[arg(
|
||||
long = "with-builtin",
|
||||
value_name = "NAME",
|
||||
|
|
@ -1333,7 +1342,14 @@ async fn handle_mcp_command(server: McpCommand) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
async fn handle_serve_command(host: String, port: u16, builtins: Vec<String>) -> Result<()> {
|
||||
async fn handle_serve_command(
|
||||
host: String,
|
||||
port: u16,
|
||||
tls: bool,
|
||||
tls_cert_path: Option<String>,
|
||||
tls_key_path: Option<String>,
|
||||
builtins: Vec<String>,
|
||||
) -> Result<()> {
|
||||
use goose::acp::server_factory::{AcpServer, AcpServerFactoryConfig};
|
||||
use goose::acp::transport::create_router;
|
||||
use goose::config::paths::Paths;
|
||||
|
|
@ -1380,15 +1396,55 @@ async fn handle_serve_command(host: String, port: u16, builtins: Vec<String>) ->
|
|||
let secret_key = env_secret.unwrap_or_else(generate_serve_secret_key);
|
||||
let router = create_router(server, secret_key, require_token);
|
||||
|
||||
let addr: SocketAddr = format!("{}:{}", host, port).parse()?;
|
||||
info!("Starting ACP server on {}", addr);
|
||||
let config = Config::global();
|
||||
let tls_cert_path =
|
||||
tls_cert_path.or_else(|| config.get_param::<String>("GOOSE_TLS_CERT_PATH").ok());
|
||||
let tls_key_path =
|
||||
tls_key_path.or_else(|| config.get_param::<String>("GOOSE_TLS_KEY_PATH").ok());
|
||||
let tls = tls
|
||||
|| config.get_param::<bool>("GOOSE_TLS").unwrap_or(false)
|
||||
|| tls_cert_path.is_some()
|
||||
|| tls_key_path.is_some();
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
axum::serve(
|
||||
listener,
|
||||
router.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.await?;
|
||||
let addr: SocketAddr = format!("{}:{}", host, port).parse()?;
|
||||
if tls {
|
||||
#[cfg(any(feature = "rustls-tls", feature = "native-tls"))]
|
||||
{
|
||||
let tls_setup = goose::acp::transport::tls::setup_tls(
|
||||
tls_cert_path.as_deref(),
|
||||
tls_key_path.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
info!("Starting ACP server on https://{}", addr);
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
axum_server::bind_rustls(addr, tls_setup.config)
|
||||
.serve(router.into_make_service_with_connect_info::<SocketAddr>())
|
||||
.await?;
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
axum_server::bind_openssl(addr, tls_setup.config)
|
||||
.serve(router.into_make_service_with_connect_info::<SocketAddr>())
|
||||
.await?;
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "rustls-tls", feature = "native-tls")))]
|
||||
{
|
||||
let _ = (tls_cert_path, tls_key_path);
|
||||
anyhow::bail!(
|
||||
"TLS was requested but no TLS backend is enabled. \
|
||||
Enable the `rustls-tls` or `native-tls` feature."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
info!("Starting ACP server on http://{}", addr);
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
axum::serve(
|
||||
listener,
|
||||
router.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -2099,8 +2155,11 @@ pub async fn cli() -> anyhow::Result<()> {
|
|||
Some(Command::Serve {
|
||||
host,
|
||||
port,
|
||||
tls,
|
||||
tls_cert_path,
|
||||
tls_key_path,
|
||||
builtins,
|
||||
}) => handle_serve_command(host, port, builtins).await,
|
||||
}) => handle_serve_command(host, port, tls, tls_cert_path, tls_key_path, builtins).await,
|
||||
Some(Command::Session {
|
||||
command: Some(cmd), ..
|
||||
}) => handle_session_subcommand(cmd).await,
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@ rustls-tls = [
|
|||
"reqwest/rustls",
|
||||
"axum-server/tls-rustls",
|
||||
"dep:rustls",
|
||||
"dep:aws-lc-rs",
|
||||
"goose/rustls-tls",
|
||||
"goose-mcp/rustls-tls",
|
||||
"goose-providers/rustls-tls",
|
||||
|
|
@ -45,7 +44,6 @@ rustls-tls = [
|
|||
native-tls = [
|
||||
"reqwest/native-tls",
|
||||
"axum-server/tls-openssl",
|
||||
"dep:openssl",
|
||||
"goose/native-tls",
|
||||
"goose-mcp/native-tls",
|
||||
"goose-providers/native-tls",
|
||||
|
|
@ -82,11 +80,7 @@ rand = { workspace = true }
|
|||
hex = { version = "0.4.3", default-features = false, features = ["std"] }
|
||||
rustls = { workspace = true, optional = true }
|
||||
uuid = { workspace = true }
|
||||
rcgen = { version = "0.14", default-features = false, features = ["aws_lc_rs", "crypto", "pem"] }
|
||||
axum-server = { version = "0.8", default-features = false }
|
||||
aws-lc-rs = { version = "1.17", default-features = false, optional = true }
|
||||
openssl = { version = "0.10.66", default-features = false, optional = true }
|
||||
pem = { version = "3.0.2", default-features = false, features = ["std"] }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
winreg = { version = "0.56", default-features = false }
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@ use goose::acp::transport::create_acp_router;
|
|||
use goose::agents::GoosePlatform;
|
||||
use goose::config::paths::Paths;
|
||||
use goose_server::auth::{check_acp_token, check_token};
|
||||
#[cfg(any(feature = "rustls-tls", feature = "native-tls"))]
|
||||
use goose_server::tls::setup_tls;
|
||||
use std::sync::Arc;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use tracing::info;
|
||||
|
|
@ -90,7 +88,7 @@ pub async fn run() -> Result<()> {
|
|||
#[cfg(any(feature = "rustls-tls", feature = "native-tls"))]
|
||||
{
|
||||
boot_marker("tls setup start");
|
||||
let tls_setup = setup_tls(
|
||||
let tls_setup = goose::acp::transport::tls::setup_tls(
|
||||
settings.tls_cert_path.as_deref(),
|
||||
settings.tls_key_path.as_deref(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,210 +1,4 @@
|
|||
//! TLS configuration for the goose server.
|
||||
//!
|
||||
//! Two TLS backends are supported for the HTTPS listener via `axum-server`:
|
||||
//!
|
||||
//! - **`rustls-tls`** (enabled by default) – uses `axum-server/tls-rustls` with
|
||||
//! the `aws-lc-rs` crypto provider.
|
||||
//! - **`native-tls`** – uses `axum-server/tls-openssl`, which links against the
|
||||
//! platform's OpenSSL (or a compatible fork such as LibreSSL / BoringSSL).
|
||||
//! On Linux this *is* the platform-native TLS stack; on macOS/Windows the
|
||||
//! `native-tls` crate used by the HTTP *client* delegates to Security.framework
|
||||
//! / SChannel respectively, but `axum-server` does not offer those backends so
|
||||
//! the server listener always uses OpenSSL when this feature is active.
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use goose::config::paths::Paths;
|
||||
use rcgen::{CertificateParams, DnType, KeyPair, SanType};
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
pub type TlsConfig = axum_server::tls_rustls::RustlsConfig;
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
pub type TlsConfig = axum_server::tls_openssl::OpenSSLConfig;
|
||||
|
||||
pub struct TlsSetup {
|
||||
pub config: TlsConfig,
|
||||
pub fingerprint: String,
|
||||
}
|
||||
|
||||
fn generate_self_signed_cert() -> Result<(rcgen::Certificate, KeyPair)> {
|
||||
let mut params = CertificateParams::default();
|
||||
params
|
||||
.distinguished_name
|
||||
.push(DnType::CommonName, "goosed localhost");
|
||||
params.subject_alt_names = vec![
|
||||
SanType::IpAddress(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)),
|
||||
SanType::DnsName("localhost".try_into()?),
|
||||
];
|
||||
|
||||
let key_pair = KeyPair::generate()?;
|
||||
let cert = params.self_signed(&key_pair)?;
|
||||
Ok((cert, key_pair))
|
||||
}
|
||||
|
||||
fn sha256_fingerprint(der: &[u8]) -> String {
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
{
|
||||
let sha256 = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, der);
|
||||
sha256
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|b| format!("{b:02X}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(":")
|
||||
}
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
{
|
||||
use openssl::hash::MessageDigest;
|
||||
let digest =
|
||||
openssl::hash::hash(MessageDigest::sha256(), der).expect("SHA-256 hash failed");
|
||||
digest
|
||||
.iter()
|
||||
.map(|b| format!("{b:02X}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(":")
|
||||
}
|
||||
}
|
||||
|
||||
/// Load TLS configuration from user-provided PEM certificate and key files.
|
||||
///
|
||||
/// The SHA-256 fingerprint of the leaf certificate is computed and printed to
|
||||
/// stdout so the parent process (e.g. Electron) can pin it, just like the
|
||||
/// self-signed path.
|
||||
pub async fn from_pem_files(cert_path: &Path, key_path: &Path) -> Result<TlsSetup> {
|
||||
let cert_pem = std::fs::read(cert_path)?;
|
||||
let key_pem = std::fs::read(key_path)?;
|
||||
|
||||
// Parse the first PEM block to extract the DER-encoded certificate for fingerprinting.
|
||||
let der = pem::parse(&cert_pem)?.into_contents();
|
||||
let fingerprint = sha256_fingerprint(&der);
|
||||
println!("GOOSED_CERT_FINGERPRINT={fingerprint}");
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
let config = {
|
||||
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||
axum_server::tls_rustls::RustlsConfig::from_pem(cert_pem, key_pem.clone()).await?
|
||||
};
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
let config = axum_server::tls_openssl::OpenSSLConfig::from_pem(&cert_pem, &key_pem)?;
|
||||
|
||||
Ok(TlsSetup {
|
||||
config,
|
||||
fingerprint,
|
||||
})
|
||||
}
|
||||
|
||||
/// Set up TLS, using user-provided PEM files if both paths are given,
|
||||
/// otherwise generating a self-signed certificate.
|
||||
pub async fn setup_tls(cert_path: Option<&str>, key_path: Option<&str>) -> Result<TlsSetup> {
|
||||
match (cert_path, key_path) {
|
||||
(Some(cert), Some(key)) => from_pem_files(Path::new(cert), Path::new(key)).await,
|
||||
(None, None) => self_signed_config().await,
|
||||
_ => bail!("Both GOOSE_TLS_CERT_PATH and GOOSE_TLS_KEY_PATH must be set, or neither"),
|
||||
}
|
||||
}
|
||||
|
||||
fn tls_cache_dir() -> std::path::PathBuf {
|
||||
Paths::config_dir().join("tls")
|
||||
}
|
||||
|
||||
fn write_private_key(path: &std::path::Path, contents: &[u8]) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
let result = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.mode(0o600)
|
||||
.open(path);
|
||||
if let Ok(mut file) = result {
|
||||
let _ = file.write_all(contents);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = std::fs::write(path, contents);
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_cached_tls() -> Option<TlsSetup> {
|
||||
let dir = tls_cache_dir();
|
||||
let cert_pem = std::fs::read(dir.join("server.pem")).ok()?;
|
||||
let key_pem = std::fs::read(dir.join("server.key")).ok()?;
|
||||
|
||||
let der = pem::parse(&cert_pem).ok()?.into_contents();
|
||||
let fingerprint = sha256_fingerprint(&der);
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
let config = axum_server::tls_rustls::RustlsConfig::from_pem(cert_pem, key_pem)
|
||||
.await
|
||||
.ok()?;
|
||||
#[cfg(feature = "native-tls")]
|
||||
let config = axum_server::tls_openssl::OpenSSLConfig::from_pem(&cert_pem, &key_pem).ok()?;
|
||||
|
||||
Some(TlsSetup {
|
||||
config,
|
||||
fingerprint,
|
||||
})
|
||||
}
|
||||
|
||||
/// All errors are silently ignored — this is a best-effort optimisation and
|
||||
/// must never prevent the server from starting.
|
||||
fn save_tls_to_cache(cert_pem: &str, key_pem: &str) {
|
||||
let dir = tls_cache_dir();
|
||||
if std::fs::create_dir_all(&dir).is_err() {
|
||||
return;
|
||||
}
|
||||
let _ = std::fs::write(dir.join("server.pem"), cert_pem);
|
||||
write_private_key(&dir.join("server.key"), key_pem.as_bytes());
|
||||
}
|
||||
|
||||
/// Generate a self-signed TLS certificate for localhost (127.0.0.1) and
|
||||
/// return a [`TlsSetup`] containing the server config and the SHA-256
|
||||
/// fingerprint of the generated certificate (colon-separated hex).
|
||||
///
|
||||
/// The fingerprint is printed to stdout so the parent process (e.g. Electron)
|
||||
/// can pin it and reject connections from any other certificate.
|
||||
pub async fn self_signed_config() -> Result<TlsSetup> {
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||
|
||||
// Fast path: reuse a previously cached certificate if one exists.
|
||||
if let Some(cached) = load_cached_tls().await {
|
||||
println!("GOOSED_CERT_FINGERPRINT={}", cached.fingerprint);
|
||||
return Ok(cached);
|
||||
}
|
||||
|
||||
let (cert, key_pair) = generate_self_signed_cert()?;
|
||||
|
||||
let fingerprint = sha256_fingerprint(cert.der());
|
||||
println!("GOOSED_CERT_FINGERPRINT={fingerprint}");
|
||||
|
||||
let cert_pem = cert.pem();
|
||||
let key_pem = key_pair.serialize_pem();
|
||||
|
||||
// Persist for future restarts before moving the strings into the config.
|
||||
save_tls_to_cache(&cert_pem, &key_pem);
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
let config = axum_server::tls_rustls::RustlsConfig::from_pem(
|
||||
cert_pem.into_bytes(),
|
||||
key_pem.into_bytes(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
let config =
|
||||
axum_server::tls_openssl::OpenSSLConfig::from_pem(cert_pem.as_bytes(), key_pem.as_bytes())?;
|
||||
|
||||
Ok(TlsSetup {
|
||||
config,
|
||||
fingerprint,
|
||||
})
|
||||
}
|
||||
#[cfg(any(feature = "rustls-tls", feature = "native-tls"))]
|
||||
pub use goose::acp::transport::tls::{
|
||||
from_pem_files, self_signed_config, setup_tls, TlsConfig, TlsSetup,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -44,6 +44,11 @@ vulkan = ["local-inference", "llama-cpp-2/vulkan"]
|
|||
mlx = ["local-inference", "dep:mlx-rs", "dep:mlx-lm", "dep:mlx-lm-utils"]
|
||||
rustls-tls = [
|
||||
"dep:rustls",
|
||||
"dep:rcgen",
|
||||
"dep:pem",
|
||||
"dep:axum-server",
|
||||
"dep:aws-lc-rs",
|
||||
"axum-server/tls-rustls",
|
||||
"reqwest/rustls",
|
||||
"rmcp/reqwest",
|
||||
"smithy-transport-reqwest?/rustls",
|
||||
|
|
@ -55,6 +60,11 @@ rustls-tls = [
|
|||
"goose-providers/rustls-tls",
|
||||
]
|
||||
native-tls = [
|
||||
"dep:rcgen",
|
||||
"dep:pem",
|
||||
"dep:axum-server",
|
||||
"dep:openssl",
|
||||
"axum-server/tls-openssl",
|
||||
"reqwest/native-tls",
|
||||
"rmcp/reqwest-native-tls",
|
||||
"smithy-transport-reqwest?/native-tls",
|
||||
|
|
@ -203,6 +213,11 @@ process-wrap = { version = "9", default-features = false, features = ["std"] }
|
|||
nostr = { version = "0.44", default-features = false, features = ["nip44", "std"], optional = true }
|
||||
nostr-sdk = { version = "0.44", default-features = false, features = ["nip44"], optional = true }
|
||||
rustls = { workspace = true, optional = true }
|
||||
rcgen = { version = "0.14", default-features = false, features = ["aws_lc_rs", "crypto", "pem"], optional = true }
|
||||
axum-server = { version = "0.8", default-features = false, optional = true }
|
||||
aws-lc-rs = { version = "1.17", default-features = false, optional = true }
|
||||
openssl = { version = "0.10.66", default-features = false, optional = true }
|
||||
pem = { version = "3.0.2", default-features = false, features = ["std"], optional = true }
|
||||
hf-hub = { version = "1.0.0-rc.1", default-features = false, optional = true }
|
||||
mlx-rs = { version = "0.25.3", default-features = false, features = ["accelerate", "metal", "safetensors"], optional = true }
|
||||
mlx-lm = { git = "https://github.com/jh-block/mlx-lm", optional = true }
|
||||
|
|
|
|||
|
|
@ -8,14 +8,25 @@ impl GooseAcpAgent {
|
|||
method: &str,
|
||||
params: serde_json::Value,
|
||||
) -> Result<serde_json::Value, agent_client_protocol::Error> {
|
||||
if <SaveRecipeRequest as agent_client_protocol::JsonRpcMessage>::matches_method(method) {
|
||||
let req = recipe::deserialize_save_recipe_request(params)?;
|
||||
let result = self.on_save_recipe(req).await?;
|
||||
return serde_json::to_value(&result)
|
||||
.map_err(|e| agent_client_protocol::Error::internal_error().data(e.to_string()));
|
||||
let result = async {
|
||||
if <SaveRecipeRequest as agent_client_protocol::JsonRpcMessage>::matches_method(method)
|
||||
{
|
||||
let req = recipe::deserialize_save_recipe_request(params)?;
|
||||
let result = self.on_save_recipe(req).await?;
|
||||
return serde_json::to_value(&result).map_err(|e| {
|
||||
agent_client_protocol::Error::internal_error().data(e.to_string())
|
||||
});
|
||||
}
|
||||
|
||||
self.handle_custom_request(method, params).await
|
||||
}
|
||||
.await;
|
||||
|
||||
if let Err(error) = &result {
|
||||
tracing::error!(method, error = ?error, "ACP custom request failed");
|
||||
}
|
||||
|
||||
self.handle_custom_request(method, params).await
|
||||
result
|
||||
}
|
||||
|
||||
#[custom_method(AddSessionExtensionRequest)]
|
||||
|
|
|
|||
|
|
@ -56,11 +56,17 @@ impl HandleDispatchFrom<Client> for GooseAcpHandler {
|
|||
let agent = agent.clone();
|
||||
let cx_clone = cx.clone();
|
||||
cx.spawn(async move {
|
||||
let session_id = req.session_id.0.to_string();
|
||||
match agent.on_load_session(&cx_clone, req).await {
|
||||
Ok(response) => {
|
||||
responder.respond(response)?;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!(
|
||||
session_id = %session_id,
|
||||
error = ?e,
|
||||
"ACP load_session failed"
|
||||
);
|
||||
responder.respond_with_error(e)?;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
pub mod auth;
|
||||
#[cfg(any(feature = "rustls-tls", feature = "native-tls"))]
|
||||
pub mod tls;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
|
|
|
|||
179
crates/goose/src/acp/transport/tls.rs
Normal file
179
crates/goose/src/acp/transport/tls.rs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
use crate::config::paths::Paths;
|
||||
use anyhow::{bail, Result};
|
||||
use rcgen::{CertificateParams, DnType, KeyPair, SanType};
|
||||
use std::path::Path;
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
pub type TlsConfig = axum_server::tls_rustls::RustlsConfig;
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
pub type TlsConfig = axum_server::tls_openssl::OpenSSLConfig;
|
||||
|
||||
pub struct TlsSetup {
|
||||
pub config: TlsConfig,
|
||||
pub fingerprint: String,
|
||||
}
|
||||
|
||||
fn generate_self_signed_cert() -> Result<(rcgen::Certificate, KeyPair)> {
|
||||
let mut params = CertificateParams::default();
|
||||
params
|
||||
.distinguished_name
|
||||
.push(DnType::CommonName, "goosed localhost");
|
||||
params.subject_alt_names = vec![
|
||||
SanType::IpAddress(std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)),
|
||||
SanType::DnsName("localhost".try_into()?),
|
||||
];
|
||||
|
||||
let key_pair = KeyPair::generate()?;
|
||||
let cert = params.self_signed(&key_pair)?;
|
||||
Ok((cert, key_pair))
|
||||
}
|
||||
|
||||
fn sha256_fingerprint(der: &[u8]) -> String {
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
{
|
||||
let sha256 = aws_lc_rs::digest::digest(&aws_lc_rs::digest::SHA256, der);
|
||||
sha256
|
||||
.as_ref()
|
||||
.iter()
|
||||
.map(|b| format!("{b:02X}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(":")
|
||||
}
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
{
|
||||
use openssl::hash::MessageDigest;
|
||||
let digest =
|
||||
openssl::hash::hash(MessageDigest::sha256(), der).expect("SHA-256 hash failed");
|
||||
digest
|
||||
.iter()
|
||||
.map(|b| format!("{b:02X}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(":")
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn from_pem_files(cert_path: &Path, key_path: &Path) -> Result<TlsSetup> {
|
||||
let cert_pem = std::fs::read(cert_path)?;
|
||||
let key_pem = std::fs::read(key_path)?;
|
||||
|
||||
let der = pem::parse(&cert_pem)?.into_contents();
|
||||
let fingerprint = sha256_fingerprint(&der);
|
||||
println!("GOOSED_CERT_FINGERPRINT={fingerprint}");
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
let config = {
|
||||
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||
axum_server::tls_rustls::RustlsConfig::from_pem(cert_pem, key_pem.clone()).await?
|
||||
};
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
let config = axum_server::tls_openssl::OpenSSLConfig::from_pem(&cert_pem, &key_pem)?;
|
||||
|
||||
Ok(TlsSetup {
|
||||
config,
|
||||
fingerprint,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn setup_tls(cert_path: Option<&str>, key_path: Option<&str>) -> Result<TlsSetup> {
|
||||
match (cert_path, key_path) {
|
||||
(Some(cert), Some(key)) => from_pem_files(Path::new(cert), Path::new(key)).await,
|
||||
(None, None) => self_signed_config().await,
|
||||
_ => bail!("Both GOOSE_TLS_CERT_PATH and GOOSE_TLS_KEY_PATH must be set, or neither"),
|
||||
}
|
||||
}
|
||||
|
||||
fn tls_cache_dir() -> std::path::PathBuf {
|
||||
Paths::config_dir().join("tls")
|
||||
}
|
||||
|
||||
fn write_private_key(path: &std::path::Path, contents: &[u8]) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::io::Write;
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
|
||||
let result = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.mode(0o600)
|
||||
.open(path);
|
||||
if let Ok(mut file) = result {
|
||||
let _ = file.write_all(contents);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
let _ = std::fs::write(path, contents);
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_cached_tls() -> Option<TlsSetup> {
|
||||
let dir = tls_cache_dir();
|
||||
let cert_pem = std::fs::read(dir.join("server.pem")).ok()?;
|
||||
let key_pem = std::fs::read(dir.join("server.key")).ok()?;
|
||||
|
||||
let der = pem::parse(&cert_pem).ok()?.into_contents();
|
||||
let fingerprint = sha256_fingerprint(&der);
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
let config = axum_server::tls_rustls::RustlsConfig::from_pem(cert_pem, key_pem)
|
||||
.await
|
||||
.ok()?;
|
||||
#[cfg(feature = "native-tls")]
|
||||
let config = axum_server::tls_openssl::OpenSSLConfig::from_pem(&cert_pem, &key_pem).ok()?;
|
||||
|
||||
Some(TlsSetup {
|
||||
config,
|
||||
fingerprint,
|
||||
})
|
||||
}
|
||||
|
||||
fn try_save_tls_to_cache(cert_pem: &str, key_pem: &str) {
|
||||
let dir = tls_cache_dir();
|
||||
if std::fs::create_dir_all(&dir).is_err() {
|
||||
return;
|
||||
}
|
||||
let _ = std::fs::write(dir.join("server.pem"), cert_pem);
|
||||
write_private_key(&dir.join("server.key"), key_pem.as_bytes());
|
||||
}
|
||||
|
||||
pub async fn self_signed_config() -> Result<TlsSetup> {
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
|
||||
|
||||
if let Some(cached) = load_cached_tls().await {
|
||||
println!("GOOSED_CERT_FINGERPRINT={}", cached.fingerprint);
|
||||
return Ok(cached);
|
||||
}
|
||||
|
||||
let (cert, key_pair) = generate_self_signed_cert()?;
|
||||
|
||||
let fingerprint = sha256_fingerprint(cert.der());
|
||||
println!("GOOSED_CERT_FINGERPRINT={fingerprint}");
|
||||
|
||||
let cert_pem = cert.pem();
|
||||
let key_pem = key_pair.serialize_pem();
|
||||
|
||||
try_save_tls_to_cache(&cert_pem, &key_pem);
|
||||
|
||||
#[cfg(feature = "rustls-tls")]
|
||||
let config = axum_server::tls_rustls::RustlsConfig::from_pem(
|
||||
cert_pem.into_bytes(),
|
||||
key_pem.into_bytes(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
#[cfg(feature = "native-tls")]
|
||||
let config =
|
||||
axum_server::tls_openssl::OpenSSLConfig::from_pem(cert_pem.as_bytes(), key_pem.as_bytes())?;
|
||||
|
||||
Ok(TlsSetup {
|
||||
config,
|
||||
fingerprint,
|
||||
})
|
||||
}
|
||||
|
|
@ -145,6 +145,7 @@ async function loadSession(sessionId: string, options: AcpLoadSessionOptions = {
|
|||
acpChatSessionActions.finishSessionLoad(sessionId, sessionInfoToSession(sessionInfo, meta));
|
||||
options.onSessionLoaded?.();
|
||||
} catch (error) {
|
||||
console.error('Failed to load ACP session:', error);
|
||||
acpChatSessionActions.failSessionLoad(sessionId, errorMessage(error));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,10 @@ const i18n = defineMessages({
|
|||
id: 'externalBackendSection.urlProtocolError',
|
||||
defaultMessage: 'URL must use http or https protocol',
|
||||
},
|
||||
fingerprintRequiresHttps: {
|
||||
id: 'externalBackendSection.fingerprintRequiresHttps',
|
||||
defaultMessage: 'Certificate fingerprint requires an https URL',
|
||||
},
|
||||
urlFormatError: {
|
||||
id: 'externalBackendSection.urlFormatError',
|
||||
defaultMessage: 'Invalid URL format',
|
||||
|
|
@ -82,7 +86,10 @@ export default function ExternalBackendSection() {
|
|||
loadSettings();
|
||||
}, []);
|
||||
|
||||
const validateUrl = (value: string): boolean => {
|
||||
const validateUrl = (
|
||||
value: string,
|
||||
certFingerprint = config.certFingerprint
|
||||
): boolean => {
|
||||
if (!value) {
|
||||
setUrlError(null);
|
||||
return true;
|
||||
|
|
@ -93,6 +100,10 @@ export default function ExternalBackendSection() {
|
|||
setUrlError(intl.formatMessage(i18n.urlProtocolError));
|
||||
return false;
|
||||
}
|
||||
if (certFingerprint?.trim() && parsed.protocol !== 'https:') {
|
||||
setUrlError(intl.formatMessage(i18n.fingerprintRequiresHttps));
|
||||
return false;
|
||||
}
|
||||
setUrlError(null);
|
||||
return true;
|
||||
} catch {
|
||||
|
|
@ -132,6 +143,17 @@ export default function ExternalBackendSection() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleCertFingerprintChange = (value: string) => {
|
||||
updateField('certFingerprint', value);
|
||||
validateUrl(config.url, value);
|
||||
};
|
||||
|
||||
const handleCertFingerprintBlur = async () => {
|
||||
if (validateUrl(config.url)) {
|
||||
await saveConfig(config);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="external-backend" className="space-y-4 pr-4 mt-1">
|
||||
<Card className="pb-2">
|
||||
|
|
@ -210,8 +232,8 @@ export default function ExternalBackendSection() {
|
|||
type="text"
|
||||
placeholder={intl.formatMessage(i18n.certFingerprintPlaceholder)}
|
||||
value={config.certFingerprint || ''}
|
||||
onChange={(e) => updateField('certFingerprint', e.target.value)}
|
||||
onBlur={() => saveConfig(config)}
|
||||
onChange={(e) => handleCertFingerprintChange(e.target.value)}
|
||||
onBlur={handleCertFingerprintBlur}
|
||||
disabled={isSaving}
|
||||
className="font-mono text-xs"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,9 @@
|
|||
"externalBackendSection.description": {
|
||||
"defaultMessage": "Standardmäßig startet goose einen Server für Sie. Verwenden Sie dies, um eine Verbindung zu einem externen goose-Server herzustellen"
|
||||
},
|
||||
"externalBackendSection.fingerprintRequiresHttps": {
|
||||
"defaultMessage": "Certificate fingerprint requires an https URL"
|
||||
},
|
||||
"externalBackendSection.restartNote": {
|
||||
"defaultMessage": "Änderungen werden erst nach einem Neustart von Goose wirksam. Neue Chat-Fenster verbinden sich mit dem externen Server."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,9 @@
|
|||
"externalBackendSection.description": {
|
||||
"defaultMessage": "By default goose launches a server for you, use this to connect to an external goose server"
|
||||
},
|
||||
"externalBackendSection.fingerprintRequiresHttps": {
|
||||
"defaultMessage": "Certificate fingerprint requires an https URL"
|
||||
},
|
||||
"externalBackendSection.restartNote": {
|
||||
"defaultMessage": "Changes require restarting Goose to take effect. New chat windows will connect to the external server."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,9 @@
|
|||
"externalBackendSection.description": {
|
||||
"defaultMessage": "Por defecto goose lanza un servidor por ti; usa esto para conectarte a un servidor goose externo"
|
||||
},
|
||||
"externalBackendSection.fingerprintRequiresHttps": {
|
||||
"defaultMessage": "Certificate fingerprint requires an https URL"
|
||||
},
|
||||
"externalBackendSection.restartNote": {
|
||||
"defaultMessage": "Los cambios requieren reiniciar Goose para que surtan efecto. Las nuevas ventanas de chat se conectarán al servidor externo."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,9 @@
|
|||
"externalBackendSection.description": {
|
||||
"defaultMessage": "Par défaut, goose lance un serveur pour vous ; utilisez ceci pour vous connecter à un serveur goose externe"
|
||||
},
|
||||
"externalBackendSection.fingerprintRequiresHttps": {
|
||||
"defaultMessage": "Certificate fingerprint requires an https URL"
|
||||
},
|
||||
"externalBackendSection.restartNote": {
|
||||
"defaultMessage": "Les modifications nécessitent le redémarrage de Goose pour prendre effet. Les nouvelles fenêtres de discussion se connecteront au serveur externe."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,9 @@
|
|||
"externalBackendSection.description": {
|
||||
"defaultMessage": "डिफ़ॉल्ट रूप से goose आपके लिए एक सर्वर लॉन्च करता है, बाहरी goose सर्वर से कनेक्ट करने के लिए इसका उपयोग करें"
|
||||
},
|
||||
"externalBackendSection.fingerprintRequiresHttps": {
|
||||
"defaultMessage": "Certificate fingerprint requires an https URL"
|
||||
},
|
||||
"externalBackendSection.restartNote": {
|
||||
"defaultMessage": "परिवर्तनों को प्रभावी होने के लिए Goose को पुनः आरंभ करने की आवश्यकता है। नई चैट विंडो बाहरी सर्वर से कनेक्ट होंगी."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,9 @@
|
|||
"externalBackendSection.description": {
|
||||
"defaultMessage": "Secara bawaan goose menjalankan server untuk Anda, gunakan ini untuk terhubung ke server goose eksternal"
|
||||
},
|
||||
"externalBackendSection.fingerprintRequiresHttps": {
|
||||
"defaultMessage": "Certificate fingerprint requires an https URL"
|
||||
},
|
||||
"externalBackendSection.restartNote": {
|
||||
"defaultMessage": "Perubahan memerlukan mulai ulang Goose agar berlaku. Jendela obrolan baru akan terhubung ke server eksternal."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,9 @@
|
|||
"externalBackendSection.description": {
|
||||
"defaultMessage": "Per impostazione predefinita goose avvia un server per te; usa questa opzione per connetterti a un server goose esterno"
|
||||
},
|
||||
"externalBackendSection.fingerprintRequiresHttps": {
|
||||
"defaultMessage": "Certificate fingerprint requires an https URL"
|
||||
},
|
||||
"externalBackendSection.restartNote": {
|
||||
"defaultMessage": "Le modifiche richiedono il riavvio di Goose per avere effetto. Le nuove finestre di chat si connetteranno al server esterno."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,9 @@
|
|||
"externalBackendSection.description": {
|
||||
"defaultMessage": "デフォルトでは goose がサーバーを起動します。外部の goose サーバーに接続する場合に使用してください。"
|
||||
},
|
||||
"externalBackendSection.fingerprintRequiresHttps": {
|
||||
"defaultMessage": "Certificate fingerprint requires an https URL"
|
||||
},
|
||||
"externalBackendSection.restartNote": {
|
||||
"defaultMessage": "変更を有効にするには Goose の再起動が必要です。新しいチャットウィンドウは外部サーバーに接続します。"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,9 @@
|
|||
"externalBackendSection.description": {
|
||||
"defaultMessage": "기본적으로 goose가 서버를 자동으로 실행합니다. 외부 goose 서버에 연결하려면 이 옵션을 사용하세요."
|
||||
},
|
||||
"externalBackendSection.fingerprintRequiresHttps": {
|
||||
"defaultMessage": "Certificate fingerprint requires an https URL"
|
||||
},
|
||||
"externalBackendSection.restartNote": {
|
||||
"defaultMessage": "변경 사항을 적용하려면 goose를 다시 시작해야 합니다. 새 채팅 창은 외부 서버에 연결됩니다."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,9 @@
|
|||
"externalBackendSection.description": {
|
||||
"defaultMessage": "Secara lalai goose melancarkan pelayan untuk anda, gunakan ini untuk menyambung ke pelayan goose luaran"
|
||||
},
|
||||
"externalBackendSection.fingerprintRequiresHttps": {
|
||||
"defaultMessage": "Certificate fingerprint requires an https URL"
|
||||
},
|
||||
"externalBackendSection.restartNote": {
|
||||
"defaultMessage": "Perubahan memerlukan memulakan semula Goose untuk berkuat kuasa. Tetingkap sembang baharu akan menyambung ke pelayan luaran."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,9 @@
|
|||
"externalBackendSection.description": {
|
||||
"defaultMessage": "Por padrão, o goose inicia um servidor para você; use isto para conectar-se a um servidor goose externo"
|
||||
},
|
||||
"externalBackendSection.fingerprintRequiresHttps": {
|
||||
"defaultMessage": "Certificate fingerprint requires an https URL"
|
||||
},
|
||||
"externalBackendSection.restartNote": {
|
||||
"defaultMessage": "As alterações exigem reiniciar o Goose para entrar em vigor. As novas janelas de chat se conectarão ao servidor externo."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,9 @@
|
|||
"externalBackendSection.description": {
|
||||
"defaultMessage": "По умолчанию goose запускает сервер за вас; используйте это для подключения к внешнему серверу goose"
|
||||
},
|
||||
"externalBackendSection.fingerprintRequiresHttps": {
|
||||
"defaultMessage": "Certificate fingerprint requires an https URL"
|
||||
},
|
||||
"externalBackendSection.restartNote": {
|
||||
"defaultMessage": "Чтобы изменения вступили в силу, требуется перезапуск Goose. Новые окна чата будут подключаться к внешнему серверу."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,9 @@
|
|||
"externalBackendSection.description": {
|
||||
"defaultMessage": "Varsayılan olarak goose sizin için bir sunucu başlatır; harici bir goose sunucusuna bağlanmak için bunu kullanın"
|
||||
},
|
||||
"externalBackendSection.fingerprintRequiresHttps": {
|
||||
"defaultMessage": "Certificate fingerprint requires an https URL"
|
||||
},
|
||||
"externalBackendSection.restartNote": {
|
||||
"defaultMessage": "Değişikliklerin etkili olması için Goose'nin yeniden başlatılması gerekir. Yeni sohbet pencereleri harici sunucuya bağlanacaktır."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,9 @@
|
|||
"externalBackendSection.description": {
|
||||
"defaultMessage": "Theo mặc định goose khởi chạy một máy chủ cho bạn, hãy dùng tùy chọn này để kết nối tới một máy chủ goose bên ngoài"
|
||||
},
|
||||
"externalBackendSection.fingerprintRequiresHttps": {
|
||||
"defaultMessage": "Certificate fingerprint requires an https URL"
|
||||
},
|
||||
"externalBackendSection.restartNote": {
|
||||
"defaultMessage": "Các thay đổi cần khởi động lại Goose để có hiệu lực. Các cửa sổ trò chuyện mới sẽ kết nối tới máy chủ bên ngoài."
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,9 @@
|
|||
"externalBackendSection.description": {
|
||||
"defaultMessage": "默认情况下 goose 会为你启动一个服务器,此选项用于连接到外部的 goose 服务器"
|
||||
},
|
||||
"externalBackendSection.fingerprintRequiresHttps": {
|
||||
"defaultMessage": "Certificate fingerprint requires an https URL"
|
||||
},
|
||||
"externalBackendSection.restartNote": {
|
||||
"defaultMessage": "更改需要重启 Goose 才能生效。新的聊天窗口将连接到外部服务器。"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1169,6 +1169,9 @@
|
|||
"externalBackendSection.description": {
|
||||
"defaultMessage": "goose 預設會為您啟動伺服器,使用此選項以連線至外部 goose 伺服器"
|
||||
},
|
||||
"externalBackendSection.fingerprintRequiresHttps": {
|
||||
"defaultMessage": "Certificate fingerprint requires an https URL"
|
||||
},
|
||||
"externalBackendSection.restartNote": {
|
||||
"defaultMessage": "變更需要重新啟動 Goose 才會生效。新的聊天視窗將連線至外部伺服器。"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -599,10 +599,11 @@ async function handleProtocolUrl(url: string, parsedUrl: URL) {
|
|||
existingWindows.length > 0
|
||||
? existingWindows[0]
|
||||
: await createChat(app, { dir: openDir || undefined });
|
||||
if (!targetWindow) return;
|
||||
await processProtocolUrl(url, parsedUrl, targetWindow);
|
||||
} else {
|
||||
const existingWindows = BrowserWindow.getAllWindows();
|
||||
let targetWindow: BrowserWindow;
|
||||
let targetWindow: BrowserWindow | undefined;
|
||||
if (existingWindows.length > 0) {
|
||||
targetWindow = existingWindows[0];
|
||||
if (targetWindow.isMinimized()) {
|
||||
|
|
@ -613,6 +614,8 @@ async function handleProtocolUrl(url: string, parsedUrl: URL) {
|
|||
targetWindow = await createChat(app, { dir: openDir || undefined });
|
||||
}
|
||||
|
||||
if (!targetWindow) return;
|
||||
|
||||
if (targetWindow.webContents.isLoadingMainFrame()) {
|
||||
queuePendingDeepLink(targetWindow.id, url);
|
||||
} else {
|
||||
|
|
@ -709,6 +712,7 @@ app.on('open-url', async (_event, url) => {
|
|||
} else {
|
||||
openUrlHandledLaunch = true;
|
||||
const newWindow = await createChat(app, { dir: openDir || undefined });
|
||||
if (!newWindow) return;
|
||||
queuePendingDeepLink(newWindow.id, url);
|
||||
}
|
||||
}
|
||||
|
|
@ -944,7 +948,10 @@ interface CreateChatOptions {
|
|||
recipeParameters?: Record<string, string>;
|
||||
}
|
||||
|
||||
const createChat = async (app: App, options: CreateChatOptions = {}) => {
|
||||
const createChat = async (
|
||||
app: App,
|
||||
options: CreateChatOptions = {}
|
||||
): Promise<BrowserWindow | undefined> => {
|
||||
const {
|
||||
initialMessage,
|
||||
initialMessageNoAutoSubmit,
|
||||
|
|
@ -957,6 +964,42 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => {
|
|||
recipeParameters,
|
||||
} = options;
|
||||
const settings = getSettings();
|
||||
|
||||
if (settings.externalGoosed?.enabled && settings.externalGoosed.certFingerprint) {
|
||||
const url = settings.externalGoosed.url;
|
||||
const usesHttps = (() => {
|
||||
try {
|
||||
return new URL(url).protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
if (!usesHttps) {
|
||||
const response = dialog.showMessageBoxSync({
|
||||
type: 'error',
|
||||
title: 'External Backend Misconfigured',
|
||||
message: 'Certificate fingerprint requires an HTTPS external backend URL.',
|
||||
detail: 'Use an https:// URL or remove the configured certificate fingerprint.',
|
||||
buttons: ['Disable External Backend & Retry', 'Quit'],
|
||||
defaultId: 0,
|
||||
cancelId: 1,
|
||||
});
|
||||
|
||||
if (response === 0) {
|
||||
updateSettings((s) => {
|
||||
if (s.externalGoosed) {
|
||||
s.externalGoosed.enabled = false;
|
||||
}
|
||||
});
|
||||
return createChat(app, options);
|
||||
}
|
||||
|
||||
app.quit();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const serverSecret = getServerSecret(settings);
|
||||
|
||||
// Update the cached trusted-external-hostname so the TLS handlers allow
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue