mirror of
https://github.com/block/buzz.git
synced 2026-10-29 13:31:44 +00:00
keyless: add a mock broker host and run instructions
- examples/mock_broker.rs: a throwaway host that speaks just enough of the contract (POST /v1/action, echoes requestId/action, canned outcomes) to exercise the keyless CLI end to end before a real broker exists. Signs nothing, touches no relay. - KEYLESS.md: brief instructions — build, run against the mock, and point the CLI at your own broker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Joel Robotham <jrobotham@squareup.com>
This commit is contained in:
72
crates/buzz-cli/KEYLESS.md
Normal file
72
crates/buzz-cli/KEYLESS.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Keyless mode (agent broker client)
|
||||
|
||||
Run the `buzz` CLI without a signing key. In keyless mode the CLI holds no nsec
|
||||
and opens no relay connection — every operation is sent to a **broker host** that
|
||||
performs it on the agent's behalf. This is the client side of the agent-broker
|
||||
contract ([#6790](https://github.com/block/buzz/pull/6790)).
|
||||
|
||||
The backend is selected by `--agent-mode` (env `BUZZ_AGENT_MODE`), default
|
||||
`local`:
|
||||
|
||||
- `local` — hold the key, sign locally, talk to the relay (the unchanged default).
|
||||
- `broker` — keyless; route every operation through a broker host.
|
||||
|
||||
This first slice covers the wake→reply loop: `messages get` (with
|
||||
`--mentions-only`, the wake path) and `messages send` / reply.
|
||||
|
||||
## Build
|
||||
|
||||
```sh
|
||||
source bin/activate-hermit
|
||||
cargo build -p buzz-cli --bin buzz --example mock_broker
|
||||
```
|
||||
|
||||
## Try it against the bundled mock host
|
||||
|
||||
The repo ships a throwaway mock host so you can exercise the round trip without a
|
||||
real broker. It signs nothing and returns canned outcomes — it exists only to
|
||||
prove the client wiring end to end.
|
||||
|
||||
Terminal 1 — the mock host:
|
||||
|
||||
```sh
|
||||
cargo run -p buzz-cli --example mock_broker
|
||||
# listening on http://127.0.0.1:8787
|
||||
```
|
||||
|
||||
Terminal 2 — the keyless client (note: no key in the environment):
|
||||
|
||||
```sh
|
||||
export BUZZ_AGENT_MODE=broker
|
||||
export BUZZ_BROKER_URL=http://127.0.0.1:8787
|
||||
export BUZZ_BROKER_CREDENTIAL=dev-token
|
||||
unset BUZZ_PRIVATE_KEY
|
||||
|
||||
CH=<channel-uuid>
|
||||
buzz messages send --channel "$CH" --content "hello from a keyless client"
|
||||
buzz messages send --channel "$CH" --reply-to <event-id> --content "on it"
|
||||
buzz messages get --channel "$CH" --mentions-only --limit 10
|
||||
```
|
||||
|
||||
Terminal 1 logs each action, the bearer credential, and the args it received.
|
||||
|
||||
## Point it at your own broker
|
||||
|
||||
Swap the two broker vars for your host's endpoint and a credential it issued:
|
||||
|
||||
```sh
|
||||
export BUZZ_BROKER_URL=https://your-broker.example
|
||||
export BUZZ_BROKER_CREDENTIAL=<token your broker accepts>
|
||||
```
|
||||
|
||||
Your host must accept `POST /v1/action` with `Authorization: Bearer <credential>`
|
||||
and return a broker-result envelope per the contract. `examples/mock_broker.rs`
|
||||
is a minimal reference for the wire shape.
|
||||
|
||||
## Notes
|
||||
|
||||
- Keyless mode **fails closed** if a private key is present (`--private-key` /
|
||||
`BUZZ_PRIVATE_KEY`): supplying a key in broker mode is a provisioning error,
|
||||
not silently ignored.
|
||||
- Credential issuance, authorization, and custody are the host's concern; the
|
||||
client only needs an endpoint and a token to present.
|
||||
85
crates/buzz-cli/examples/mock_broker.rs
Normal file
85
crates/buzz-cli/examples/mock_broker.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
//! A throwaway broker host for local keyless-CLI development.
|
||||
//!
|
||||
//! It speaks just enough of the agent-broker contract (block/buzz#6790) to let
|
||||
//! `buzz --agent-mode broker …` complete a round trip before the real beekeeper
|
||||
//! host exists: it accepts `POST /v1/action`, echoes the request's `requestId`
|
||||
//! and `action` back (so correlation holds), and returns a canned success
|
||||
//! outcome per action. It signs nothing and touches no relay — it exists only to
|
||||
//! prove the client wiring end to end.
|
||||
//!
|
||||
//! Run it in one terminal:
|
||||
//! cargo run -p buzz-cli --example mock_broker
|
||||
//! then drive the CLI from another (see the crate's keyless docs).
|
||||
|
||||
use axum::body::{Body, Bytes};
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::Response;
|
||||
use axum::routing::post;
|
||||
use axum::Router;
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
const ADDR: &str = "127.0.0.1:8787";
|
||||
const FAKE_EVENT_ID: &str = "cacf5f811cc8ef3f4af3f92cc222f92a86cdf6a26728a144c8e63b74ab6db359";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let app = Router::new().route("/v1/action", post(action));
|
||||
let listener = TcpListener::bind(ADDR).await.expect("bind");
|
||||
eprintln!("mock broker listening on http://{ADDR} (Ctrl-C to stop)");
|
||||
axum::serve(listener, app).await.expect("serve");
|
||||
}
|
||||
|
||||
/// Answer one action with a canned success envelope for the wake→reply slice.
|
||||
async fn action(headers: axum::http::HeaderMap, body: Bytes) -> Response {
|
||||
let request: serde_json::Value = serde_json::from_slice(&body).unwrap_or_default();
|
||||
let request_id = request
|
||||
.get("requestId")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("");
|
||||
let action = request.get("action").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let credential = headers
|
||||
.get("authorization")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("<none>");
|
||||
eprintln!(
|
||||
"→ {action} requestId={request_id} auth={credential}\n args={}",
|
||||
request.get("args").unwrap_or(&serde_json::Value::Null)
|
||||
);
|
||||
|
||||
// A host verdict always rides in the body at HTTP 200; only the shape varies.
|
||||
let body = match action {
|
||||
"message.post" | "message.reply" => succeeded(
|
||||
request_id,
|
||||
action,
|
||||
serde_json::json!({ "eventId": FAKE_EVENT_ID, "kind": 9, "createdAt": 1_700_000_000u64 }),
|
||||
),
|
||||
"channel.read" => succeeded(request_id, action, serde_json::json!({ "messages": [] })),
|
||||
other => serde_json::json!({
|
||||
"type": "broker_result",
|
||||
"protocolVersion": 1,
|
||||
"requestId": request_id,
|
||||
"status": "failed",
|
||||
"error": {
|
||||
"code": "unimplemented",
|
||||
"message": format!("mock broker does not implement '{other}'"),
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(body.to_string()))
|
||||
.expect("response")
|
||||
}
|
||||
|
||||
fn succeeded(request_id: &str, action: &str, outcome: serde_json::Value) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"type": "broker_result",
|
||||
"protocolVersion": 1,
|
||||
"requestId": request_id,
|
||||
"status": "succeeded",
|
||||
"action": action,
|
||||
"outcome": outcome,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user