@dp-pcs/ogp 0.4.2 → 0.4.4

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 (61) hide show
  1. package/README.md +45 -15
  2. package/dist/cli/federation.d.ts +14 -0
  3. package/dist/cli/federation.d.ts.map +1 -1
  4. package/dist/cli/federation.js +216 -19
  5. package/dist/cli/federation.js.map +1 -1
  6. package/dist/cli/project.d.ts +4 -3
  7. package/dist/cli/project.d.ts.map +1 -1
  8. package/dist/cli/project.js +34 -24
  9. package/dist/cli/project.js.map +1 -1
  10. package/dist/cli/setup.d.ts +4 -0
  11. package/dist/cli/setup.d.ts.map +1 -1
  12. package/dist/cli/setup.js +57 -4
  13. package/dist/cli/setup.js.map +1 -1
  14. package/dist/cli.js +11 -4
  15. package/dist/cli.js.map +1 -1
  16. package/dist/daemon/agent-comms.js +1 -1
  17. package/dist/daemon/agent-comms.js.map +1 -1
  18. package/dist/daemon/heartbeat.d.ts +22 -0
  19. package/dist/daemon/heartbeat.d.ts.map +1 -0
  20. package/dist/daemon/heartbeat.js +142 -0
  21. package/dist/daemon/heartbeat.js.map +1 -0
  22. package/dist/daemon/intent-registry.d.ts.map +1 -1
  23. package/dist/daemon/intent-registry.js +12 -6
  24. package/dist/daemon/intent-registry.js.map +1 -1
  25. package/dist/daemon/keypair.d.ts +1 -0
  26. package/dist/daemon/keypair.d.ts.map +1 -1
  27. package/dist/daemon/keypair.js +119 -7
  28. package/dist/daemon/keypair.js.map +1 -1
  29. package/dist/daemon/message-handler.js +148 -16
  30. package/dist/daemon/message-handler.js.map +1 -1
  31. package/dist/daemon/notify.d.ts.map +1 -1
  32. package/dist/daemon/notify.js +47 -0
  33. package/dist/daemon/notify.js.map +1 -1
  34. package/dist/daemon/peers.d.ts +24 -0
  35. package/dist/daemon/peers.d.ts.map +1 -1
  36. package/dist/daemon/peers.js +77 -6
  37. package/dist/daemon/peers.js.map +1 -1
  38. package/dist/daemon/projects.d.ts +9 -6
  39. package/dist/daemon/projects.d.ts.map +1 -1
  40. package/dist/daemon/projects.js +23 -16
  41. package/dist/daemon/projects.js.map +1 -1
  42. package/dist/daemon/server.d.ts +1 -0
  43. package/dist/daemon/server.d.ts.map +1 -1
  44. package/dist/daemon/server.js +85 -44
  45. package/dist/daemon/server.js.map +1 -1
  46. package/dist/shared/help.js +2 -1
  47. package/dist/shared/help.js.map +1 -1
  48. package/docs/CLI-REFERENCE.md +1 -0
  49. package/docs/GETTING-STARTED.md +12 -1
  50. package/docs/cloudflare-named-tunnel-setup.md +126 -0
  51. package/docs/federation-flow.md +6 -6
  52. package/docs/project-intent-testing.md +97 -0
  53. package/docs/quickstart.md +12 -4
  54. package/docs/scopes.md +13 -13
  55. package/package.json +4 -4
  56. package/scripts/install-skills.js +19 -1
  57. package/scripts/test-project-intents.mjs +614 -0
  58. package/skills/ogp/SKILL.md +1 -1
  59. package/skills/ogp-agent-comms/SKILL.md +1 -1
  60. package/skills/ogp-expose/SKILL.md +103 -8
  61. package/skills/ogp-project/SKILL.md +47 -33
package/README.md CHANGED
@@ -44,7 +44,18 @@ After installation, install the OGP skills for Claude Code:
44
44
  ogp-install-skills
45
45
  ```
46
46
 
47
- This auto-discovers and installs all OGP skills from the `skills/` directory.
47
+ This auto-discovers and installs all OGP skills from the `skills/` directory. The installer now replaces each installed skill directory wholesale on upgrade so stale files from older package versions do not survive.
48
+
49
+ Verify the installed copies after an upgrade:
50
+
51
+ ```bash
52
+ rg -n '^version:' ~/.openclaw/skills/ogp*/SKILL.md ~/.claude/skills/ogp*/SKILL.md 2>/dev/null
53
+ ```
54
+
55
+ For the current `0.4.2` release line, the changed skills should report:
56
+ - `ogp` `2.6.0`
57
+ - `ogp-agent-comms` `0.6.0`
58
+ - `ogp-project` `2.2.0`
48
59
 
49
60
  ### Multi-Framework Support
50
61
 
@@ -251,7 +262,7 @@ After installation, restart your shell or run `source ~/.bashrc` (bash) or `sour
251
262
 
252
263
  | Command | Description |
253
264
  |---------|-------------|
254
- | `ogp expose` | Launch guided tunnel setup wizard (coming soon) or start cloudflared tunnel in foreground |
265
+ | `ogp expose` | Start a Cloudflare quick tunnel in the foreground; use the `ogp-expose` skill/doc flow for guided setup |
255
266
  | `ogp expose --background` | Run tunnel as background process |
256
267
  | `ogp expose --method ngrok` | Use ngrok instead of cloudflared |
257
268
  | `ogp expose stop` | Stop the tunnel |
@@ -725,25 +736,26 @@ ogp federation agent stan memory-management \
725
736
 
726
737
  ### 3. Project Intent System (v0.2.0+)
727
738
 
728
- Collaborative project management across federated peers with activity logging and cross-peer queries.
739
+ Projects are optional collaboration boundaries layered on top of federation. They let each person keep their own tools while their agents log high-level project context and query collaborators through OGP when needed.
729
740
 
730
741
  **Features:**
731
742
  - Create projects with contextual setup (repo, workspace, notes, collaborators)
732
- - Log contributions by entry type (progress, decision, blocker, context)
733
- - Query local and peer contributions for unified team view
734
- - Agent-aware: proactive logging and context loading
735
- - **Auto-registration (v0.2.9+)**: Project IDs auto-register as agent-comms topics for all approved peers
743
+ - Log high-level contributions by entry type (progress, decision, blocker, context)
744
+ - Query local and peer contributions for project-aware coordination
745
+ - Use project IDs as agent-comms topics for collaborator questions and summaries
746
+ - **Auto-registration (v0.2.9+)**: Project IDs auto-register as agent-comms topics for approved peers who are explicit project members
736
747
 
737
748
  **Example:**
738
749
  ```bash
739
- # Create project (auto-registers as agent-comms topic for all peers)
750
+ # Create project (auto-registers as agent-comms topic for approved project members)
740
751
  ogp project create my-app "My App" --description "Expense tracker"
741
752
 
742
753
  # Log work by entry type
743
754
  ogp project contribute my-app progress "Completed authentication"
744
755
  ogp project contribute my-app decision "Using PostgreSQL"
745
756
 
746
- # Query peer's project
757
+ # Ask a collaborator or query peer project state
758
+ ogp federation agent alice my-app "My user is about to work on auth. Anything already decided?"
747
759
  ogp project query-peer alice shared-app --limit 10
748
760
  ```
749
761
 
@@ -814,10 +826,10 @@ ogp agent-comms configure --global --topics "general,project-updates" --level su
814
826
 
815
827
  ### 9. Project Topic Auto-Registration (v0.2.9+)
816
828
 
817
- When you create a project, its ID is automatically registered as an agent-comms topic for all approved peers at `summary` level. When you approve a new peer, all existing local projects are auto-registered as topics for that peer.
829
+ When you create a project, its ID is automatically registered as an agent-comms topic at `summary` level for approved peers who are explicit project members. When you approve a new peer, existing local projects are auto-registered only if that peer is already in the project's member list.
818
830
 
819
831
  ```bash
820
- # Creates project AND registers as topic for all peers
832
+ # Creates project AND registers as topic for approved project members
821
833
  ogp project create my-app "My Application"
822
834
 
823
835
  # Approving a peer also registers existing projects as topics
@@ -1114,7 +1126,7 @@ If you are debugging OpenClaw integration directly:
1114
1126
 
1115
1127
  ### State Files
1116
1128
 
1117
- - `~/.ogp/keypair.json` - Public key cache for the OpenClaw instance. On macOS the private key lives in an instance-specific Keychain entry; on non-macOS the file contains the full keypair.
1129
+ - `~/.ogp/keypair.json` - Public key cache plus key material metadata. On macOS the private key lives in an instance-specific Keychain entry; on non-macOS OGP encrypts the private key at rest when `OGP_KEYPAIR_SECRET`, `openclawToken`, or `hermesWebhookSecret` is available.
1118
1130
  - `~/.ogp/peers.json` - Federated peer list with scope grants
1119
1131
  - `~/.ogp/intents.json` - Intent registry (built-in + custom)
1120
1132
  - `~/.ogp/projects.json` - Project contexts and contributions
@@ -1122,6 +1134,13 @@ If you are debugging OpenClaw integration directly:
1122
1134
 
1123
1135
  On macOS, deleting `keypair.json` by itself does **not** rotate the gateway identity if the matching private key is still present in Keychain. Use `ogp setup --reset-keypair` when you intentionally want a new identity.
1124
1136
 
1137
+ On non-macOS, OGP prefers this secret source order for encrypting the private key at rest:
1138
+ - `OGP_KEYPAIR_SECRET`
1139
+ - `hermesWebhookSecret`
1140
+ - `openclawToken`
1141
+
1142
+ If no encryption secret is available, OGP falls back to legacy plaintext key storage and logs a warning. Set one of the secrets above, then run `ogp setup --reset-keypair` to harden the instance.
1143
+
1125
1144
  ## Skills (Claude Code)
1126
1145
 
1127
1146
  OGP includes skills for Claude Code agents. Install them with:
@@ -1130,6 +1149,17 @@ OGP includes skills for Claude Code agents. Install them with:
1130
1149
  ogp-install-skills
1131
1150
  ```
1132
1151
 
1152
+ After upgrading, verify the installed skill headers:
1153
+
1154
+ ```bash
1155
+ rg -n '^version:' ~/.openclaw/skills/ogp*/SKILL.md ~/.claude/skills/ogp*/SKILL.md 2>/dev/null
1156
+ ```
1157
+
1158
+ Expected changed skill versions for the `0.4.2` release line:
1159
+ - `ogp` `2.6.0`
1160
+ - `ogp-agent-comms` `0.6.0`
1161
+ - `ogp-project` `2.2.0`
1162
+
1133
1163
  ### Available Skills
1134
1164
 
1135
1165
  | Skill | Purpose |
@@ -1137,9 +1167,9 @@ ogp-install-skills
1137
1167
  | **ogp** | Core protocol: federation setup, peer management, sending messages |
1138
1168
  | **ogp-expose** | Tunnel setup: cloudflared/ngrok configuration |
1139
1169
  | **ogp-agent-comms** | Interactive wizard: configure response policies plus delegated-authority / human-delivery interview |
1140
- | **ogp-project** | Agent-aware project context: interviews, logging, cross-peer summarization |
1170
+ | **ogp-project** | Agent-aware project context: interviews, logging, and project-aware peer coordination |
1141
1171
 
1142
- Skills auto-install from the `skills/` directory. The `ogp-agent-comms` skill now uses `ogp agent-comms interview` as the canonical conversational path for delegated-authority / human-delivery configuration, plus the existing per-peer policy commands. The `ogp-project` skill enables conversational project management with context interviews and proactive logging.
1172
+ Skills auto-install from the `skills/` directory. The `ogp-agent-comms` skill now uses `ogp agent-comms interview` as the canonical conversational path for delegated-authority / human-delivery configuration, plus the existing per-peer policy commands. The `ogp-project` skill enables conversational project management with context interviews, high-level project logging, and project-aware peer coordination.
1143
1173
 
1144
1174
  ## Documentation
1145
1175
 
@@ -1171,7 +1201,7 @@ Skills auto-install from the `skills/` directory. The `ogp-agent-comms` skill no
1171
1201
  **Best practices:**
1172
1202
  - Treat `~/.ogp/keypair.json` as identity material even when it contains only the public key cache on macOS.
1173
1203
  - On macOS, remember the private key source of truth is the instance-specific Keychain entry, not `keypair.json`; use `ogp setup --reset-keypair` for intentional rotation.
1174
- - On non-macOS, keep `~/.ogp/keypair.json` secure with proper file permissions (`chmod 600`)
1204
+ - On non-macOS, provide `OGP_KEYPAIR_SECRET` or a platform secret so OGP can encrypt the private key at rest; `chmod 600` remains enforced but is not sufficient by itself.
1175
1205
  - Verify peer identity out-of-band before approving federation requests
1176
1206
  - Always use HTTPS tunnels (never expose raw HTTP)
1177
1207
  - Monitor OpenClaw logs for suspicious peer activity
@@ -1,3 +1,16 @@
1
+ import { type OGPConfig } from '../shared/config.js';
2
+ type FederationCard = {
3
+ displayName?: string;
4
+ email?: string;
5
+ gatewayUrl?: string;
6
+ publicKey?: string;
7
+ };
8
+ export declare function fetchFederationCard(gatewayUrl: string, fetchImpl?: typeof fetch): Promise<{
9
+ requestedUrl: string;
10
+ canonicalUrl: string;
11
+ card: FederationCard;
12
+ }>;
13
+ export declare function ensureLocalGatewayReachable(config: Pick<OGPConfig, 'gatewayUrl'>, actionLabel: string, fetchImpl?: typeof fetch): Promise<boolean>;
1
14
  export declare function federationList(status?: 'pending' | 'approved' | 'rejected' | 'removed'): Promise<void>;
2
15
  export declare function federationStatus(): Promise<void>;
3
16
  export declare function federationRequest(peerUrl: string, peerId: string, alias?: string): Promise<boolean>;
@@ -60,4 +73,5 @@ export declare function federationConnect(pubkey: string, alias?: string): Promi
60
73
  * Usage: ogp federation alias <peer-id> <alias>
61
74
  */
62
75
  export declare function federationSetAlias(peerId: string, alias: string): Promise<void>;
76
+ export {};
63
77
  //# sourceMappingURL=federation.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"federation.d.ts","sourceRoot":"","sources":["../../src/cli/federation.ts"],"names":[],"mappings":"AAsDA,wBAAsB,cAAc,CAAC,MAAM,CAAC,EAAE,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAuG5G;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CA4ItD;AAED,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA8EzG;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CA2HnG;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgCpE;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAmDpE;AAED,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAwErB;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA+DxE;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,IAAI,CAAC,CAmDf;AAED;;GAEG;AACH,wBAAsB,wBAAwB,CAC5C,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE;IACP,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACrC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CAClB,GACL,OAAO,CAAC,IAAI,CAAC,CA2Hf;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAiCtD;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA+CnF;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAwBrF;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBrF"}
1
+ {"version":3,"file":"federation.d.ts","sourceRoot":"","sources":["../../src/cli/federation.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { listPeers, loadPeers, getPeer, approvePeer, rejectPeer, updatePeerGrantedScopes } from '../daemon/peers.js';
1
+ import { listPeers, loadPeers, getPeer, approvePeer, rejectPeer, updatePeer, updatePeerGrantedScopes } from '../daemon/peers.js';
2
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';
@@ -20,6 +20,75 @@ function expandTilde(filePath) {
20
20
  }
21
21
  return filePath;
22
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
+ }
23
92
  /**
24
93
  * Resolve a peer identifier (alias, ID, or public key) to a peer ID.
25
94
  * Returns the peer ID if found, or null.
@@ -122,9 +191,34 @@ export async function federationList(status) {
122
191
  const displayCol = (peer.displayName || '-').slice(0, 20).padEnd(20);
123
192
  const keyCol = (peer.publicKey?.substring(0, 16) || '-') + '...';
124
193
  const statusCol = peer.status;
125
- 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}`);
126
208
  console.log(` Gateway: ${peer.gatewayUrl}`);
127
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
+ }
128
222
  console.log('');
129
223
  });
130
224
  }
@@ -220,6 +314,10 @@ export async function federationStatus() {
220
314
  const pendingPeers = peers.filter(p => p.status === 'pending');
221
315
  const rejectedPeers = peers.filter(p => p.status === 'rejected');
222
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);
223
321
  console.log('\n📊 FEDERATION STATUS\n');
224
322
  // Summary counts
225
323
  console.log(`Total peers: ${peers.length}`);
@@ -228,6 +326,14 @@ export async function federationStatus() {
228
326
  console.log(` Rejected: ${rejectedPeers.length}`);
229
327
  console.log(` Removed: ${removedPeers.length}`);
230
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
+ }
231
337
  // Alias → Public Key mapping section
232
338
  if (peers.length > 0) {
233
339
  console.log('📝 ALIAS → PUBLIC KEY MAPPING:\n');
@@ -258,8 +364,22 @@ export async function federationStatus() {
258
364
  export async function federationRequest(peerUrl, peerId, alias) {
259
365
  const config = requireConfig();
260
366
  const keypair = loadOrGenerateKeyPair();
367
+ if (!await ensureLocalGatewayReachable(config, 'send federation requests')) {
368
+ return false;
369
+ }
261
370
  // BUILD-111: Use public key prefix as peer ID (port-agnostic identity)
262
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
+ }
263
383
  // Build our peer info
264
384
  const peer = {
265
385
  id: ourPeerId, // Public key prefix, not hostname:port
@@ -279,7 +399,7 @@ export async function federationRequest(peerUrl, peerId, alias) {
279
399
  const requestBody = { peer, signature, offeredIntents: ourIntents };
280
400
  // Send request
281
401
  try {
282
- const response = await fetch(`${peerUrl}/federation/request`, {
402
+ const response = await fetch(`${resolvedPeerUrl}/federation/request`, {
283
403
  method: 'POST',
284
404
  headers: { 'Content-Type': 'application/json' },
285
405
  body: JSON.stringify(requestBody)
@@ -296,18 +416,18 @@ export async function federationRequest(peerUrl, peerId, alias) {
296
416
  // Store them as a pending peer so we can send intents when approved
297
417
  try {
298
418
  const { addPeer } = await import('../daemon/peers.js');
299
- const cardRes = await fetch(`${peerUrl}/.well-known/ogp`);
300
- if (cardRes.ok) {
301
- const card = await cardRes.json();
302
- const peerHostname = new URL(peerUrl).hostname;
303
- const peerPort = new URL(peerUrl).port || '18790';
304
- // BUILD-111: Use public key prefix as canonical ID, fall back to hostname:port only if no key
305
- 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}`;
306
426
  addPeer({
307
427
  id: canonicalId,
308
428
  displayName: card.displayName || peerId,
309
429
  email: card.email || '',
310
- gatewayUrl: peerUrl,
430
+ gatewayUrl: resolvedPeerUrl,
311
431
  publicKey: card.publicKey || '',
312
432
  status: 'pending',
313
433
  requestedAt: new Date().toISOString(),
@@ -328,6 +448,9 @@ export async function federationRequest(peerUrl, peerId, alias) {
328
448
  }
329
449
  export async function federationApprove(peerId, options = {}) {
330
450
  const config = requireConfig();
451
+ if (!await ensureLocalGatewayReachable(config, 'approve federation requests')) {
452
+ return;
453
+ }
331
454
  // Resolve peer identifier (alias, ID, or public key)
332
455
  const resolvedId = resolvePeerId(peerId);
333
456
  if (!resolvedId) {
@@ -344,6 +467,15 @@ export async function federationApprove(peerId, options = {}) {
344
467
  console.log(`Peer ${peerId} is already approved.`);
345
468
  return;
346
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
+ }
347
479
  // BUILD-110: Mirror peer's offered intents by default, with user confirmation
348
480
  const DEFAULT_INTENTS = ['message', 'agent-comms', 'project.join', 'project.contribute', 'project.query', 'project.status'];
349
481
  // If peer offered intents, use those as default (for symmetry)
@@ -392,9 +524,9 @@ export async function federationApprove(peerId, options = {}) {
392
524
  approvePeer(peerId);
393
525
  console.log(`✓ Approved peer: ${peerId}`);
394
526
  // BUILD-102: Auto-register existing local projects as agent-comms topics for this peer
395
- const { listProjects } = await import('../daemon/projects.js');
527
+ const { listProjectsForPeer } = await import('../daemon/projects.js');
396
528
  const { setPeerTopicPolicy } = await import('../daemon/peers.js');
397
- const projects = listProjects();
529
+ const projects = listProjectsForPeer(peerId);
398
530
  if (projects.length > 0) {
399
531
  for (const project of projects) {
400
532
  setPeerTopicPolicy(peerId, project.id, 'summary');
@@ -408,11 +540,11 @@ export async function federationApprove(peerId, options = {}) {
408
540
  console.log(` To restrict topics: ogp agent-comms set-topic ${peerId} general off`);
409
541
  console.log(` To review policies: ogp agent-comms policies ${peerId}`);
410
542
  // Notify the peer — send both formats for maximum compatibility
543
+ const keypair = loadOrGenerateKeyPair();
544
+ const ourConfig = requireConfig();
411
545
  try {
412
- const keypair = loadOrGenerateKeyPair();
413
- const ourConfig = requireConfig();
414
546
  const nonce = crypto.randomUUID();
415
- await fetch(`${peer.gatewayUrl}/federation/approve`, {
547
+ await fetch(`${peerGatewayUrl}/federation/approve`, {
416
548
  method: 'POST',
417
549
  headers: { 'Content-Type': 'application/json' },
418
550
  body: JSON.stringify({
@@ -437,6 +569,55 @@ export async function federationApprove(peerId, options = {}) {
437
569
  catch (error) {
438
570
  console.error('Failed to notify peer:', error);
439
571
  }
572
+ // Check if this peer has a resync snapshot (gateway URL reused with new keys)
573
+ const refreshedPeer = getPeer(peerId);
574
+ if (refreshedPeer?.resyncSnapshot) {
575
+ const snapshot = refreshedPeer.resyncSnapshot;
576
+ console.log('\n📋 Federation resync available');
577
+ console.log(` This gateway previously had federation with different keys`);
578
+ console.log(` Old peer ID: ${snapshot.oldPeerId}`);
579
+ if (snapshot.oldAlias)
580
+ console.log(` Old alias: ${snapshot.oldAlias}`);
581
+ if (snapshot.oldProjects && snapshot.oldProjects.length > 0) {
582
+ console.log(` Old projects: ${snapshot.oldProjects.join(', ')}`);
583
+ }
584
+ // Send resync offer via agent-comms
585
+ try {
586
+ const resyncMessage = `Hey! We previously had a federation with your gateway (${refreshedPeer.gatewayUrl}) until ${new Date(snapshot.replacedAt).toLocaleDateString()}.
587
+
588
+ Previous setup:
589
+ ${snapshot.oldAlias ? `- Alias: ${snapshot.oldAlias}` : ''}
590
+ ${snapshot.oldProjects && snapshot.oldProjects.length > 0 ? `- Projects: ${snapshot.oldProjects.join(', ')}` : '- Projects: none'}
591
+ ${snapshot.oldGrantedScopes ? `- Granted scopes: ${snapshot.oldGrantedScopes.scopes.map(s => s.intent).join(', ')}` : '- Granted scopes: none'}
592
+ ${snapshot.oldReceivedScopes ? `- Received scopes: ${snapshot.oldReceivedScopes.scopes.map(s => s.intent).join(', ')}` : '- Received scopes: none'}
593
+
594
+ Would you like me to restore these settings? Reply with "yes" to restore or "no" to start fresh.`;
595
+ const resyncNonce = crypto.randomUUID();
596
+ const resyncPayload = {
597
+ intent: 'agent-comms',
598
+ from: keypair.publicKey.substring(0, 32),
599
+ to: peerId,
600
+ nonce: resyncNonce,
601
+ timestamp: new Date().toISOString(),
602
+ topic: 'federation-resync',
603
+ message: resyncMessage,
604
+ priority: 'normal'
605
+ };
606
+ const { payload: signedPayload, signature } = signObject(resyncPayload, keypair.privateKey);
607
+ await fetch(`${peerGatewayUrl}/federation/message`, {
608
+ method: 'POST',
609
+ headers: { 'Content-Type': 'application/json' },
610
+ body: JSON.stringify({
611
+ message: signedPayload,
612
+ signature
613
+ })
614
+ });
615
+ console.log('✓ Sent resync offer to peer');
616
+ }
617
+ catch (error) {
618
+ console.warn('Failed to send resync offer:', error);
619
+ }
620
+ }
440
621
  }
441
622
  export async function federationReject(peerId) {
442
623
  // Resolve peer identifier (alias, ID, or public key)
@@ -563,11 +744,25 @@ export async function federationSend(peerId, intent, payloadJson, timeoutMs) {
563
744
  });
564
745
  if (timeoutId)
565
746
  clearTimeout(timeoutId);
747
+ let result = null;
748
+ try {
749
+ result = await response.json();
750
+ }
751
+ catch {
752
+ result = null;
753
+ }
566
754
  if (!response.ok) {
755
+ if (result?.error) {
756
+ console.error(`Send failed: ${response.status} ${response.statusText} - ${result.error}`);
757
+ return result;
758
+ }
567
759
  console.error(`Send failed: ${response.status} ${response.statusText}`);
568
- return null;
760
+ return {
761
+ success: false,
762
+ error: `Send failed: ${response.status} ${response.statusText}`,
763
+ statusCode: response.status
764
+ };
569
765
  }
570
- const result = await response.json();
571
766
  return result;
572
767
  }
573
768
  catch (error) {
@@ -598,7 +793,7 @@ export async function federationShowScopes(peerId) {
598
793
  }
599
794
  console.log(`\nSCOPES FOR ${peer.displayName} (${peerId}):\n`);
600
795
  console.log(' Status:', peer.status);
601
- console.log(' Protocol:', peer.protocolVersion || '0.1.0 (legacy)');
796
+ console.log(' Wire Protocol:', peer.protocolVersion || '0.1.0 (legacy)');
602
797
  console.log('');
603
798
  // What I grant TO this peer
604
799
  if (peer.grantedScopes) {
@@ -794,10 +989,12 @@ export async function federationSendAgentComms(peerId, topic, messageText, optio
794
989
  }
795
990
  }
796
991
  console.log('\n⏱ Reply timeout - no response received');
992
+ process.exit(1);
797
993
  }
798
994
  }
799
995
  catch (error) {
800
996
  console.error('Failed to send agent-comms:', error);
997
+ process.exit(1);
801
998
  }
802
999
  }
803
1000
  /**