@dp-pcs/ogp 0.6.0 → 0.7.0-rc.1

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.
Files changed (87) hide show
  1. package/README.md +47 -11
  2. package/dist/cli/agent-targeting.d.ts +21 -0
  3. package/dist/cli/agent-targeting.d.ts.map +1 -0
  4. package/dist/cli/agent-targeting.js +44 -0
  5. package/dist/cli/agent-targeting.js.map +1 -0
  6. package/dist/cli/config.d.ts +4 -0
  7. package/dist/cli/config.d.ts.map +1 -1
  8. package/dist/cli/config.js +48 -0
  9. package/dist/cli/config.js.map +1 -1
  10. package/dist/cli/federation.d.ts +2 -1
  11. package/dist/cli/federation.d.ts.map +1 -1
  12. package/dist/cli/federation.js +162 -92
  13. package/dist/cli/federation.js.map +1 -1
  14. package/dist/cli/keychain.d.ts +22 -0
  15. package/dist/cli/keychain.d.ts.map +1 -0
  16. package/dist/cli/keychain.js +213 -0
  17. package/dist/cli/keychain.js.map +1 -0
  18. package/dist/cli/project.d.ts +1 -0
  19. package/dist/cli/project.d.ts.map +1 -1
  20. package/dist/cli/project.js +9 -2
  21. package/dist/cli/project.js.map +1 -1
  22. package/dist/cli/setup.d.ts +37 -0
  23. package/dist/cli/setup.d.ts.map +1 -1
  24. package/dist/cli/setup.js +130 -0
  25. package/dist/cli/setup.js.map +1 -1
  26. package/dist/cli.js +40 -6
  27. package/dist/cli.js.map +1 -1
  28. package/dist/daemon/heartbeat.d.ts +37 -0
  29. package/dist/daemon/heartbeat.d.ts.map +1 -1
  30. package/dist/daemon/heartbeat.js +195 -21
  31. package/dist/daemon/heartbeat.js.map +1 -1
  32. package/dist/daemon/keypair.d.ts.map +1 -1
  33. package/dist/daemon/keypair.js +144 -22
  34. package/dist/daemon/keypair.js.map +1 -1
  35. package/dist/daemon/message-handler.d.ts +8 -0
  36. package/dist/daemon/message-handler.d.ts.map +1 -1
  37. package/dist/daemon/message-handler.js +60 -18
  38. package/dist/daemon/message-handler.js.map +1 -1
  39. package/dist/daemon/notify.d.ts +6 -0
  40. package/dist/daemon/notify.d.ts.map +1 -1
  41. package/dist/daemon/notify.js +9 -2
  42. package/dist/daemon/notify.js.map +1 -1
  43. package/dist/daemon/openclaw-bridge.d.ts +6 -0
  44. package/dist/daemon/openclaw-bridge.d.ts.map +1 -1
  45. package/dist/daemon/openclaw-bridge.js +10 -2
  46. package/dist/daemon/openclaw-bridge.js.map +1 -1
  47. package/dist/daemon/peers.d.ts +31 -0
  48. package/dist/daemon/peers.d.ts.map +1 -1
  49. package/dist/daemon/peers.js +66 -4
  50. package/dist/daemon/peers.js.map +1 -1
  51. package/dist/daemon/rendezvous.d.ts.map +1 -1
  52. package/dist/daemon/rendezvous.js +9 -7
  53. package/dist/daemon/rendezvous.js.map +1 -1
  54. package/dist/daemon/reply-handler.d.ts.map +1 -1
  55. package/dist/daemon/reply-handler.js +2 -1
  56. package/dist/daemon/reply-handler.js.map +1 -1
  57. package/dist/daemon/scopes.d.ts +8 -0
  58. package/dist/daemon/scopes.d.ts.map +1 -1
  59. package/dist/daemon/scopes.js.map +1 -1
  60. package/dist/daemon/server.d.ts +128 -1
  61. package/dist/daemon/server.d.ts.map +1 -1
  62. package/dist/daemon/server.js +260 -57
  63. package/dist/daemon/server.js.map +1 -1
  64. package/dist/shared/config.d.ts +93 -0
  65. package/dist/shared/config.d.ts.map +1 -1
  66. package/dist/shared/config.js +111 -0
  67. package/dist/shared/config.js.map +1 -1
  68. package/dist/shared/help.js +1 -0
  69. package/dist/shared/help.js.map +1 -1
  70. package/dist/shared/signing.d.ts +49 -0
  71. package/dist/shared/signing.d.ts.map +1 -1
  72. package/dist/shared/signing.js +68 -0
  73. package/dist/shared/signing.js.map +1 -1
  74. package/dist/shared/tls.d.ts +27 -0
  75. package/dist/shared/tls.d.ts.map +1 -0
  76. package/dist/shared/tls.js +37 -0
  77. package/dist/shared/tls.js.map +1 -0
  78. package/docs/ARCHITECTURE.md +146 -0
  79. package/docs/CLI-REFERENCE.md +170 -2
  80. package/docs/MULTI-AGENT-PERSONAS-DESIGN.md +925 -0
  81. package/package.json +1 -1
  82. package/scripts/completion.bash +25 -1
  83. package/scripts/completion.zsh +9 -4
  84. package/scripts/render-ogp-overview-video.mjs +417 -0
  85. package/skills/ogp/SKILL.md +1 -1
  86. package/skills/ogp-expose/SKILL.md +1 -1
  87. package/skills/ogp-project/SKILL.md +1 -1
@@ -0,0 +1,925 @@
1
+ # Multi-Agent Personas — Design Document
2
+
3
+ > **Bead:** B0032
4
+ > **Status:** Design (pre-implementation)
5
+ > **Target version:** v0.7.0
6
+ > **Author:** Relay (per David Proctor)
7
+ > **Last updated:** 2026-04-28 (revision 3 — all 8 open questions decided; Hermes confirmed as single-persona; default persona grant = primary-only)
8
+
9
+ ## Goal
10
+
11
+ Allow a single OGP daemon — one keypair, one peer ID, one human-level trust relationship — to advertise and route to **multiple addressable agent personas**, with **per-(peer, persona) access control** so a single federation can expose different personas to different peers. A peer that federates with `David @ ogp.sarcastek.com` should be able to discover David's agents `Junior`, `Sterling`, and `Apollo`, address messages to a specific persona, and only see/reach the personas David has explicitly granted them — without re-federating, exchanging new keys, or going through a second human-approval handshake.
12
+
13
+ ## What's in v0.7.0
14
+
15
+ Five linked capabilities that ship together:
16
+
17
+ 1. **Multi-persona advertisement.** One federation card lists N agent personas under one keypair.
18
+ 2. **Per-persona inbound routing.** A `toAgent` field in the message envelope routes to a specific persona.
19
+ 3. **Per-persona scope grants** (3D access control: peer × intent × persona). A peer's grant can name which personas it covers.
20
+ 4. **Framework auto-sync.** OGP detects OpenClaw/Hermes agents on the host and pre-populates personas with sensible defaults — zero-config persona setup.
21
+ 5. **Internal peer registry endpoint.** A localhost-only API surface so in-gateway agents can introspect their human's federation graph.
22
+
23
+ ## Non-goals
24
+
25
+ - **Per-persona keypairs.** Each persona is metadata under one human-level trust relationship, not a separate cryptographic identity. Use multiple daemons (existing meta-config support) if you need separate keypairs.
26
+ - **A2A-style per-agent endpoints.** No persona gets its own `/.well-known/` URL, port, or DNS entry.
27
+ - **Agent-to-agent message routing inside the gateway.** That's OpenClaw/Hermes's job; OGP only delivers the inbound message with a routing hint.
28
+ - **Per-persona rate limits.** Rate limits stay keyed at `{peerId}:{intent}` to prevent a misbehaving peer from multiplying their effective rate by the number of personas. Per-persona rate limits remain a v2 question (see Security § 4).
29
+ - **OGP-level intra-gateway agent-to-agent routing.** If Junior wants to message Sterling on the same gateway, that's a framework concern (OpenClaw's internal channels, Hermes's session model). OGP exposes peer registry data to local agents via the internal endpoint, but routing inside the gateway is the framework's job.
30
+
31
+ ## Background — current state
32
+
33
+ ### One daemon, one identity (today)
34
+
35
+ ```
36
+ Today:
37
+ ┌───────────────────────────────────────────┐
38
+ │ Daemon (port 18790) │
39
+ │ keypair: 302a300506032b65... │
40
+ │ displayName: "David - Junior" │
41
+ │ agentName: "Junior" │
42
+ │ │
43
+ │ Inbound federated message │
44
+ │ │ │
45
+ │ ▼ │
46
+ │ Hook: /hooks/agent │
47
+ │ { agentId: "main", message: ... } │ ← always "main"
48
+ │ │ │
49
+ │ ▼ │
50
+ │ OpenClaw → Junior │
51
+ └───────────────────────────────────────────┘
52
+ ```
53
+
54
+ The OGP daemon has one identity. All inbound messages get routed to OpenClaw with `agentId: 'main'` (see `src/daemon/openclaw-bridge.ts:292`). To address multiple agents today, you must run multiple daemons via the meta-config registry — each gets its own port, keypair, and config dir. Three agents = three daemons = three federations to manage. The peer sees three unrelated humans.
55
+
56
+ ### Plumbing already in place
57
+
58
+ Three pieces of infrastructure make this feature cheap to build:
59
+
60
+ 1. **`agentId` in the OpenClaw hook payload** already exists (`openclaw-bridge.ts:292`). The daemon just always passes `'main'`.
61
+ 2. **Identity fields in `OGPConfig`** (`humanName`, `agentName`, `organization`) — `src/shared/config.ts:130–132`. These already establish the framing that human ≠ agent.
62
+ 3. **`AuthorIdentity` snapshots in project contributions** — `src/daemon/projects.ts:8–14`. The `agentName` field is already part of the contribution payload; v0.7 just needs to make it the *active routing target*, not just attribution metadata.
63
+
64
+ ## Design overview
65
+
66
+ ```
67
+ With Multi-Agent Personas (v0.7):
68
+ ┌───────────────────────────────────────────────────────┐
69
+ │ Daemon (port 18790) │
70
+ │ keypair: 302a300506032b65... ← UNCHANGED │
71
+ │ humanName: "David Proctor" │
72
+ │ agents: │
73
+ │ - id: "junior", role: primary │
74
+ │ - id: "sterling", role: specialist │
75
+ │ - id: "apollo", role: specialist │
76
+ │ │
77
+ │ Inbound federated message │
78
+ │ { ..., toAgent: "sterling" } ← NEW envelope field │
79
+ │ │ │
80
+ │ ▼ │
81
+ │ Persona resolution: │
82
+ │ toAgent="sterling" → agentId="sterling" │
83
+ │ toAgent="junior" → agentId="main" (alias) │
84
+ │ toAgent=undefined → agentId="main" (default) │
85
+ │ │ │
86
+ │ ▼ │
87
+ │ Hook: /hooks/agent │
88
+ │ { agentId: "<resolved>", message: ... } │
89
+ └───────────────────────────────────────────────────────┘
90
+ ```
91
+
92
+ **One trust unit (the keypair). Many addressable personas (the `agents[]` advertisement and `toAgent` routing field).**
93
+
94
+ ## Wire format changes
95
+
96
+ ### 1. Federation card (`/.well-known/ogp`)
97
+
98
+ Add an optional `agents[]` array. Pre-v0.7 peers ignore it; v0.7+ peers use it for discovery.
99
+
100
+ **Current** (`src/daemon/server.ts:349–366`):
101
+ ```jsonc
102
+ {
103
+ "version": "0.6.0",
104
+ "displayName": "David - Junior",
105
+ "email": "...",
106
+ "gatewayUrl": "https://ogp.sarcastek.com",
107
+ "publicKey": "302a300506032b65...",
108
+ "capabilities": {
109
+ "intents": ["message", "agent-comms", "project.join", ...],
110
+ "features": ["scope-negotiation", "reply-callback", "bidirectional-health"]
111
+ },
112
+ "endpoints": { ... }
113
+ }
114
+ ```
115
+
116
+ **v0.7** — add `agents[]` and a new capability flag `multi-agent-personas`:
117
+ ```jsonc
118
+ {
119
+ "version": "0.7.0",
120
+ "displayName": "David Proctor (Junior)",
121
+ "humanName": "David Proctor",
122
+ "email": "...",
123
+ "gatewayUrl": "https://ogp.sarcastek.com",
124
+ "publicKey": "302a300506032b65...",
125
+ "capabilities": {
126
+ "intents": [...],
127
+ "features": ["scope-negotiation", "reply-callback", "bidirectional-health", "multi-agent-personas"]
128
+ },
129
+ "endpoints": { ... },
130
+
131
+ "agents": [
132
+ {
133
+ "id": "junior",
134
+ "displayName": "Junior",
135
+ "role": "primary",
136
+ "displayIcon": "⭐",
137
+ "description": "Main coordination agent",
138
+ "skills": ["code", "ops", "general"]
139
+ },
140
+ {
141
+ "id": "sterling",
142
+ "displayName": "Sterling",
143
+ "role": "specialist",
144
+ "displayIcon": "💰",
145
+ "description": "Finance and data analysis",
146
+ "skills": ["finance", "data-analysis"]
147
+ },
148
+ {
149
+ "id": "apollo",
150
+ "displayName": "Apollo",
151
+ "role": "specialist",
152
+ "displayIcon": "🔬",
153
+ "description": "Long-form research and writing",
154
+ "skills": ["research", "writing"]
155
+ }
156
+ ]
157
+ }
158
+ ```
159
+
160
+ **Field semantics:**
161
+
162
+ | Field | Required | Notes |
163
+ |---|---|---|
164
+ | `id` | yes | Stable persona identifier. Lowercase, alphanumeric + dash/underscore. Used as the routing key. |
165
+ | `displayName` | yes | Human-readable name. May contain spaces, capitals. |
166
+ | `role` | yes | `primary` (exactly one — the default routing target) or `specialist` (any number). Future: `archived`, `experimental`. |
167
+ | `description` | no | Free-text. Surfaced by `ogp federation peers --show-agents`. |
168
+ | `skills` | no | Array of free-text capability hints. Discoverability only — not enforced. |
169
+ | `displayIcon` | no | Optional emoji or URL for chat UIs. Pure presentation, not enforced. |
170
+
171
+ **Invariants:**
172
+ - Exactly one persona MUST have `role: "primary"`. The federation card is invalid otherwise.
173
+ - If `agents[]` is omitted entirely, the daemon synthesizes a single primary persona from the legacy `agentName` field for backward compatibility (see "Backward compatibility" below).
174
+
175
+ ### 2. Federation message envelope (`FederationMessage`)
176
+
177
+ Add an optional `toAgent` field. Routes the message to a specific persona.
178
+
179
+ **Current** (`src/daemon/message-handler.ts:51–63`):
180
+ ```ts
181
+ export interface FederationMessage {
182
+ intent: string;
183
+ from: string; // peer ID
184
+ to: string; // our peer ID (= keypair fingerprint)
185
+ nonce: string;
186
+ timestamp: string;
187
+ payload: any;
188
+ replyTo?: string;
189
+ conversationId?: string;
190
+ projectId?: string;
191
+ }
192
+ ```
193
+
194
+ **v0.7**:
195
+ ```ts
196
+ export interface FederationMessage {
197
+ intent: string;
198
+ from: string;
199
+ to: string; // our peer ID — the trust unit (unchanged)
200
+ toAgent?: string; // NEW: persona id within `to`. Undefined = primary.
201
+ nonce: string;
202
+ timestamp: string;
203
+ payload: any;
204
+ replyTo?: string;
205
+ conversationId?: string;
206
+ projectId?: string;
207
+ }
208
+ ```
209
+
210
+ **Routing semantics:**
211
+
212
+ | `toAgent` value | Behavior |
213
+ |---|---|
214
+ | Omitted / `undefined` | Route to the primary persona. Backward-compatible with v0.6.x peers. |
215
+ | Matches a defined persona `id` | Route to that persona. |
216
+ | Matches no defined persona | Reject with `404 unknown-agent` (signed). The sender gets a clear error and can retry against the primary or a different persona. |
217
+ | Empty string `""` | Treat as omitted (route to primary). |
218
+
219
+ **Why 404, not silent fallback to primary:** silent fallback is friendly but covers up bugs. If a peer thinks they're talking to Sterling and they're actually talking to Junior, that's worse than a 404 they can recover from. The signed rejection lets them surface the error to the human or retry against the listed personas.
220
+
221
+ ### 3. Project contributions
222
+
223
+ Already supports `authorIdentity.agentName` (see `src/daemon/projects.ts:8–14`). When a contribution arrives via `project.contribute`, the daemon should:
224
+
225
+ 1. Use `toAgent` to determine which persona received the contribution.
226
+ 2. Stamp the local persona's `displayName` into the contribution if the receiving daemon has multi-agent enabled.
227
+
228
+ This makes the contribution log clearer: "David's Sterling received this contribution from Stan's research-bot" instead of just "the gateway received it."
229
+
230
+ ## Schema changes
231
+
232
+ ### `OGPConfig` (`src/shared/config.ts`)
233
+
234
+ Add an optional `agents` array. Keep legacy `agentName`/`displayName` for backward compatibility.
235
+
236
+ ```ts
237
+ export interface AgentPersona {
238
+ id: string; // "junior", "sterling"
239
+ displayName: string; // "Junior", "Sterling"
240
+ role: 'primary' | 'specialist'; // Exactly one persona MUST have role: "primary"
241
+ description?: string;
242
+ skills?: string[];
243
+ displayIcon?: string; // Optional emoji or URL for chat UIs. Pure presentation.
244
+ hookAgentId?: string; // Override the framework `agentId` for this persona.
245
+ // Defaults: primary → 'main' (back-compat); specialist → `id`.
246
+ }
247
+
248
+ export interface OGPConfig {
249
+ // ... existing fields ...
250
+ displayName: string; // Legacy: kept for backward compatibility
251
+ humanName?: string;
252
+ agentName?: string; // Legacy: synthesized into agents[] if agents[] missing
253
+ organization?: string;
254
+ // NEW:
255
+ agents?: AgentPersona[]; // If undefined, synthesized from agentName + 'primary' role
256
+ }
257
+ ```
258
+
259
+ **`hookAgentId` lets each persona route to a different OpenClaw agent.** Example:
260
+
261
+ ```yaml
262
+ agents:
263
+ - id: junior
264
+ displayName: Junior
265
+ role: primary
266
+ hookAgentId: main # OpenClaw's "main" agent
267
+ - id: sterling
268
+ displayName: Sterling
269
+ role: specialist
270
+ hookAgentId: sterling # OpenClaw must have an agent named "sterling"
271
+ - id: apollo
272
+ displayName: Apollo
273
+ role: specialist
274
+ hookAgentId: apollo # Different OpenClaw agent
275
+ ```
276
+
277
+ **The user is responsible for ensuring the underlying framework actually has agents matching these `hookAgentId` values.** OGP doesn't create OpenClaw agents; it just routes to ones the user has set up. If `hookAgentId: "sterling"` is configured but OpenClaw doesn't have a "sterling" agent, the hook call will fail and the daemon will return a structured error to the sending peer.
278
+
279
+ ## Routing changes
280
+
281
+ ### Message-handler (`src/daemon/message-handler.ts`)
282
+
283
+ Add a persona-resolution step before the existing intent-dispatch logic:
284
+
285
+ ```ts
286
+ // Pseudo-code, in handleMessage() after auth/scope checks pass
287
+ const personas = loadPersonasFromConfig(); // helper from config.ts
288
+ const targetPersona = resolveTargetPersona(message.toAgent, personas);
289
+
290
+ if (!targetPersona) {
291
+ return {
292
+ success: false,
293
+ nonce: message.nonce,
294
+ error: `Unknown agent: '${message.toAgent}'`,
295
+ statusCode: 404
296
+ };
297
+ }
298
+
299
+ // Pass targetPersona.hookAgentId down to the bridge / openclaw-bridge call
300
+ // so /hooks/agent gets the right agentId.
301
+ ```
302
+
303
+ ### `resolveTargetPersona` rules
304
+
305
+ ```ts
306
+ function resolveTargetPersona(
307
+ toAgent: string | undefined,
308
+ personas: AgentPersona[]
309
+ ): AgentPersona | null {
310
+ // Empty/undefined → primary
311
+ if (!toAgent || toAgent === '') {
312
+ return personas.find(p => p.role === 'primary') ?? null;
313
+ }
314
+ // Exact match → that persona
315
+ const match = personas.find(p => p.id === toAgent);
316
+ return match ?? null;
317
+ }
318
+ ```
319
+
320
+ ### OpenClaw bridge (`src/daemon/openclaw-bridge.ts`)
321
+
322
+ The bridge already accepts an `agentId`. The change is to plumb `targetPersona.hookAgentId` (or `targetPersona.id` as fallback) instead of the hardcoded `config.agentId`.
323
+
324
+ ```ts
325
+ // Current (line ~292):
326
+ agentId: config.agentId || 'main',
327
+
328
+ // New:
329
+ agentId: persona?.hookAgentId ?? persona?.id ?? config.agentId ?? 'main',
330
+ ```
331
+
332
+ The `persona` argument flows from the message handler. For pre-v0.7 messages with no `toAgent`, the resolver returns the primary persona, which keeps existing behavior identical.
333
+
334
+ ## CLI changes
335
+
336
+ ### Sending side
337
+
338
+ Add `--to-agent` flag to all outbound message commands.
339
+
340
+ ```bash
341
+ # Send a generic message
342
+ ogp federation send <peer-id> message <payload> [--to-agent <persona>]
343
+
344
+ # Send an agent-comms message
345
+ ogp federation agent <peer-id> <topic> <message> [--to-agent <persona>]
346
+
347
+ # Send a project contribution (existing command, new flag)
348
+ ogp project contribute <project-id> --topic <t> --summary <s> [--to-agent <persona>]
349
+ ```
350
+
351
+ If `--to-agent` is omitted, the message goes to the peer's primary persona (current behavior, no breaking change).
352
+
353
+ ### Discovery side
354
+
355
+ ```bash
356
+ # List peers with their advertised personas
357
+ ogp --for all federation peers --show-agents
358
+
359
+ # Output:
360
+ # Peer: Stan @ Hermes (1a2b3c4d5e6f7890)
361
+ # Status: established (out 2m, in 30s)
362
+ # Agents:
363
+ # ⭐ shadow (primary) "Stan's main agent"
364
+ # ⚙ research-bot (specialist) "Long-form research"
365
+ # ⚙ code-bot (specialist) "Code review and PRs"
366
+ ```
367
+
368
+ ```bash
369
+ # Show identity (current ogp whoami) updated to list local personas
370
+ ogp whoami
371
+
372
+ # Output:
373
+ # Identity: David Proctor (peer ID 302a300506032b65...)
374
+ # Gateway: https://ogp.sarcastek.com
375
+ # Agents (3):
376
+ # ⭐ junior (primary) → OpenClaw agentId: main
377
+ # ⚙ sterling (specialist) → OpenClaw agentId: sterling
378
+ # ⚙ apollo (specialist) → OpenClaw agentId: apollo
379
+ ```
380
+
381
+ ### Configuration
382
+
383
+ ```bash
384
+ # Add a new persona
385
+ ogp config add-agent --id sterling --display-name "Sterling" --role specialist \
386
+ --description "Finance and data analysis" --hook-agent-id sterling \
387
+ --skills finance,data-analysis
388
+
389
+ # Remove a persona (cannot remove primary unless replacing)
390
+ ogp config remove-agent <id>
391
+
392
+ # Promote a specialist to primary (demotes the current primary to specialist)
393
+ ogp config set-primary <id>
394
+
395
+ # List configured personas
396
+ ogp config list-agents
397
+ ```
398
+
399
+ ## Backward compatibility
400
+
401
+ This is the load-bearing section. The wire-format change must not break federation with v0.6.x peers.
402
+
403
+ ### Outbound (v0.7 → v0.6.x peer)
404
+
405
+ When a v0.7 daemon sends a message to a v0.6.x peer, it MUST omit the `toAgent` field unless the peer's federation card advertises `multi-agent-personas` in `capabilities.features`. Sending `toAgent` to a v0.6.x peer is undefined behavior — they'll likely ignore it (no harm) but the spec shouldn't depend on that.
406
+
407
+ CLI behavior: if the user passes `--to-agent <id>` to a peer that doesn't support multi-agent, the CLI rejects with:
408
+ ```
409
+ Error: peer 'Apollo @ Hermes' is on OGP v0.6.x and does not advertise multi-agent personas.
410
+ Drop --to-agent or upgrade the peer.
411
+ ```
412
+
413
+ ### Inbound (v0.6.x peer → v0.7 daemon)
414
+
415
+ Messages arriving from v0.6.x peers won't include `toAgent`. The resolver returns the primary persona. The hook gets called with `agentId: 'main'` (or the primary's `hookAgentId`). Behavior is identical to today.
416
+
417
+ ### Local config without `agents[]`
418
+
419
+ If `OGPConfig.agents` is undefined or empty, the daemon synthesizes:
420
+
421
+ ```ts
422
+ [{
423
+ id: config.agentName?.toLowerCase().replace(/[^a-z0-9_-]/g, '-') ?? 'main',
424
+ displayName: config.agentName ?? config.displayName ?? 'Agent',
425
+ role: 'primary',
426
+ hookAgentId: config.agentId ?? 'main'
427
+ }]
428
+ ```
429
+
430
+ So a config that says `agentName: "Junior"` (current state) becomes a one-persona setup with primary `junior` automatically. No migration script needed.
431
+
432
+ ### Federation card without `agents[]`
433
+
434
+ When parsing a peer's federation card, if `agents[]` is missing, synthesize a single primary persona on the local side:
435
+
436
+ ```ts
437
+ [{
438
+ id: peer.agentName?.toLowerCase() ?? 'main',
439
+ displayName: peer.agentName ?? peer.displayName,
440
+ role: 'primary'
441
+ }]
442
+ ```
443
+
444
+ So v0.7 daemons can talk to v0.6.x peers and represent them as single-persona peers in the local registry. No special-casing throughout the codebase — the persona array is always populated.
445
+
446
+ ## Security considerations
447
+
448
+ ### 1. Persona advertisement is unauthenticated discovery
449
+
450
+ The `/.well-known/ogp` endpoint is public (per F-12 in the threat model). Anyone can see the list of personas. This is by design — the personas are presence advertisements, not secrets. Don't put sensitive metadata in `description` or `skills`.
451
+
452
+ ### 2. Persona spoofing in `from` field
453
+
454
+ The `from` field still identifies the **gateway** (the peer ID = the keypair). Personas don't have keypairs. There's no separate cryptographic identity for "Junior vs Sterling" — they're metadata under one trust relationship.
455
+
456
+ This means: a peer cannot send a message *claiming to be from Sterling specifically* and have OGP cryptographically verify the claim. They can only send a message from `David's gateway`. If David then says "Sterling sent this," it's an OGP-internal attribution — accurate as long as David's daemon and routing are honest, but not externally verifiable.
457
+
458
+ For most use cases this is fine. If you need cryptographically distinct agent identities (e.g., a corporate setup where Junior and Sterling are owned by different teams who shouldn't trust each other's messages), use multiple daemons with separate keypairs — that's exactly what the existing meta-config registry is for.
459
+
460
+ ### 3. Rate limits
461
+
462
+ Rate limits are keyed on `{peerId}:{intent}` today (`src/daemon/doorman.ts:181`). Multi-agent does NOT change this key.
463
+
464
+ **Recommendation: keep peer-level rate limits in v0.7.** Per-persona rate limits is a v2 question. A peer flooding "Sterling" is using the same trust budget as flooding "Junior" — the abuse signal is the same, and per-persona limits would let a misbehaving peer multiply their effective rate by the number of personas. Better to keep one bucket per peer.
465
+
466
+ ## Per-persona scope grants (in-scope, v0.7)
467
+
468
+ The privacy model: when you federate with someone, you decide *which of your personas they can reach*. Without this, every federation is "all-or-nothing" — and the moment a user has a public-facing agent next to a private one, they have a leak.
469
+
470
+ ### Schema change to `ScopeGrant`
471
+
472
+ Add an optional `personas[]` array to the existing `ScopeGrant` (`src/daemon/scopes.ts:14–28`):
473
+
474
+ ```ts
475
+ export interface ScopeGrant {
476
+ intent: string;
477
+ enabled: boolean;
478
+ rateLimit?: RateLimit;
479
+ topics?: string[];
480
+ // NEW in v0.7:
481
+ personas?: string[]; // If present and non-empty, restrict this grant to these personas.
482
+ // If absent or empty, grant applies to ALL personas (backward compat).
483
+ expiresAt?: string;
484
+ }
485
+ ```
486
+
487
+ **Semantics:**
488
+
489
+ | `personas` value | Meaning |
490
+ |---|---|
491
+ | Absent (`undefined`) | **Backward-compat ONLY** — grants from v0.6.x peers and grants migrated from pre-v0.7 configs apply to all personas. New v0.7 grants always populate `personas`. |
492
+ | Empty array `[]` | Same as absent. Grant applies to every persona. (Rare; mostly produced by accidental over-deletion.) |
493
+ | `["junior"]` | Grant covers only the `junior` persona. Other personas reject this peer's traffic with `403`. |
494
+ | `["junior", "sterling"]` | Grant covers both. `apollo` rejects. |
495
+ | Contains an unknown id | Unknown ids are ignored at runtime (forward-compat); known ids are enforced. |
496
+
497
+ **Default for new federations (v0.7):** when a peer is approved without an explicit `--personas` flag, the granted personas list defaults to `[<primary.id>]` — i.e., the primary persona only. This is a privacy-safer default than "all personas" and preserves scripted flows (no interactive prompt blocks `ogp federation approve` in CI).
498
+
499
+ If the user wants to grant a peer access to additional personas at approval time, they pass `--personas junior,sterling` explicitly. They can extend later with `ogp federation grant <peer-id> --personas <list>`.
500
+
501
+ **Why default-primary-only is correct:**
502
+ - A v0.6.x user upgrading to v0.7 has only one persona (their legacy `agentName` synthesized as primary). Default-primary-only is a no-op for them — same behavior as today.
503
+ - A user with multiple personas who federates with someone new probably wants the conservative default. Granting access to private agents (Sterling-finance, Apollo-research) should be an explicit act, not an accident of approving a peer.
504
+ - Scripted flows (`ogp federation approve <peer-id> --intents agent-comms` in CI) get a sensible default and don't block on a prompt.
505
+
506
+ ### Doorman enforcement
507
+
508
+ Add a persona check to `checkAccess()` in `src/daemon/doorman.ts` between the existing scope-coverage check (step 5) and the rate-limit check (step 6):
509
+
510
+ ```ts
511
+ // Step 5.5: persona scope check (NEW in v0.7)
512
+ if (grant.personas && grant.personas.length > 0) {
513
+ const personas = loadPersonasFromConfig();
514
+ const targetPersonaId = message.toAgent ?? personas.find(p => p.role === 'primary')?.id;
515
+
516
+ if (!targetPersonaId || !grant.personas.includes(targetPersonaId)) {
517
+ return {
518
+ allowed: false,
519
+ reason: `Persona '${targetPersonaId}' not in granted scope for intent '${intent}'`,
520
+ statusCode: 403,
521
+ isV1Peer
522
+ };
523
+ }
524
+ }
525
+ ```
526
+
527
+ The 6-step checkAccess algorithm becomes 7 steps with persona check inserted between scope-coverage and rate-limit.
528
+
529
+ ### CLI
530
+
531
+ ```bash
532
+ # Approve Stan to talk to Junior only
533
+ ogp federation approve <stan-peer-id> \
534
+ --intents agent-comms,project.contribute \
535
+ --personas junior
536
+
537
+ # Later: extend Stan's reach to Sterling for project work
538
+ ogp federation grant <stan-peer-id> \
539
+ --intent project.contribute \
540
+ --personas junior,sterling
541
+
542
+ # Show current grants per peer (existing command, updated output)
543
+ ogp federation scopes <stan-peer-id>
544
+
545
+ # Output:
546
+ # Peer: Stan @ Hermes (1a2b3c4d5e6f7890)
547
+ # agent-comms → personas: junior (rate: 100/3600)
548
+ # project.contribute → personas: junior, sterling (rate: 100/3600)
549
+ # project.query → personas: ALL (no restriction) (rate: 100/3600)
550
+ ```
551
+
552
+ ### Backward compatibility
553
+
554
+ - **Existing v0.6.x grants** have no `personas` field. They continue to apply to all personas (the new default). No migration needed.
555
+ - **v0.7 daemon talking to v0.6.x peer** sends grants without `personas`. The peer wouldn't know what to do with the field anyway.
556
+ - **v0.6.x peer talking to v0.7 daemon** can still federate. Their messages get routed to the primary persona. If the v0.7 daemon's grant for that peer has a `personas` restriction that doesn't include primary, the message is rejected with the new 403 — same enforcement as for v0.7 peers.
557
+
558
+ ### Why this is the third dimension of access control
559
+
560
+ | Dimension | Question it answers | Existing protocols |
561
+ |---|---|---|
562
+ | **Per-peer** | Who is allowed to talk to me at all? | OAuth (per-client), Matrix (per-server) |
563
+ | **Per-intent** | Which actions can they take? | OAuth (scopes), MCP (per-tool capability), OGP today |
564
+ | **Per-persona** | Which of MY agents can they reach? | **Nothing in published prior art** |
565
+
566
+ This is the new claim worth surfacing in the patent disclosure. It's not just "scope grants" — it's three-dimensional scope grants that map to a real privacy model (different personas serve different audiences).
567
+
568
+ ## Framework auto-sync
569
+
570
+ OpenClaw and Hermes already define agents in their working directories. Today, OGP makes you re-declare them as personas. v0.7 fixes that.
571
+
572
+ ### Detection paths
573
+
574
+ | Framework | Detection signal | Agent enumeration |
575
+ |---|---|---|
576
+ | OpenClaw | `~/.openclaw/` exists; `framework-detection.ts` already detects this | Subdirectories under `agents/`. Each is an agent with `IDENTITY.md`, `AGENT.md`, optional `config.json`. Returns N personas. |
577
+ | Hermes | `~/.hermes/` exists; `framework-detection.ts` already detects this | **Hermes is a single integrated runtime, not a multi-agent host.** Auto-sync returns exactly one persona derived from `~/.hermes/IDENTITY.md` + `config.yaml`. The persona is always `role: primary`, with `displayName` taken from the Hermes identity. There is no `agents/` directory in Hermes by design. |
578
+ | Standalone | No underlying framework | One synthesized persona from legacy `agentName` (no auto-sync available — manual `add-agent` only). |
579
+
580
+ **Why Hermes is the trivial case:** Per `docs/extending-to-hermes.md`, Hermes is a single conversational AI runtime — fundamentally different from OpenClaw's "many agents under one gateway" architecture. Multi-persona advertisement on a Hermes daemon means advertising one persona (the Hermes runtime itself, e.g. "Apollo @ Hermes" in David's setup). Per-agent inbound routing has no target other than Hermes itself, so `toAgent` from a peer either matches the one persona's id or returns 404. This is correct behavior, not a bug.
581
+
582
+ If Hermes evolves to support multiple internal agents in the future, this design accommodates it without protocol changes — the auto-sync logic just starts returning N personas instead of one.
583
+
584
+ ### CLI
585
+
586
+ ```bash
587
+ # One-shot sync: detect, propose, apply
588
+ ogp config sync-agents
589
+
590
+ # Output:
591
+ # Detected OpenClaw at ~/.openclaw/
592
+ # Found 4 agents in agents/:
593
+ # main → "Junior" (channel: telegram-direct)
594
+ # sterling → "Sterling" (channel: telegram-finance)
595
+ # apollo → "Apollo" (channel: telegram-research)
596
+ # journal → "Journal" (channel: file-only, no human channel)
597
+ #
598
+ # Proposed OGP personas:
599
+ # ⭐ junior (primary) → hookAgentId: main, channel: telegram-direct
600
+ # ⚙ sterling (specialist) → hookAgentId: sterling, channel: telegram-finance
601
+ # ⚙ apollo (specialist) → hookAgentId: apollo, channel: telegram-research
602
+ # ⚙ journal (specialist) → hookAgentId: journal, channel: file-only
603
+ #
604
+ # Apply? [y / n / edit]
605
+ ```
606
+
607
+ ```bash
608
+ # Show what auto-sync would do without applying (dry run)
609
+ ogp config sync-agents --dry-run
610
+
611
+ # Force re-sync (overwrites manually edited persona definitions, with confirmation)
612
+ ogp config sync-agents --force
613
+ ```
614
+
615
+ ### Setup wizard integration
616
+
617
+ `ogp setup` currently asks for `humanName`, `agentName`, `organization`. After the framework is detected, it should add an interactive step:
618
+
619
+ ```
620
+ Detected OpenClaw with 4 agents. Auto-import them as OGP personas? [Y/n]
621
+ ⭐ junior (primary, was 'main')
622
+ ⚙ sterling
623
+ ⚙ apollo
624
+ ⚙ journal
625
+ ```
626
+
627
+ ### Detection helper
628
+
629
+ A new module `src/shared/framework-agents.ts` (or extension of existing `framework-detection.ts`) exposes:
630
+
631
+ ```ts
632
+ export interface DetectedAgent {
633
+ id: string;
634
+ displayName: string;
635
+ hookAgentId: string;
636
+ channel?: string; // Best-effort detection
637
+ description?: string; // Pulled from IDENTITY.md if available
638
+ }
639
+
640
+ export function detectFrameworkAgents(framework: 'openclaw' | 'hermes' | 'standalone'): DetectedAgent[];
641
+ ```
642
+
643
+ Implementation reads the relevant working directory and parses what it finds. For OpenClaw specifically:
644
+
645
+ 1. `cat ~/.openclaw/agents/<name>/IDENTITY.md` → extract first `# Name` line for `displayName`
646
+ 2. `cat ~/.openclaw/agents/<name>/config.json` (if present) → extract channel/description
647
+ 3. Generate persona id from directory name (sanitized to lowercase alphanumeric + dash)
648
+
649
+ If detection fails for a specific agent (missing files, parse errors), skip it but log a warning. Don't block sync on one bad agent.
650
+
651
+ ### Why this matters
652
+
653
+ Two reasons:
654
+
655
+ 1. **UX.** "Run `ogp config sync-agents` once" beats "edit a YAML file with 10 fields per persona." The persona advertisement feature is useless if setting it up is friction-laden.
656
+ 2. **Patent claim.** "Automatic discovery of underlying framework agent configurations and zero-config federation persona setup" is itself a non-obvious integration. A2A has nothing analogous — its discovery model is purely outbound (signed Agent Cards) with no notion of introspecting an existing framework's agent registry. This is another claim to surface in the disclosure.
657
+
658
+ ## Internal peer registry endpoint
659
+
660
+ OGP's peer registry is daemon-internal today. To support framework-side decisions ("should Junior know that Apollo is federated with AICOE?"), expose it via a localhost-only API.
661
+
662
+ ### Endpoint shape
663
+
664
+ ```
665
+ GET http://localhost:18790/internal/peers
666
+ Authorization: Bearer <local-token>
667
+ ```
668
+
669
+ The token is a daemon-managed secret rotated on daemon start.
670
+
671
+ ### Discoverability — push, not pull
672
+
673
+ Rather than making framework code hunt for the daemon's port and token, the daemon publishes its internal-API config to a well-known location whenever it starts:
674
+
675
+ ```
676
+ ~/.ogp-{framework}/internal-config.json (mode 0600)
677
+
678
+ {
679
+ "version": "0.7.0",
680
+ "endpoint": "http://localhost:18790/internal",
681
+ "token": "<rotating-secret-token>",
682
+ "tokenIssuedAt": "2026-04-28T15:00:00Z",
683
+ "framework": "openclaw"
684
+ }
685
+ ```
686
+
687
+ In-gateway agents read this file to discover the endpoint and authenticate. The file's `0600` mode ensures only the same OS user can read it, providing the same access control as a token-only scheme but without making the framework code re-derive the daemon's port.
688
+
689
+ **Token rotation:** the file is regenerated on every daemon start. Long-lived agents must re-read on `ECONNREFUSED` or `401` responses — the helper library should retry once after re-reading the file before surfacing an error. The daemon writes atomically (write to `.tmp`, fsync, rename) so a reader that races with daemon startup never observes a partial file.
690
+
691
+ **On daemon shutdown:** the file is left in place but the token is invalidated. Any agent calling the endpoint with a stale token gets `401`, which signals "re-read the config file."
692
+
693
+ **Response:**
694
+
695
+ ```jsonc
696
+ {
697
+ "version": "0.7.0",
698
+ "personas": [
699
+ { "id": "junior", "displayName": "Junior", "role": "primary" },
700
+ { "id": "sterling", "displayName": "Sterling", "role": "specialist" }
701
+ ],
702
+ "peers": [
703
+ {
704
+ "id": "1a2b3c4d5e6f7890",
705
+ "displayName": "Stan @ Hermes",
706
+ "humanName": "Stan Huseletov",
707
+ "agentName": "Shadow",
708
+ "federationState": "established",
709
+ "personasGranted": ["junior", "sterling"],
710
+ "lastSeen": "2026-04-28T10:30:00Z"
711
+ }
712
+ ]
713
+ }
714
+ ```
715
+
716
+ The framework-side agent (Junior) calls this endpoint, sees that Stan is a peer with grants on both `junior` and `sterling`, and can decide whether/how to surface that to its human.
717
+
718
+ ### What this enables (without making it OGP's problem)
719
+
720
+ - **Cross-persona awareness.** Junior can render "you're federated with Stan; he can talk to me and Sterling" in its UI.
721
+ - **Routing decisions in the framework.** OpenClaw can decide "if Junior receives a question about finance, suggest forwarding to Sterling because Stan has access to both."
722
+ - **Audit visibility.** A monitoring agent in the gateway can query "show me everyone my human is federated with and what they're allowed to do" without scraping config files.
723
+
724
+ OGP doesn't make the routing decisions; it just exposes the data. The framework decides what to do with it.
725
+
726
+ ### Security
727
+
728
+ - **Localhost-only binding.** Endpoint is only available on `127.0.0.1`, not external. The daemon refuses requests with non-localhost source addresses.
729
+ - **Token-gated.** Even on localhost, requests need the bearer token from the protected file. Other users on a multi-user system can't read the registry.
730
+ - **Read-only.** The endpoint exposes data; it cannot mutate peer state. Mutations still go through the existing CLI/HTTP federation endpoints which have their own auth.
731
+
732
+ ### Implementation
733
+
734
+ New route handler in `src/daemon/server.ts` after the existing `/.well-known/ogp` block. Token generation/rotation in a new `src/daemon/internal-auth.ts`. Reads existing peer state via `listPeers()`, joins with persona config and current grants.
735
+
736
+ ## Test plan
737
+
738
+ New tests in `test/` (mapped to phase):
739
+
740
+ | File | Phase | Coverage |
741
+ |---|---|---|
742
+ | `test/multi-agent-personas-config.test.ts` | P1 | Config parsing, persona-array synthesis from legacy fields, primary-role invariant, `hookAgentId` defaulting |
743
+ | `test/multi-agent-personas-wire.test.ts` | P2 | Federation card serialization with/without `agents[]`; message envelope with/without `toAgent`; `personas[]` field on `ScopeGrant` round-trips |
744
+ | `test/multi-agent-personas-routing.test.ts` | P3 | `resolveTargetPersona` truth table: undefined → primary, exact match, no match → null |
745
+ | `test/multi-agent-personas-handler.test.ts` | P3 | Inbound `toAgent="sterling"` routes to hook with `agentId="sterling"`; unknown `toAgent` returns 404; missing `toAgent` routes to primary |
746
+ | `test/multi-agent-personas-cli-outbound.test.ts` | P4 | `--to-agent` flag wired to `federation send`/`agent`/`project contribute`; rejects when peer doesn't advertise `multi-agent-personas` |
747
+ | `test/multi-agent-personas-cli-discovery.test.ts` | P5 | `federation peers --show-agents` formatting; `whoami` output; `config add-agent`/`remove-agent`/`set-primary`/`list-agents` |
748
+ | `test/multi-agent-personas-scopes.test.ts` | P6 | `personas[]` field semantics (absent/empty/populated); CLI `--personas` flag wiring on `approve` and `grant`; `federation scopes` output renders restrictions |
749
+ | `test/doorman-persona-scope.test.ts` | P6 | `checkAccess()` step 5.5 enforcement: persona-restricted grant rejects out-of-list personas with 403; absent/empty personas grants apply universally |
750
+ | `test/framework-agents-detection.test.ts` | P7 | `detectFrameworkAgents()` reads OpenClaw `agents/` directory; parses `IDENTITY.md` for displayName; sanitizes ids; skips malformed entries with warning |
751
+ | `test/multi-agent-personas-sync-cli.test.ts` | P7 | `ogp config sync-agents` happy path; `--dry-run` prints without applying; `--force` overrides existing personas with confirmation |
752
+ | `test/internal-peers-endpoint.test.ts` | P8 | Endpoint binds to localhost only (refuses external); requires bearer token; returns expected join of personas + peers + grants; read-only (rejects PUT/POST) |
753
+ | `test/internal-auth.test.ts` | P8 | Token generation, file mode `0600`, rotation on daemon restart, token-file location per framework |
754
+ | `test/multi-agent-personas-backwards-compat.test.ts` | P9 | All compatibility matrices: v0.7↔v0.7, v0.7↔v0.6.x, missing config, missing card, persona scopes absent vs empty vs populated, multi-framework with mixed v0.6/v0.7 daemons |
755
+
756
+ ### End-to-end scenarios (manual or scripted)
757
+
758
+ Scenario 1 — basic multi-persona routing:
759
+ 1. Set up daemon with three personas (junior, sterling, apollo) via `ogp config sync-agents` against a real OpenClaw setup.
760
+ 2. Federate with a peer running v0.7.
761
+ 3. Verify peer sees three personas via `ogp federation peers --show-agents`.
762
+ 4. Send `--to-agent sterling` from peer; verify message lands at OpenClaw with `agentId: sterling`.
763
+ 5. Send no `--to-agent`; verify message lands at OpenClaw with `agentId: main` (primary).
764
+ 6. Send `--to-agent nonexistent`; verify peer gets 404 with clear error.
765
+
766
+ Scenario 2 — per-persona scope enforcement:
767
+ 1. From scenario 1's setup, run `ogp federation grant <peer-id> --intent agent-comms --personas junior` (lock peer to junior only).
768
+ 2. Send `--to-agent junior` from peer with `agent-comms` intent → succeeds.
769
+ 3. Send `--to-agent sterling` from peer with `agent-comms` intent → 403 with `Persona 'sterling' not in granted scope`.
770
+ 4. Send `--to-agent sterling` with a *different* intent that wasn't restricted → still rejected unless that intent's grant also includes sterling.
771
+ 5. Run `ogp federation grant <peer-id> --intent agent-comms --personas junior,sterling`. Step 3 now succeeds.
772
+
773
+ Scenario 3 — auto-sync from OpenClaw:
774
+ 1. Configure OpenClaw with a fresh agent `journal` (no entry in OGP config yet).
775
+ 2. Run `ogp config sync-agents --dry-run` → output proposes adding `journal` as specialist.
776
+ 3. Run `ogp config sync-agents` → confirms and applies; persona registered.
777
+ 4. Federation card now lists journal; peers running v0.7+ see it on next discovery refresh.
778
+
779
+ Scenario 4 — internal endpoint introspection:
780
+ 1. With the daemon running, an in-gateway agent calls `GET /internal/peers` with the bearer token from `~/.ogp-{framework}/internal-config.json`.
781
+ 2. Response includes the agent's own persona list, peer list, and per-peer grants.
782
+ 3. The agent uses this data to render "you're federated with Stan; he can address junior and sterling" in its UI.
783
+ 4. Try the same call from a different host (e.g., a Docker container that proxies localhost) — refused with 403.
784
+
785
+ Scenario 5 — backward compatibility:
786
+ 1. Federate v0.7 daemon with a v0.6.x daemon.
787
+ 2. v0.7 sends `--to-agent` (CLI rejects locally because peer doesn't advertise the capability).
788
+ 3. v0.6.x sends regular agent-comms (no `toAgent`); v0.7 routes to primary persona.
789
+ 4. Verify both daemons stay in `established` lifecycle state; no errors logged.
790
+
791
+ ## Phased implementation
792
+
793
+ Ten phases. P1–P3 are sequential (schema → wire → routing). P4–P5 are independent of each other and can parallelize. P6 (per-persona scopes) builds on P3. P7 (auto-sync) and P8 (internal endpoint) are independent of the rest. P9 and P10 are wrap-up.
794
+
795
+ | Phase | Scope | Depends on | Files touched |
796
+ |---|---|---|---|
797
+ | **P1: Schema + config** | Add `AgentPersona` interface, `OGPConfig.agents`, persona synthesis from legacy fields, persistence. No wire/runtime changes yet. | — | `src/shared/config.ts`, `test/multi-agent-personas-config.test.ts` |
798
+ | **P2: Wire format** | Add `agents[]` to `/.well-known/ogp` response. Add `toAgent` to `FederationMessage`. Add `multi-agent-personas` capability flag. Add `personas[]` field to `ScopeGrant` (carried over the wire even if not yet enforced). | P1 | `src/daemon/server.ts`, `src/daemon/message-handler.ts`, `src/daemon/scopes.ts`, `test/multi-agent-personas-wire.test.ts` |
799
+ | **P3: Routing** | Implement `resolveTargetPersona`. Plumb persona through to `openclaw-bridge`. Use `hookAgentId` in the hook call. Return 404 for unknown personas. | P2 | `src/daemon/message-handler.ts`, `src/daemon/openclaw-bridge.ts`, `test/multi-agent-personas-routing.test.ts`, `test/multi-agent-personas-handler.test.ts` |
800
+ | **P4: CLI — outbound** | Add `--to-agent` flag to `federation send`, `federation agent`, `project contribute`. Reject if peer doesn't advertise the capability. | P2 | `src/cli/federation.ts`, `src/cli/project.ts` |
801
+ | **P5: CLI — discovery & local config** | `ogp federation peers --show-agents`, `ogp whoami` shows local personas, `ogp config add-agent / remove-agent / set-primary / list-agents`. | P1 | `src/cli/federation.ts`, `src/cli/config.ts`, `src/cli/setup.ts` (interview update) |
802
+ | **P6: Per-persona scope grants** | Add `personas[]` enforcement to Doorman (step 5.5 in `checkAccess`). Update `ogp federation approve` and `ogp federation grant` with `--personas` flag. Update `ogp federation scopes` output to show persona restrictions. | P2, P3 | `src/daemon/doorman.ts`, `src/cli/federation.ts`, `test/multi-agent-personas-scopes.test.ts`, `test/doorman-persona-scope.test.ts` |
803
+ | **P7: Framework auto-sync** | New `framework-agents.ts` module. New `ogp config sync-agents [--dry-run] [--force]` command. Setup-wizard integration. OpenClaw enumerates `~/.openclaw/agents/<name>/` to N personas; Hermes is the trivial case (single persona derived from `~/.hermes/IDENTITY.md`); standalone is unsupported (manual config only). | P1 | `src/shared/framework-agents.ts` (new), `src/cli/config.ts`, `src/cli/setup.ts`, `test/framework-agents-detection.test.ts` |
804
+ | **P8: Internal peer registry endpoint** | New `GET /internal/peers` route on localhost only. Token generation/rotation in `src/daemon/internal-auth.ts`. Read-only join of personas + peers + grants. | P2, P6 | `src/daemon/server.ts`, `src/daemon/internal-auth.ts` (new), `test/internal-peers-endpoint.test.ts` |
805
+ | **P9: Backwards-compat tests** | Cover all matrices: v0.7↔v0.7, v0.7↔v0.6.x, missing config, missing card, persona scope absent vs empty vs populated. | All prior phases | `test/multi-agent-personas-backwards-compat.test.ts` |
806
+ | **P10: Docs** | Update `docs/PROTOCOL.md` with new envelope and capability flag. Update `docs/ARCHITECTURE.md` to add the multi-persona section. Add `docs/MULTI-AGENT-PERSONAS-IMPL.md` companion. Update `README.md` quickstart. Update `CHANGELOG.md`. | All prior phases | `docs/`, `README.md`, `CHANGELOG.md` |
807
+
808
+ ### Dependency graph
809
+
810
+ ```
811
+ P1 (schema)
812
+ / | \
813
+ P2 P5 P7 ← P5 depends on P1 only; P7 depends on P1 only
814
+ |
815
+ ┌──┴──┐
816
+ P3 P4 ← P4 depends on P2; P3 depends on P2
817
+
818
+ P6 (per-persona scopes — needs P2 wire + P3 routing)
819
+
820
+ P8 (internal endpoint — needs P2 wire + P6 grants)
821
+
822
+ P9 (backcompat tests — needs everything)
823
+
824
+ P10 (docs — needs everything)
825
+ ```
826
+
827
+ ### Parallelization
828
+
829
+ | Track | Phases | Notes |
830
+ |---|---|---|
831
+ | Track A (schema → wire → routing) | P1 → P2 → P3 → P6 → P8 | Sequential, the spine |
832
+ | Track B (CLI surface) | P4, P5 | Both depend on P1/P2 only; can run together |
833
+ | Track C (auto-sync) | P7 | Independent after P1; can run alongside Track A |
834
+
835
+ If single-dev: 3–4 weeks at a sustainable pace. If three parallel tracks: ~10 working days. The critical path is Track A (P1 → P2 → P3 → P6 → P8). P9 and P10 are post-merge wrap-up against whatever lands.
836
+
837
+ **Estimated effort:** ~3 weeks single-developer or ~10 working days dispatched in three parallel tracks. P6 and P7 are the work that grew the scope vs the original v1 design — ~1 extra week if going alone.
838
+
839
+ ## Decisions
840
+
841
+ All Open Questions from revision 1 have been resolved. Recorded here for traceability and to prevent re-bikeshedding during implementation.
842
+
843
+ | # | Question | Decision | Notes |
844
+ |---|---|---|---|
845
+ | 1 | `role` enum vocabulary | **`primary \| specialist`** | One persona must be primary; rest are specialists. Future enum extensions (`archived`, `experimental`) reserved but not in v0.7. |
846
+ | 2 | Per-persona icons/avatars | **Yes — optional `displayIcon` field** | Emoji or URL. Pure presentation; not validated; not enforced. Defaults to `⭐` for primary, `⚙` for specialist if unset (CLI rendering only). |
847
+ | 3 | `hookAgentId` default | **Asymmetric: primary → `'main'`, specialist → `id`** | Preserves backward compatibility (legacy daemons hardcode `agentId: 'main'`) while making specialist routing predictable. |
848
+ | 4 | Hermes routing + auto-sync | **Hermes is single-persona by design** | Hermes is one integrated runtime, not a multi-agent host. Auto-sync returns exactly one persona. No per-agent routing work needed for Hermes in v0.7. If Hermes evolves to support multiple internal agents later, this design accommodates it without protocol changes. |
849
+ | 5 | Default persona granting on federation approval | **Primary only by default** | Granting access to specialist personas (Sterling-finance, Apollo-research) must be an explicit act via `--personas`. Preserves scripted CI flows (no interactive prompt). Privacy-safer default. |
850
+ | 6 | Per-persona CLI flag style | **Comma-separated `--personas a,b,c`** | Matches existing `--intents` and `--topics` flag conventions. |
851
+ | 7 | Internal endpoint discoverability | **Push via `~/.ogp-{framework}/internal-config.json`** | Daemon writes config file on start (mode `0600`, atomic rename). Token rotates per daemon process. Agents re-read on 401. |
852
+ | 8 | Article timing | **Deferred** | Article will be drafted alongside or after release ship. Not a blocker for implementation. |
853
+
854
+ ## What this differentiates against
855
+
856
+ ### Functional comparison
857
+
858
+ | Concept | A2A | OGP today | OGP v0.7 |
859
+ |---|---|---|---|
860
+ | Trust unit | Per agent | Per gateway | **Per human** |
861
+ | Discovery | One Agent Card per agent (per origin) | One federation card per gateway | **One federation card with N personas** |
862
+ | Federation cost | N handshakes for N agents | 1 handshake | 1 handshake regardless of persona count |
863
+ | Agent addressing | Implicit (each agent has its own URL) | Always primary | Explicit `toAgent` routing |
864
+ | Cross-organization friction | DNS, certs, identity provider per agent | One peer record | One peer record, N addressable personas |
865
+ | Inbound persona access control | Implicit (each agent gates itself) | All-or-nothing | **Per-(peer × intent × persona) grants** |
866
+ | Framework agent auto-discovery | None | None | **`ogp config sync-agents`** |
867
+ | Internal agent introspection of federation | N/A | None | **Localhost `/internal/peers` endpoint** |
868
+
869
+ The architectural claim: **A2A scales linearly with agent count; OGP scales linearly with human/gateway count.** For personal and small-team deployments, the latter is dramatically cheaper.
870
+
871
+ ### 3D access-control matrix (for patent disclosure)
872
+
873
+ Per-persona scopes complete a 3D access control model that no published prior art implements in this combination:
874
+
875
+ | System | Per-peer | Per-intent | Per-persona |
876
+ |---|---|---|---|
877
+ | OAuth 2.0 / OIDC | ✅ (per-client) | ✅ (scopes) | ❌ (no persona concept) |
878
+ | MCP | ❌ | ✅ (per-tool) | ❌ |
879
+ | A2A | ❌ (no peer-level scope, capability-advertised only) | ❌ (capabilities, not gated) | ❌ |
880
+ | Matrix | ✅ (per-server) | partial (room-level) | ❌ |
881
+ | ActivityPub | partial (block lists) | ❌ | ❌ |
882
+ | DIDComm | ✅ (per-DID) | partial (per-protocol) | ❌ |
883
+ | OGP v0.6 | ✅ | ✅ | ❌ |
884
+ | **OGP v0.7** | **✅** | **✅** | **✅** |
885
+
886
+ This 3D matrix combined with cryptographic signing, lifecycle state preservation, and bilateral human-approved establishment is the inventive step. Each individual element has prior art. The combination, applied to AI agent gateways with containment preservation as a first-class invariant, does not.
887
+
888
+ ### What v0.7 adds vs v0.6 (claim deltas for the patent disclosure)
889
+
890
+ | New claim element | What it enables | Why it's not in prior art |
891
+ |---|---|---|
892
+ | Multi-persona advertisement under one keypair | Discovery of N agents via one federation card | A2A requires per-agent cards; gateway-card protocols (Matrix, ActivityPub) don't address agent personas |
893
+ | `toAgent` routing field with signed envelope | Cross-domain addressed delivery to a specific persona | No comparable gateway-internal routing primitive in cited prior art |
894
+ | Per-(peer × intent × persona) scope grants | Granular privacy: federate once, expose only chosen personas | Closest is OAuth's per-client scopes, but OAuth doesn't model gateway-internal personas at all |
895
+ | Framework auto-sync (`sync-agents`) | Zero-config persona setup from underlying OpenClaw/Hermes config | No published cross-framework discovery integration of this shape |
896
+ | Read-only internal peer registry endpoint | In-gateway agents can introspect their human's federation graph without protocol changes | No equivalent in agent communication protocols; closest analog is BGP's MIB but that's network-routing-specific |
897
+
898
+ ## Reference
899
+
900
+ ### Beads
901
+ - **B0032** (`.agent/memory/BEADS.md`) — Multi-Agent Personas, primary bead, v0.7.0
902
+ - **B0038** (to be created) — `agent-comms` policy per-persona overrides (deferred to v0.7.x or v0.8)
903
+ - **B0039** (to be created) — Hermes per-internal-agent parity if Hermes evolves to host multiple agents (currently the trivial single-persona case is sufficient)
904
+ - **B0040** (to be created) — Per-persona rate limits (deferred to v2 / v0.8)
905
+
906
+ ### Code anchors (current state, pre-implementation)
907
+ - `src/shared/config.ts:122–134` — current OGPConfig identity fields
908
+ - `src/daemon/server.ts:308–367` — current `/.well-known/ogp` endpoint
909
+ - `src/daemon/server.ts:349–366` — federation card response shape
910
+ - `src/daemon/message-handler.ts:51–63` — current `FederationMessage` interface
911
+ - `src/daemon/openclaw-bridge.ts:285–298` — current OpenClaw hook call site
912
+ - `src/daemon/scopes.ts:14–28` — current `ScopeGrant` shape
913
+ - `src/daemon/doorman.ts:81–171` — current `checkAccess()` 6-step flow (becomes 7-step in P6)
914
+ - `src/shared/framework-detection.ts` — existing framework detection (P7 extends this)
915
+
916
+ ### Prior shipped work this builds on
917
+ - v0.6.0 identity-snapshot work (commits `aa6f8ec`, `a964aa5`, `e0b1733`, `d3143c7`)
918
+ - v0.6.0 OSPF-inspired federation lifecycle (PRs #11, #12, #13)
919
+ - v0.4.0 multi-framework meta-config registry (`docs/MULTI-FRAMEWORK-DESIGN.md`)
920
+ - v0.2.0 scope negotiation model (`docs/scopes.md`)
921
+
922
+ ### Article series alignment
923
+ - Article 04 (*Breaking Up with OpenClaw*) — establishes the gateway-as-trust-boundary framing this design depends on
924
+ - Article 06 (*OSPF for Agents*, draft) — establishes the lifecycle state machine which the internal endpoint exposes
925
+ - Article 07 (proposed, *One Handshake, Many Agents*) — the multi-persona pitch; publish with v0.7.0 ship