@dp-pcs/ogp 0.6.0 → 0.7.0
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/README.md +47 -11
- package/dist/cli/agent-targeting.d.ts +21 -0
- package/dist/cli/agent-targeting.d.ts.map +1 -0
- package/dist/cli/agent-targeting.js +44 -0
- package/dist/cli/agent-targeting.js.map +1 -0
- package/dist/cli/config.d.ts +4 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +48 -0
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/federation.d.ts +2 -1
- package/dist/cli/federation.d.ts.map +1 -1
- package/dist/cli/federation.js +162 -92
- package/dist/cli/federation.js.map +1 -1
- package/dist/cli/keychain.d.ts +22 -0
- package/dist/cli/keychain.d.ts.map +1 -0
- package/dist/cli/keychain.js +213 -0
- package/dist/cli/keychain.js.map +1 -0
- package/dist/cli/project.d.ts +1 -0
- package/dist/cli/project.d.ts.map +1 -1
- package/dist/cli/project.js +9 -2
- package/dist/cli/project.js.map +1 -1
- package/dist/cli/setup.d.ts +37 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +130 -0
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli.js +40 -6
- package/dist/cli.js.map +1 -1
- package/dist/daemon/heartbeat.d.ts +37 -0
- package/dist/daemon/heartbeat.d.ts.map +1 -1
- package/dist/daemon/heartbeat.js +195 -21
- package/dist/daemon/heartbeat.js.map +1 -1
- package/dist/daemon/keypair.d.ts.map +1 -1
- package/dist/daemon/keypair.js +144 -22
- package/dist/daemon/keypair.js.map +1 -1
- package/dist/daemon/message-handler.d.ts +8 -0
- package/dist/daemon/message-handler.d.ts.map +1 -1
- package/dist/daemon/message-handler.js +60 -18
- package/dist/daemon/message-handler.js.map +1 -1
- package/dist/daemon/notify.d.ts +6 -0
- package/dist/daemon/notify.d.ts.map +1 -1
- package/dist/daemon/notify.js +9 -2
- package/dist/daemon/notify.js.map +1 -1
- package/dist/daemon/openclaw-bridge.d.ts +6 -0
- package/dist/daemon/openclaw-bridge.d.ts.map +1 -1
- package/dist/daemon/openclaw-bridge.js +10 -2
- package/dist/daemon/openclaw-bridge.js.map +1 -1
- package/dist/daemon/peers.d.ts +31 -0
- package/dist/daemon/peers.d.ts.map +1 -1
- package/dist/daemon/peers.js +66 -4
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/rendezvous.d.ts.map +1 -1
- package/dist/daemon/rendezvous.js +9 -7
- package/dist/daemon/rendezvous.js.map +1 -1
- package/dist/daemon/reply-handler.d.ts.map +1 -1
- package/dist/daemon/reply-handler.js +2 -1
- package/dist/daemon/reply-handler.js.map +1 -1
- package/dist/daemon/scopes.d.ts +8 -0
- package/dist/daemon/scopes.d.ts.map +1 -1
- package/dist/daemon/scopes.js.map +1 -1
- package/dist/daemon/server.d.ts +128 -1
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +260 -57
- package/dist/daemon/server.js.map +1 -1
- package/dist/shared/config.d.ts +93 -0
- package/dist/shared/config.d.ts.map +1 -1
- package/dist/shared/config.js +111 -0
- package/dist/shared/config.js.map +1 -1
- package/dist/shared/help.js +1 -0
- package/dist/shared/help.js.map +1 -1
- package/dist/shared/signing.d.ts +49 -0
- package/dist/shared/signing.d.ts.map +1 -1
- package/dist/shared/signing.js +68 -0
- package/dist/shared/signing.js.map +1 -1
- package/dist/shared/tls.d.ts +27 -0
- package/dist/shared/tls.d.ts.map +1 -0
- package/dist/shared/tls.js +37 -0
- package/dist/shared/tls.js.map +1 -0
- package/docs/ARCHITECTURE.md +146 -0
- package/docs/CLI-REFERENCE.md +170 -2
- package/docs/MULTI-AGENT-PERSONAS-DESIGN.md +925 -0
- package/docs/RC1-FEDERATION-TEST-CHECKLIST.md +477 -0
- package/package.json +1 -1
- package/scripts/completion.bash +25 -1
- package/scripts/completion.zsh +9 -4
- package/scripts/render-ogp-overview-video.mjs +417 -0
- package/skills/ogp/SKILL.md +1 -1
- package/skills/ogp-expose/SKILL.md +1 -1
- package/skills/ogp-project/SKILL.md +1 -1
package/dist/cli/federation.js
CHANGED
|
@@ -11,6 +11,7 @@ import { loadIntents } from '../daemon/intent-registry.js';
|
|
|
11
11
|
import { loadMetaConfig } from '../shared/meta-config.js';
|
|
12
12
|
import { logActivity } from '../daemon/agent-comms.js';
|
|
13
13
|
import { deliverLocalSessionText } from '../daemon/notify.js';
|
|
14
|
+
import { validateTargetAgent } from './agent-targeting.js';
|
|
14
15
|
/**
|
|
15
16
|
* Expand tilde in paths
|
|
16
17
|
*/
|
|
@@ -20,6 +21,42 @@ function expandTilde(filePath) {
|
|
|
20
21
|
}
|
|
21
22
|
return filePath;
|
|
22
23
|
}
|
|
24
|
+
function formatRelative(iso) {
|
|
25
|
+
if (!iso)
|
|
26
|
+
return 'never';
|
|
27
|
+
const ms = Date.now() - new Date(iso).getTime();
|
|
28
|
+
if (ms < 0)
|
|
29
|
+
return 'in the future';
|
|
30
|
+
const m = Math.floor(ms / 60000);
|
|
31
|
+
if (m < 1)
|
|
32
|
+
return '<1m';
|
|
33
|
+
if (m < 60)
|
|
34
|
+
return `${m}m ago`;
|
|
35
|
+
const h = Math.floor(m / 60);
|
|
36
|
+
if (h < 24)
|
|
37
|
+
return `${h}h ago`;
|
|
38
|
+
return `${Math.floor(h / 24)}d ago`;
|
|
39
|
+
}
|
|
40
|
+
function healthStateLabel(state) {
|
|
41
|
+
switch (state) {
|
|
42
|
+
case 'established': return { icon: '✓', label: 'established' };
|
|
43
|
+
case 'degraded-outbound': return { icon: '⚠', label: 'degraded-out' };
|
|
44
|
+
case 'degraded-inbound': return { icon: '⚠', label: 'degraded-in' };
|
|
45
|
+
case 'down': return { icon: '✗', label: 'down' };
|
|
46
|
+
default: return { icon: '?', label: 'unknown' };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function federationStateLabel(state) {
|
|
50
|
+
switch (state) {
|
|
51
|
+
case 'init': return { icon: '◐', label: 'INIT' };
|
|
52
|
+
case 'twoWay': return { icon: '◑', label: 'TWO-WAY' };
|
|
53
|
+
case 'established': return { icon: '✓', label: 'ESTABLISHED' };
|
|
54
|
+
case 'degraded': return { icon: '⚠', label: 'DEGRADED' };
|
|
55
|
+
case 'down': return { icon: '✗', label: 'DOWN' };
|
|
56
|
+
case 'tombstoned': return { icon: '☠', label: 'TOMBSTONED' };
|
|
57
|
+
default: return { icon: '?', label: 'UNKNOWN' };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
23
60
|
function normalizeGatewayUrl(url) {
|
|
24
61
|
const trimmed = url.trim();
|
|
25
62
|
if (!trimmed) {
|
|
@@ -148,25 +185,22 @@ export async function federationList(status, filterTag) {
|
|
|
148
185
|
peers.forEach(peer => {
|
|
149
186
|
const aliasDisplay = peer.alias || peer.displayName || '-';
|
|
150
187
|
const displayName = peer.alias ? peer.displayName : '';
|
|
151
|
-
const keyShort = peer.publicKey?.substring(0, 16) || '-';
|
|
152
188
|
// Status and health icons
|
|
153
189
|
let statusIcon = peer.status === 'approved' ? '✓' : peer.status === 'pending' ? '?' : '✗';
|
|
154
190
|
if (peer.status === 'approved' && peer.healthy === false) {
|
|
155
191
|
statusIcon = '✗'; // Show unhealthy status
|
|
156
192
|
}
|
|
157
|
-
//
|
|
193
|
+
// Issue #3 + #5: directional health summary for approved peers
|
|
158
194
|
let healthInfo = '';
|
|
159
195
|
if (peer.status === 'approved') {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
healthInfo = ' [never seen]';
|
|
169
|
-
}
|
|
196
|
+
const { icon, label } = healthStateLabel(peer.healthState);
|
|
197
|
+
const out = peer.lastOutboundCheckFailedAt && (!peer.lastOutboundCheckAt || peer.lastOutboundCheckFailedAt > peer.lastOutboundCheckAt)
|
|
198
|
+
? `out: FAIL ${formatRelative(peer.lastOutboundCheckFailedAt)}`
|
|
199
|
+
: `out: ${formatRelative(peer.lastOutboundCheckAt)}`;
|
|
200
|
+
const inboundLabel = peer.inboundHealthReport
|
|
201
|
+
? `in (reported): ${peer.inboundHealthReport.healthy ? 'OK' : 'FAIL'} ${formatRelative(peer.inboundHealthReport.receivedAt)}`
|
|
202
|
+
: `in: ${formatRelative(peer.lastInboundContactAt)}`;
|
|
203
|
+
healthInfo = ` [${icon} ${label} ${out} ${inboundLabel}]`;
|
|
170
204
|
if (peer.healthCheckFailures && peer.healthCheckFailures > 0) {
|
|
171
205
|
healthInfo += ` (${peer.healthCheckFailures} failures)`;
|
|
172
206
|
}
|
|
@@ -219,19 +253,10 @@ export async function federationList(status, filterTag) {
|
|
|
219
253
|
const displayCol = (peer.displayName || '-').slice(0, 20).padEnd(20);
|
|
220
254
|
const keyCol = (peer.publicKey?.substring(0, 16) || '-') + '...';
|
|
221
255
|
const statusCol = peer.status;
|
|
222
|
-
// Health status indicator
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
healthIcon = '✓';
|
|
227
|
-
}
|
|
228
|
-
else if (peer.healthy === false) {
|
|
229
|
-
healthIcon = '✗';
|
|
230
|
-
}
|
|
231
|
-
else {
|
|
232
|
-
healthIcon = '?'; // Unknown health status
|
|
233
|
-
}
|
|
234
|
-
}
|
|
256
|
+
// Health status indicator (Issue #3: directional)
|
|
257
|
+
const { icon: healthIcon } = peer.status === 'approved'
|
|
258
|
+
? healthStateLabel(peer.healthState)
|
|
259
|
+
: { icon: '' };
|
|
235
260
|
console.log(` ${healthIcon ? healthIcon + ' ' : ''}${aliasCol} ${displayCol} ${keyCol.padEnd(20)} ${statusCol}`);
|
|
236
261
|
console.log(` Gateway: ${peer.gatewayUrl}`);
|
|
237
262
|
console.log(` ID: ${peer.id}`);
|
|
@@ -250,13 +275,23 @@ export async function federationList(status, filterTag) {
|
|
|
250
275
|
if (peer.tags && peer.tags.length > 0) {
|
|
251
276
|
console.log(` Tags: ${peer.tags.join(', ')}`);
|
|
252
277
|
}
|
|
253
|
-
//
|
|
278
|
+
// Issue #4: federation lifecycle line for any non-tombstoned peer.
|
|
279
|
+
if (peer.status !== 'rejected' && peer.status !== 'removed') {
|
|
280
|
+
const { label: fedLabel } = federationStateLabel(peer.federationState);
|
|
281
|
+
const since = peer.federationStateChangedAt ? ` since ${formatRelative(peer.federationStateChangedAt)}` : '';
|
|
282
|
+
const reason = peer.federationStateReason ? ` — ${peer.federationStateReason}` : '';
|
|
283
|
+
console.log(` Federation: ${fedLabel}${since}${reason}`);
|
|
284
|
+
}
|
|
285
|
+
// Show health details for approved peers (Issue #3: directional, Issue #5: authoritative inbound)
|
|
254
286
|
if (peer.status === 'approved') {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
287
|
+
const { label } = healthStateLabel(peer.healthState);
|
|
288
|
+
console.log(` Health state: ${label}${peer.healthStateChangedAt ? ` (since ${formatRelative(peer.healthStateChangedAt)})` : ''}`);
|
|
289
|
+
console.log(` Outbound: last ok ${formatRelative(peer.lastOutboundCheckAt)}, last fail ${formatRelative(peer.lastOutboundCheckFailedAt)}`);
|
|
290
|
+
console.log(` Inbound: last contact ${formatRelative(peer.lastInboundContactAt)}`);
|
|
291
|
+
if (peer.inboundHealthReport) {
|
|
292
|
+
const r = peer.inboundHealthReport;
|
|
293
|
+
console.log(` Inbound (reported): ${r.healthy ? 'healthy' : 'unhealthy'} as of ${formatRelative(r.receivedAt)}` +
|
|
294
|
+
(r.healthCheckFailures && r.healthCheckFailures > 0 ? ` (${r.healthCheckFailures} failures from peer)` : ''));
|
|
260
295
|
}
|
|
261
296
|
if (peer.healthCheckFailures && peer.healthCheckFailures > 0) {
|
|
262
297
|
console.log(` Health check failures: ${peer.healthCheckFailures}`);
|
|
@@ -319,42 +354,17 @@ export async function federationStatus() {
|
|
|
319
354
|
console.log('\n Approved peers:');
|
|
320
355
|
for (const peer of approvedPeers) {
|
|
321
356
|
const aliasDisplay = peer.alias || peer.displayName || 'no alias';
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
healthIcon = '✗';
|
|
329
|
-
}
|
|
330
|
-
else {
|
|
331
|
-
healthIcon = '?';
|
|
332
|
-
}
|
|
333
|
-
// Format last seen time
|
|
334
|
-
let lastSeenStr = '';
|
|
335
|
-
if (peer.lastSeenAt) {
|
|
336
|
-
const lastSeen = new Date(peer.lastSeenAt);
|
|
337
|
-
const now = new Date();
|
|
338
|
-
const minutesAgo = Math.floor((now.getTime() - lastSeen.getTime()) / 60000);
|
|
339
|
-
if (minutesAgo < 60) {
|
|
340
|
-
lastSeenStr = `${minutesAgo}m ago`;
|
|
341
|
-
}
|
|
342
|
-
else if (minutesAgo < 1440) {
|
|
343
|
-
lastSeenStr = `${Math.floor(minutesAgo / 60)}h ago`;
|
|
344
|
-
}
|
|
345
|
-
else {
|
|
346
|
-
lastSeenStr = `${Math.floor(minutesAgo / 1440)}d ago`;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
else {
|
|
350
|
-
lastSeenStr = 'never';
|
|
351
|
-
}
|
|
352
|
-
// Show health failures if any
|
|
357
|
+
// Issue #3: directional health
|
|
358
|
+
const { icon: healthIcon, label } = healthStateLabel(peer.healthState);
|
|
359
|
+
const out = peer.lastOutboundCheckFailedAt && (!peer.lastOutboundCheckAt || peer.lastOutboundCheckFailedAt > peer.lastOutboundCheckAt)
|
|
360
|
+
? `out FAIL ${formatRelative(peer.lastOutboundCheckFailedAt)}`
|
|
361
|
+
: `out ${formatRelative(peer.lastOutboundCheckAt)}`;
|
|
362
|
+
const inb = `in ${formatRelative(peer.lastInboundContactAt)}`;
|
|
353
363
|
const failuresStr = peer.healthCheckFailures && peer.healthCheckFailures > 0
|
|
354
364
|
? ` (${peer.healthCheckFailures} failures)`
|
|
355
365
|
: '';
|
|
356
366
|
const scopes = peer.grantedScopes?.scopes.map(s => s.intent).join(', ') || 'none';
|
|
357
|
-
console.log(` ${healthIcon} ${aliasDisplay.padEnd(20)}
|
|
367
|
+
console.log(` ${healthIcon} ${aliasDisplay.padEnd(20)} ${label.padEnd(13)} ${out.padEnd(20)} ${inb.padEnd(15)}${failuresStr} ${scopes}`);
|
|
358
368
|
}
|
|
359
369
|
}
|
|
360
370
|
}
|
|
@@ -394,10 +404,18 @@ export async function federationStatus() {
|
|
|
394
404
|
const pendingPeers = peers.filter(p => p.status === 'pending');
|
|
395
405
|
const rejectedPeers = peers.filter(p => p.status === 'rejected');
|
|
396
406
|
const removedPeers = peers.filter(p => p.status === 'removed');
|
|
397
|
-
// Health statistics for approved peers
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
407
|
+
// Health statistics for approved peers (Issue #3: directional)
|
|
408
|
+
const stateCounts = {
|
|
409
|
+
established: 0,
|
|
410
|
+
'degraded-outbound': 0,
|
|
411
|
+
'degraded-inbound': 0,
|
|
412
|
+
down: 0,
|
|
413
|
+
unknown: 0
|
|
414
|
+
};
|
|
415
|
+
for (const p of approvedPeers) {
|
|
416
|
+
const state = p.healthState ?? 'unknown';
|
|
417
|
+
stateCounts[state] = (stateCounts[state] ?? 0) + 1;
|
|
418
|
+
}
|
|
401
419
|
console.log('\n📊 FEDERATION STATUS\n');
|
|
402
420
|
// Summary counts
|
|
403
421
|
console.log(`Total peers: ${peers.length}`);
|
|
@@ -406,12 +424,38 @@ export async function federationStatus() {
|
|
|
406
424
|
console.log(` Rejected: ${rejectedPeers.length}`);
|
|
407
425
|
console.log(` Removed: ${removedPeers.length}`);
|
|
408
426
|
console.log('');
|
|
409
|
-
// Health summary for approved peers
|
|
427
|
+
// Health summary for approved peers (Issue #3: directional state breakdown)
|
|
410
428
|
if (approvedPeers.length > 0) {
|
|
411
429
|
console.log('🏥 PEER HEALTH:\n');
|
|
412
|
-
console.log(`
|
|
413
|
-
console.log(`
|
|
414
|
-
console.log(`
|
|
430
|
+
console.log(` Established: ${stateCounts.established} (✓)`);
|
|
431
|
+
console.log(` Degraded out: ${stateCounts['degraded-outbound']} (⚠ I can't reach them)`);
|
|
432
|
+
console.log(` Degraded in: ${stateCounts['degraded-inbound']} (⚠ they haven't reached me)`);
|
|
433
|
+
console.log(` Down: ${stateCounts.down} (✗)`);
|
|
434
|
+
console.log(` Unknown: ${stateCounts.unknown} (?)`);
|
|
435
|
+
console.log('');
|
|
436
|
+
}
|
|
437
|
+
// Issue #4: federation lifecycle state breakdown across all non-tombstoned peers.
|
|
438
|
+
const lifecycleCounts = {
|
|
439
|
+
init: 0, twoWay: 0, established: 0, degraded: 0, down: 0, unknown: 0
|
|
440
|
+
};
|
|
441
|
+
for (const p of peers) {
|
|
442
|
+
if (p.status === 'rejected' || p.status === 'removed')
|
|
443
|
+
continue;
|
|
444
|
+
const s = p.federationState ?? 'unknown';
|
|
445
|
+
if (s === 'tombstoned')
|
|
446
|
+
continue;
|
|
447
|
+
lifecycleCounts[s] = (lifecycleCounts[s] ?? 0) + 1;
|
|
448
|
+
}
|
|
449
|
+
if (peers.some(p => p.status !== 'rejected' && p.status !== 'removed')) {
|
|
450
|
+
console.log('🔗 FEDERATION LIFECYCLE:\n');
|
|
451
|
+
console.log(` Init: ${lifecycleCounts.init}`);
|
|
452
|
+
console.log(` Two-way: ${lifecycleCounts.twoWay}`);
|
|
453
|
+
console.log(` Established: ${lifecycleCounts.established}`);
|
|
454
|
+
console.log(` Degraded: ${lifecycleCounts.degraded}`);
|
|
455
|
+
console.log(` Down: ${lifecycleCounts.down}`);
|
|
456
|
+
if (lifecycleCounts.unknown > 0) {
|
|
457
|
+
console.log(` Unknown: ${lifecycleCounts.unknown}`);
|
|
458
|
+
}
|
|
415
459
|
console.log('');
|
|
416
460
|
}
|
|
417
461
|
// Alias → Public Key mapping section
|
|
@@ -472,15 +516,18 @@ export async function federationRequest(peerUrl, peerId, alias) {
|
|
|
472
516
|
...(config.agentName ? { agentName: config.agentName } : {}),
|
|
473
517
|
...(config.organization ? { organization: config.organization } : {}),
|
|
474
518
|
};
|
|
475
|
-
const { sign } = await import('../shared/signing.js');
|
|
476
|
-
const signature = sign(JSON.stringify(peer), keypair.privateKey);
|
|
477
519
|
// Fetch our capabilities to include in the request (BUILD-110: intent negotiation)
|
|
478
520
|
let ourIntents = loadIntents().map((i) => i.name);
|
|
479
521
|
if (ourIntents.length === 0) {
|
|
480
522
|
// Fallback to default intents if none registered
|
|
481
523
|
ourIntents = ['message', 'agent-comms', 'project.join', 'project.contribute', 'project.query', 'project.status'];
|
|
482
524
|
}
|
|
483
|
-
|
|
525
|
+
// SECURITY (F-04): Send a signed canonical envelope. The receiver verifies
|
|
526
|
+
// the signature against peer.publicKey from the payload — proves we hold
|
|
527
|
+
// the private key for the publicKey we're announcing.
|
|
528
|
+
const { signCanonical } = await import('../shared/signing.js');
|
|
529
|
+
const { payloadStr, signature } = signCanonical({ peer, offeredIntents: ourIntents }, keypair.privateKey);
|
|
530
|
+
const requestBody = { payloadStr, signature };
|
|
484
531
|
// Send request
|
|
485
532
|
try {
|
|
486
533
|
const response = await fetch(`${resolvedPeerUrl}/federation/request`, {
|
|
@@ -628,25 +675,27 @@ export async function federationApprove(peerId, options = {}) {
|
|
|
628
675
|
const ourConfig = requireConfig();
|
|
629
676
|
try {
|
|
630
677
|
const nonce = crypto.randomUUID();
|
|
678
|
+
// SECURITY (F-01): Approval is a canonical signed envelope.
|
|
679
|
+
const { signCanonical } = await import('../shared/signing.js');
|
|
680
|
+
const { payloadStr, signature } = signCanonical({
|
|
681
|
+
// Package format
|
|
682
|
+
peerId: peer.id,
|
|
683
|
+
approved: true,
|
|
684
|
+
// Fork format (for interoperability)
|
|
685
|
+
fromGatewayId: `${new URL(ourConfig.gatewayUrl).hostname}:${ourConfig.daemonPort}`,
|
|
686
|
+
fromDisplayName: ourConfig.displayName,
|
|
687
|
+
fromGatewayUrl: ourConfig.gatewayUrl,
|
|
688
|
+
fromPublicKey: keypair.publicKey,
|
|
689
|
+
fromEmail: ourConfig.email,
|
|
690
|
+
nonce,
|
|
691
|
+
// v0.2.0: Include scope grants
|
|
692
|
+
protocolVersion: '0.2.0',
|
|
693
|
+
scopeGrants
|
|
694
|
+
}, keypair.privateKey);
|
|
631
695
|
await fetch(`${peerGatewayUrl}/federation/approve`, {
|
|
632
696
|
method: 'POST',
|
|
633
697
|
headers: { 'Content-Type': 'application/json' },
|
|
634
|
-
body: JSON.stringify({
|
|
635
|
-
// Package format
|
|
636
|
-
peerId: peer.id,
|
|
637
|
-
approved: true,
|
|
638
|
-
// Fork format (for interoperability)
|
|
639
|
-
fromGatewayId: `${new URL(ourConfig.gatewayUrl).hostname}:${ourConfig.daemonPort}`,
|
|
640
|
-
fromDisplayName: ourConfig.displayName,
|
|
641
|
-
fromGatewayUrl: ourConfig.gatewayUrl,
|
|
642
|
-
fromPublicKey: keypair.publicKey,
|
|
643
|
-
fromEmail: ourConfig.email,
|
|
644
|
-
timestamp: new Date().toISOString(),
|
|
645
|
-
nonce,
|
|
646
|
-
// v0.2.0: Include scope grants
|
|
647
|
-
protocolVersion: '0.2.0',
|
|
648
|
-
scopeGrants
|
|
649
|
-
})
|
|
698
|
+
body: JSON.stringify({ payloadStr, signature })
|
|
650
699
|
});
|
|
651
700
|
console.log('✓ Notified peer of approval');
|
|
652
701
|
}
|
|
@@ -782,7 +831,7 @@ export async function federationRemove(peerId) {
|
|
|
782
831
|
removePeer(peerId);
|
|
783
832
|
console.log(`✓ Removed peer: ${peerId} (${peer.displayName})`);
|
|
784
833
|
}
|
|
785
|
-
export async function federationSend(peerId, intent, payloadJson, timeoutMs) {
|
|
834
|
+
export async function federationSend(peerId, intent, payloadJson, timeoutMs, toAgent) {
|
|
786
835
|
const config = requireConfig();
|
|
787
836
|
// Resolve peer identifier (alias, ID, or public key)
|
|
788
837
|
const resolvedId = resolvePeerId(peerId);
|
|
@@ -800,6 +849,14 @@ export async function federationSend(peerId, intent, payloadJson, timeoutMs) {
|
|
|
800
849
|
console.error(`Peer ${peerId} is not approved`);
|
|
801
850
|
return null;
|
|
802
851
|
}
|
|
852
|
+
// B0032 P4: capability check before honoring --to-agent.
|
|
853
|
+
if (toAgent) {
|
|
854
|
+
const targeting = await validateTargetAgent({ id: peer.id, gatewayUrl: peer.gatewayUrl, displayName: peer.displayName }, toAgent);
|
|
855
|
+
if (!targeting.ok) {
|
|
856
|
+
console.error(targeting.reason);
|
|
857
|
+
return null;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
803
860
|
const payload = JSON.parse(payloadJson);
|
|
804
861
|
const keypair = loadOrGenerateKeyPair();
|
|
805
862
|
// BUILD-111: Use public key prefix as our ID (not hostname:port)
|
|
@@ -808,6 +865,7 @@ export async function federationSend(peerId, intent, payloadJson, timeoutMs) {
|
|
|
808
865
|
intent,
|
|
809
866
|
from: ourId,
|
|
810
867
|
to: peerId,
|
|
868
|
+
...(toAgent ? { toAgent } : {}),
|
|
811
869
|
nonce: crypto.randomUUID(),
|
|
812
870
|
timestamp: new Date().toISOString(),
|
|
813
871
|
payload
|
|
@@ -1122,6 +1180,14 @@ export async function federationSendAgentComms(peerId, topic, messageText, optio
|
|
|
1122
1180
|
console.error(`Peer ${peerId} is not approved`);
|
|
1123
1181
|
return;
|
|
1124
1182
|
}
|
|
1183
|
+
// B0032 P4: capability check before honoring --to-agent.
|
|
1184
|
+
if (options.toAgent) {
|
|
1185
|
+
const targeting = await validateTargetAgent({ id: peer.id, gatewayUrl: peer.gatewayUrl, displayName: peer.displayName }, options.toAgent);
|
|
1186
|
+
if (!targeting.ok) {
|
|
1187
|
+
console.error(targeting.reason);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1125
1191
|
const keypair = loadOrGenerateKeyPair();
|
|
1126
1192
|
// Use 32-char public key prefix as our ID (avoids Ed25519 DER header collision with 16-char)
|
|
1127
1193
|
const ourId = keypair.publicKey.substring(0, 32);
|
|
@@ -1135,6 +1201,7 @@ export async function federationSendAgentComms(peerId, topic, messageText, optio
|
|
|
1135
1201
|
intent: 'agent-comms',
|
|
1136
1202
|
from: ourId,
|
|
1137
1203
|
to: peerId,
|
|
1204
|
+
...(options.toAgent ? { toAgent: options.toAgent } : {}),
|
|
1138
1205
|
nonce,
|
|
1139
1206
|
timestamp: new Date().toISOString(),
|
|
1140
1207
|
replyTo,
|
|
@@ -1232,10 +1299,13 @@ export async function federationInvite() {
|
|
|
1232
1299
|
const pubkey = getPublicKey();
|
|
1233
1300
|
const port = config.daemonPort ?? 18790;
|
|
1234
1301
|
try {
|
|
1302
|
+
// SECURITY (F-02): /invite now requires a signed envelope, same as /register.
|
|
1303
|
+
const { signCanonical } = await import('../shared/signing.js');
|
|
1304
|
+
const { payloadStr, signature } = signCanonical({ pubkey, port }, getPrivateKey());
|
|
1235
1305
|
const res = await fetch(`${config.rendezvous.url}/invite`, {
|
|
1236
1306
|
method: 'POST',
|
|
1237
1307
|
headers: { 'Content-Type': 'application/json' },
|
|
1238
|
-
body: JSON.stringify({
|
|
1308
|
+
body: JSON.stringify({ payloadStr, signature }),
|
|
1239
1309
|
signal: AbortSignal.timeout(10000),
|
|
1240
1310
|
});
|
|
1241
1311
|
if (!res.ok) {
|