@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.
- package/README.md +45 -15
- package/dist/cli/federation.d.ts +14 -0
- package/dist/cli/federation.d.ts.map +1 -1
- package/dist/cli/federation.js +216 -19
- package/dist/cli/federation.js.map +1 -1
- package/dist/cli/project.d.ts +4 -3
- package/dist/cli/project.d.ts.map +1 -1
- package/dist/cli/project.js +34 -24
- package/dist/cli/project.js.map +1 -1
- package/dist/cli/setup.d.ts +4 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +57 -4
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli.js +11 -4
- package/dist/cli.js.map +1 -1
- package/dist/daemon/agent-comms.js +1 -1
- package/dist/daemon/agent-comms.js.map +1 -1
- package/dist/daemon/heartbeat.d.ts +22 -0
- package/dist/daemon/heartbeat.d.ts.map +1 -0
- package/dist/daemon/heartbeat.js +142 -0
- package/dist/daemon/heartbeat.js.map +1 -0
- package/dist/daemon/intent-registry.d.ts.map +1 -1
- package/dist/daemon/intent-registry.js +12 -6
- package/dist/daemon/intent-registry.js.map +1 -1
- package/dist/daemon/keypair.d.ts +1 -0
- package/dist/daemon/keypair.d.ts.map +1 -1
- package/dist/daemon/keypair.js +119 -7
- package/dist/daemon/keypair.js.map +1 -1
- package/dist/daemon/message-handler.js +148 -16
- package/dist/daemon/message-handler.js.map +1 -1
- package/dist/daemon/notify.d.ts.map +1 -1
- package/dist/daemon/notify.js +47 -0
- package/dist/daemon/notify.js.map +1 -1
- package/dist/daemon/peers.d.ts +24 -0
- package/dist/daemon/peers.d.ts.map +1 -1
- package/dist/daemon/peers.js +77 -6
- package/dist/daemon/peers.js.map +1 -1
- package/dist/daemon/projects.d.ts +9 -6
- package/dist/daemon/projects.d.ts.map +1 -1
- package/dist/daemon/projects.js +23 -16
- package/dist/daemon/projects.js.map +1 -1
- package/dist/daemon/server.d.ts +1 -0
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +85 -44
- package/dist/daemon/server.js.map +1 -1
- package/dist/shared/help.js +2 -1
- package/dist/shared/help.js.map +1 -1
- package/docs/CLI-REFERENCE.md +1 -0
- package/docs/GETTING-STARTED.md +12 -1
- package/docs/cloudflare-named-tunnel-setup.md +126 -0
- package/docs/federation-flow.md +6 -6
- package/docs/project-intent-testing.md +97 -0
- package/docs/quickstart.md +12 -4
- package/docs/scopes.md +13 -13
- package/package.json +4 -4
- package/scripts/install-skills.js +19 -1
- package/scripts/test-project-intents.mjs +614 -0
- package/skills/ogp/SKILL.md +1 -1
- package/skills/ogp-agent-comms/SKILL.md +1 -1
- package/skills/ogp-expose/SKILL.md +103 -8
- 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` |
|
|
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
|
-
|
|
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
|
|
734
|
-
-
|
|
735
|
-
- **Auto-registration (v0.2.9+)**: Project IDs auto-register as agent-comms topics for
|
|
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
|
|
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
|
-
#
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
package/dist/cli/federation.d.ts
CHANGED
|
@@ -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":"
|
|
1
|
+
{"version":3,"file":"federation.d.ts","sourceRoot":"","sources":["../../src/cli/federation.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/federation.js
CHANGED
|
@@ -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
|
-
|
|
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(`${
|
|
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
|
|
300
|
-
if (
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
//
|
|
305
|
-
const canonicalId = card.publicKey?.substring(0,
|
|
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:
|
|
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 {
|
|
527
|
+
const { listProjectsForPeer } = await import('../daemon/projects.js');
|
|
396
528
|
const { setPeerTopicPolicy } = await import('../daemon/peers.js');
|
|
397
|
-
const projects =
|
|
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(`${
|
|
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
|
|
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
|
/**
|