@dp-pcs/ogp 0.9.0 → 0.10.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.
Files changed (48) hide show
  1. package/dist/cli/agent-comms.d.ts +1 -1
  2. package/dist/cli/agent-comms.d.ts.map +1 -1
  3. package/dist/cli/agent-comms.js +9 -2
  4. package/dist/cli/agent-comms.js.map +1 -1
  5. package/dist/cli/config.d.ts +1 -1
  6. package/dist/cli/config.d.ts.map +1 -1
  7. package/dist/cli/config.js +81 -1
  8. package/dist/cli/config.js.map +1 -1
  9. package/dist/cli/federation.d.ts +47 -3
  10. package/dist/cli/federation.d.ts.map +1 -1
  11. package/dist/cli/federation.js +219 -62
  12. package/dist/cli/federation.js.map +1 -1
  13. package/dist/cli/tunnel.d.ts +7 -1
  14. package/dist/cli/tunnel.d.ts.map +1 -1
  15. package/dist/cli/tunnel.js +12 -3
  16. package/dist/cli/tunnel.js.map +1 -1
  17. package/dist/cli.js +63 -20
  18. package/dist/cli.js.map +1 -1
  19. package/dist/daemon/agent-comms.d.ts +9 -0
  20. package/dist/daemon/agent-comms.d.ts.map +1 -1
  21. package/dist/daemon/agent-comms.js +65 -0
  22. package/dist/daemon/agent-comms.js.map +1 -1
  23. package/dist/daemon/relay-client.d.ts +16 -0
  24. package/dist/daemon/relay-client.d.ts.map +1 -0
  25. package/dist/daemon/relay-client.js +312 -0
  26. package/dist/daemon/relay-client.js.map +1 -0
  27. package/dist/daemon/rendezvous.d.ts +56 -2
  28. package/dist/daemon/rendezvous.d.ts.map +1 -1
  29. package/dist/daemon/rendezvous.js +99 -4
  30. package/dist/daemon/rendezvous.js.map +1 -1
  31. package/dist/daemon/server.d.ts +1 -0
  32. package/dist/daemon/server.d.ts.map +1 -1
  33. package/dist/daemon/server.js +140 -26
  34. package/dist/daemon/server.js.map +1 -1
  35. package/dist/shared/config.d.ts +28 -0
  36. package/dist/shared/config.d.ts.map +1 -1
  37. package/dist/shared/config.js +7 -0
  38. package/dist/shared/config.js.map +1 -1
  39. package/dist/shared/meta-config.d.ts.map +1 -1
  40. package/dist/shared/meta-config.js +23 -9
  41. package/dist/shared/meta-config.js.map +1 -1
  42. package/dist/shared/relay-protocol.d.ts +91 -0
  43. package/dist/shared/relay-protocol.d.ts.map +1 -0
  44. package/dist/shared/relay-protocol.js +55 -0
  45. package/dist/shared/relay-protocol.js.map +1 -0
  46. package/docs/CLI-REFERENCE.md +38 -0
  47. package/docs/TRANSPORT-MODES-DESIGN.md +164 -0
  48. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { listPeers, loadPeers, getPeer, approvePeer, rejectPeer, updatePeer, updatePeerGrantedScopes } from '../daemon/peers.js';
2
2
  import { requireConfig, loadConfig } from '../shared/config.js';
3
- import { lookupPeer } from '../daemon/rendezvous.js';
3
+ import { lookupPeer, lookupPeerTransport } from '../daemon/rendezvous.js';
4
+ import { deliverViaRelay } from '../daemon/relay-client.js';
4
5
  import { getPublicKey, getPrivateKey, loadOrGenerateKeyPair } from '../daemon/keypair.js';
5
6
  import { signObject, sign } from '../shared/signing.js';
6
7
  import * as crypto from 'node:crypto';
@@ -144,7 +145,24 @@ function resolvePeerId(identifier) {
144
145
  }
145
146
  return null;
146
147
  }
147
- export async function federationList(status, filterTag) {
148
+ /** Pure projection of peers to the stable `--json` wire shape. */
149
+ export function peersToJson(peers) {
150
+ return peers.map(p => ({
151
+ id: p.id,
152
+ alias: p.alias,
153
+ displayName: p.displayName,
154
+ status: p.status,
155
+ gatewayUrl: p.gatewayUrl,
156
+ publicKey: p.publicKey,
157
+ healthState: p.healthState,
158
+ healthy: p.healthy,
159
+ grantedScopes: p.grantedScopes,
160
+ offeredIntents: p.offeredIntents,
161
+ lastSeenAt: p.lastSeenAt,
162
+ tags: p.tags,
163
+ }));
164
+ }
165
+ export async function federationList(status, filterTag, json = false) {
148
166
  // Check if --for all was specified
149
167
  if (process.env.OGP_FOR_ALL === 'true') {
150
168
  const metaConfig = loadMetaConfig();
@@ -153,10 +171,14 @@ export async function federationList(status, filterTag) {
153
171
  console.error('Error: No enabled frameworks found. Run "ogp setup" first.');
154
172
  process.exit(1);
155
173
  }
174
+ // Accumulator for --json output (per-framework peers).
175
+ const perFramework = [];
156
176
  // Print header
157
- console.log('\n═══════════════════════════════════════════════════════════════');
158
- console.log(`Federation Peers (All Frameworks)`);
159
- console.log('═══════════════════════════════════════════════════════════════\n');
177
+ if (!json) {
178
+ console.log('\n═══════════════════════════════════════════════════════════════');
179
+ console.log(`Federation Peers (All Frameworks)`);
180
+ console.log('═══════════════════════════════════════════════════════════════\n');
181
+ }
160
182
  let totalPeers = 0;
161
183
  // Iterate through each framework
162
184
  for (const framework of enabledFrameworks) {
@@ -165,15 +187,25 @@ export async function federationList(status, filterTag) {
165
187
  try {
166
188
  const config = loadConfig();
167
189
  if (!config) {
168
- console.log(`${framework.name} (${framework.displayName || framework.id})`);
169
- console.log('───────────────────────────────────────────────────────────────');
170
- console.log(' No config found - run setup');
171
- console.log('');
190
+ if (json) {
191
+ perFramework.push({ framework: framework.id, peers: [] });
192
+ }
193
+ else {
194
+ console.log(`${framework.name} (${framework.displayName || framework.id})`);
195
+ console.log('───────────────────────────────────────────────────────────────');
196
+ console.log(' No config found - run setup');
197
+ console.log('');
198
+ }
172
199
  continue;
173
200
  }
174
201
  // Load peers for this framework
175
202
  const allPeers = loadPeers();
176
203
  const peers = status ? allPeers.filter(p => p.status === status) : allPeers.filter(p => p.status !== 'removed');
204
+ if (json) {
205
+ totalPeers += peers.length;
206
+ perFramework.push({ framework: framework.id, peers: peersToJson(peers) });
207
+ continue;
208
+ }
177
209
  // Print framework header
178
210
  console.log(`${framework.name} (${framework.displayName || framework.id})`);
179
211
  console.log('───────────────────────────────────────────────────────────────');
@@ -211,10 +243,15 @@ export async function federationList(status, filterTag) {
211
243
  console.log('');
212
244
  }
213
245
  catch (error) {
214
- console.log(`${framework.name} (${framework.displayName || framework.id})`);
215
- console.log('───────────────────────────────────────────────────────────────');
216
- console.log(` Error: ${error.message}`);
217
- console.log('');
246
+ if (json) {
247
+ perFramework.push({ framework: framework.id, peers: [] });
248
+ }
249
+ else {
250
+ console.log(`${framework.name} (${framework.displayName || framework.id})`);
251
+ console.log('───────────────────────────────────────────────────────────────');
252
+ console.log(` Error: ${error.message}`);
253
+ console.log('');
254
+ }
218
255
  }
219
256
  finally {
220
257
  // Restore original OGP_HOME
@@ -226,6 +263,10 @@ export async function federationList(status, filterTag) {
226
263
  }
227
264
  }
228
265
  }
266
+ if (json) {
267
+ console.log(JSON.stringify(perFramework, null, 2));
268
+ return;
269
+ }
229
270
  console.log(`Total: ${totalPeers} peer${totalPeers !== 1 ? 's' : ''} across ${enabledFrameworks.length} framework${enabledFrameworks.length !== 1 ? 's' : ''}`);
230
271
  return;
231
272
  }
@@ -237,6 +278,10 @@ export async function federationList(status, filterTag) {
237
278
  if (filterTag) {
238
279
  peers = peers.filter(p => p.tags && p.tags.includes(filterTag));
239
280
  }
281
+ if (json) {
282
+ console.log(JSON.stringify(peersToJson(peers), null, 2));
283
+ return;
284
+ }
240
285
  if (peers.length === 0) {
241
286
  console.log('No peers found.');
242
287
  return;
@@ -300,7 +345,7 @@ export async function federationList(status, filterTag) {
300
345
  console.log('');
301
346
  });
302
347
  }
303
- export async function federationStatus() {
348
+ export async function federationStatus(json = false) {
304
349
  // Check if --for all was specified
305
350
  if (process.env.OGP_FOR_ALL === 'true') {
306
351
  const metaConfig = loadMetaConfig();
@@ -309,6 +354,42 @@ export async function federationStatus() {
309
354
  console.error('Error: No enabled frameworks found. Run "ogp setup" first.');
310
355
  process.exit(1);
311
356
  }
357
+ // --json across all frameworks: emit a per-framework array (mirrors federationList).
358
+ if (json) {
359
+ const perFramework = [];
360
+ for (const framework of enabledFrameworks) {
361
+ const originalOgpHome = process.env.OGP_HOME;
362
+ process.env.OGP_HOME = expandTilde(framework.configDir);
363
+ try {
364
+ const config = loadConfig();
365
+ if (!config) {
366
+ perFramework.push({ framework: framework.id, total: 0, approved: [], pending: [], rejected: [] });
367
+ continue;
368
+ }
369
+ const fwPeers = listPeers();
370
+ perFramework.push({
371
+ framework: framework.id,
372
+ total: fwPeers.length,
373
+ approved: peersToJson(fwPeers.filter(p => p.status === 'approved')),
374
+ pending: peersToJson(fwPeers.filter(p => p.status === 'pending')),
375
+ rejected: peersToJson(fwPeers.filter(p => p.status === 'rejected')),
376
+ });
377
+ }
378
+ catch {
379
+ perFramework.push({ framework: framework.id, total: 0, approved: [], pending: [], rejected: [] });
380
+ }
381
+ finally {
382
+ if (originalOgpHome) {
383
+ process.env.OGP_HOME = originalOgpHome;
384
+ }
385
+ else {
386
+ delete process.env.OGP_HOME;
387
+ }
388
+ }
389
+ }
390
+ console.log(JSON.stringify(perFramework, null, 2));
391
+ return;
392
+ }
312
393
  // Print header
313
394
  console.log('\n═══════════════════════════════════════════════════════════════');
314
395
  console.log(`Federation Status (All Frameworks)`);
@@ -404,6 +485,15 @@ export async function federationStatus() {
404
485
  const pendingPeers = peers.filter(p => p.status === 'pending');
405
486
  const rejectedPeers = peers.filter(p => p.status === 'rejected');
406
487
  const removedPeers = peers.filter(p => p.status === 'removed');
488
+ if (json) {
489
+ console.log(JSON.stringify({
490
+ total: peers.length,
491
+ approved: peersToJson(approvedPeers),
492
+ pending: peersToJson(pendingPeers),
493
+ rejected: peersToJson(rejectedPeers),
494
+ }, null, 2));
495
+ return;
496
+ }
407
497
  // Health statistics for approved peers (Issue #3: directional)
408
498
  const stateCounts = {
409
499
  established: 0,
@@ -485,10 +575,12 @@ export async function federationStatus() {
485
575
  }
486
576
  }
487
577
  }
488
- export async function federationRequest(peerUrl, peerId, alias) {
578
+ export async function federationRequest(peerUrl, peerId, alias, json = false) {
489
579
  const config = requireConfig();
490
580
  const keypair = loadOrGenerateKeyPair();
491
581
  if (!await ensureLocalGatewayReachable(config, 'send federation requests')) {
582
+ if (json)
583
+ console.log(JSON.stringify({ ok: false, peerUrl, peerId, status: 'failed', error: 'local gateway not reachable' }));
492
584
  return false;
493
585
  }
494
586
  // BUILD-111: Use public key prefix as peer ID (port-agnostic identity)
@@ -501,7 +593,12 @@ export async function federationRequest(peerUrl, peerId, alias) {
501
593
  peerCard = resolved.card;
502
594
  }
503
595
  catch (error) {
504
- console.error(error.message);
596
+ if (json) {
597
+ console.log(JSON.stringify({ ok: false, peerUrl, peerId, status: 'failed', error: error.message }));
598
+ }
599
+ else {
600
+ console.error(error.message);
601
+ }
505
602
  return false;
506
603
  }
507
604
  // Build our peer info
@@ -536,13 +633,20 @@ export async function federationRequest(peerUrl, peerId, alias) {
536
633
  body: JSON.stringify(requestBody)
537
634
  });
538
635
  if (!response.ok) {
539
- console.error(`Request failed: ${response.status} ${response.statusText}`);
636
+ if (json) {
637
+ console.log(JSON.stringify({ ok: false, peerUrl: resolvedPeerUrl, peerId, status: 'failed', error: `${response.status} ${response.statusText}` }));
638
+ }
639
+ else {
640
+ console.error(`Request failed: ${response.status} ${response.statusText}`);
641
+ }
540
642
  return false;
541
643
  }
542
644
  const result = await response.json();
543
- console.log('✓ Federation request sent');
544
- console.log(` Status: ${result.status}`);
545
- console.log(` Message: ${result.message}`);
645
+ if (!json) {
646
+ console.log('✓ Federation request sent');
647
+ console.log(` Status: ${result.status}`);
648
+ console.log(` Message: ${result.message}`);
649
+ }
546
650
  // Fetch their federation card to get their actual identity
547
651
  // Store them as a pending peer so we can send intents when approved
548
652
  try {
@@ -570,10 +674,18 @@ export async function federationRequest(peerUrl, peerId, alias) {
570
674
  }
571
675
  }
572
676
  catch { /* non-fatal */ }
677
+ if (json) {
678
+ console.log(JSON.stringify({ ok: true, peerUrl: resolvedPeerUrl, peerId, status: result.status ?? 'requested', message: result.message }));
679
+ }
573
680
  return true;
574
681
  }
575
682
  catch (error) {
576
- console.error('Failed to send request:', error);
683
+ if (json) {
684
+ console.log(JSON.stringify({ ok: false, peerUrl: resolvedPeerUrl, peerId, status: 'failed', error: error instanceof Error ? error.message : String(error) }));
685
+ }
686
+ else {
687
+ console.error('Failed to send request:', error);
688
+ }
577
689
  return false;
578
690
  }
579
691
  }
@@ -831,6 +943,79 @@ export async function federationRemove(peerId) {
831
943
  removePeer(peerId);
832
944
  console.log(`✓ Removed peer: ${peerId} (${peer.displayName})`);
833
945
  }
946
+ /**
947
+ * Deliver an already-signed federation envelope to a peer, branching on the
948
+ * peer's advertised transport (bd-b7em Phase 2).
949
+ *
950
+ * - DIRECT (default, and the fallback for everything): byte-identical to the
951
+ * original behavior — POST {message, messageStr, signature} to
952
+ * `${peer.gatewayUrl}/federation/message`. A flaky/disabled rendezvous lookup
953
+ * MUST fall through to direct so it can never break the default path.
954
+ * - RELAY: route the same opaque frame over a WebSocket to the relay, which
955
+ * forwards it to the peer's persistent socket and returns their response.
956
+ *
957
+ * Returns a normalized shape so callers can keep their existing response
958
+ * handling: { ok, status?, result }. `result` is the peer's MessageResponse
959
+ * (or a synthesized failure for relay errors).
960
+ */
961
+ export async function deliverFederationMessage(peer, frame, opts) {
962
+ // Resolve transport. Any failure (rendezvous disabled, lookup throws/null,
963
+ // direct, or iroh) ⇒ direct. Relay never preempts the default unless the peer
964
+ // explicitly advertised a relay descriptor inside their signed registration.
965
+ let relayUrl = null;
966
+ if (opts.config.rendezvous?.enabled && peer.publicKey) {
967
+ try {
968
+ const resolved = await lookupPeerTransport(opts.config.rendezvous, peer.publicKey);
969
+ if (resolved && resolved.mode === 'relay')
970
+ relayUrl = resolved.relayUrl;
971
+ }
972
+ catch {
973
+ // fall through to direct — a flaky rendezvous must not break direct sends
974
+ }
975
+ }
976
+ if (relayUrl) {
977
+ try {
978
+ const result = await deliverViaRelay(relayUrl, peer.publicKey, frame, opts.timeoutMs);
979
+ const r = result;
980
+ return { ok: r?.success !== false, status: r?.statusCode, result };
981
+ }
982
+ catch (err) {
983
+ return {
984
+ ok: false,
985
+ result: { success: false, error: `peer not connected via relay: ${err.message}` }
986
+ };
987
+ }
988
+ }
989
+ // DIRECT PATH — unchanged wire behavior.
990
+ const controller = new AbortController();
991
+ const timeoutId = opts.timeoutMs ? setTimeout(() => controller.abort(), opts.timeoutMs) : null;
992
+ try {
993
+ const response = await fetch(`${peer.gatewayUrl}/federation/message`, {
994
+ method: 'POST',
995
+ headers: { 'Content-Type': 'application/json' },
996
+ body: JSON.stringify({
997
+ message: frame.message,
998
+ messageStr: frame.messageStr, // raw signed string for exact verification
999
+ signature: frame.signature
1000
+ }),
1001
+ signal: controller.signal
1002
+ });
1003
+ if (timeoutId)
1004
+ clearTimeout(timeoutId);
1005
+ let result = null;
1006
+ try {
1007
+ result = await response.json();
1008
+ }
1009
+ catch {
1010
+ result = null;
1011
+ }
1012
+ return { ok: response.ok, status: response.status, result };
1013
+ }
1014
+ finally {
1015
+ if (timeoutId)
1016
+ clearTimeout(timeoutId);
1017
+ }
1018
+ }
834
1019
  export async function federationSend(peerId, intent, payloadJson, timeoutMs, toAgent) {
835
1020
  const config = requireConfig();
836
1021
  // Resolve peer identifier (alias, ID, or public key)
@@ -872,37 +1057,17 @@ export async function federationSend(peerId, intent, payloadJson, timeoutMs, toA
872
1057
  };
873
1058
  const { payload: signedPayload, payloadStr, signature } = signObject(message, getPrivateKey());
874
1059
  try {
875
- const controller = new AbortController();
876
- const timeoutId = timeoutMs ? setTimeout(() => controller.abort(), timeoutMs) : null;
877
- const response = await fetch(`${peer.gatewayUrl}/federation/message`, {
878
- method: 'POST',
879
- headers: { 'Content-Type': 'application/json' },
880
- body: JSON.stringify({
881
- message: signedPayload,
882
- messageStr: payloadStr, // raw signed string for exact verification
883
- signature
884
- }),
885
- signal: controller.signal
886
- });
887
- if (timeoutId)
888
- clearTimeout(timeoutId);
889
- let result = null;
890
- try {
891
- result = await response.json();
892
- }
893
- catch {
894
- result = null;
895
- }
896
- if (!response.ok) {
1060
+ const { ok, status, result } = await deliverFederationMessage(peer, { message: signedPayload, messageStr: payloadStr, signature }, { timeoutMs, config });
1061
+ if (!ok) {
897
1062
  if (result?.error) {
898
- console.error(`Send failed: ${response.status} ${response.statusText} - ${result.error}`);
1063
+ console.error(`Send failed: ${status ? `${status} ` : ''}${result.error}`);
899
1064
  return result;
900
1065
  }
901
- console.error(`Send failed: ${response.status} ${response.statusText}`);
1066
+ console.error(`Send failed${status ? `: ${status}` : ''}`);
902
1067
  return {
903
1068
  success: false,
904
- error: `Send failed: ${response.status} ${response.statusText}`,
905
- statusCode: response.status
1069
+ error: `Send failed${status ? `: ${status}` : ''}`,
1070
+ statusCode: status
906
1071
  };
907
1072
  }
908
1073
  return result;
@@ -1214,30 +1379,22 @@ export async function federationSendAgentComms(peerId, topic, messageText, optio
1214
1379
  };
1215
1380
  const { payload: signedPayload, payloadStr, signature } = signObject(message, getPrivateKey());
1216
1381
  try {
1217
- const response = await fetch(`${peer.gatewayUrl}/federation/message`, {
1218
- method: 'POST',
1219
- headers: { 'Content-Type': 'application/json' },
1220
- body: JSON.stringify({
1221
- message: signedPayload,
1222
- messageStr: payloadStr, // raw signed string for exact verification
1223
- signature
1224
- })
1225
- });
1226
- if (!response.ok) {
1227
- const body = await response.text();
1228
- if (response.status === 403) {
1382
+ const { ok, status, result: delivered } = await deliverFederationMessage(peer, { message: signedPayload, messageStr: payloadStr, signature }, { config });
1383
+ if (!ok) {
1384
+ const body = delivered?.error ? String(delivered.error) : (delivered ? JSON.stringify(delivered) : '');
1385
+ if (status === 403) {
1229
1386
  console.error(`Access denied: ${body}`);
1230
1387
  console.log('Hint: Peer may not have granted you agent-comms scope for this topic.');
1231
1388
  }
1232
- else if (response.status === 429) {
1389
+ else if (status === 429) {
1233
1390
  console.error(`Rate limited: ${body}`);
1234
1391
  }
1235
1392
  else {
1236
- console.error(`Send failed: ${response.status} ${response.statusText}`);
1393
+ console.error(`Send failed${status ? `: ${status}` : ''}${body ? ` - ${body}` : ''}`);
1237
1394
  }
1238
1395
  return;
1239
1396
  }
1240
- const result = await response.json();
1397
+ const result = (delivered ?? {});
1241
1398
  logActivity({
1242
1399
  direction: 'out',
1243
1400
  peerId,