Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
38 changes: 33 additions & 5 deletions crates/goose-server/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ use axum::{
};
use subtle::ConstantTimeEq;

fn token_matches(candidate: Option<&str>, expected: &str) -> bool {
candidate
.map(|key| bool::from(key.as_bytes().ct_eq(expected.as_bytes())))
.unwrap_or(false)
}

pub async fn check_token(
State(state): State<String>,
request: Request,
Expand All @@ -24,10 +30,32 @@ pub async fn check_token(
.get("X-Secret-Key")
.and_then(|value| value.to_str().ok());

match secret_key {
Some(key) if bool::from(key.as_bytes().ct_eq(state.as_bytes())) => {
Ok(next.run(request).await)
}
_ => Err(StatusCode::UNAUTHORIZED),
if token_matches(secret_key, &state) {
Ok(next.run(request).await)
} else {
Err(StatusCode::UNAUTHORIZED)
}
}

pub async fn check_acp_token(
State(state): State<String>,
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let header_token = request
.headers()
.get("X-Secret-Key")
.and_then(|value| value.to_str().ok());

let query_token = request.uri().query().and_then(|query| {
url::form_urlencoded::parse(query.as_bytes())
.find(|(key, _)| key == "token")
.map(|(_, value)| value.into_owned())
});

if token_matches(header_token, &state) || token_matches(query_token.as_deref(), &state) {
Ok(next.run(request).await)
} else {
Err(StatusCode::UNAUTHORIZED)
}
}
35 changes: 28 additions & 7 deletions crates/goose-server/src/commands/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@ use crate::state;
use anyhow::Result;
use axum::middleware;
use axum_server::Handle;
use goose_server::auth::check_token;
use goose::acp::server_factory::{AcpServer, AcpServerFactoryConfig};
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;

Expand Down Expand Up @@ -64,12 +69,28 @@ pub async fn run() -> Result<()> {
.allow_methods(Any)
.allow_headers(Any);

let app = crate::routes::configure(app_state.clone(), secret_key.clone())
.layer(middleware::from_fn_with_state(
secret_key.clone(),
check_token,
))
.layer(cors);
// TODO(acp-migration): When ui/desktop launches `goose serve` directly,
// move any goosed-only ACP setup into the goose serve path before deleting
// this bridge. In particular, verify everything ACP currently gets from
// goosed startup/AppState initialization, including builtin extension
// registration and the desktop platform identity.
let acp_server = Arc::new(AcpServer::new(AcpServerFactoryConfig {
builtins: vec!["developer".to_string()],
data_dir: Paths::data_dir(),
config_dir: Paths::config_dir(),
goose_platform: GoosePlatform::GooseDesktop,
additional_source_roots: Vec::new(),
}));

let rest_router = crate::routes::configure(app_state.clone(), secret_key.clone()).layer(
middleware::from_fn_with_state(secret_key.clone(), check_token),
);
let acp_router = create_acp_router(acp_server).layer(middleware::from_fn_with_state(
secret_key.clone(),
check_acp_token,
));
Comment on lines +88 to +91

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Exempt ACP CORS preflight from token middleware

check_acp_token is applied as an outer layer on the ACP router, so unauthenticated OPTIONS /acp preflight requests are rejected with 401 before the inner CORS layer can answer them. Any browser client using ACP over HTTP (POST/DELETE with application/json) will fail CORS negotiation even with a valid token on the real request, because preflight requests do not carry that token. Please either skip auth for OPTIONS in check_acp_token or place CORS outside auth for the ACP branch.

Useful? React with 👍 / 👎.


let app = rest_router.merge(acp_router).layer(cors);

let addr = settings.socket_addr();

Expand Down
7 changes: 6 additions & 1 deletion crates/goose/src/acp/server/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ impl GooseAcpAgent {
pub(super) async fn on_get_extensions(
&self,
) -> Result<GetExtensionsResponse, agent_client_protocol::Error> {
let extensions = crate::config::extensions::get_all_extensions();
let extensions = crate::config::extensions::get_all_extensions()
.into_iter()
.filter(|ext| {
!crate::agents::extension_manager::is_hidden_extension(&ext.config.name())
})
.collect::<Vec<_>>();
let warnings = crate::config::extensions::get_warnings();
let extensions_json = extensions
.into_iter()
Expand Down
26 changes: 18 additions & 8 deletions crates/goose/src/acp/transport/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,8 @@ async fn health() -> &'static str {
"ok"
}

pub fn create_router(server: Arc<AcpServer>, secret_key: String) -> Router {
let registry = Arc::new(connection::ConnectionRegistry::new(server));

let cors = CorsLayer::new()
fn acp_cors_layer() -> CorsLayer {
CorsLayer::new()
.allow_origin(Any)
.allow_methods([Method::GET, Method::POST, Method::DELETE, Method::OPTIONS])
.allow_headers([
Expand All @@ -113,14 +111,26 @@ pub fn create_router(server: Arc<AcpServer>, secret_key: String) -> Router {
.expose_headers([
HeaderName::from_static("acp-connection-id"),
HeaderName::from_static("acp-session-id"),
]);
])
}

fn create_acp_routes(server: Arc<AcpServer>) -> Router {
let registry = Arc::new(connection::ConnectionRegistry::new(server));

Router::new()
.route("/health", get(health))
.route("/status", get(health))
.route("/acp", post(http::handle_post).with_state(registry.clone()))
.route("/acp", get(handle_get).with_state(registry.clone()))
.route("/acp", delete(http::handle_delete).with_state(registry))
}

pub fn create_acp_router(server: Arc<AcpServer>) -> Router {
create_acp_routes(server).layer(acp_cors_layer())
}

pub fn create_router(server: Arc<AcpServer>, secret_key: String) -> Router {
create_acp_routes(server)
.route("/health", get(health))
.route("/status", get(health))
.merge(super::mcp_app_proxy::routes(secret_key))
.layer(cors)
.layer(acp_cors_layer())
}
26 changes: 18 additions & 8 deletions ui/desktop/index.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8"/>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' http://127.0.0.1:* https:; object-src 'none'; font-src 'self' data: https:; media-src 'self' mediastream:; form-action 'none'; base-uri 'self'; manifest-src 'self'; worker-src 'self'; frame-src 'self' https: http:;"/>
<meta charset="UTF-8" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' http://127.0.0.1:* https: ws: wss:; object-src 'none'; font-src 'self' data: https:; media-src 'self' mediastream:; form-action 'none'; base-uri 'self'; manifest-src 'self'; worker-src 'self'; frame-src 'self' https: http:;"
/>
<title>Goose</title>
<script>
// Initialize theme before any content loads
Expand All @@ -13,8 +16,12 @@
const useSystemTheme = localStorage.getItem('use_system_theme') === 'true';
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const savedTheme = localStorage.getItem('theme');
const isDark = useSystemTheme ? systemPrefersDark : (savedTheme ? savedTheme === 'dark' : systemPrefersDark);

const isDark = useSystemTheme
? systemPrefersDark
: savedTheme
? savedTheme === 'dark'
: systemPrefersDark;

if (isDark) {
document.documentElement.classList.add('dark');
document.documentElement.style.colorScheme = 'dark';
Expand All @@ -33,7 +40,10 @@
}
}
} catch (error) {
console.warn('Failed to initialize theme from localStorage, using system preference:', error);
console.warn(
'Failed to initialize theme from localStorage, using system preference:',
error
);
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (systemPrefersDark) {
document.documentElement.classList.add('dark');
Expand All @@ -47,16 +57,16 @@

// Run immediately
initializeTheme();

// Retry after DOM is ready if initial attempt failed
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
document.addEventListener('DOMContentLoaded', function () {
setTimeout(initializeTheme, 50);
});
}
})();
</script>
<link href="./src/styles/main.css" rel="stylesheet"/>
<link href="./src/styles/main.css" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
Expand Down
2 changes: 2 additions & 0 deletions ui/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
},
"main": ".vite/build/main.js",
"scripts": {
"postinstall": "pnpm --filter @aaif/goose-sdk run build",
"typecheck": "tsc --noEmit",
"generate-api": "openapi-ts",
"start-gui": "pnpm run generate-api && pnpm run i18n:compile && electron-forge start",
Expand Down Expand Up @@ -46,6 +47,7 @@
},
"dependencies": {
"@aaif/goose-sdk": "workspace:*",
"@agentclientprotocol/sdk": "^0.19.0",
"@mcp-ui/client": "6.1.0",
"@modelcontextprotocol/ext-apps": "^1.1.1",
"@radix-ui/react-accordion": "^1.2.12",
Expand Down
Loading
Loading