@dp-pcs/ogp 0.3.3 → 0.4.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.
Files changed (112) hide show
  1. package/README.md +317 -61
  2. package/dist/cli/completion.d.ts +5 -0
  3. package/dist/cli/completion.d.ts.map +1 -0
  4. package/dist/cli/completion.js +148 -0
  5. package/dist/cli/completion.js.map +1 -0
  6. package/dist/cli/config.d.ts +3 -0
  7. package/dist/cli/config.d.ts.map +1 -0
  8. package/dist/cli/config.js +207 -0
  9. package/dist/cli/config.js.map +1 -0
  10. package/dist/cli/expose.d.ts.map +1 -1
  11. package/dist/cli/expose.js +20 -13
  12. package/dist/cli/expose.js.map +1 -1
  13. package/dist/cli/federation.d.ts +14 -0
  14. package/dist/cli/federation.d.ts.map +1 -1
  15. package/dist/cli/federation.js +347 -23
  16. package/dist/cli/federation.js.map +1 -1
  17. package/dist/cli/project.d.ts +4 -3
  18. package/dist/cli/project.d.ts.map +1 -1
  19. package/dist/cli/project.js +34 -24
  20. package/dist/cli/project.js.map +1 -1
  21. package/dist/cli/setup.d.ts +23 -0
  22. package/dist/cli/setup.d.ts.map +1 -1
  23. package/dist/cli/setup.js +560 -32
  24. package/dist/cli/setup.js.map +1 -1
  25. package/dist/cli.js +358 -35
  26. package/dist/cli.js.map +1 -1
  27. package/dist/daemon/agent-comms.d.ts.map +1 -1
  28. package/dist/daemon/agent-comms.js +15 -10
  29. package/dist/daemon/agent-comms.js.map +1 -1
  30. package/dist/daemon/heartbeat.d.ts +22 -0
  31. package/dist/daemon/heartbeat.d.ts.map +1 -0
  32. package/dist/daemon/heartbeat.js +119 -0
  33. package/dist/daemon/heartbeat.js.map +1 -0
  34. package/dist/daemon/intent-registry.d.ts.map +1 -1
  35. package/dist/daemon/intent-registry.js +19 -10
  36. package/dist/daemon/intent-registry.js.map +1 -1
  37. package/dist/daemon/keypair.d.ts +1 -0
  38. package/dist/daemon/keypair.d.ts.map +1 -1
  39. package/dist/daemon/keypair.js +151 -18
  40. package/dist/daemon/keypair.js.map +1 -1
  41. package/dist/daemon/message-handler.d.ts.map +1 -1
  42. package/dist/daemon/message-handler.js +30 -16
  43. package/dist/daemon/message-handler.js.map +1 -1
  44. package/dist/daemon/notify.d.ts +19 -0
  45. package/dist/daemon/notify.d.ts.map +1 -1
  46. package/dist/daemon/notify.js +376 -73
  47. package/dist/daemon/notify.js.map +1 -1
  48. package/dist/daemon/openclaw-bridge.d.ts +34 -0
  49. package/dist/daemon/openclaw-bridge.d.ts.map +1 -0
  50. package/dist/daemon/openclaw-bridge.js +261 -0
  51. package/dist/daemon/openclaw-bridge.js.map +1 -0
  52. package/dist/daemon/peers.d.ts +21 -0
  53. package/dist/daemon/peers.d.ts.map +1 -1
  54. package/dist/daemon/peers.js +125 -20
  55. package/dist/daemon/peers.js.map +1 -1
  56. package/dist/daemon/projects.d.ts +9 -6
  57. package/dist/daemon/projects.d.ts.map +1 -1
  58. package/dist/daemon/projects.js +30 -20
  59. package/dist/daemon/projects.js.map +1 -1
  60. package/dist/daemon/server.d.ts +17 -0
  61. package/dist/daemon/server.d.ts.map +1 -1
  62. package/dist/daemon/server.js +188 -77
  63. package/dist/daemon/server.js.map +1 -1
  64. package/dist/shared/config.d.ts +52 -1
  65. package/dist/shared/config.d.ts.map +1 -1
  66. package/dist/shared/config.js +18 -11
  67. package/dist/shared/config.js.map +1 -1
  68. package/dist/shared/framework-detection.d.ts +31 -0
  69. package/dist/shared/framework-detection.d.ts.map +1 -0
  70. package/dist/shared/framework-detection.js +91 -0
  71. package/dist/shared/framework-detection.js.map +1 -0
  72. package/dist/shared/help.d.ts +5 -0
  73. package/dist/shared/help.d.ts.map +1 -0
  74. package/dist/shared/help.js +281 -0
  75. package/dist/shared/help.js.map +1 -0
  76. package/dist/shared/meta-config.d.ts +44 -0
  77. package/dist/shared/meta-config.d.ts.map +1 -0
  78. package/dist/shared/meta-config.js +89 -0
  79. package/dist/shared/meta-config.js.map +1 -0
  80. package/dist/shared/migration.d.ts +57 -0
  81. package/dist/shared/migration.d.ts.map +1 -0
  82. package/dist/shared/migration.js +255 -0
  83. package/dist/shared/migration.js.map +1 -0
  84. package/docs/CLI-REFERENCE.md +1361 -0
  85. package/docs/GETTING-STARTED.md +953 -0
  86. package/docs/MIGRATION.md +202 -0
  87. package/docs/MULTI-FRAMEWORK-DEMO.md +352 -0
  88. package/docs/MULTI-FRAMEWORK-DESIGN.md +378 -0
  89. package/docs/MULTI-FRAMEWORK-IMPL.md +197 -0
  90. package/docs/case-studies/CRASH_RESOLUTION_20260407.md +190 -0
  91. package/docs/case-studies/OpenClaw_Hermes_Status_Report_20260407.md +142 -0
  92. package/docs/case-studies/OpenClaw_Stability_Fix_Summary.md +209 -0
  93. package/docs/case-studies/README.md +40 -0
  94. package/docs/case-studies/crash_observations.md +250 -0
  95. package/docs/cloudflare-named-tunnel-setup.md +126 -0
  96. package/docs/federation-flow.md +27 -37
  97. package/docs/hermes-implementation-checklist.md +4 -0
  98. package/docs/project-intent-testing.md +97 -0
  99. package/docs/quickstart.md +12 -4
  100. package/docs/rendezvous.md +13 -14
  101. package/docs/scopes.md +13 -13
  102. package/package.json +12 -6
  103. package/scripts/completion.bash +123 -0
  104. package/scripts/completion.zsh +372 -0
  105. package/scripts/install-skills.js +19 -1
  106. package/scripts/test-migration-execute.js +74 -0
  107. package/scripts/test-migration.js +42 -0
  108. package/scripts/test-project-intents.mjs +614 -0
  109. package/skills/ogp/SKILL.md +197 -64
  110. package/skills/ogp-agent-comms/SKILL.md +107 -41
  111. package/skills/ogp-expose/SKILL.md +183 -25
  112. package/skills/ogp-project/SKILL.md +110 -88
@@ -1,11 +1,94 @@
1
- import { listPeers, loadPeers, getPeer, approvePeer, rejectPeer, updatePeerGrantedScopes } from '../daemon/peers.js';
2
- import { requireConfig } from '../shared/config.js';
1
+ import { listPeers, loadPeers, getPeer, approvePeer, rejectPeer, updatePeer, updatePeerGrantedScopes } from '../daemon/peers.js';
2
+ import { requireConfig, loadConfig } from '../shared/config.js';
3
3
  import { lookupPeer } from '../daemon/rendezvous.js';
4
4
  import { getPublicKey, getPrivateKey, loadOrGenerateKeyPair } from '../daemon/keypair.js';
5
5
  import { signObject, sign } from '../shared/signing.js';
6
6
  import * as crypto from 'node:crypto';
7
+ import * as os from 'node:os';
8
+ import * as path from 'node:path';
7
9
  import { createScopeBundle, createScopeGrant, parseRateLimit, formatRateLimit, DEFAULT_RATE_LIMIT } from '../daemon/scopes.js';
8
10
  import { loadIntents } from '../daemon/intent-registry.js';
11
+ import { loadMetaConfig } from '../shared/meta-config.js';
12
+ import { logActivity } from '../daemon/agent-comms.js';
13
+ import { deliverLocalSessionText } from '../daemon/notify.js';
14
+ /**
15
+ * Expand tilde in paths
16
+ */
17
+ function expandTilde(filePath) {
18
+ if (filePath.startsWith('~/') || filePath === '~') {
19
+ return path.join(os.homedir(), filePath.slice(2));
20
+ }
21
+ return filePath;
22
+ }
23
+ function normalizeGatewayUrl(url) {
24
+ const trimmed = url.trim();
25
+ if (!trimmed) {
26
+ return '';
27
+ }
28
+ // Add https:// if no protocol specified
29
+ const withProtocol = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)
30
+ ? trimmed
31
+ : `https://${trimmed}`;
32
+ // Remove trailing slashes
33
+ return withProtocol.replace(/\/+$/, '');
34
+ }
35
+ export async function fetchFederationCard(gatewayUrl, fetchImpl = fetch) {
36
+ const requestedUrl = normalizeGatewayUrl(gatewayUrl);
37
+ const wellKnownUrl = `${requestedUrl}/.well-known/ogp`;
38
+ const response = await fetchImpl(wellKnownUrl);
39
+ if (!response.ok) {
40
+ throw new Error(`Could not fetch ${wellKnownUrl}: ${response.status} ${response.statusText}`);
41
+ }
42
+ const card = await response.json();
43
+ const canonicalUrl = card.gatewayUrl ? normalizeGatewayUrl(card.gatewayUrl) : requestedUrl;
44
+ return { requestedUrl, canonicalUrl, card };
45
+ }
46
+ export async function ensureLocalGatewayReachable(config, actionLabel, fetchImpl = fetch) {
47
+ const configuredGatewayUrl = normalizeGatewayUrl(config.gatewayUrl || '');
48
+ if (!configuredGatewayUrl) {
49
+ console.error(`Error: gatewayUrl is not set. Run "ogp expose" or update your config before you ${actionLabel}.`);
50
+ return false;
51
+ }
52
+ try {
53
+ const { canonicalUrl } = await fetchFederationCard(configuredGatewayUrl, fetchImpl);
54
+ if (canonicalUrl !== configuredGatewayUrl) {
55
+ console.error(`Error: configured gatewayUrl is stale.`);
56
+ console.error(` Config: ${configuredGatewayUrl}`);
57
+ console.error(` Live card: ${canonicalUrl}`);
58
+ console.error(` Update your config before you ${actionLabel}.`);
59
+ return false;
60
+ }
61
+ return true;
62
+ }
63
+ catch (error) {
64
+ console.error(`Error: your gatewayUrl is not reachable at ${configuredGatewayUrl}.`);
65
+ console.error(` Run "ogp expose" or fix gatewayUrl before you ${actionLabel}.`);
66
+ console.error(` Details: ${error.message}`);
67
+ return false;
68
+ }
69
+ }
70
+ async function resolvePeerGatewayUrl(gatewayUrl, contextLabel) {
71
+ try {
72
+ const { requestedUrl, canonicalUrl, card } = await fetchFederationCard(gatewayUrl);
73
+ if (canonicalUrl !== requestedUrl) {
74
+ console.log(`ℹ ${contextLabel}: peer advertises canonical gateway URL ${canonicalUrl}; using it instead of ${requestedUrl}`);
75
+ }
76
+ return { gatewayUrl: canonicalUrl, card };
77
+ }
78
+ catch (error) {
79
+ throw new Error(`${contextLabel}: peer gateway is not reachable or missing /.well-known/ogp. ${error.message}`);
80
+ }
81
+ }
82
+ async function refreshPeerGatewayUrlForApproval(peer) {
83
+ const { gatewayUrl, card } = await resolvePeerGatewayUrl(peer.gatewayUrl, 'Preflight');
84
+ if (card.publicKey && peer.publicKey && card.publicKey !== peer.publicKey) {
85
+ throw new Error(`Preflight: peer gateway identity mismatch. Expected ${peer.publicKey.substring(0, 32)}, got ${card.publicKey.substring(0, 32)}.`);
86
+ }
87
+ if (gatewayUrl !== peer.gatewayUrl) {
88
+ updatePeer(peer.id, { gatewayUrl });
89
+ }
90
+ return gatewayUrl;
91
+ }
9
92
  /**
10
93
  * Resolve a peer identifier (alias, ID, or public key) to a peer ID.
11
94
  * Returns the peer ID if found, or null.
@@ -25,6 +108,73 @@ function resolvePeerId(identifier) {
25
108
  return null;
26
109
  }
27
110
  export async function federationList(status) {
111
+ // Check if --for all was specified
112
+ if (process.env.OGP_FOR_ALL === 'true') {
113
+ const metaConfig = loadMetaConfig();
114
+ const enabledFrameworks = metaConfig.frameworks.filter(f => f.enabled);
115
+ if (enabledFrameworks.length === 0) {
116
+ console.error('Error: No enabled frameworks found. Run "ogp setup" first.');
117
+ process.exit(1);
118
+ }
119
+ // Print header
120
+ console.log('\n═══════════════════════════════════════════════════════════════');
121
+ console.log(`Federation Peers (All Frameworks)`);
122
+ console.log('═══════════════════════════════════════════════════════════════\n');
123
+ let totalPeers = 0;
124
+ // Iterate through each framework
125
+ for (const framework of enabledFrameworks) {
126
+ const originalOgpHome = process.env.OGP_HOME;
127
+ process.env.OGP_HOME = expandTilde(framework.configDir);
128
+ try {
129
+ const config = loadConfig();
130
+ if (!config) {
131
+ console.log(`${framework.name} (${framework.displayName || framework.id})`);
132
+ console.log('───────────────────────────────────────────────────────────────');
133
+ console.log(' No config found - run setup');
134
+ console.log('');
135
+ continue;
136
+ }
137
+ // Load peers for this framework
138
+ const allPeers = loadPeers();
139
+ const peers = status ? allPeers.filter(p => p.status === status) : allPeers.filter(p => p.status !== 'removed');
140
+ // Print framework header
141
+ console.log(`${framework.name} (${framework.displayName || framework.id})`);
142
+ console.log('───────────────────────────────────────────────────────────────');
143
+ if (peers.length === 0) {
144
+ console.log(' No peers found');
145
+ }
146
+ else {
147
+ totalPeers += peers.length;
148
+ peers.forEach(peer => {
149
+ const aliasDisplay = peer.alias || peer.displayName || '-';
150
+ const displayName = peer.alias ? peer.displayName : '';
151
+ const keyShort = peer.publicKey?.substring(0, 16) || '-';
152
+ const statusIcon = peer.status === 'approved' ? '✓' : peer.status === 'pending' ? '?' : '✗';
153
+ console.log(` ${statusIcon} ${aliasDisplay.padEnd(15)} ${(displayName || '').padEnd(25)} ${peer.status.padEnd(10)} ${peer.grantedScopes?.scopes.map(s => s.intent).join(', ') || 'none'}`);
154
+ });
155
+ }
156
+ console.log('');
157
+ }
158
+ catch (error) {
159
+ console.log(`${framework.name} (${framework.displayName || framework.id})`);
160
+ console.log('───────────────────────────────────────────────────────────────');
161
+ console.log(` Error: ${error.message}`);
162
+ console.log('');
163
+ }
164
+ finally {
165
+ // Restore original OGP_HOME
166
+ if (originalOgpHome) {
167
+ process.env.OGP_HOME = originalOgpHome;
168
+ }
169
+ else {
170
+ delete process.env.OGP_HOME;
171
+ }
172
+ }
173
+ }
174
+ console.log(`Total: ${totalPeers} peer${totalPeers !== 1 ? 's' : ''} across ${enabledFrameworks.length} framework${enabledFrameworks.length !== 1 ? 's' : ''}`);
175
+ return;
176
+ }
177
+ // Single framework mode (existing behavior)
28
178
  // listPeers() doesn't filter 'removed' — load all and filter manually if needed
29
179
  const allPeers = loadPeers();
30
180
  const peers = status ? allPeers.filter(p => p.status === status) : allPeers.filter(p => p.status !== 'removed');
@@ -41,18 +191,133 @@ export async function federationList(status) {
41
191
  const displayCol = (peer.displayName || '-').slice(0, 20).padEnd(20);
42
192
  const keyCol = (peer.publicKey?.substring(0, 16) || '-') + '...';
43
193
  const statusCol = peer.status;
44
- console.log(` ${aliasCol} ${displayCol} ${keyCol.padEnd(20)} ${statusCol}`);
194
+ // Health status indicator
195
+ let healthIcon = '';
196
+ if (peer.status === 'approved') {
197
+ if (peer.healthy === true) {
198
+ healthIcon = '✓';
199
+ }
200
+ else if (peer.healthy === false) {
201
+ healthIcon = '✗';
202
+ }
203
+ else {
204
+ healthIcon = '?'; // Unknown health status
205
+ }
206
+ }
207
+ console.log(` ${healthIcon ? healthIcon + ' ' : ''}${aliasCol} ${displayCol} ${keyCol.padEnd(20)} ${statusCol}`);
45
208
  console.log(` Gateway: ${peer.gatewayUrl}`);
46
209
  console.log(` ID: ${peer.id}`);
210
+ // Show health details for approved peers
211
+ if (peer.status === 'approved') {
212
+ if (peer.lastSeenAt) {
213
+ const lastSeen = new Date(peer.lastSeenAt);
214
+ const now = new Date();
215
+ const minutesAgo = Math.floor((now.getTime() - lastSeen.getTime()) / 60000);
216
+ console.log(` Last seen: ${minutesAgo < 60 ? minutesAgo + 'm ago' : Math.floor(minutesAgo / 60) + 'h ago'}`);
217
+ }
218
+ if (peer.healthCheckFailures && peer.healthCheckFailures > 0) {
219
+ console.log(` Health check failures: ${peer.healthCheckFailures}`);
220
+ }
221
+ }
47
222
  console.log('');
48
223
  });
49
224
  }
50
225
  export async function federationStatus() {
226
+ // Check if --for all was specified
227
+ if (process.env.OGP_FOR_ALL === 'true') {
228
+ const metaConfig = loadMetaConfig();
229
+ const enabledFrameworks = metaConfig.frameworks.filter(f => f.enabled);
230
+ if (enabledFrameworks.length === 0) {
231
+ console.error('Error: No enabled frameworks found. Run "ogp setup" first.');
232
+ process.exit(1);
233
+ }
234
+ // Print header
235
+ console.log('\n═══════════════════════════════════════════════════════════════');
236
+ console.log(`Federation Status (All Frameworks)`);
237
+ console.log('═══════════════════════════════════════════════════════════════\n');
238
+ let totalApproved = 0;
239
+ let totalPending = 0;
240
+ let totalRejected = 0;
241
+ let totalRemoved = 0;
242
+ // Iterate through each framework
243
+ for (const framework of enabledFrameworks) {
244
+ const originalOgpHome = process.env.OGP_HOME;
245
+ process.env.OGP_HOME = expandTilde(framework.configDir);
246
+ try {
247
+ const config = loadConfig();
248
+ if (!config) {
249
+ console.log(`${framework.name} (${framework.displayName || framework.id})`);
250
+ console.log('───────────────────────────────────────────────────────────────');
251
+ console.log(' No config found - run setup');
252
+ console.log('');
253
+ continue;
254
+ }
255
+ // Load peers for this framework
256
+ const peers = listPeers();
257
+ const approvedPeers = peers.filter(p => p.status === 'approved');
258
+ const pendingPeers = peers.filter(p => p.status === 'pending');
259
+ const rejectedPeers = peers.filter(p => p.status === 'rejected');
260
+ const removedPeers = peers.filter(p => p.status === 'removed');
261
+ // Update totals
262
+ totalApproved += approvedPeers.length;
263
+ totalPending += pendingPeers.length;
264
+ totalRejected += rejectedPeers.length;
265
+ totalRemoved += removedPeers.length;
266
+ // Print framework header
267
+ console.log(`${framework.name} (${framework.displayName || framework.id})`);
268
+ console.log('───────────────────────────────────────────────────────────────');
269
+ if (peers.length === 0) {
270
+ console.log(' No peers configured');
271
+ }
272
+ else {
273
+ console.log(` Total: ${peers.length} | Approved: ${approvedPeers.length} | Pending: ${pendingPeers.length} | Rejected: ${rejectedPeers.length} | Removed: ${removedPeers.length}`);
274
+ // Show aliases for approved peers
275
+ if (approvedPeers.length > 0) {
276
+ console.log('\n Approved peers:');
277
+ for (const peer of approvedPeers) {
278
+ const aliasDisplay = peer.alias || peer.displayName || 'no alias';
279
+ const scopes = peer.grantedScopes?.scopes.map(s => s.intent).join(', ') || 'none';
280
+ console.log(` ${aliasDisplay.padEnd(20)} ${scopes}`);
281
+ }
282
+ }
283
+ }
284
+ console.log('');
285
+ }
286
+ catch (error) {
287
+ console.log(`${framework.name} (${framework.displayName || framework.id})`);
288
+ console.log('───────────────────────────────────────────────────────────────');
289
+ console.log(` Error: ${error.message}`);
290
+ console.log('');
291
+ }
292
+ finally {
293
+ // Restore original OGP_HOME
294
+ if (originalOgpHome) {
295
+ process.env.OGP_HOME = originalOgpHome;
296
+ }
297
+ else {
298
+ delete process.env.OGP_HOME;
299
+ }
300
+ }
301
+ }
302
+ console.log('═══════════════════════════════════════════════════════════════');
303
+ console.log(`Total across all frameworks:`);
304
+ console.log(` Approved: ${totalApproved}`);
305
+ console.log(` Pending: ${totalPending}`);
306
+ console.log(` Rejected: ${totalRejected}`);
307
+ console.log(` Removed: ${totalRemoved}`);
308
+ console.log('');
309
+ return;
310
+ }
311
+ // Single framework mode (existing behavior)
51
312
  const peers = listPeers();
52
313
  const approvedPeers = peers.filter(p => p.status === 'approved');
53
314
  const pendingPeers = peers.filter(p => p.status === 'pending');
54
315
  const rejectedPeers = peers.filter(p => p.status === 'rejected');
55
316
  const removedPeers = peers.filter(p => p.status === 'removed');
317
+ // Health statistics for approved peers
318
+ const healthyPeers = approvedPeers.filter(p => p.healthy === true);
319
+ const unhealthyPeers = approvedPeers.filter(p => p.healthy === false);
320
+ const unknownHealthPeers = approvedPeers.filter(p => p.healthy === undefined);
56
321
  console.log('\n📊 FEDERATION STATUS\n');
57
322
  // Summary counts
58
323
  console.log(`Total peers: ${peers.length}`);
@@ -61,6 +326,14 @@ export async function federationStatus() {
61
326
  console.log(` Rejected: ${rejectedPeers.length}`);
62
327
  console.log(` Removed: ${removedPeers.length}`);
63
328
  console.log('');
329
+ // Health summary for approved peers
330
+ if (approvedPeers.length > 0) {
331
+ console.log('🏥 PEER HEALTH:\n');
332
+ console.log(` Healthy: ${healthyPeers.length} (✓)`);
333
+ console.log(` Unhealthy: ${unhealthyPeers.length} (✗)`);
334
+ console.log(` Unknown: ${unknownHealthPeers.length} (?)`);
335
+ console.log('');
336
+ }
64
337
  // Alias → Public Key mapping section
65
338
  if (peers.length > 0) {
66
339
  console.log('📝 ALIAS → PUBLIC KEY MAPPING:\n');
@@ -91,8 +364,22 @@ export async function federationStatus() {
91
364
  export async function federationRequest(peerUrl, peerId, alias) {
92
365
  const config = requireConfig();
93
366
  const keypair = loadOrGenerateKeyPair();
367
+ if (!await ensureLocalGatewayReachable(config, 'send federation requests')) {
368
+ return false;
369
+ }
94
370
  // BUILD-111: Use public key prefix as peer ID (port-agnostic identity)
95
371
  const ourPeerId = keypair.publicKey.substring(0, 16);
372
+ let resolvedPeerUrl = normalizeGatewayUrl(peerUrl);
373
+ let peerCard = null;
374
+ try {
375
+ const resolved = await resolvePeerGatewayUrl(resolvedPeerUrl, 'Preflight');
376
+ resolvedPeerUrl = resolved.gatewayUrl;
377
+ peerCard = resolved.card;
378
+ }
379
+ catch (error) {
380
+ console.error(error.message);
381
+ return false;
382
+ }
96
383
  // Build our peer info
97
384
  const peer = {
98
385
  id: ourPeerId, // Public key prefix, not hostname:port
@@ -112,7 +399,7 @@ export async function federationRequest(peerUrl, peerId, alias) {
112
399
  const requestBody = { peer, signature, offeredIntents: ourIntents };
113
400
  // Send request
114
401
  try {
115
- const response = await fetch(`${peerUrl}/federation/request`, {
402
+ const response = await fetch(`${resolvedPeerUrl}/federation/request`, {
116
403
  method: 'POST',
117
404
  headers: { 'Content-Type': 'application/json' },
118
405
  body: JSON.stringify(requestBody)
@@ -129,18 +416,18 @@ export async function federationRequest(peerUrl, peerId, alias) {
129
416
  // Store them as a pending peer so we can send intents when approved
130
417
  try {
131
418
  const { addPeer } = await import('../daemon/peers.js');
132
- const cardRes = await fetch(`${peerUrl}/.well-known/ogp`);
133
- if (cardRes.ok) {
134
- const card = await cardRes.json();
135
- const peerHostname = new URL(peerUrl).hostname;
136
- const peerPort = new URL(peerUrl).port || '18790';
137
- // BUILD-111: Use public key prefix as canonical ID, fall back to hostname:port only if no key
138
- const canonicalId = card.publicKey?.substring(0, 16) || `${peerHostname}:${peerPort}`;
419
+ const card = peerCard;
420
+ if (card) {
421
+ const peerHostname = new URL(resolvedPeerUrl).hostname;
422
+ const peerPort = new URL(resolvedPeerUrl).port || '18790';
423
+ // BUILD-111: Use a 32-char public key prefix as canonical ID to avoid
424
+ // duplicate short/full peer IDs across request/approve flows.
425
+ const canonicalId = card.publicKey?.substring(0, 32) || `${peerHostname}:${peerPort}`;
139
426
  addPeer({
140
427
  id: canonicalId,
141
428
  displayName: card.displayName || peerId,
142
429
  email: card.email || '',
143
- gatewayUrl: peerUrl,
430
+ gatewayUrl: resolvedPeerUrl,
144
431
  publicKey: card.publicKey || '',
145
432
  status: 'pending',
146
433
  requestedAt: new Date().toISOString(),
@@ -161,6 +448,9 @@ export async function federationRequest(peerUrl, peerId, alias) {
161
448
  }
162
449
  export async function federationApprove(peerId, options = {}) {
163
450
  const config = requireConfig();
451
+ if (!await ensureLocalGatewayReachable(config, 'approve federation requests')) {
452
+ return;
453
+ }
164
454
  // Resolve peer identifier (alias, ID, or public key)
165
455
  const resolvedId = resolvePeerId(peerId);
166
456
  if (!resolvedId) {
@@ -177,6 +467,15 @@ export async function federationApprove(peerId, options = {}) {
177
467
  console.log(`Peer ${peerId} is already approved.`);
178
468
  return;
179
469
  }
470
+ let peerGatewayUrl = peer.gatewayUrl;
471
+ try {
472
+ peerGatewayUrl = await refreshPeerGatewayUrlForApproval(peer);
473
+ }
474
+ catch (error) {
475
+ console.error(error.message);
476
+ console.error('Ask the peer to fix their gatewayUrl and resend the federation request.');
477
+ return;
478
+ }
180
479
  // BUILD-110: Mirror peer's offered intents by default, with user confirmation
181
480
  const DEFAULT_INTENTS = ['message', 'agent-comms', 'project.join', 'project.contribute', 'project.query', 'project.status'];
182
481
  // If peer offered intents, use those as default (for symmetry)
@@ -225,9 +524,9 @@ export async function federationApprove(peerId, options = {}) {
225
524
  approvePeer(peerId);
226
525
  console.log(`✓ Approved peer: ${peerId}`);
227
526
  // BUILD-102: Auto-register existing local projects as agent-comms topics for this peer
228
- const { listProjects } = await import('../daemon/projects.js');
527
+ const { listProjectsForPeer } = await import('../daemon/projects.js');
229
528
  const { setPeerTopicPolicy } = await import('../daemon/peers.js');
230
- const projects = listProjects();
529
+ const projects = listProjectsForPeer(peerId);
231
530
  if (projects.length > 0) {
232
531
  for (const project of projects) {
233
532
  setPeerTopicPolicy(peerId, project.id, 'summary');
@@ -245,7 +544,7 @@ export async function federationApprove(peerId, options = {}) {
245
544
  const keypair = loadOrGenerateKeyPair();
246
545
  const ourConfig = requireConfig();
247
546
  const nonce = crypto.randomUUID();
248
- await fetch(`${peer.gatewayUrl}/federation/approve`, {
547
+ await fetch(`${peerGatewayUrl}/federation/approve`, {
249
548
  method: 'POST',
250
549
  headers: { 'Content-Type': 'application/json' },
251
550
  body: JSON.stringify({
@@ -319,7 +618,7 @@ export async function federationRemove(peerId) {
319
618
  try {
320
619
  const keypair = loadOrGenerateKeyPair();
321
620
  // BUILD-111: Use public key prefix as our ID (not hostname:port)
322
- const ourId = keypair.publicKey.substring(0, 16);
621
+ const ourId = keypair.publicKey.substring(0, 32);
323
622
  const timestamp = new Date().toISOString();
324
623
  // Sign the removal payload
325
624
  const payload = { peerId: ourId, timestamp };
@@ -371,7 +670,7 @@ export async function federationSend(peerId, intent, payloadJson, timeoutMs) {
371
670
  const payload = JSON.parse(payloadJson);
372
671
  const keypair = loadOrGenerateKeyPair();
373
672
  // BUILD-111: Use public key prefix as our ID (not hostname:port)
374
- const ourId = keypair.publicKey.substring(0, 16);
673
+ const ourId = keypair.publicKey.substring(0, 32);
375
674
  const message = {
376
675
  intent,
377
676
  from: ourId,
@@ -396,11 +695,25 @@ export async function federationSend(peerId, intent, payloadJson, timeoutMs) {
396
695
  });
397
696
  if (timeoutId)
398
697
  clearTimeout(timeoutId);
698
+ let result = null;
699
+ try {
700
+ result = await response.json();
701
+ }
702
+ catch {
703
+ result = null;
704
+ }
399
705
  if (!response.ok) {
706
+ if (result?.error) {
707
+ console.error(`Send failed: ${response.status} ${response.statusText} - ${result.error}`);
708
+ return result;
709
+ }
400
710
  console.error(`Send failed: ${response.status} ${response.statusText}`);
401
- return null;
711
+ return {
712
+ success: false,
713
+ error: `Send failed: ${response.status} ${response.statusText}`,
714
+ statusCode: response.status
715
+ };
402
716
  }
403
- const result = await response.json();
404
717
  return result;
405
718
  }
406
719
  catch (error) {
@@ -431,7 +744,7 @@ export async function federationShowScopes(peerId) {
431
744
  }
432
745
  console.log(`\nSCOPES FOR ${peer.displayName} (${peerId}):\n`);
433
746
  console.log(' Status:', peer.status);
434
- console.log(' Protocol:', peer.protocolVersion || '0.1.0 (legacy)');
747
+ console.log(' Wire Protocol:', peer.protocolVersion || '0.1.0 (legacy)');
435
748
  console.log('');
436
749
  // What I grant TO this peer
437
750
  if (peer.grantedScopes) {
@@ -544,9 +857,10 @@ export async function federationSendAgentComms(peerId, topic, messageText, optio
544
857
  return;
545
858
  }
546
859
  const keypair = loadOrGenerateKeyPair();
547
- // BUILD-111: Use public key prefix as our ID (not hostname:port)
548
- const ourId = keypair.publicKey.substring(0, 16);
860
+ // Use 32-char public key prefix as our ID (avoids Ed25519 DER header collision with 16-char)
861
+ const ourId = keypair.publicKey.substring(0, 32);
549
862
  const nonce = crypto.randomUUID();
863
+ const conversationId = options.conversationId || nonce;
550
864
  // Build replyTo URL if we want to receive callbacks
551
865
  const replyTo = options.waitForReply
552
866
  ? `${config.gatewayUrl}/federation/reply/${nonce}`
@@ -558,7 +872,7 @@ export async function federationSendAgentComms(peerId, topic, messageText, optio
558
872
  nonce,
559
873
  timestamp: new Date().toISOString(),
560
874
  replyTo,
561
- conversationId: options.conversationId,
875
+ conversationId,
562
876
  payload: {
563
877
  topic,
564
878
  message: messageText,
@@ -591,6 +905,14 @@ export async function federationSendAgentComms(peerId, topic, messageText, optio
591
905
  return;
592
906
  }
593
907
  const result = await response.json();
908
+ logActivity({
909
+ direction: 'out',
910
+ peerId,
911
+ peerName: peer.displayName,
912
+ topic,
913
+ message: messageText
914
+ });
915
+ await deliverLocalSessionText(`[OGP Agent-Comms Sent] To ${peer.displayName} (${topic}): ${messageText}`);
594
916
  console.log(`✓ Agent-comms sent to ${peer.displayName}`);
595
917
  console.log(` Topic: ${topic}`);
596
918
  console.log(` Message: ${messageText}`);
@@ -618,10 +940,12 @@ export async function federationSendAgentComms(peerId, topic, messageText, optio
618
940
  }
619
941
  }
620
942
  console.log('\n⏱ Reply timeout - no response received');
943
+ process.exit(1);
621
944
  }
622
945
  }
623
946
  catch (error) {
624
947
  console.error('Failed to send agent-comms:', error);
948
+ process.exit(1);
625
949
  }
626
950
  }
627
951
  /**