server

package
v0.22.171 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 26, 2026 License: MIT Imports: 40 Imported by: 0

Documentation

Overview

Package server — `GET /v1/biam/subscribe` SSE handler (ADR-024 Phase 4: A2A asynchronous push).

Wire shape:

GET /v1/biam/subscribe?task_id=<id>
  Accept: text/event-stream            (advisory — we always
                                        emit SSE for this path)
  Last-Event-ID: <u64>                  (optional; resume after a
                                        prior disconnect)

Response:

HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

id: 1
event: task
data: {"task_id":"…","status":"active",…}

id: 2
event: frame
data: {"task_id":"…","line":"hello","kind":"stdout","ts":"…"}

…

Lifecycle:

  • On connect, replay every event for task_id whose ID is greater than the parsed Last-Event-ID header (default 0 = full ring).
  • Then block on the per-task notify channel; on each wake re-read the ring's tail and stream new events.
  • Close on (a) terminal-status event (kind == "terminal"), (b) client disconnect (ctx.Done from r.Context()), or (c) daemon shutdown propagated through the same ctx chain.

The buffer is fed by the broadcast hooks installed at daemon boot (see internal/agents/biam.WirePushHooks). The handler itself touches biam.Events only as a reader + subscriber.

Package server — gateway dashboard (ADR-024).

`/dashboard` serves a single self-contained HTML page that renders this node's local agents plus every LAN peer the mDNS browser has discovered. It is the human-facing view of the AI gateway: open it from the tray ("Open dashboard") and you see, on one screen, what's running here and which paired machines on the network are reachable.

The page is static markup with no secrets; it drives itself entirely off same-origin fetches to /v1/agents, /v1/peers, and /v1/peers/{id}/card. On the local daemon those endpoints run without auth (loopback is the trust boundary — see HTTPOptions.NoAuth), so the dashboard works out of the box. On an auth'd deployment the data fetches return 401 and the page shows a clear "token required" hint rather than failing silently.

internal/server/firewall_hook.go — auto-firewall hook called from ServeHTTP when --listen 0.0.0.0 --allow-lan is set and --no-firewall-setup wasn't passed.

The hook delegates to internal/firewall.DefaultBackend so each platform handles the work in its own idiomatic way:

  • Windows: one UAC-elevated PowerShell batch that installs clawtool-mdns-inbound (UDP 5353) + clawtool-gateway-inbound (TCP <port>) into Defender Firewall's Private profile. The LAN CIDR is auto-detected by the backend.
  • Linux: `sudo ufw allow ...` when ufw is active, otherwise no-op.
  • macOS: no-op (pf default-allow + Application Firewall covers it).

The hook is intentionally best-effort. UAC declined, sudo declined, PowerShell missing — any of these turn into a stderr warning and the daemon continues to boot. The mDNS announce goroutine still starts; it just won't reach LAN peers until the operator runs `clawtool firewall add` later.

Package server — HTTP gateway (ADR-014 Phase 2, v0.11).

`clawtool serve --listen :8080 --token-file <path>` mounts a thin HTTP surface that proxies prompts to the supervisor and exposes the agent registry. Every dispatch goes through Supervisor.Send (same call site as the CLI / MCP). Auth is bearer-token at the edge — non-negotiable; the relay opens an exec-arbitrary-code-on-host surface.

TLS is not terminated here. Operators front this with nginx / caddy / Cloudflare Tunnel — we do not invent a cert story (see ADR-014 Rationale).

Mcp-Method / Mcp-Name HTTP header handling for the Streamable HTTP transport (SEP-2243, finalized 2026-04-17).

SEP-2243 adds two request headers on Streamable HTTP POSTs so load balancers, rate-limiters, and metrics pipelines can route without parsing the JSON-RPC body:

  • Mcp-Method: <method-name> e.g. "tools/call", "tools/list"
  • Mcp-Name: <tool-or-prompt> e.g. "mcp__clawtool__SendMessage"

Mcp-Name is only meaningful for methods that carry a sub-target (`tools/call` → params.name, `prompts/get` → params.name); for methods like `notifications/initialized` we OMIT the header rather than send empty — the spec language ("for methods with a sub-target") reads as "don't include otherwise" and an absent header is unambiguous to proxies, where empty-string is a matchable value that complicates rules.

Server side (mcpHeaderMiddleware): reads incoming Mcp-Method, falls back to body inspection when absent, prefers the body's JSON-RPC method when the two disagree (logging a stderr warning), exposes the resolved values via context, and echoes them on the response so clients can see what we processed.

Client side (BuildMCPRequest): sets Mcp-Method from the JSON-RPC body's method field and sets Mcp-Name from params.name only when the method is one that carries a sub-target. Used by any code in clawtool that POSTs to an upstream Streamable HTTP MCP server (today: tests + future outbound transports).

MCP-over-HTTP Accept-header content negotiation shim.

The mark3labs/mcp-go StreamableHTTPServer (v0.49.0) always replies to a single-response /mcp POST with `Content-Type: application/json` and a bare JSON-RPC body, regardless of the client's Accept header (see streamable_http.go:546 in that release).

rmcp (the Rust MCP SDK that codex's HTTP client is built on) opens initialize with `Accept: text/event-stream` only. When the upstream answers with raw JSON the rmcp parser tries to decode the body as SSE-framed (`data: <json>\n\n`), finds no `event:` lines, and surfaces the misleading

Deserialize error: data did not match any variant of untagged
enum JsonRpcMessage when send initialize request

MCP Streamable-HTTP (2025-06-18) says the server SHOULD honor the client's Accept by responding with `text/event-stream` framed as `data: <json>\n\n`; a single SSE event is a valid response shape.

mcpAcceptShim wraps the streamable handler and post-processes the outgoing response when the client asked for SSE — buffer the `application/json` body the inner handler emits, then write it back as a single `data: ...\n\n` SSE event with the right Content-Type. When the inner handler already chose `text/event-stream` (multi-event drain path, the upgradedHeader branch in mcp-go) we pass through unchanged.

internal/server/networking_hook.go — auto-networking hook called from ServeHTTP when the listener binds beyond loopback and the host is WSL2 with non-mirrored networking. Sibling of firewall_hook.go from PR #23; same default-on, best-effort shape.

The hook detects WSL2 + checks .wslconfig; when mirrored mode is supported but not yet configured, it prompts the operator (default Yes per operator preference). On confirmation, it edits the .wslconfig and surfaces a prominent `wsl --shutdown` next-step. The daemon then continues booting in the current (pre-restart) networking mode — clawtool cannot restart its own WSL2 distro from inside.

On Windows 10 / pre-22H2 hosts, surfaces the unsupported recommendation and returns without writing. Daemon boot is never blocked by this hook: every failure path is a stderr warning + degraded-mode continuation.

Package server — `/v1/peers` REST surface (ADR-024 Phase 1).

Four endpoints, all bearer-authed by the same authMiddleware every other /v1/* path uses:

GET    /v1/peers                       — list with status / backend / circle / path filters
POST   /v1/peers/register              — body: a2a.RegisterInput; returns the assigned Peer
POST   /v1/peers/{peer_id}/heartbeat   — refresh last_seen + status
DELETE /v1/peers/{peer_id}             — explicit deregister on session end

Wire shape mirrors prassanna-ravishankar/repowire's /peers + /peers/by-pane endpoints so an existing repowire dashboard can be re-pointed at a clawtool daemon with a one-line URL change. Difference: clawtool's auth model is bearer-token (the daemon-wide token in ~/.config/clawtool/listener-token), not repowire's per-peer auth_token; we already have the daemon-shared token so a second layer is unnecessary at this phase.

Registry lifecycle: the handlers fetch a2a.GetGlobal() on every request. buildMCPServer's Phase-1 boot installs a registry into the global slot (with persistence at ~/.config/clawtool/peers.json); daemon shutdown clears it. Handlers return 503 when the global is nil so a misconfigured boot doesn't 500 — operator gets a clear "registry not initialised" hint instead.

Package server — POST /v1/relay, the cross-device message ingress.

`clawtool peer send <remote-peer>` on one device relays the message to the target daemon's /v1/relay over the LAN, authenticated with the shared circle key (X-Clawtool-Circle, same gate /v1/agents uses). Here we enforce the operator's chosen trust model — "circle key + first-contact pairing approval": a sender we haven't paired with is recorded pending (with a short code) and the message is REFUSED (202, pairing_required) until the receiving operator approves it via `clawtool peer pair approve <code>`. Once approved, the message is delivered into every local agent's inbox so whatever agent is live here surfaces it on its next prompt.

Package server starts the clawtool MCP server.

Per ADR-004, clawtool exposes itself as one MCP server over stdio. Per ADR-006, core tools use PascalCase names (Bash, Read, Edit, ...). Per ADR-008, configured sources spawn as child MCP servers and their tools are aggregated under `<instance>__<tool>` wire names.

Boot order on every `clawtool serve`:

  1. Load config + secrets.
  2. Build sources.Manager and start each configured source. Failures on individual sources are non-fatal; their tools just don't show up.
  3. Build a search.Index from descriptors of every tool we plan to register: enabled core tools + ToolSearch + aggregated source tools. This index powers the ToolSearch primitive — see ADR-005 for why search-first is the prerequisite that lets a 50+ tool catalog scale.
  4. Register all tools on the parent MCP server. ToolSearch closes over the index reference; aggregated source-tool handlers route via the manager.

Index

Constants

View Source
const (
	HeaderMcpMethod = "Mcp-Method"
	HeaderMcpName   = "Mcp-Name"
)

HTTP header names. Exported so callers building requests can reference them without re-declaring the constants.

Variables

This section is empty.

Functions

func BuildMCPRequest added in v0.22.116

func BuildMCPRequest(ctx context.Context, url string, body []byte) (*http.Request, error)

BuildMCPRequest constructs an *http.Request for a Streamable HTTP MCP POST with SEP-2243 headers populated from `body`.

  • body must be a JSON-encoded JSON-RPC request (object). Method + (when applicable) params.name are extracted by re-parsing — caller doesn't have to pass them separately.
  • Mcp-Method is always set when the body has a non-empty method field.
  • Mcp-Name is set only when the method is `tools/call` or `prompts/get` AND params.name is non-empty. For any other method, the header is OMITTED entirely (not sent as empty-string) — this matches the spec wording and keeps proxy rule-matching unambiguous.
  • Content-Type is always set to application/json so the caller doesn't have to remember.

Returns a request ready to hand to http.Client.Do.

func InitTokenFile added in v0.20.0

func InitTokenFile(path string) (string, error)

InitTokenFile generates a fresh 32-byte (256-bit) hex token and writes it to path with 0600. Used by `clawtool serve init-token` and by tests that need a working credential.

func IsLoopbackAddress added in v0.22.157

func IsLoopbackAddress(addr string) bool

IsLoopbackAddress returns true when addr is safe to treat as a loopback bind for the --allow-lan guard. Recognised loopback forms:

  • "127.0.0.1:<port>" — IPv4 loopback (explicit)
  • "[::1]:<port>" — IPv6 loopback (explicit)
  • "localhost:<port>" — name resolution to loopback
  • ":<port>" — bare wildcard. Technically binds to 0.0.0.0 (all interfaces) but the historical clawtool convention (CLI default, e2e tests, internal/daemon) is to treat it as "the local listener" — the actual exposure surface depends on the host's firewall, and the daemon's other safety layers (bearer token, single-user mode) cover the LAN- reachability case. Pass an explicit "0.0.0.0:<port>" to trigger the --allow-lan guard.

Any other form (including non-loopback IPs and explicit 0.0.0.0 / [::] wildcards) returns false.

func MCPMethodFromContext added in v0.22.116

func MCPMethodFromContext(ctx context.Context) string

MCPMethodFromContext returns the JSON-RPC method resolved by the middleware (body wins on mismatch). Empty string when the middleware did not run or the body was unparseable.

func MCPNameFromContext added in v0.22.116

func MCPNameFromContext(ctx context.Context) string

MCPNameFromContext returns the sub-target (tool or prompt name) for tools/call / prompts/get. Empty string for other methods or when params.name is missing.

func ServeHTTP added in v0.20.0

func ServeHTTP(ctx context.Context, opts HTTPOptions) error

ServeHTTP runs clawtool as an HTTP gateway. Blocks until the listener returns. Mirrors ServeStdio's lifecycle: build the MCP server (so the same agents/recipes/tools are available), then route HTTP requests through it.

MCP-over-HTTP (`--mcp-http`) mounts the full toolset at /mcp via mark3labs/mcp-go's StreamableHTTPServer (the persistent shared daemon every host fans into; see internal/daemon).

func ServeStdio

func ServeStdio(ctx context.Context) error

ServeStdio runs clawtool as an MCP server speaking over stdio. It blocks until stdin closes (the conventional MCP shutdown signal) or an unrecoverable error occurs.

Types

type HTTPOptions added in v0.20.0

type HTTPOptions struct {
	Listen    string // ":8080" or "0.0.0.0:8080" — passed to http.ListenAndServe.
	TokenFile string // path to a 0600 file containing the bearer token. Refused if missing/empty unless NoAuth is set.
	MCPHTTP   bool   // when true, mount the MCP toolset at /mcp via mcp-go's Streamable HTTP transport.

	// NoAuth runs the listener without bearer-token enforcement. The
	// shared local daemon flips this on by default — the operator's
	// machine is the trust boundary, codex / gemini hit /mcp over
	// loopback without pre-setting CLAWTOOL_TOKEN. Daemon / relay
	// deployments (multi-user, exposed beyond loopback) keep auth on
	// by leaving NoAuth false and supplying TokenFile.
	NoAuth bool

	// AllowLAN gates --listen 0.0.0.0 (or any non-loopback bind).
	// Default false: ParseServeFlags refuses to bind to a non-
	// loopback address without this flag, on the operator-foot-gun
	// reasoning that someone passing --listen 0.0.0.0 may not
	// realise it exposes the MCP surface to every device on the
	// LAN. Setting AllowLAN=true is an explicit "I know what I'm
	// doing" acknowledgement.
	AllowLAN bool

	// SkipFirewallSetup suppresses the auto-firewall hook the
	// daemon otherwise fires on the first Windows --listen 0.0.0.0
	// --allow-lan run. The hook triggers a single UAC prompt and
	// installs the clawtool-mdns-inbound + clawtool-gateway-inbound
	// rules into Windows Defender Firewall so LAN peer discovery
	// works without manual New-NetFirewallRule recipes. Operators
	// on ops-controlled hosts (GPO / MDM-managed firewall) flip
	// this on to keep the daemon out of the rule store.
	SkipFirewallSetup bool

	// SkipNetworkingSetup suppresses the auto-networking hook
	// the daemon otherwise fires on the first WSL2 --listen
	// 0.0.0.0 --allow-lan run. The hook detects WSL2 + checks
	// .wslconfig, then prompts the operator (default Yes) to
	// add networkingMode=mirrored under [wsl2] so LAN peer
	// discovery via mDNS works without NAT bridging. Operators
	// who manage .wslconfig themselves (or run a corporate WSL
	// where the file is owned by IT) flip this on.
	SkipNetworkingSetup bool
}

HTTPOptions configures the listener.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL