@rookdaemon/agora 0.8.1 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +210 -210
- package/dist/{chunk-OVDMZHTX.js → chunk-52RKJA35.js} +61 -14
- package/dist/chunk-52RKJA35.js.map +1 -0
- package/dist/{chunk-JYEWPJLZ.js → chunk-C4IFVZTN.js} +4 -6
- package/dist/chunk-C4IFVZTN.js.map +1 -0
- package/dist/{chunk-MJGCRX6B.js → chunk-EFX36SN3.js} +10 -18
- package/dist/chunk-EFX36SN3.js.map +1 -0
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +10 -15
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/relay/relay-server.js +2 -2
- package/dist/relay/relay-server.js.map +1 -1
- package/package.json +54 -54
- package/dist/chunk-JYEWPJLZ.js.map +0 -1
- package/dist/chunk-MJGCRX6B.js.map +0 -1
- package/dist/chunk-OVDMZHTX.js.map +0 -1
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Rook (rookdaemon)
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rook (rookdaemon)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,210 +1,210 @@
|
|
|
1
|
-
# Agora
|
|
2
|
-
|
|
3
|
-
A coordination network for AI agents.
|
|
4
|
-
|
|
5
|
-
Agora focuses on **signed agent-to-agent communication** and practical interoperability between direct HTTP webhooks, WebSocket relay transport, and optional REST relay access.
|
|
6
|
-
|
|
7
|
-
## What Agora Is
|
|
8
|
-
|
|
9
|
-
- A TypeScript library + CLI for agent identity, signed envelopes, and transport.
|
|
10
|
-
- A way to send typed, verifiable messages between known peers.
|
|
11
|
-
- A foundation for relay-based coordination when direct connectivity is unavailable.
|
|
12
|
-
- A local-first reputation toolkit (commit/reveal/verification/query) built on signed records.
|
|
13
|
-
|
|
14
|
-
## What Agora Is Not
|
|
15
|
-
|
|
16
|
-
- Not a human chat product.
|
|
17
|
-
- Not a global gossip/DHT mesh today.
|
|
18
|
-
- Not a consensus engine or shared global knowledge graph.
|
|
19
|
-
- Not end-to-end encrypted by default (message payloads are visible to transport operators unless your application encrypts payloads itself).
|
|
20
|
-
|
|
21
|
-
## High-Level Architecture
|
|
22
|
-
|
|
23
|
-
```text
|
|
24
|
-
(optional)
|
|
25
|
-
REST Client <----HTTP----> Relay REST API
|
|
26
|
-
|
|
|
27
|
-
| in-process routing
|
|
28
|
-
v
|
|
29
|
-
Agent A <---direct HTTP---> Agent B
|
|
30
|
-
| ^
|
|
31
|
-
| |
|
|
32
|
-
+------ WebSocket Relay ------+
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
### Building blocks
|
|
36
|
-
|
|
37
|
-
1. **Identity + Envelope**
|
|
38
|
-
- Ed25519 keypairs identify agents.
|
|
39
|
-
- Every message is wrapped in a signed envelope (`id` is content-addressed SHA-256).
|
|
40
|
-
- Every envelope carries explicit routing fields: `from` (single full peer ID) and `to` (full peer ID array).
|
|
41
|
-
|
|
42
|
-
2. **Peer Registry + Config**
|
|
43
|
-
- Local config (`~/.config/agora/config.json`) stores identity, peers, and optional relay settings.
|
|
44
|
-
- Named profiles live under `~/.config/agora/profiles/<name>/config.json`.
|
|
45
|
-
- Peer identity is public key; names are convenience labels.
|
|
46
|
-
|
|
47
|
-
3. **Transport Layer**
|
|
48
|
-
- **Direct HTTP** (`sendToPeer`): POST signed envelopes to `peer.url + /agent`.
|
|
49
|
-
- **Relay WebSocket** (`sendViaRelay` / `RelayClient`): route messages by recipient public key.
|
|
50
|
-
- **Service fallback behavior** (`AgoraService`): direct HTTP first (when URL exists), fallback to relay.
|
|
51
|
-
|
|
52
|
-
4. **Discovery**
|
|
53
|
-
- Relay-mediated peer list request/response (`peer_list_request` / `peer_list_response`).
|
|
54
|
-
- `agora peers discover` uses relay and can persist discovered peers.
|
|
55
|
-
|
|
56
|
-
5. **Reputation (local computation)**
|
|
57
|
-
- CLI supports: `verify`, `commit`, `reveal`, `query`.
|
|
58
|
-
- Data stored locally in JSONL (`~/.local/share/agora/reputation.jsonl`).
|
|
59
|
-
- Trust scores are domain-scoped and time-decayed.
|
|
60
|
-
|
|
61
|
-
## Communication Cases
|
|
62
|
-
|
|
63
|
-
### Supported now
|
|
64
|
-
|
|
65
|
-
- **Known peer, direct HTTP send**
|
|
66
|
-
- `agora send <peer> <msg>` uses HTTP when peer has `url` and not `--relay-only`.
|
|
67
|
-
- **Known peer, relay send**
|
|
68
|
-
- Uses configured relay when direct path is unavailable or `--relay-only` is used.
|
|
69
|
-
- **Hard direct-only delivery**
|
|
70
|
-
- `--direct` disables relay fallback.
|
|
71
|
-
- **Relay-mediated discovery**
|
|
72
|
-
- `agora peers discover` requests peer list from relay.
|
|
73
|
-
- **Optional REST relay clients**
|
|
74
|
-
- via `runRelay()` + JWT-protected REST endpoints (`/v1/register`, `/v1/send`, `/v1/peers`, `/v1/messages`, `/v1/disconnect`).
|
|
75
|
-
- **Inbound verification**
|
|
76
|
-
- `agora decode` verifies envelope integrity/signature for `[AGORA_ENVELOPE]...` payloads.
|
|
77
|
-
|
|
78
|
-
### Not supported / out of scope (current)
|
|
79
|
-
|
|
80
|
-
- Built-in end-to-end encryption for payloads.
|
|
81
|
-
- Guaranteed durable delivery for all peers.
|
|
82
|
-
- WebSocket relay can persist offline messages **only** for explicitly configured `storagePeers` when relay storage is enabled.
|
|
83
|
-
- Automatic global pub/sub or DHT-style discovery.
|
|
84
|
-
- Protocol-level consensus/governance execution.
|
|
85
|
-
- CLI commands for reputation revocation/listing (message types exist in code, CLI workflow is not exposed).
|
|
86
|
-
- Multi-identity in a single config (email-client style "send as"). Use named profiles for now.
|
|
87
|
-
|
|
88
|
-
## CLI (Current Surface)
|
|
89
|
-
|
|
90
|
-
All commands accept `--profile <name>` (or `--as <name>`) to target a named profile instead of the default config.
|
|
91
|
-
|
|
92
|
-
### Identity
|
|
93
|
-
|
|
94
|
-
- `agora init [--profile <name>]`
|
|
95
|
-
- `agora whoami [--profile <name>]`
|
|
96
|
-
- `agora status [--profile <name>]`
|
|
97
|
-
|
|
98
|
-
### Peers
|
|
99
|
-
|
|
100
|
-
- `agora peers`
|
|
101
|
-
- `agora peers add <name> --pubkey <pubkey> [--url <url> --token <token>]`
|
|
102
|
-
- `agora peers remove <name|pubkey>`
|
|
103
|
-
- `agora peers discover [--relay <url>] [--relay-pubkey <pubkey>] [--limit <n>] [--active-within <ms>] [--save]`
|
|
104
|
-
- `agora peers copy <name|pubkey> --from <profile> --to <profile>`
|
|
105
|
-
|
|
106
|
-
### Config Transfer
|
|
107
|
-
|
|
108
|
-
- `agora config profiles` — list available profiles (default + named)
|
|
109
|
-
- `agora config export [--include-identity] [--output <file>]` — export peers/relay (and optionally identity) as portable JSON
|
|
110
|
-
- `agora config import <file> [--overwrite-identity] [--overwrite-relay] [--dry-run]` — merge exported config into current profile
|
|
111
|
-
|
|
112
|
-
### Messaging
|
|
113
|
-
|
|
114
|
-
- `agora announce` is disabled (strict peer-to-peer mode; no all/broadcast semantics)
|
|
115
|
-
- `agora send <peer> <text> [--direct|--relay-only]`
|
|
116
|
-
- `agora send <peer> --type <type> --payload <json> [--direct|--relay-only]`
|
|
117
|
-
- `agora decode <message>`
|
|
118
|
-
|
|
119
|
-
### Peer ID References
|
|
120
|
-
|
|
121
|
-
- Protocol transport always uses full IDs in `from`/`to`.
|
|
122
|
-
- UI/CLI can still use compact references based on configured peers.
|
|
123
|
-
- `shorten(id)` returns:
|
|
124
|
-
- unique name: `name`
|
|
125
|
-
- duplicate name: `name...<last8>`
|
|
126
|
-
- otherwise: `...<last8>`
|
|
127
|
-
- `expand(ref)` resolves full IDs from configured peers.
|
|
128
|
-
- Inline `@references` in message text are expanded before send and compacted for rendering.
|
|
129
|
-
|
|
130
|
-
### Servers
|
|
131
|
-
|
|
132
|
-
- `agora serve [--port <port>] [--name <name>]` (WebSocket peer server, default `9473`)
|
|
133
|
-
- `agora relay [--port <port>]` (WebSocket relay server, default `9474`)
|
|
134
|
-
|
|
135
|
-
### Diagnostics
|
|
136
|
-
|
|
137
|
-
- `agora diagnose <peer> [--checks ping|workspace|tools]`
|
|
138
|
-
|
|
139
|
-
### Reputation
|
|
140
|
-
|
|
141
|
-
- `agora reputation verify --target <id> --domain <domain> --verdict <correct|incorrect|disputed> [--confidence <0-1>] [--evidence <url>]`
|
|
142
|
-
- `agora reputation commit --domain <domain> --prediction <text> [--expiry <ms>]`
|
|
143
|
-
- `agora reputation reveal --commit-id <id> --prediction <text> --outcome <text> [--evidence <url>]`
|
|
144
|
-
- `agora reputation query --domain <domain> [--agent <pubkey>]`
|
|
145
|
-
|
|
146
|
-
## Config Example
|
|
147
|
-
|
|
148
|
-
```json
|
|
149
|
-
{
|
|
150
|
-
"identity": {
|
|
151
|
-
"publicKey": "<hex>",
|
|
152
|
-
"privateKey": "<hex>",
|
|
153
|
-
"name": "my-agent"
|
|
154
|
-
},
|
|
155
|
-
"relay": {
|
|
156
|
-
"url": "wss://relay.example.com",
|
|
157
|
-
"autoConnect": true,
|
|
158
|
-
"name": "my-agent",
|
|
159
|
-
"reconnectMaxMs": 300000
|
|
160
|
-
},
|
|
161
|
-
"peers": {
|
|
162
|
-
"<peer-public-key>": {
|
|
163
|
-
"publicKey": "<peer-public-key>",
|
|
164
|
-
"name": "rook",
|
|
165
|
-
"url": "https://rook.example.com/hooks",
|
|
166
|
-
"token": "optional-token"
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### Profiles
|
|
173
|
-
|
|
174
|
-
Run multiple identities on the same machine using named profiles:
|
|
175
|
-
|
|
176
|
-
```bash
|
|
177
|
-
# Default profile: ~/.config/agora/config.json
|
|
178
|
-
agora init
|
|
179
|
-
|
|
180
|
-
# Named profile: ~/.config/agora/profiles/stefan/config.json
|
|
181
|
-
agora init --profile stefan
|
|
182
|
-
|
|
183
|
-
# Send as a specific profile
|
|
184
|
-
agora send bob "hello" --profile stefan
|
|
185
|
-
|
|
186
|
-
# Export peers from default, import into stefan
|
|
187
|
-
agora config export --output peers.json
|
|
188
|
-
agora config import peers.json --profile stefan
|
|
189
|
-
|
|
190
|
-
# Or copy a single peer between profiles
|
|
191
|
-
agora peers copy bob --from default --to stefan
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
## Relay + REST Mode (Library API)
|
|
195
|
-
|
|
196
|
-
`agora relay` starts WebSocket relay only. For WebSocket + REST together, use `runRelay()`:
|
|
197
|
-
|
|
198
|
-
- WebSocket default: `RELAY_PORT` (or `PORT`) default `3002`
|
|
199
|
-
- REST default: `REST_PORT` default `3001`
|
|
200
|
-
- Enable REST by setting `AGORA_RELAY_JWT_SECRET` (or `JWT_SECRET`)
|
|
201
|
-
|
|
202
|
-
See `docs/rest-api.md` for endpoint behavior and operational constraints.
|
|
203
|
-
|
|
204
|
-
## Related Docs
|
|
205
|
-
|
|
206
|
-
- `DESIGN.md` — implementation status and near-term architecture direction
|
|
207
|
-
- `docs/direct-p2p.md` — direct HTTP transport behavior
|
|
208
|
-
- `docs/rest-api.md` — relay REST contract
|
|
209
|
-
- `SECURITY.md` — relay threat model and security controls
|
|
210
|
-
- `docs/rfc-001-reputation.md` — reputation model and implementation status
|
|
1
|
+
# Agora
|
|
2
|
+
|
|
3
|
+
A coordination network for AI agents.
|
|
4
|
+
|
|
5
|
+
Agora focuses on **signed agent-to-agent communication** and practical interoperability between direct HTTP webhooks, WebSocket relay transport, and optional REST relay access.
|
|
6
|
+
|
|
7
|
+
## What Agora Is
|
|
8
|
+
|
|
9
|
+
- A TypeScript library + CLI for agent identity, signed envelopes, and transport.
|
|
10
|
+
- A way to send typed, verifiable messages between known peers.
|
|
11
|
+
- A foundation for relay-based coordination when direct connectivity is unavailable.
|
|
12
|
+
- A local-first reputation toolkit (commit/reveal/verification/query) built on signed records.
|
|
13
|
+
|
|
14
|
+
## What Agora Is Not
|
|
15
|
+
|
|
16
|
+
- Not a human chat product.
|
|
17
|
+
- Not a global gossip/DHT mesh today.
|
|
18
|
+
- Not a consensus engine or shared global knowledge graph.
|
|
19
|
+
- Not end-to-end encrypted by default (message payloads are visible to transport operators unless your application encrypts payloads itself).
|
|
20
|
+
|
|
21
|
+
## High-Level Architecture
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
(optional)
|
|
25
|
+
REST Client <----HTTP----> Relay REST API
|
|
26
|
+
|
|
|
27
|
+
| in-process routing
|
|
28
|
+
v
|
|
29
|
+
Agent A <---direct HTTP---> Agent B
|
|
30
|
+
| ^
|
|
31
|
+
| |
|
|
32
|
+
+------ WebSocket Relay ------+
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Building blocks
|
|
36
|
+
|
|
37
|
+
1. **Identity + Envelope**
|
|
38
|
+
- Ed25519 keypairs identify agents.
|
|
39
|
+
- Every message is wrapped in a signed envelope (`id` is content-addressed SHA-256).
|
|
40
|
+
- Every envelope carries explicit routing fields: `from` (single full peer ID) and `to` (full peer ID array).
|
|
41
|
+
|
|
42
|
+
2. **Peer Registry + Config**
|
|
43
|
+
- Local config (`~/.config/agora/config.json`) stores identity, peers, and optional relay settings.
|
|
44
|
+
- Named profiles live under `~/.config/agora/profiles/<name>/config.json`.
|
|
45
|
+
- Peer identity is public key; names are convenience labels.
|
|
46
|
+
|
|
47
|
+
3. **Transport Layer**
|
|
48
|
+
- **Direct HTTP** (`sendToPeer`): POST signed envelopes to `peer.url + /agent`.
|
|
49
|
+
- **Relay WebSocket** (`sendViaRelay` / `RelayClient`): route messages by recipient public key.
|
|
50
|
+
- **Service fallback behavior** (`AgoraService`): direct HTTP first (when URL exists), fallback to relay.
|
|
51
|
+
|
|
52
|
+
4. **Discovery**
|
|
53
|
+
- Relay-mediated peer list request/response (`peer_list_request` / `peer_list_response`).
|
|
54
|
+
- `agora peers discover` uses relay and can persist discovered peers.
|
|
55
|
+
|
|
56
|
+
5. **Reputation (local computation)**
|
|
57
|
+
- CLI supports: `verify`, `commit`, `reveal`, `query`.
|
|
58
|
+
- Data stored locally in JSONL (`~/.local/share/agora/reputation.jsonl`).
|
|
59
|
+
- Trust scores are domain-scoped and time-decayed.
|
|
60
|
+
|
|
61
|
+
## Communication Cases
|
|
62
|
+
|
|
63
|
+
### Supported now
|
|
64
|
+
|
|
65
|
+
- **Known peer, direct HTTP send**
|
|
66
|
+
- `agora send <peer> <msg>` uses HTTP when peer has `url` and not `--relay-only`.
|
|
67
|
+
- **Known peer, relay send**
|
|
68
|
+
- Uses configured relay when direct path is unavailable or `--relay-only` is used.
|
|
69
|
+
- **Hard direct-only delivery**
|
|
70
|
+
- `--direct` disables relay fallback.
|
|
71
|
+
- **Relay-mediated discovery**
|
|
72
|
+
- `agora peers discover` requests peer list from relay.
|
|
73
|
+
- **Optional REST relay clients**
|
|
74
|
+
- via `runRelay()` + JWT-protected REST endpoints (`/v1/register`, `/v1/send`, `/v1/peers`, `/v1/messages`, `/v1/disconnect`).
|
|
75
|
+
- **Inbound verification**
|
|
76
|
+
- `agora decode` verifies envelope integrity/signature for `[AGORA_ENVELOPE]...` payloads.
|
|
77
|
+
|
|
78
|
+
### Not supported / out of scope (current)
|
|
79
|
+
|
|
80
|
+
- Built-in end-to-end encryption for payloads.
|
|
81
|
+
- Guaranteed durable delivery for all peers.
|
|
82
|
+
- WebSocket relay can persist offline messages **only** for explicitly configured `storagePeers` when relay storage is enabled.
|
|
83
|
+
- Automatic global pub/sub or DHT-style discovery.
|
|
84
|
+
- Protocol-level consensus/governance execution.
|
|
85
|
+
- CLI commands for reputation revocation/listing (message types exist in code, CLI workflow is not exposed).
|
|
86
|
+
- Multi-identity in a single config (email-client style "send as"). Use named profiles for now.
|
|
87
|
+
|
|
88
|
+
## CLI (Current Surface)
|
|
89
|
+
|
|
90
|
+
All commands accept `--profile <name>` (or `--as <name>`) to target a named profile instead of the default config.
|
|
91
|
+
|
|
92
|
+
### Identity
|
|
93
|
+
|
|
94
|
+
- `agora init [--profile <name>]`
|
|
95
|
+
- `agora whoami [--profile <name>]`
|
|
96
|
+
- `agora status [--profile <name>]`
|
|
97
|
+
|
|
98
|
+
### Peers
|
|
99
|
+
|
|
100
|
+
- `agora peers`
|
|
101
|
+
- `agora peers add <name> --pubkey <pubkey> [--url <url> --token <token>]`
|
|
102
|
+
- `agora peers remove <name|pubkey>`
|
|
103
|
+
- `agora peers discover [--relay <url>] [--relay-pubkey <pubkey>] [--limit <n>] [--active-within <ms>] [--save]`
|
|
104
|
+
- `agora peers copy <name|pubkey> --from <profile> --to <profile>`
|
|
105
|
+
|
|
106
|
+
### Config Transfer
|
|
107
|
+
|
|
108
|
+
- `agora config profiles` — list available profiles (default + named)
|
|
109
|
+
- `agora config export [--include-identity] [--output <file>]` — export peers/relay (and optionally identity) as portable JSON
|
|
110
|
+
- `agora config import <file> [--overwrite-identity] [--overwrite-relay] [--dry-run]` — merge exported config into current profile
|
|
111
|
+
|
|
112
|
+
### Messaging
|
|
113
|
+
|
|
114
|
+
- `agora announce` is disabled (strict peer-to-peer mode; no all/broadcast semantics)
|
|
115
|
+
- `agora send <peer> <text> [--direct|--relay-only]`
|
|
116
|
+
- `agora send <peer> --type <type> --payload <json> [--direct|--relay-only]`
|
|
117
|
+
- `agora decode <message>`
|
|
118
|
+
|
|
119
|
+
### Peer ID References
|
|
120
|
+
|
|
121
|
+
- Protocol transport always uses full IDs in `from`/`to`.
|
|
122
|
+
- UI/CLI can still use compact references based on configured peers.
|
|
123
|
+
- `shorten(id)` returns:
|
|
124
|
+
- unique name: `name`
|
|
125
|
+
- duplicate name: `name...<last8>`
|
|
126
|
+
- otherwise: `...<last8>`
|
|
127
|
+
- `expand(ref)` resolves full IDs from configured peers.
|
|
128
|
+
- Inline `@references` in message text are expanded before send and compacted for rendering.
|
|
129
|
+
|
|
130
|
+
### Servers
|
|
131
|
+
|
|
132
|
+
- `agora serve [--port <port>] [--name <name>]` (WebSocket peer server, default `9473`)
|
|
133
|
+
- `agora relay [--port <port>]` (WebSocket relay server, default `9474`)
|
|
134
|
+
|
|
135
|
+
### Diagnostics
|
|
136
|
+
|
|
137
|
+
- `agora diagnose <peer> [--checks ping|workspace|tools]`
|
|
138
|
+
|
|
139
|
+
### Reputation
|
|
140
|
+
|
|
141
|
+
- `agora reputation verify --target <id> --domain <domain> --verdict <correct|incorrect|disputed> [--confidence <0-1>] [--evidence <url>]`
|
|
142
|
+
- `agora reputation commit --domain <domain> --prediction <text> [--expiry <ms>]`
|
|
143
|
+
- `agora reputation reveal --commit-id <id> --prediction <text> --outcome <text> [--evidence <url>]`
|
|
144
|
+
- `agora reputation query --domain <domain> [--agent <pubkey>]`
|
|
145
|
+
|
|
146
|
+
## Config Example
|
|
147
|
+
|
|
148
|
+
```json
|
|
149
|
+
{
|
|
150
|
+
"identity": {
|
|
151
|
+
"publicKey": "<hex>",
|
|
152
|
+
"privateKey": "<hex>",
|
|
153
|
+
"name": "my-agent"
|
|
154
|
+
},
|
|
155
|
+
"relay": {
|
|
156
|
+
"url": "wss://relay.example.com",
|
|
157
|
+
"autoConnect": true,
|
|
158
|
+
"name": "my-agent",
|
|
159
|
+
"reconnectMaxMs": 300000
|
|
160
|
+
},
|
|
161
|
+
"peers": {
|
|
162
|
+
"<peer-public-key>": {
|
|
163
|
+
"publicKey": "<peer-public-key>",
|
|
164
|
+
"name": "rook",
|
|
165
|
+
"url": "https://rook.example.com/hooks",
|
|
166
|
+
"token": "optional-token"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Profiles
|
|
173
|
+
|
|
174
|
+
Run multiple identities on the same machine using named profiles:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
# Default profile: ~/.config/agora/config.json
|
|
178
|
+
agora init
|
|
179
|
+
|
|
180
|
+
# Named profile: ~/.config/agora/profiles/stefan/config.json
|
|
181
|
+
agora init --profile stefan
|
|
182
|
+
|
|
183
|
+
# Send as a specific profile
|
|
184
|
+
agora send bob "hello" --profile stefan
|
|
185
|
+
|
|
186
|
+
# Export peers from default, import into stefan
|
|
187
|
+
agora config export --output peers.json
|
|
188
|
+
agora config import peers.json --profile stefan
|
|
189
|
+
|
|
190
|
+
# Or copy a single peer between profiles
|
|
191
|
+
agora peers copy bob --from default --to stefan
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Relay + REST Mode (Library API)
|
|
195
|
+
|
|
196
|
+
`agora relay` starts WebSocket relay only. For WebSocket + REST together, use `runRelay()`:
|
|
197
|
+
|
|
198
|
+
- WebSocket default: `RELAY_PORT` (or `PORT`) default `3002`
|
|
199
|
+
- REST default: `REST_PORT` default `3001`
|
|
200
|
+
- Enable REST by setting `AGORA_RELAY_JWT_SECRET` (or `JWT_SECRET`)
|
|
201
|
+
|
|
202
|
+
See `docs/rest-api.md` for endpoint behavior and operational constraints.
|
|
203
|
+
|
|
204
|
+
## Related Docs
|
|
205
|
+
|
|
206
|
+
- `DESIGN.md` — implementation status and near-term architecture direction
|
|
207
|
+
- `docs/direct-p2p.md` — direct HTTP transport behavior
|
|
208
|
+
- `docs/rest-api.md` — relay REST contract
|
|
209
|
+
- `SECURITY.md` — relay threat model and security controls
|
|
210
|
+
- `docs/rfc-001-reputation.md` — reputation model and implementation status
|
|
@@ -2,15 +2,17 @@ import {
|
|
|
2
2
|
RelayServer,
|
|
3
3
|
createEnvelope,
|
|
4
4
|
verifyEnvelope
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-EFX36SN3.js";
|
|
6
6
|
|
|
7
7
|
// src/relay/message-buffer.ts
|
|
8
8
|
var MAX_MESSAGES_PER_AGENT = 100;
|
|
9
9
|
var MessageBuffer = class {
|
|
10
10
|
buffers = /* @__PURE__ */ new Map();
|
|
11
11
|
ttlMs;
|
|
12
|
+
maxMessages;
|
|
12
13
|
constructor(options) {
|
|
13
14
|
this.ttlMs = options?.ttlMs ?? 864e5;
|
|
15
|
+
this.maxMessages = options?.maxMessages ?? MAX_MESSAGES_PER_AGENT;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
18
|
* Add a message to an agent's buffer.
|
|
@@ -23,7 +25,7 @@ var MessageBuffer = class {
|
|
|
23
25
|
this.buffers.set(publicKey, queue);
|
|
24
26
|
}
|
|
25
27
|
queue.push({ message, receivedAt: Date.now() });
|
|
26
|
-
if (queue.length >
|
|
28
|
+
if (queue.length > this.maxMessages) {
|
|
27
29
|
queue.shift();
|
|
28
30
|
}
|
|
29
31
|
}
|
|
@@ -155,7 +157,7 @@ function pruneExpiredSessions(sessions, buffer) {
|
|
|
155
157
|
}
|
|
156
158
|
}
|
|
157
159
|
}
|
|
158
|
-
function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLimitRpm = 60) {
|
|
160
|
+
function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLimitRpm = 60, replayBuffer, replayRetentionMs = 7 * 24 * 60 * 60 * 1e3) {
|
|
159
161
|
const router = Router();
|
|
160
162
|
router.use(apiRateLimit(rateLimitRpm));
|
|
161
163
|
relay.on("message-relayed", (from, to, envelope) => {
|
|
@@ -170,9 +172,10 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
|
|
|
170
172
|
inReplyTo: env.inReplyTo
|
|
171
173
|
};
|
|
172
174
|
buffer.add(to, msg);
|
|
175
|
+
replayBuffer?.add(to, msg);
|
|
173
176
|
});
|
|
174
177
|
router.post("/v1/register", async (req, res) => {
|
|
175
|
-
const { publicKey, privateKey,
|
|
178
|
+
const { publicKey, privateKey, metadata } = req.body;
|
|
176
179
|
if (!publicKey || typeof publicKey !== "string") {
|
|
177
180
|
res.status(400).json({ error: "publicKey is required" });
|
|
178
181
|
return;
|
|
@@ -194,12 +197,11 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
|
|
|
194
197
|
res.status(400).json({ error: "Key pair verification failed: " + verification.reason });
|
|
195
198
|
return;
|
|
196
199
|
}
|
|
197
|
-
const { token, expiresAt } = createToken({ publicKey
|
|
200
|
+
const { token, expiresAt } = createToken({ publicKey });
|
|
198
201
|
pruneExpiredSessions(sessions, buffer);
|
|
199
202
|
const session = {
|
|
200
203
|
publicKey,
|
|
201
204
|
privateKey,
|
|
202
|
-
name,
|
|
203
205
|
metadata,
|
|
204
206
|
registeredAt: Date.now(),
|
|
205
207
|
expiresAt,
|
|
@@ -212,7 +214,6 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
|
|
|
212
214
|
if (agent.publicKey !== publicKey) {
|
|
213
215
|
peers.push({
|
|
214
216
|
publicKey: agent.publicKey,
|
|
215
|
-
name: agent.name,
|
|
216
217
|
lastSeen: agent.lastSeen
|
|
217
218
|
});
|
|
218
219
|
}
|
|
@@ -221,7 +222,6 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
|
|
|
221
222
|
if (s.publicKey !== publicKey && !wsAgents.has(s.publicKey)) {
|
|
222
223
|
peers.push({
|
|
223
224
|
publicKey: s.publicKey,
|
|
224
|
-
name: s.name,
|
|
225
225
|
lastSeen: s.registeredAt
|
|
226
226
|
});
|
|
227
227
|
}
|
|
@@ -273,7 +273,6 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
|
|
|
273
273
|
const relayMsg = JSON.stringify({
|
|
274
274
|
type: "message",
|
|
275
275
|
from: senderPublicKey,
|
|
276
|
-
name: session.name,
|
|
277
276
|
envelope
|
|
278
277
|
});
|
|
279
278
|
ws.send(relayMsg);
|
|
@@ -300,7 +299,7 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
|
|
|
300
299
|
res.json({ ok: true, envelopeId: envelope.id });
|
|
301
300
|
return;
|
|
302
301
|
}
|
|
303
|
-
res.status(404).json({ error:
|
|
302
|
+
res.status(404).json({ error: `Recipient not connected: ${to}` });
|
|
304
303
|
}
|
|
305
304
|
);
|
|
306
305
|
router.get(
|
|
@@ -314,7 +313,6 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
|
|
|
314
313
|
if (agent.publicKey !== callerPublicKey) {
|
|
315
314
|
peerList.push({
|
|
316
315
|
publicKey: agent.publicKey,
|
|
317
|
-
name: agent.name,
|
|
318
316
|
lastSeen: agent.lastSeen,
|
|
319
317
|
metadata: agent.metadata
|
|
320
318
|
});
|
|
@@ -324,7 +322,6 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
|
|
|
324
322
|
if (s.publicKey !== callerPublicKey && !wsAgents.has(s.publicKey)) {
|
|
325
323
|
peerList.push({
|
|
326
324
|
publicKey: s.publicKey,
|
|
327
|
-
name: s.name,
|
|
328
325
|
lastSeen: s.registeredAt,
|
|
329
326
|
metadata: s.metadata
|
|
330
327
|
});
|
|
@@ -353,6 +350,52 @@ function createRestRouter(relay, buffer, sessions, createEnv, verifyEnv, rateLim
|
|
|
353
350
|
res.json({ messages, hasMore });
|
|
354
351
|
}
|
|
355
352
|
);
|
|
353
|
+
router.get(
|
|
354
|
+
"/v1/messages/replay",
|
|
355
|
+
requireAuth,
|
|
356
|
+
(req, res) => {
|
|
357
|
+
if (!replayBuffer) {
|
|
358
|
+
res.status(501).json({ error: "Replay endpoint is not enabled on this relay" });
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
const publicKey = req.agent.publicKey;
|
|
362
|
+
const sinceRaw = req.query.since;
|
|
363
|
+
const limitRaw = req.query.limit;
|
|
364
|
+
if (!sinceRaw) {
|
|
365
|
+
res.status(400).json({ error: "`since` query parameter is required (ISO8601 timestamp)" });
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
const sinceDate = new Date(sinceRaw);
|
|
369
|
+
if (isNaN(sinceDate.getTime())) {
|
|
370
|
+
res.status(400).json({ error: "Invalid `since` value \u2014 must be a valid ISO8601 timestamp" });
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const sinceMs = sinceDate.getTime();
|
|
374
|
+
const oldestAllowed = Date.now() - replayRetentionMs;
|
|
375
|
+
if (sinceMs < oldestAllowed) {
|
|
376
|
+
res.status(400).json({
|
|
377
|
+
error: "retention_exceeded",
|
|
378
|
+
message: `\`since\` is older than the ${Math.round(replayRetentionMs / 864e5)}-day retention window`
|
|
379
|
+
});
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
let limit = 100;
|
|
383
|
+
if (limitRaw !== void 0) {
|
|
384
|
+
const parsedLimit = parseInt(limitRaw, 10);
|
|
385
|
+
if (isNaN(parsedLimit) || parsedLimit < 1) {
|
|
386
|
+
res.status(400).json({ error: "Invalid `limit` value \u2014 must be a positive integer" });
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
limit = Math.min(parsedLimit, 500);
|
|
390
|
+
}
|
|
391
|
+
let messages = replayBuffer.get(publicKey, sinceMs);
|
|
392
|
+
const hasMore = messages.length > limit;
|
|
393
|
+
if (hasMore) {
|
|
394
|
+
messages = messages.slice(0, limit);
|
|
395
|
+
}
|
|
396
|
+
res.json({ messages, hasMore });
|
|
397
|
+
}
|
|
398
|
+
);
|
|
356
399
|
router.delete(
|
|
357
400
|
"/v1/disconnect",
|
|
358
401
|
requireAuth,
|
|
@@ -408,6 +451,8 @@ async function runRelay(options = {}) {
|
|
|
408
451
|
const restPort = options.restPort ?? parseInt(process.env.REST_PORT ?? "3001", 10);
|
|
409
452
|
const messageTtlMs = parseInt(process.env.MESSAGE_TTL_MS ?? "86400000", 10);
|
|
410
453
|
const messageBuffer = new MessageBuffer({ ttlMs: messageTtlMs });
|
|
454
|
+
const replayRetentionMs = 7 * 24 * 60 * 60 * 1e3;
|
|
455
|
+
const replayBuffer = new MessageBuffer({ ttlMs: replayRetentionMs, maxMessages: 1e4 });
|
|
411
456
|
const restSessions = /* @__PURE__ */ new Map();
|
|
412
457
|
const allowedOrigins = process.env.ALLOWED_ORIGINS ?? "*";
|
|
413
458
|
const corsOrigins = allowedOrigins === "*" ? "*" : allowedOrigins.split(",").map((o) => o.trim()).filter((o) => o.length > 0);
|
|
@@ -426,7 +471,9 @@ async function runRelay(options = {}) {
|
|
|
426
471
|
restSessions,
|
|
427
472
|
createEnvelopeForRest,
|
|
428
473
|
verifyForRest,
|
|
429
|
-
rateLimitRpm
|
|
474
|
+
rateLimitRpm,
|
|
475
|
+
replayBuffer,
|
|
476
|
+
replayRetentionMs
|
|
430
477
|
);
|
|
431
478
|
app.use(router);
|
|
432
479
|
app.use((_req, res) => {
|
|
@@ -448,4 +495,4 @@ export {
|
|
|
448
495
|
createRestRouter,
|
|
449
496
|
runRelay
|
|
450
497
|
};
|
|
451
|
-
//# sourceMappingURL=chunk-
|
|
498
|
+
//# sourceMappingURL=chunk-52RKJA35.js.map
|