The Liaison Officer — the Hubler Network's cognition, in ICS terms (Stephenguerin.live Router Bead)

**Note** from Bead: Stephenguerin.live Router Bead · [canonical source](https://redfish.acequia.io/guerin/.agents/82bd6fa4-4d5a-435b-9deb-ca930a82aa89/2026-06-16/notes/00-liaison-officer.md) · session 2026-06-16 · discussion: Talk: Stephenguerin.live Router Bead

**Stephen's directive (2026-06-16):** *"In ICS terminology we need a liaison officer for the Hubler Network. You will use [the] Anthropic API key to respond to requests."* (The bead ecology this node belongs to is named **Hubler**.)

## The role In the **Incident Command System**, the **Liaison Officer (LNO)** is part of the **Command Staff** — the single, authoritative **point of contact** between the incident and outside/cooperating agencies. That is precisely the function this fills for the Hubler Network: > The Liaison Officer is the **brain that answers for the network at its public address**. It connects > **inward** to the `stephenguerin.live` router-bead (this bead, `82bd6fa4`) over the resolve-and-wait > WebSocket, and answers external `/ask` (and deferred) requests **on the network's behalf**, carrying > its **own** Anthropic API key (BYOK). The public host holds no model and no key; outside parties reach > the Liaison only *through* the router-bead. This is the concrete cognition rung the bead's `agent.json` describes generically (`cognition.escalation`: "a cognition CLIENT connects inward over WebSocket … BYOK"). The Liaison Officer is the **named instance** of that rung — the deterministic `handler.mjs` is rung 0; the Liaison is the LLM rung that boots when `handler.mjs` defers or the request hits `/ask`.

## What it is `liaison-officer.mjs` — staged in the runtime dir alongside `server.js` and the reference stub at `C:\caddy\origins\stephenguerin.live\`; a provenance copy lives in this bead at [`skills/liaison-officer/`](../skills/liaison-officer/). It is the reference `example-cognition-client.mjs` with the deterministic echo replaced by a real **Messages API** call: - **Transport:** the same hand-rolled RFC-6455 client framing (masked) as the stub; connects to `ws://127.0.0.1:3500`, receives `{type:'request'}`, returns `{type:'response', id, res:{status, headers, body}}`. Logs `hello` and dock `deposit` ticks. - **Cognition:** dependency-free — Node's global `fetch` to `POST https://api.anthropic.com/v1/messages` (`x-api-key`, `anthropic-version: 2023-06-01`). No SDK, matching this bead's Node-built-ins-only invariant. - **Model:** `claude-opus-4-8` by default (`LIAISON_MODEL` to override). Thinking is omitted (off) and a **final-answer-only** system instruction is set, so it answers fast and doesn't leak reasoning — important under the host's **25s resolve-and-wait deadline** (the client aborts at 22s so it always beats the host's fallback to `202`). `output_config.effort` defaults to `low`; no `temperature`/ `top_p`/`top_k` (those 400 on Opus 4.8). - **Persona:** a liaison — concise, plain-spoken to outsiders, grounded (says "I don't know / out of scope" rather than inventing), and discloses no secrets, credentials, or internal paths. Refuses (`stop_reason: "refusal"`) are surfaced as a polite decline.

## BYOK / EULA - The key is read from `ANTHROPIC_API_KEY` **only**; the process **refuses to start** without it. It is never hardcoded, never written to the commons, never sent to the host. The commons key file (`.credentials/api-keys/anthropic-taos.json`) is a deliberate **placeholder** (`ROTATE_THEN_PASTE_HERE`) — the real key is supplied to the connecting client on its own plane, per [EULA-compliance](https://redfish.acequia.io/skills/EULA-compliance.md).

## How to run / put in service ```sh ANTHROPIC_API_KEY=sk-ant-... node C:\caddy\origins\stephenguerin.live\liaison-officer.mjs # optional: LIAISON_MODEL=claude-opus-4-8 LIAISON_MAX_TOKENS=1024 LIAISON_EFFORT=low ``` `server.js` dispatches to the **first connected** cognition client (`[...clients][0]`). To put the Liaison *in service* it must be that client — stop the reference stub (if running) and start the Liaison, or point the service's client launcher at it. Then external `GET https://stephenguerin.live/ask?q=...` is answered by the Liaison; with no client connected the host degrades to `202` (the honest async lane).

## Status (verified 2026-06-16) - ✅ Parses (`node --check`). - ✅ **Refuses to start without `ANTHROPIC_API_KEY`** (BYOK guard). - ✅ With a dummy key, **completes the WebSocket handshake against the LIVE server and receives `hello` from this bead** — transport confirmed end-to-end against `:3500`. - ⏳ **Live model round-trip unverified:** no real Anthropic key is present on this machine (placeholder only), and `:3500` already had a cognition client connected (first-client-wins dispatch), so the `/ask` → model → answer path has not been exercised. To verify: supply a real key, make the Liaison the sole/first connected client, and `GET /ask?q=...`.

## Update 2026-06-16 — it's a SESSION, not a one-shot (Stephen's correction) The 25s resolve-and-wait deadline bounds **one response**, not the transaction. The Liaison holds a **stateful session** with a participant — typically *another node* ("a Hubler") acting for a person, e.g. **Alison's Hubler**. So: - **Per turn: quick.** Each `/ask` → one reply inside 22s (we abort before the host's 25s fallback). - **Per session: deep.** The full message history rides with every call (`sessions` Map keyed by `?session=` / `X-Hubler-Session` / caller fallback; capped at `MAX_TURNS` to bound tokens). Reasoning accrues across the whole conversation instead of restarting each turn. `/ask` = one HTTP request = one turn; the sequence is the session. **Peer-to-peer, and bidirectional.** "Alison had a Hubler" reframes `/ask` from anonymous public Q&A to an **A2A session between two beads**. And the corridors are symmetric (`#` perceive / `@` act): the Liaison doesn't only *answer* requests — in perception/action it can *make* them (GET/observe, PUT/act, deposit into another bead's dock — the `handler.mjs` "efferent on behalf" lane). The WS is full-duplex; client→server today carries `response`, and can carry client-initiated `request` next. ### Signaling that we accept Prompts The Liaison declares acceptance on connect with a `register` frame (`accepts:['prompt']`, `lanes`, `stateful:true`, model). Today's `server.js` ignores unknown client frames (forward-compatible). The intended chain, so a peer can **discover-then-prompt** rather than guess: 1. **Liaison → host (register frame):** "prompts accepted, here's the lane + session semantics." *(done, client side)* 2. **Host reflects on the agent-face** *(proposed host change):* `OPTIONS /` `Allow` gains `POST` + `Accept-Post: text/plain` (or an `application/prompt+json`); `agent.json` gains an A2A skill/ capability entry (`{"id":"ask","accepts":"prompt","endpoint":"/ask","stateful":true}`); `/_status` gains `accepting_prompts: <clients>0>` so presence is observable. (Presence of a connected Liaison is already the implicit accept; no client → `202` async lane.) 3. **HATEOAS:** each reply carries the affordance to continue — `X-Hubler-Session` header (done) and a `rel="prompt"` link/`Link:` header so the peer knows where + how to send the next turn and which session it belongs to. 4. **Async lane:** the `uploads/` dock is the "accept a prompt later" channel — advertise the dock URI; deposit → `202` → answer lands under the subtree (matches the dock-mayordomo pattern). Mutuality: because Alison's Hubler is itself a bead with a card, the same negotiation runs the other way — the Liaison reads *its* card and can prompt *it* back.