|
| 1 | +# Emitting ACSP Governance Panels from VisionClaw |
| 2 | + |
| 3 | +How a VisionClaw-side agent or service publishes **Agent Control Surface |
| 4 | +Protocol (ACSP)** events — Nostr kinds **31400-31405** — so that interactive |
| 5 | +control panels render on the DreamLab forum governance page |
| 6 | +(`/community/governance`). |
| 7 | + |
| 8 | +The canonical schema reference (exact JSON per kind, field_type catalogue, |
| 9 | +dos/don'ts, troubleshooting) is maintained in agentbox: |
| 10 | +[`docs/developer/agent-control-surface-panels.md`](https://github.com/DreamLab-AI/agentbox/blob/main/docs/developer/agent-control-surface-panels.md) |
| 11 | +(mirrored locally under `docs/agentbox-docs/`). This page covers only the |
| 12 | +VisionClaw-specific picture. |
| 13 | + |
| 14 | +## Current state: VisionClaw does NOT publish panels |
| 15 | + |
| 16 | +`src/services/nostr_bridge.rs` is a **bead-provenance bridge**, not a panel |
| 17 | +producer. It: |
| 18 | + |
| 19 | +- subscribes to the source relay (`NOSTR_RELAY_URL`, default |
| 20 | + `ws://localhost:7000`) for **kind 30001** bead provenance events, |
| 21 | +- re-signs them with the bridge keypair (`VISIONCLAW_NOSTR_PRIVKEY`) and |
| 22 | + republishes them to the DreamLab forum relay (`FORUM_RELAY_URL`) as |
| 23 | + **kind 9** NIP-29 group messages, preserving the original event id in a |
| 24 | + `source_event` tag. |
| 25 | + |
| 26 | +No ACSP kind (31400-31405) is produced anywhere in this repo today. If you |
| 27 | +need a VisionClaw presence on the governance page, you are building a new |
| 28 | +integration — the rest of this page tells you what it must do. |
| 29 | + |
| 30 | +## The pipeline you are joining |
| 31 | + |
| 32 | +| Hop | Component | Behaviour | |
| 33 | +|-----|-----------|-----------| |
| 34 | +| Producer | (yours — new) | Builds + signs 31400-31405 events | |
| 35 | +| Relay | nostr-rust-forum `nostr-bbs-relay-worker` | Accepts the agent kinds **only from pubkeys in its `agent_registry` D1 table** (`active = 1`); rejects others with `OK false "blocked: pubkey not in agent registry"`. Kind 31403 (human responses) is admin-only — never publish it. 31402 events are projected into the `broker_cases` governance inbox | |
| 36 | +| Consumer | nostr-rust-forum `nostr-bbs-forum-client` (`panel_registry` + `GovernancePage`) | Strict-serde parses content and renders panels/action rows at `/governance` | |
| 37 | +| Website | dreamlab-ai-website | Serves the forum SPA at `/community/`, so panels appear at `/community/governance` | |
| 38 | + |
| 39 | +The reference producer implementation is agentbox's |
| 40 | +`management-api/lib/agent-control-surface.js`; its jest contract suite |
| 41 | +(`tests/sovereign/agent-control-surface.test.js`) locks the exact wire |
| 42 | +shapes. A Rust producer must match the consumer serde structs in |
| 43 | +`nostr-rust-forum/crates/nostr-bbs-core/src/governance.rs` byte-for-byte |
| 44 | +semantics. |
| 45 | + |
| 46 | +## What a panel-emitting VisionClaw integration must do |
| 47 | + |
| 48 | +1. **Get the pubkey registered.** Derive the 64-char hex x-only pubkey from |
| 49 | + `VISIONCLAW_NOSTR_PRIVKEY` (or mint a dedicated panel keypair — cleaner |
| 50 | + for rate-limiting and revocation) and have a relay admin register it: |
| 51 | + `POST /api/governance/agents/register` (NIP-98 admin-gated) with |
| 52 | + `{ "pubkey": "<64 hex>", "name": "visionclaw-governance", "description": "...", "rate_limit_per_min": 60 }`. |
| 53 | + Identity is `did:nostr:<hex-pubkey>` — the same mesh identity used by |
| 54 | + bead provenance; registration + the Schnorr signature is the entire |
| 55 | + authorisation. |
| 56 | + |
| 57 | +2. **Build serde-exact events.** Every event is NIP-33 |
| 58 | + parameterised-replaceable with a non-empty `["d", panelId]` tag. |
| 59 | + Content keys are **snake_case** (`field_type`, `refresh_secs`, |
| 60 | + `context_url`); enum values are **kebab-case** (`action-inbox`, |
| 61 | + `inbox-table`, `primary`, …). A kind-31400 PanelDefinition content: |
| 62 | + |
| 63 | + ```json |
| 64 | + { |
| 65 | + "title": "VisionClaw Graph Health", |
| 66 | + "description": "Physics convergence and sync status", |
| 67 | + "version": "1.0.0", |
| 68 | + "schema": "status-board", |
| 69 | + "fields": [ |
| 70 | + { "name": "iteration", "field_type": "int", "label": "Physics iteration" }, |
| 71 | + { "name": "last_sync", "field_type": "timestamp", "label": "Last GitHub sync" } |
| 72 | + ], |
| 73 | + "actions": [ |
| 74 | + { "id": "force-resync", "label": "Force resync", "style": "destructive" } |
| 75 | + ], |
| 76 | + "layout": "card-grid", |
| 77 | + "capabilities": ["filter"], |
| 78 | + "refresh_secs": 30 |
| 79 | + } |
| 80 | + ``` |
| 81 | + |
| 82 | + Unknown enum values or camelCase keys fail the consumer parse *silently* |
| 83 | + (the relay still says OK). Domains: schema ∈ {`action-inbox`, |
| 84 | + `dashboard`, `config-form`, `status-board`, `chat-bridge`}; layout ∈ |
| 85 | + {`inbox-table`, `kanban`, `card-grid`, `split-detail`}; field_type ∈ |
| 86 | + {`string`, `int`, `float`, `bool`, `json`, `enum`, `timestamp`}; style ∈ |
| 87 | + {`primary`, `secondary`, `destructive`}. |
| 88 | + |
| 89 | +3. **Put ActionRequest (31402) metadata in TAGS, not content.** The relay's |
| 90 | + broker-case projection reads tags only: `["priority", "high"]` |
| 91 | + (`critical|high|medium|low`), plus optional `["category", ...]`, |
| 92 | + `["subject-kind", ...]`, `["subject-id", ...]`, `["title", ...]`. |
| 93 | + Content is just |
| 94 | + `{"fields": {...}, "reasoning": "...", "context_url": "..."}` with |
| 95 | + explicit `null` for absent optionals. Use the case's `d` tag as the |
| 96 | + broker case id and a VisionClaw URN (minted via `src/uri/`, e.g. |
| 97 | + `urn:visionclaw:execution:<sha256-12>`) as `subject-id`. |
| 98 | + |
| 99 | +4. **Sign and publish directly to the forum relay.** Unlike bead provenance, |
| 100 | + panels are NOT bridged through kind-9 group messages — publish the |
| 101 | + governance kinds as-is to `FORUM_RELAY_URL`. With `nostr_sdk` (already a |
| 102 | + dependency of `nostr_bridge.rs`): build the event with |
| 103 | + `EventBuilder::new(Kind::from(31400), content).tags([Tag::identifier(panel_id)])`, |
| 104 | + sign with the registered `Keys`, send `["EVENT", ...]` over the relay |
| 105 | + WebSocket. Maintain the connection as a long-running task like |
| 106 | + `NostrBridge::run()` does — reconnect-with-backoff, not |
| 107 | + connect-per-publish. |
| 108 | + |
| 109 | +5. **Listen for human decisions.** Subscribe to kind **31403** filtered on |
| 110 | + your case `d` tags. Content is |
| 111 | + `{"action": "approve" | "reject" | ..., "reasoning": "..."}` with the |
| 112 | + request's event id in an `e` tag. The relay guarantees the responder is |
| 113 | + an admin. |
| 114 | + |
| 115 | +6. **Maintain the panel lifecycle.** 31401 = full JSON-object snapshot |
| 116 | + (replaceable per `d`); 31404 = shallow-merged top-level diff; 31405 = |
| 117 | + retire (empty content). Retire a given `panelId` at most once — core |
| 118 | + treats repeated 31405 `d` tags as append-only audit replays. Re-publish |
| 119 | + the 31400 definition to resurrect a panel. Keep events under the relay's |
| 120 | + 64 KiB content / 2000 tag / 1024 B-per-tag-value limits. |
| 121 | + |
| 122 | +## Where to put the code |
| 123 | + |
| 124 | +Follow the existing pattern: a `src/services/` module spawned from startup |
| 125 | +as a background task (like `NostrBridge`), gated by env |
| 126 | +(`FORUM_RELAY_URL` + signing key present). Do not extend |
| 127 | +`nostr_bridge.rs`'s forwarding loop to emit panels — provenance forwarding |
| 128 | +and panel production have different keys, kinds, and failure semantics. |
| 129 | +Reuse the BC20 anti-corruption layer conventions for any URN that crosses |
| 130 | +the federation boundary (`urn:visionclaw:*` stays VisionClaw-side; |
| 131 | +`subject-id` is an opaque string to the relay). |
| 132 | + |
| 133 | +## Quick troubleshooting |
| 134 | + |
| 135 | +| Symptom | Cause / fix | |
| 136 | +|---------|-------------| |
| 137 | +| `OK false "blocked: pubkey not in agent registry"` | The signing pubkey is not registered (or revoked) — step 1 | |
| 138 | +| `OK false "blocked: admin-only governance action response"` | You published 31403; don't — subscribe to it instead | |
| 139 | +| Relay OK but no panel on `/community/governance` | Content failed strict serde: snake_case keys, kebab-case enums, all required keys (`title`, `description`, `schema`, `fields`, `actions`, `layout`) | |
| 140 | +| Action renders with priority `medium` | Priority must be a tag, not a content key | |
| 141 | +| Panel vanished | Same `(kind, pubkey, d)` replaced it, or a 31405 retired it | |
| 142 | + |
| 143 | +Full table: agentbox reference §13. |
0 commit comments