feat(maestro): daemon brokers cross-daemon create/kill instead of 501/404 (NAP-327)

The home daemon's `POST /api/sessions` (peer `daemon_id`) and
`DELETE /api/sessions/:id` (peer-owned) now broker the op to the owning daemon
over the registry control link rather than returning 501/404. This fixes
cross-daemon spawn/kill for *all* local clients — notably maestro-web, which
talks only to the local daemon and had no registry fallback.

- broker: `BrokerHandle::{forward_create,forward_kill}` + completion handlers,
  mirroring `resolve_attach` (fail-fast while disconnected, RAII pending
  cleanup, RESOLVE_TIMEOUT).
- registry_client: route `Forward{Create,Kill}Result` back to the broker.
- local_api: create forwards on peer `daemon_id`; kill looks up the owner in
  the catalog mirror and forwards (only a truly-unknown session is 404).
  `broker_error_response` maps BrokerError → HTTP (503/504/502/404/...).
- cli: thread the broker into `LocalApiState`.

Tests: broker round-trip / fail-fast / registry-err propagation; updated the
local_api integration test (peer create now 503s without a registry link
instead of 501, proving it brokers).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 16:24:48 +00:00
co-authored by Claude Opus 4.8
parent daeacc750f
commit 1e4b65c73c
5 changed files with 281 additions and 29 deletions
+13 -9
View File
@@ -34,17 +34,19 @@ async fn spawn(daemon_id: &str, hostname: &str, daemon_url: &str) -> Harness {
let manager = fx.manager.clone();
let catalog = CatalogMirror::new();
let events = LocalEventBus::new();
// NAP-247/NAP-327: tests don't exercise the cross-host broker path; hand the
// routers an isolated BrokerHandle whose receiver is dropped, so any
// resolve_attach / forward_create / forward_kill fails fast with
// NoControlLink (the handle starts disconnected).
let (broker, _broker_rx) = BrokerHandle::new();
let state = local_api::state_for(
manager.clone(),
hostname,
daemon_url,
catalog.clone(),
events,
broker.clone(),
);
// NAP-247: tests don't exercise the cross-host broker path; hand the
// router an isolated BrokerHandle whose receiver is dropped, so any
// resolve_attach attempt fails fast with NoControlLink.
let (broker, _broker_rx) = BrokerHandle::new();
let broker_state = local_api::BrokerState {
manager: manager.clone(),
catalog: catalog.clone(),
@@ -210,9 +212,11 @@ async fn list_sessions_starts_empty_and_reflects_creates() {
}
#[tokio::test]
async fn post_to_peer_daemon_id_returns_501_not_proxy() {
// Cross-daemon broker is NAP-247; until it lands a daemon must refuse
// (rather than silently misroute) a create targeting a peer.
async fn post_to_peer_daemon_id_brokers_and_503s_without_registry_link() {
// Cross-daemon create now brokers to the owner over the registry control
// link (NAP-327). This harness has no registry link (the broker starts
// disconnected), so the daemon fails fast with a retryable 503 instead of
// the old 501 — proving it attempts to broker rather than refusing.
let h = spawn("laptop", "naps-laptop", "ws://10.0.0.1:7070").await;
let req = CreateSessionRequest {
daemon_id: "konishi".into(),
@@ -228,9 +232,9 @@ async fn post_to_peer_daemon_id_returns_501_not_proxy() {
.send()
.await
.unwrap();
assert_eq!(resp.status(), reqwest::StatusCode::NOT_IMPLEMENTED);
assert_eq!(resp.status(), reqwest::StatusCode::SERVICE_UNAVAILABLE);
let body: serde_json::Value = resp.json().await.unwrap();
assert_eq!(body["error"]["code"], "remote_create_unsupported");
assert_eq!(body["error"]["code"], "no_registry_link");
}
#[tokio::test]