Skip to content

Commit 35b59a4

Browse files
brettchienclaude
andcommitted
feat(acp): serve /acp from the embedded openab run gateway
#1260 mounted the ACP endpoint only on the standalone `openab-gateway` binary (`serve()`). Fleet deployments run the unified binary's embedded gateway (`openab run`, main.rs) instead, which never called `serve()` — so `/acp` was unreachable there. Mount `/acp` on the embedded gateway too (gated by `OPENAB_ACP_ENABLED`), and route ACP replies back through the unified adapter's `dispatch_reply` (`platform == "acp"`). Also start the embedded HTTP server when ACP is enabled even if only non-webhook platforms (e.g. Discord, which the core connects to directly) are configured — otherwise the listener never binds. Seed the `acp` platform into the gateway trust registry so its identity gating honours `GATEWAY_ALLOW_ALL_USERS` / `GATEWAY_ALLOWED_USERS` (the ACP sender id is `acp_client`); without this the acp platform defaulted to deny-all and every prompt was rejected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 68d0748 commit 35b59a4

3 files changed

Lines changed: 39 additions & 5 deletions

File tree

docs/adr/acp-server-websocket-phase1.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,16 @@ third-party ACP clients (Zed, JetBrains, …) interoperate — no custom method
2525

2626
## 2. Decision — Phase 1 primitive surface (ACP-conformant)
2727

28-
Transport: `GET /acp`, feature-gated `acp` + runtime `OPENAB_ACP_ENABLED`. Token auth
29-
on the WS upgrade via timing-safe compare (`subtle::ConstantTimeEq`,
30-
`OPENAB_ACP_AUTH_KEY`). JSON-RPC 2.0; non-`"2.0"` rejected with `-32600`.
28+
Transport: `GET /acp`, feature-gated `acp` + runtime `OPENAB_ACP_ENABLED`. Mounted on
29+
**both** the standalone `openab-gateway` binary (`serve()`) **and** the embedded
30+
gateway of `openab run` (the unified binary) — so fleet deployments that run
31+
`openab run` (not the standalone gateway) serve ACP too. Requires the embedded
32+
gateway to be active (at least one platform/`[gateway]` configured). ACP replies are
33+
routed back via the unified adapter's `dispatch_reply` (`platform == "acp"`).
34+
35+
Token auth on the WS upgrade via timing-safe compare (`subtle::ConstantTimeEq`,
36+
`OPENAB_ACP_AUTH_KEY`; unset ⇒ unauthenticated). JSON-RPC 2.0; non-`"2.0"` rejected
37+
with `-32600`.
3138

3239
### Client → Agent (requests)
3340

src/main.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ async fn main() -> anyhow::Result<()> {
419419
let allow_all_users = env_bool("GATEWAY_ALLOW_ALL_USERS", false);
420420
let allowed_users = env_set("GATEWAY_ALLOWED_USERS");
421421
let mut reg = PlatformTrustConfigs::new();
422-
for platform in ["telegram", "line", "feishu", "wecom", "googlechat", "teams"] {
422+
for platform in ["telegram", "line", "feishu", "wecom", "googlechat", "teams", "acp"] {
423423
reg.insert(
424424
platform,
425425
TrustConfig::new(
@@ -910,7 +910,14 @@ async fn main() -> anyhow::Result<()> {
910910
let (_unified_handle, shared_unified_adapter) = {
911911
use openab_core::gateway::{process_gateway_event, GatewayEventContext};
912912

913-
if unified_platform_enabled || cfg.telegram.is_some() {
913+
// The ACP endpoint (mounted below) needs this embedded HTTP server too —
914+
// start it even when only non-webhook platforms (e.g. Discord, which the
915+
// core connects to directly) are configured.
916+
let acp_enabled = cfg!(feature = "acp")
917+
&& std::env::var("OPENAB_ACP_ENABLED")
918+
.map(|v| v == "true" || v == "1")
919+
.unwrap_or(false);
920+
if unified_platform_enabled || cfg.telegram.is_some() || acp_enabled {
914921
let listen_addr =
915922
std::env::var("GATEWAY_LISTEN").unwrap_or_else(|_| "0.0.0.0:8080".into());
916923

@@ -1113,6 +1120,20 @@ async fn main() -> anyhow::Result<()> {
11131120
);
11141121
}
11151122

1123+
// ACP server endpoint — mount on the embedded gateway so `openab run`
1124+
// (not just the standalone gateway binary) serves ACP over WebSocket.
1125+
#[cfg(feature = "acp")]
1126+
if std::env::var("OPENAB_ACP_ENABLED")
1127+
.map(|v| v == "true" || v == "1")
1128+
.unwrap_or(false)
1129+
{
1130+
info!("unified: ACP server endpoint enabled at /acp");
1131+
app = app.route(
1132+
"/acp",
1133+
axum::routing::get(openab_gateway::adapters::acp_server::ws_upgrade),
1134+
);
1135+
}
1136+
11161137
let app = app.with_state(gw_state.clone());
11171138

11181139
// Bridge task: receive events from adapters via event_tx, dispatch to core

src/unified_adapter.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ impl UnifiedGatewayAdapter {
8989
.await;
9090
}
9191
}
92+
#[cfg(feature = "acp")]
93+
"acp" => {
94+
if let Some(ref registry) = self.gw_state.acp_reply_registry {
95+
openab_gateway::adapters::acp_server::handle_reply(reply, registry).await;
96+
}
97+
}
9298
other => {
9399
tracing::warn!(platform = other, "unified adapter: unknown platform, cannot route reply");
94100
}

0 commit comments

Comments
 (0)