9dfa06ffee
Docker image / Build (linux/amd64) (push) Has been cancelled
Docker image / Build (linux/arm64) (push) Has been cancelled
Docker image / Merge release multi-arch manifest (push) Has been cancelled
Docker image / Merge debug multi-arch manifest (push) Has been cancelled
Docker image / Build public push gateway (linux/amd64) (push) Has been cancelled
Docker image / Build public push gateway (linux/arm64) (push) Has been cancelled
Docker image / Publish public push gateway image (push) Has been cancelled
Sprig image / Build (linux/amd64) (push) Has been cancelled
Sprig image / Build (linux/arm64) (push) Has been cancelled
Sprig image / Merge multi-arch manifest (push) Has been cancelled
Harbor Buzz Orchestra / Python tests and lint (push) Has been cancelled
CI / Detect Changed Paths (push) Has been cancelled
CI / Rust Lint (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Desktop Core (push) Has been cancelled
CI / Desktop Smoke E2E (1) (push) Has been cancelled
CI / Desktop Smoke E2E (2) (push) Has been cancelled
CI / Desktop Smoke E2E (3) (push) Has been cancelled
CI / Desktop Smoke E2E (4) (push) Has been cancelled
CI / Desktop (push) Has been cancelled
CI / Desktop E2E Relay (push) Has been cancelled
CI / Desktop E2E Integration (1/2) (push) Has been cancelled
CI / Desktop E2E Integration (2/2) (push) Has been cancelled
CI / Desktop E2E Integration (push) Has been cancelled
CI / Backend Integration (relay e2e) (push) Has been cancelled
CI / Relay E2E (push) Has been cancelled
CI / Web (push) Has been cancelled
CI / Mobile (push) Has been cancelled
CI / Security (push) Has been cancelled
CI / Dead Token Reference Guard (push) Has been cancelled
CI / Server Cross-Compile (aarch64-unknown-linux-musl) (push) Has been cancelled
CI / Server Cross-Compile (x86_64-unknown-linux-musl) (push) Has been cancelled
CI / Windows Rust (x86_64-pc-windows-msvc) (push) Has been cancelled
CI / Desktop Build (macOS) (push) Has been cancelled
helm chart / lint + unittest + render matrix (push) Has been cancelled
helm chart / install on kind (gated) (push) Has been cancelled
helm chart / publish chart to GHCR (push) Has been cancelled
Mesh Lifecycle / Relay-Driven Mesh Lifecycle Smoke (push) Has been cancelled
Sprig / Build (aarch64-unknown-linux-musl) (push) Has been cancelled
Sprig / Build (x86_64-unknown-linux-musl) (push) Has been cancelled
Sprig / Publish rolling release (push) Has been cancelled
Sprig / Publish tagged release (push) Has been cancelled
Signed-off-by: cls_宁波本机 <908705107@qq.com>
301 lines
9.7 KiB
Markdown
301 lines
9.7 KiB
Markdown
NIP-AO
|
||
======
|
||
|
||
Agent Observability
|
||
-------------------
|
||
|
||
`draft` `optional`
|
||
|
||
This NIP defines ephemeral, encrypted event kinds for streaming internal session telemetry between AI agent processes and their owners' desktop clients via Nostr relays.
|
||
|
||
## Motivation
|
||
|
||
AI agent harnesses execute long-running sessions that invoke tools, send protocol
|
||
frames to models, and emit intermediate reasoning. Owners need real-time visibility
|
||
into this activity for debugging, auditing, and control — without that telemetry
|
||
being stored on any relay or visible to third parties.
|
||
|
||
Kind 24200 provides a dedicated, encrypted, ephemeral channel for this purpose.
|
||
It is strictly scoped to the agent↔owner relationship and carries no durable state.
|
||
|
||
## Definitions
|
||
|
||
- **Agent**: An AI process with its own Nostr keypair, executing a session on behalf of an owner.
|
||
- **Owner**: The human (or system) whose pubkey the agent was provisioned under.
|
||
- **Observer Frame**: A single kind 24200 event carrying one unit of telemetry or control.
|
||
- **Session**: A bounded agent execution correlated by a shared `sessionId`.
|
||
|
||
## Event Kinds
|
||
|
||
| Kind | Name | Direction |
|
||
|-------|-----------------------|-------------------|
|
||
| 24200 | Agent Observer Frame | agent↔owner (both)|
|
||
|
||
Kind 24200 falls in the ephemeral range (20000–29999) defined by NIP-01. Relays
|
||
MUST NOT persist it.
|
||
|
||
## Event Structure
|
||
|
||
```json
|
||
{
|
||
"kind": 24200,
|
||
"pubkey": "<sender_pubkey>",
|
||
"created_at": <unix_timestamp>,
|
||
"content": "<NIP-44 v2 ciphertext>",
|
||
"tags": [
|
||
["p", "<recipient_pubkey>"],
|
||
["agent", "<agent_pubkey>"],
|
||
["frame", "telemetry" | "control"]
|
||
]
|
||
}
|
||
```
|
||
|
||
Events MUST have exactly one `p` tag, exactly one `agent` tag, and exactly one
|
||
`frame` tag.
|
||
|
||
**Telemetry** (agent → owner): `pubkey`=agent, `p`=owner, `agent`=agent.
|
||
**Control** (owner → agent): `pubkey`=owner, `p`=agent, `agent`=agent (target).
|
||
|
||
`frame` MUST be `"telemetry"` or `"control"`. Relays SHOULD silently drop events
|
||
with unrecognized `frame` values (returning OK to the publisher for forward
|
||
compatibility). Clients MUST ignore events with unrecognized `frame` values. An `h` tag MAY be included when the session runs within a NIP-29 group
|
||
context.
|
||
|
||
## Encryption
|
||
|
||
All `content` fields MUST be encrypted with NIP-44 v2 (XChaCha20-Poly1305 over a
|
||
secp256k1 ECDH shared secret).
|
||
|
||
- **Telemetry**: encrypted with `(agent_privkey, owner_pubkey)`
|
||
- **Control**: encrypted with `(owner_privkey, agent_pubkey)`
|
||
|
||
Plaintext SHOULD be zeroized from memory immediately after encrypt/decrypt.
|
||
Decrypted payload MUST NOT exceed 65,535 bytes.
|
||
|
||
## Decrypted Payload
|
||
|
||
### Telemetry (`frame=telemetry`)
|
||
|
||
The `content` field decrypts to an `ObserverEvent` JSON object:
|
||
|
||
```json
|
||
{
|
||
"seq": <monotonic_integer>,
|
||
"timestamp": "<rfc3339_string>",
|
||
"kind": "<frame_kind>",
|
||
"agentIndex": <integer> | null,
|
||
"channelId": "<channel_uuid>" | null,
|
||
"sessionId": "<session_id>" | null,
|
||
"turnId": "<turn_id>" | null,
|
||
"payload": { ... }
|
||
}
|
||
```
|
||
|
||
`seq`, `timestamp`, `kind`, and `payload` are REQUIRED. `agentIndex`, `channelId`, `sessionId`,
|
||
and `turnId` are OPTIONAL — they MAY be `null` when the value is not yet known
|
||
(e.g., `sessionId` before session establishment). Clients MUST handle `null` values
|
||
gracefully.
|
||
|
||
`seq` is monotonically increasing per session (drop detection). `timestamp` is an
|
||
RFC 3339 datetime string with sub-second precision (e.g., `"2026-04-29T12:00:41.500Z"`).
|
||
`agentIndex` identifies the agent in multi-agent scenarios. `sessionId`/`turnId`
|
||
correlate frames across a session and turn. `payload` is kind-specific (MAY be `{}`).
|
||
Unknown `kind` values MUST be ignored.
|
||
|
||
### Frame Kinds
|
||
|
||
| `kind` | Description |
|
||
|--------------------|----------------------------------------------------------|
|
||
| `acp_read` | Inbound ACP protocol frame (model → harness) |
|
||
| `acp_write` | Outbound ACP protocol frame (harness → model) |
|
||
| `turn_started` | A new agent turn has begun |
|
||
| `session_resolved` | Session completed or terminated |
|
||
|
||
### Control (`frame=control`)
|
||
|
||
The `content` field decrypts to:
|
||
|
||
```json
|
||
{
|
||
"type": "cancel_turn",
|
||
"channelId": "<channel_uuid>"
|
||
}
|
||
```
|
||
|
||
The only defined control type is `cancel_turn`. Implementations MUST ignore
|
||
events with unrecognized `type` values.
|
||
|
||
## Ephemerality Contract
|
||
|
||
- Relays MUST NOT persist kind 24200 events to any durable storage.
|
||
- Relays MUST NOT include kind 24200 events in search indexes.
|
||
- Relays MUST NOT include kind 24200 events in audit logs.
|
||
- Relays SHOULD fan out kind 24200 events only via in-memory pub/sub,
|
||
never via a database write path.
|
||
- Clients SHOULD subscribe with `since=<now>`; historical replay is not supported.
|
||
- Clients SHOULD buffer received events in a bounded in-memory ring buffer.
|
||
|
||
## Authorization
|
||
|
||
**Telemetry** (agent → owner):
|
||
- `event.pubkey` MUST equal the agent pubkey.
|
||
- `p` tag MUST equal the owner pubkey.
|
||
- Relay MUST verify `is_agent_owner(agent, owner)` via authenticated ownership lookup.
|
||
|
||
**Control** (owner → agent):
|
||
- `event.pubkey` MUST equal the owner pubkey.
|
||
- `p` tag MUST equal the agent pubkey.
|
||
- Relay MUST verify `is_agent_owner(agent, owner)` where agent is resolved from the
|
||
`agent` tag.
|
||
|
||
Both directions require relay confirmation of the agent-owner relationship via
|
||
database lookup. `#p` tag matching alone is insufficient. Unauthorized publish or
|
||
subscribe attempts MUST be rejected with `AUTH required`.
|
||
|
||
## Relay Behavior
|
||
|
||
On receiving a kind 24200 event, a relay MUST:
|
||
|
||
1. Validate the event signature per NIP-01.
|
||
2. Verify authorization per the rules above.
|
||
3. Fan out to matching subscribers via in-memory pub/sub.
|
||
4. NOT invoke the normal event ingestion or persistence path.
|
||
|
||
Relays SHOULD enforce a rate limit of 100 events/second per agent pubkey.
|
||
Relays are RECOMMENDED to reject events whose `created_at` falls outside a ±5-minute
|
||
freshness window to prevent replay of captured events.
|
||
|
||
## Client Behavior
|
||
|
||
Clients subscribe with:
|
||
|
||
```json
|
||
{"kinds": [24200], "#p": ["<own_pubkey>"], "since": <now>}
|
||
```
|
||
|
||
On receiving an event, a client MUST:
|
||
|
||
1. Verify the event signature.
|
||
2. Decrypt `content` using own secret key and `event.pubkey`.
|
||
3. Parse the decrypted payload and dispatch on `kind` (telemetry) or `type` (control).
|
||
4. Ignore unknown `kind`/`type` values.
|
||
|
||
Clients SHOULD verify that the `agent` tag matches a known/trusted agent pubkey
|
||
before decrypting.
|
||
|
||
Clients SHOULD buffer events in a bounded ring buffer (RECOMMENDED maximum: 800 events).
|
||
Clients MUST NOT request historical kind 24200 events (no `since` in the past, no
|
||
`until`, no `ids` queries).
|
||
|
||
## Security Considerations
|
||
|
||
**Metadata leakage.** Routing tags (`p`, `agent`, `frame`, `created_at`) are
|
||
cleartext. A relay operator can observe that agent X is streaming to owner Y at what
|
||
rate. For maximum metadata privacy, implementors MAY wrap events in NIP-59 gift wrap.
|
||
|
||
**No forward secrecy.** NIP-44 does not provide forward secrecy; compromise of the
|
||
agent's private key allows decryption of any captured ciphertext.
|
||
|
||
**Replay attacks.** A captured, signed event could be replayed without a freshness
|
||
check. Relays are RECOMMENDED to enforce a `created_at` freshness window.
|
||
|
||
**Rogue relays.** The ephemerality contract is relay policy, not cryptography.
|
||
NIP-44 encryption ensures stored events remain opaque to the relay operator absent
|
||
key compromise.
|
||
|
||
**Best-effort delivery.** Control frames can be dropped during reconnect or queue
|
||
overflow. Control commands SHOULD be treated as advisory with idempotent semantics.
|
||
Agents MUST NOT rely on guaranteed delivery of control frames.
|
||
|
||
**Operational persistence vectors.** Telemetry may transiently exist in process
|
||
memory, crash dumps, and application logs. Implementations SHOULD minimize logging
|
||
of decrypted payloads and MUST NOT log it at INFO level or above.
|
||
|
||
## Relationship to Other NIPs
|
||
|
||
- **NIP-01**: Kind 24200 is in the ephemeral range (20000–29999); standard event
|
||
structure and signature rules apply.
|
||
- **NIP-42**: Recommended for relay-side authentication gating.
|
||
- **NIP-44**: Required encryption algorithm for all `content` fields.
|
||
- **NIP-29**: An `h` tag MAY be included when the agent session is scoped to a
|
||
NIP-29 group.
|
||
- **NIP-XX (PR #2226)**: NIP-XX defines the agent *output* plane; this NIP defines
|
||
the *observability* plane (internal agent activity). They are complementary and
|
||
non-overlapping.
|
||
|
||
## Examples
|
||
|
||
### 1. Telemetry Event — `acp_write` frame
|
||
|
||
**Wire event (encrypted):**
|
||
|
||
```json
|
||
{
|
||
"id": "a1b2c3d4...",
|
||
"kind": 24200,
|
||
"pubkey": "agent_pubkey_hex",
|
||
"created_at": 1777464041,
|
||
"content": "<NIP-44 v2 ciphertext>",
|
||
"tags": [
|
||
["p", "owner_pubkey_hex"],
|
||
["agent", "agent_pubkey_hex"],
|
||
["frame", "telemetry"]
|
||
],
|
||
"sig": "..."
|
||
}
|
||
```
|
||
|
||
**Decrypted payload:**
|
||
|
||
```json
|
||
{
|
||
"seq": 42,
|
||
"timestamp": "2026-04-29T12:00:41.500Z",
|
||
"kind": "acp_write",
|
||
"agentIndex": 0,
|
||
"channelId": "52a85618-0f8f-4542-94ec-599e6e1c6f2e",
|
||
"sessionId": "a1b2c3d4",
|
||
"turnId": "e5f6g7h8",
|
||
"payload": {
|
||
"jsonrpc": "2.0",
|
||
"method": "tools/call",
|
||
"params": { "name": "shell", "arguments": { "command": "ls -la" } }
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. Control Event — `cancel_turn` frame
|
||
|
||
**Wire event (encrypted):**
|
||
|
||
```json
|
||
{
|
||
"id": "e5f6a7b8...",
|
||
"kind": 24200,
|
||
"pubkey": "owner_pubkey_hex",
|
||
"created_at": 1777464042,
|
||
"content": "<NIP-44 v2 ciphertext>",
|
||
"tags": [
|
||
["p", "agent_pubkey_hex"],
|
||
["agent", "agent_pubkey_hex"],
|
||
["frame", "control"]
|
||
],
|
||
"sig": "..."
|
||
}
|
||
```
|
||
|
||
**Decrypted payload:**
|
||
|
||
```json
|
||
{
|
||
"type": "cancel_turn",
|
||
"channelId": "52a85618-0f8f-4542-94ec-599e6e1c6f2e"
|
||
}
|
||
```
|
||
|
||
## Reference Implementation
|
||
|
||
[block/sprout PR #421](https://github.com/block/sprout/pull/421)
|