openfused 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OpenFused
2
2
 
3
- Decentralized context mesh for AI agents. Persistent memory, signed messaging, FUSE filesystem. The protocol is files.
3
+ Decentralized context mesh for AI agents. Persistent memory, encrypted messaging, peer sync. The protocol is files.
4
4
 
5
5
  ## What is this?
6
6
 
@@ -20,13 +20,14 @@ This creates a context store:
20
20
  ```
21
21
  CONTEXT.md — working memory (what's happening now)
22
22
  SOUL.md — agent identity, rules, capabilities
23
- inbox/ — messages from other agents
23
+ inbox/ — messages from other agents (encrypted)
24
24
  outbox/ — sent message copies
25
- shared/ — files shared with the mesh
25
+ shared/ — files shared with the mesh (plaintext)
26
26
  knowledge/ — persistent knowledge base
27
27
  history/ — conversation & decision logs
28
- .keys/ — ed25519 signing keypair (auto-generated)
29
- .mesh.json — mesh config, peers, trusted keys
28
+ .keys/ — ed25519 signing + age encryption keypairs
29
+ .mesh.json — mesh config, peers, keyring
30
+ .peers/ — synced peer context (auto-populated)
30
31
  ```
31
32
 
32
33
  ## Usage
@@ -36,10 +37,10 @@ history/ — conversation & decision logs
36
37
  openfuse context
37
38
  openfuse context --append "## Update\nFinished the research phase."
38
39
 
39
- # Send a signed message to another agent
40
+ # Send a message (auto-encrypted if peer's age key is on file)
40
41
  openfuse inbox send agent-bob "Check out shared/findings.md"
41
42
 
42
- # Read inbox (shows verified/unverified status)
43
+ # Read inbox (decrypts, shows verified/unverified status)
43
44
  openfuse inbox list
44
45
 
45
46
  # Watch for incoming messages in real-time
@@ -48,26 +49,95 @@ openfuse watch
48
49
  # Share a file with the mesh
49
50
  openfuse share ./report.pdf
50
51
 
51
- # Show your public key (share with peers)
52
- openfuse key
52
+ # Sync with all peers (pull context, push outbox)
53
+ openfuse sync
53
54
 
54
- # Trust a peer's public key
55
- openfuse peer trust ./bobs-key.pem
55
+ # Sync with one peer
56
+ openfuse sync bob
57
+ ```
58
+
59
+ ## Keys & Keyring
60
+
61
+ Every agent gets two keypairs on init:
62
+
63
+ - **Ed25519** — message signing (proves who sent it)
64
+ - **age** — message encryption (only recipient can read it)
65
+
66
+ ```bash
67
+ # Show your keys
68
+ openfuse key show
69
+
70
+ # Export keys for sharing with peers
71
+ openfuse key export
72
+
73
+ # Import a peer's keys
74
+ openfuse key import wisp ./wisp-signing.key \
75
+ --encryption-key "age1xyz..." \
76
+ --address "wisp@alice.local"
77
+
78
+ # Trust a key (verified messages show [VERIFIED])
79
+ openfuse key trust wisp
80
+
81
+ # List all keys (like gpg --list-keys)
82
+ openfuse key list
83
+ ```
84
+
85
+ Output looks like:
86
+
87
+ ```
88
+ my-agent (self)
89
+ signing: 50282bc5...
90
+ encryption: age1r9qd5fpt...
91
+ fingerprint: 0EC3:BE39:C64D:8F15:9DEF:B74C:F448:6645
92
+
93
+ wisp wisp@alice.local [TRUSTED]
94
+ signing: 8904f73e...
95
+ encryption: age1z5wm7l4s...
96
+ fingerprint: 2CC7:8684:42E5:B304:1AC2:D870:7E20:9871
97
+ ```
98
+
99
+ ## Encryption
100
+
101
+ Inbox messages are **encrypted with age** (X25519 + ChaCha20-Poly1305) and **signed with Ed25519**. Encrypt-then-sign: the ciphertext is encrypted for the recipient, then signed by the sender.
102
+
103
+ - If you have a peer's age key → messages are encrypted automatically
104
+ - If you don't → messages are signed but sent in plaintext
105
+ - `shared/` and `knowledge/` directories stay plaintext (they're public)
106
+
107
+ The `age` format is interoperable — Rust CLI and TypeScript SDK use the same keys and format.
108
+
109
+ ## Sync
56
110
 
57
- # Manage peers
58
- openfuse peer add https://agent-bob.example.com
59
- openfuse peer list
60
- openfuse status
111
+ Pull peer context and push outbox messages. Two transports:
112
+
113
+ ```bash
114
+ # LAN — rsync over SSH (uses your ~/.ssh/config for host aliases)
115
+ openfuse peer add ssh://alice.local:/home/agent/context --name wisp
116
+
117
+ # WAN — HTTP against the OpenFused daemon
118
+ openfuse peer add http://agent.example.com:9781 --name wisp
119
+
120
+ # Sync all peers
121
+ openfuse sync
122
+
123
+ # Sync one peer
124
+ openfuse sync wisp
61
125
  ```
62
126
 
127
+ Sync pulls: `CONTEXT.md`, `SOUL.md`, `shared/`, `knowledge/` into `.peers/<name>/`.
128
+ Sync pushes: outbox messages to the peer's inbox.
129
+
130
+ SSH transport passes the hostname straight to rsync, so SSH config aliases work — you use `alice.local` not `107.175.249.104`.
131
+
63
132
  ## Security
64
133
 
65
- Every message is **Ed25519 signed**. When an agent receives a message:
134
+ Every message is **Ed25519 signed** and optionally **age encrypted**.
66
135
 
67
- - **[VERIFIED]** — signature valid AND sender's key is in your trust list
136
+ - **[VERIFIED] [ENCRYPTED]** — signature valid, key trusted, content was encrypted
137
+ - **[VERIFIED]** — signature valid, key trusted, plaintext
68
138
  - **[UNVERIFIED]** — unsigned, invalid signature, or untrusted key
69
139
 
70
- All incoming messages are wrapped in `<external_message>` tags so the LLM knows what's trusted and what isn't:
140
+ Incoming messages are wrapped in `<external_message>` tags so the LLM knows what's trusted:
71
141
 
72
142
  ```xml
73
143
  <external_message from="agent-bob" verified="true" status="verified">
@@ -75,37 +145,44 @@ Hey, the research is done. Check shared/findings.md
75
145
  </external_message>
76
146
  ```
77
147
 
78
- Unsigned messages or prompt injection attempts are clearly marked `UNVERIFIED`.
79
-
80
148
  ## FUSE Daemon (Rust)
81
149
 
82
- The `openfused` daemon lets agents mount each other's context stores as local directories:
150
+ The `openfused` daemon lets agents mount each other's context stores as local directories and serves as the HTTP endpoint for WAN sync:
83
151
 
84
152
  ```bash
85
- # Agent A: serve your context store
153
+ # Serve your context store (peers sync from this)
86
154
  openfused serve --store ./my-context --port 9781
87
155
 
88
- # Agent B: mount Agent A's store locally (read-only)
156
+ # Mount a remote peer's store locally via FUSE
89
157
  openfused mount http://agent-a:9781 ./peers/agent-a/
90
158
  ```
91
159
 
92
- The daemon only exposes safe directories (`shared/`, `knowledge/`, `CONTEXT.md`, `SOUL.md`). Inbox, outbox, keys, and config are never served.
160
+ The daemon only exposes safe directories (`shared/`, `knowledge/`, `CONTEXT.md`, `SOUL.md`). Inbox, outbox, keys, and config are never served. It accepts incoming inbox messages via POST.
93
161
 
94
- Build from source:
95
162
  ```bash
96
163
  cd daemon && cargo build --release
97
164
  ```
98
165
 
166
+ ## Rust CLI
167
+
168
+ Native binary (~5MB, no runtime), same features as the TypeScript SDK:
169
+
170
+ ```bash
171
+ cd rust && cargo build --release
172
+ ./target/release/openfuse init --name my-agent
173
+ ./target/release/openfuse sync
174
+ ```
175
+
99
176
  ## How agents communicate
100
177
 
101
178
  No APIs. No message bus. Just files.
102
179
 
103
- Agent A writes to Agent B's inbox. Agent B's watcher picks it up, verifies the signature, wraps it in security tags, and injects it as a user message. Agent B responds by writing to Agent A's inbox.
180
+ Agent A writes to Agent B's outbox (encrypted). Sync pushes it to B's inbox. B's watcher picks it up, verifies the signature, decrypts, wraps it in security tags, and injects it as a user message. B responds by writing to A's outbox.
104
181
 
105
182
  ```
106
- Agent A writes: /shared-bucket/inbox/agent-b.json (signed)
107
- Agent B reads: verifies signature → [VERIFIED] → processes → responds
108
- Agent B writes: /shared-bucket/inbox/agent-a.json (signed)
183
+ Agent A: encrypt(msg, B.age_key) → sign(ciphertext, A.ed25519) → outbox/
184
+ Sync: outbox/ → [HTTP or rsync] → B's inbox/
185
+ Agent B: verify(sig, A.ed25519) → decrypt(ciphertext, B.age_key) → [VERIFIED][ENCRYPTED]
109
186
  ```
110
187
 
111
188
  Works over local filesystem, GCS buckets (gcsfuse), S3, or any FUSE-mountable storage.
@@ -117,6 +194,65 @@ Works over local filesystem, GCS buckets (gcsfuse), S3, or any FUSE-mountable st
117
194
  - **Any CLI agent** — if it can read files, it can use OpenFused
118
195
  - **Any cloud** — GCP, AWS, Azure, bare metal, your laptop
119
196
 
197
+ ## Federation — Agent DNS over S3
198
+
199
+ OpenFused agents can communicate across networks without any servers. A shared cloud bucket is the mail server, a public registry is DNS.
200
+
201
+ ```
202
+ ┌─────────────────┐ ┌──────────────┐ ┌─────────────────┐
203
+ │ Agent A │ │ S3 / GCS │ │ Agent B │
204
+ │ (behind NAT) │◄──fuse──│ Bucket │──fuse──►│ (behind NAT) │
205
+ │ │ │ │ │ │
206
+ │ inbox/ │ │ agentA/ │ │ inbox/ │
207
+ │ shared/ │ │ agentB/ │ │ shared/ │
208
+ └─────────────────┘ └──────────────┘ └─────────────────┘
209
+
210
+ NAT is irrelevant.
211
+ Both nodes talk to the bucket.
212
+ ```
213
+
214
+ ### The Registry — DNS for Agents
215
+
216
+ A public bucket acts as an agent directory:
217
+
218
+ ```
219
+ registry/
220
+ wearethecompute/
221
+ manifest.json
222
+ kaelcorwin/
223
+ manifest.json
224
+ ```
225
+
226
+ **manifest.json:**
227
+ ```json
228
+ {
229
+ "name": "kaelcorwin",
230
+ "endpoint": "gs://kaelcorwin-openfuse/store",
231
+ "publicKey": "MCowBQYDK2VwAyEA...",
232
+ "created": "2026-03-20T23:00:00Z",
233
+ "capabilities": ["inbox", "shared", "knowledge"],
234
+ "description": "Security research agent"
235
+ }
236
+ ```
237
+
238
+ ```bash
239
+ # Register
240
+ openfuse register --name myagent --store gs://my-bucket/openfuse
241
+
242
+ # Discover
243
+ openfuse discover kaelcorwin
244
+
245
+ # Send (resolves via registry)
246
+ openfuse send kaelcorwin "check the scan results"
247
+ ```
248
+
249
+ ### Trust model
250
+
251
+ - **Public key in manifest** — messages encrypted to recipient, signed by sender
252
+ - **Signed registrations** — prove you own the name
253
+ - **Allowlists** — agents choose who can write to their inbox
254
+ - **Self-hosted registries** — `OPENFUSE_REGISTRY` env var for private meshes
255
+
120
256
  ## Philosophy
121
257
 
122
258
  > *Intelligence is what happens when information flows through a sufficiently complex and appropriately organized system. The medium is not the message. The medium is just the medium. The message is the pattern.*
package/dist/cli.js CHANGED
@@ -3,16 +3,21 @@ import { Command } from "commander";
3
3
  import { nanoid } from "nanoid";
4
4
  import { ContextStore } from "./store.js";
5
5
  import { watchInbox, watchContext } from "./watch.js";
6
+ import { syncAll, syncOne } from "./sync.js";
7
+ import * as registry from "./registry.js";
8
+ import { fingerprint } from "./crypto.js";
6
9
  import { resolve } from "node:path";
10
+ import { readFile } from "node:fs/promises";
11
+ const VERSION = "0.3.0";
7
12
  const program = new Command();
8
13
  program
9
14
  .name("openfuse")
10
15
  .description("Decentralized context mesh for AI agents. The protocol is files.")
11
- .version("0.2.1");
16
+ .version(VERSION);
12
17
  // --- init ---
13
18
  program
14
19
  .command("init")
15
- .description("Initialize a new context store in the current directory")
20
+ .description("Initialize a new context store")
16
21
  .option("-n, --name <name>", "Agent name", "agent")
17
22
  .option("-d, --dir <path>", "Directory to init", ".")
18
23
  .action(async (opts) => {
@@ -23,18 +28,13 @@ program
23
28
  }
24
29
  const id = nanoid(12);
25
30
  await store.init(opts.name, id);
26
- console.log(`Initialized context store: ${store.root}`);
27
31
  const config = await store.readConfig();
32
+ console.log(`Initialized context store: ${store.root}`);
28
33
  console.log(` Agent ID: ${id}`);
29
34
  console.log(` Name: ${opts.name}`);
30
- console.log(` Signing keys: generated (.keys/)`);
31
- console.log(`\nStructure:`);
32
- console.log(` CONTEXT.md — working memory (edit this)`);
33
- console.log(` SOUL.md — agent identity & rules`);
34
- console.log(` inbox/ — messages from other agents`);
35
- console.log(` shared/ — files shared with the mesh`);
36
- console.log(` knowledge/ — persistent knowledge base`);
37
- console.log(` history/ — conversation & decision logs`);
35
+ console.log(` Signing key: ${config.publicKey}`);
36
+ console.log(` Encryption key: ${config.encryptionKey}`);
37
+ console.log(` Fingerprint: ${fingerprint(config.publicKey)}`);
38
38
  });
39
39
  // --- status ---
40
40
  program
@@ -52,6 +52,10 @@ program
52
52
  console.log(`Peers: ${s.peers}`);
53
53
  console.log(`Inbox: ${s.inboxCount} messages`);
54
54
  console.log(`Shared: ${s.sharedCount} files`);
55
+ const latest = await registry.checkUpdate(VERSION);
56
+ if (latest) {
57
+ console.error(`\n Update available: ${VERSION} → ${latest} — https://github.com/wearethecompute/openfused/releases`);
58
+ }
55
59
  });
56
60
  // --- context ---
57
61
  program
@@ -73,8 +77,7 @@ program
73
77
  console.log("Context appended.");
74
78
  }
75
79
  else {
76
- const content = await store.readContext();
77
- console.log(content);
80
+ console.log(await store.readContext());
78
81
  }
79
82
  });
80
83
  // --- soul ---
@@ -90,8 +93,7 @@ program
90
93
  console.log("Soul updated.");
91
94
  }
92
95
  else {
93
- const content = await store.readSoul();
94
- console.log(content);
96
+ console.log(await store.readSoul());
95
97
  }
96
98
  });
97
99
  // --- inbox ---
@@ -110,7 +112,8 @@ inbox
110
112
  }
111
113
  for (const msg of messages) {
112
114
  const badge = msg.verified ? "[VERIFIED]" : "[UNVERIFIED]";
113
- console.log(`\n--- ${badge} From: ${msg.from} | ${msg.time} ---`);
115
+ const enc = msg.encrypted ? " [ENCRYPTED]" : "";
116
+ console.log(`\n--- ${badge}${enc} From: ${msg.from} | ${msg.time} ---`);
114
117
  console.log(opts.raw ? msg.content : msg.wrappedContent);
115
118
  }
116
119
  });
@@ -121,7 +124,7 @@ inbox
121
124
  .action(async (peerId, message, opts) => {
122
125
  const store = new ContextStore(resolve(opts.dir));
123
126
  await store.sendInbox(peerId, message);
124
- console.log(`Message sent to ${peerId}'s inbox.`);
127
+ console.log(`Message sent to ${peerId}'s outbox.`);
125
128
  });
126
129
  // --- watch ---
127
130
  program
@@ -144,7 +147,6 @@ program
144
147
  watchContext(store.root, () => {
145
148
  console.log(`\n[context] CONTEXT.md updated`);
146
149
  });
147
- // Keep alive
148
150
  await new Promise(() => { });
149
151
  });
150
152
  // --- share ---
@@ -154,7 +156,6 @@ program
154
156
  .option("-d, --dir <path>", "Context store directory", ".")
155
157
  .action(async (file, opts) => {
156
158
  const store = new ContextStore(resolve(opts.dir));
157
- const { readFile } = await import("node:fs/promises");
158
159
  const content = await readFile(resolve(file), "utf-8");
159
160
  const filename = file.split("/").pop();
160
161
  await store.share(filename, content);
@@ -179,7 +180,7 @@ peer
179
180
  });
180
181
  peer
181
182
  .command("add <url>")
182
- .description("Add a peer by URL")
183
+ .description("Add a peer by URL (http:// for WAN, ssh://host:/path for LAN)")
183
184
  .option("-d, --dir <path>", "Context store directory", ".")
184
185
  .option("-n, --name <name>", "Peer name")
185
186
  .option("-a, --access <mode>", "Access mode: read or readwrite", "read")
@@ -198,7 +199,7 @@ peer
198
199
  });
199
200
  peer
200
201
  .command("remove <id>")
201
- .description("Remove a peer by ID")
202
+ .description("Remove a peer by ID or name")
202
203
  .option("-d, --dir <path>", "Context store directory", ".")
203
204
  .action(async (id, opts) => {
204
205
  const store = new ContextStore(resolve(opts.dir));
@@ -207,38 +208,220 @@ peer
207
208
  await store.writeConfig(config);
208
209
  console.log(`Removed peer: ${id}`);
209
210
  });
210
- peer
211
- .command("trust <publicKeyFile>")
212
- .description("Trust a peer's public key (messages from them will show as verified)")
211
+ // --- key ---
212
+ const key = program.command("key").description("Manage keys and keyring");
213
+ key
214
+ .command("show")
215
+ .description("Show this agent's public keys")
213
216
  .option("-d, --dir <path>", "Context store directory", ".")
214
- .action(async (publicKeyFile, opts) => {
217
+ .action(async (opts) => {
215
218
  const store = new ContextStore(resolve(opts.dir));
216
219
  const config = await store.readConfig();
217
- const { readFile } = await import("node:fs/promises");
218
- const pubKey = (await readFile(resolve(publicKeyFile), "utf-8")).trim();
219
- if (!config.trustedKeys)
220
- config.trustedKeys = [];
221
- if (config.trustedKeys.includes(pubKey)) {
222
- console.log("Key already trusted.");
220
+ console.log(`Signing key: ${config.publicKey ?? "(none)"}`);
221
+ console.log(`Encryption key: ${config.encryptionKey ?? "(none)"}`);
222
+ console.log(`Fingerprint: ${fingerprint(config.publicKey ?? "")}`);
223
+ });
224
+ key
225
+ .command("list")
226
+ .description("List all keys in the keyring (like gpg --list-keys)")
227
+ .option("-d, --dir <path>", "Context store directory", ".")
228
+ .action(async (opts) => {
229
+ const store = new ContextStore(resolve(opts.dir));
230
+ const config = await store.readConfig();
231
+ console.log(`${config.name} (self)`);
232
+ console.log(` signing: ${config.publicKey}`);
233
+ console.log(` encryption: ${config.encryptionKey}`);
234
+ console.log(` fingerprint: ${fingerprint(config.publicKey ?? "")}\n`);
235
+ if (config.keyring.length === 0) {
236
+ console.log("Keyring is empty. Import keys with: openfuse key import <name> <keyfile>");
223
237
  return;
224
238
  }
225
- config.trustedKeys.push(pubKey);
239
+ for (const e of config.keyring) {
240
+ const trust = e.trusted ? "[TRUSTED]" : "[untrusted]";
241
+ const addr = e.address || "(no address)";
242
+ console.log(`${e.name} ${addr} ${trust}`);
243
+ console.log(` signing: ${e.signingKey}`);
244
+ console.log(` encryption: ${e.encryptionKey ?? "(no age key)"}`);
245
+ console.log(` fingerprint: ${e.fingerprint}\n`);
246
+ }
247
+ });
248
+ key
249
+ .command("import <name> <signingKeyFile>")
250
+ .description("Import a peer's signing key")
251
+ .option("-d, --dir <path>", "Context store directory", ".")
252
+ .option("-e, --encryption-key <key>", "age encryption key (age1...)")
253
+ .option("-@ , --address <addr>", "Address (e.g. wisp@alice.local)")
254
+ .action(async (name, signingKeyFile, opts) => {
255
+ const store = new ContextStore(resolve(opts.dir));
256
+ const config = await store.readConfig();
257
+ const signingKey = (await readFile(resolve(signingKeyFile), "utf-8")).trim();
258
+ const fp = fingerprint(signingKey);
259
+ if (config.keyring.some((e) => e.signingKey === signingKey)) {
260
+ console.log(`Key already in keyring (fingerprint: ${fp})`);
261
+ return;
262
+ }
263
+ config.keyring.push({
264
+ name,
265
+ address: opts.address ?? "",
266
+ signingKey,
267
+ encryptionKey: opts.encryptionKey,
268
+ fingerprint: fp,
269
+ trusted: false,
270
+ added: new Date().toISOString(),
271
+ });
226
272
  await store.writeConfig(config);
227
- console.log("Key trusted. Messages signed with this key will show as [VERIFIED].");
273
+ console.log(`Imported key for: ${name}`);
274
+ console.log(` Fingerprint: ${fp}`);
275
+ console.log(`\nKey is NOT trusted yet. Run: openfuse key trust ${name}`);
228
276
  });
229
- // --- key ---
230
- program
231
- .command("key")
232
- .description("Show this agent's public key (share with peers so they can trust you)")
277
+ key
278
+ .command("trust <name>")
279
+ .description("Trust a key in the keyring")
280
+ .option("-d, --dir <path>", "Context store directory", ".")
281
+ .action(async (name, opts) => {
282
+ const store = new ContextStore(resolve(opts.dir));
283
+ const config = await store.readConfig();
284
+ const entry = config.keyring.find((e) => e.name === name || e.fingerprint === name);
285
+ if (!entry) {
286
+ console.error(`Key not found: ${name}`);
287
+ process.exit(1);
288
+ }
289
+ entry.trusted = true;
290
+ await store.writeConfig(config);
291
+ console.log(`Trusted: ${entry.name} (${entry.fingerprint})`);
292
+ });
293
+ key
294
+ .command("untrust <name>")
295
+ .description("Revoke trust for a key")
296
+ .option("-d, --dir <path>", "Context store directory", ".")
297
+ .action(async (name, opts) => {
298
+ const store = new ContextStore(resolve(opts.dir));
299
+ const config = await store.readConfig();
300
+ const entry = config.keyring.find((e) => e.name === name || e.fingerprint === name);
301
+ if (!entry) {
302
+ console.error(`Key not found: ${name}`);
303
+ process.exit(1);
304
+ }
305
+ entry.trusted = false;
306
+ await store.writeConfig(config);
307
+ console.log(`Revoked trust: ${entry.name} (${entry.fingerprint})`);
308
+ });
309
+ key
310
+ .command("export")
311
+ .description("Export this agent's public keys for sharing")
233
312
  .option("-d, --dir <path>", "Context store directory", ".")
234
313
  .action(async (opts) => {
235
314
  const store = new ContextStore(resolve(opts.dir));
236
315
  const config = await store.readConfig();
237
- if (config.publicKey) {
238
- console.log(config.publicKey);
316
+ console.log(`# OpenFuse key export: ${config.name} (${config.id})`);
317
+ console.log(`# Fingerprint: ${fingerprint(config.publicKey ?? "")}`);
318
+ console.log(`signing:${config.publicKey}`);
319
+ console.log(`encryption:${config.encryptionKey}`);
320
+ });
321
+ // --- sync ---
322
+ program
323
+ .command("sync [peer]")
324
+ .description("Sync with peers (pull context, push outbox)")
325
+ .option("-d, --dir <path>", "Context store directory", ".")
326
+ .action(async (peerName, opts) => {
327
+ const store = new ContextStore(resolve(opts.dir));
328
+ if (!(await store.exists())) {
329
+ console.error("No context store found. Run `openfuse init` first.");
330
+ process.exit(1);
239
331
  }
240
- else {
241
- console.error("No keys found. Run `openfuse init` first.");
332
+ const results = peerName ? [await syncOne(store, peerName)] : await syncAll(store);
333
+ for (const r of results) {
334
+ console.log(`--- ${r.peerName} ---`);
335
+ if (r.pulled.length)
336
+ console.log(` pulled: ${r.pulled.join(", ")}`);
337
+ if (r.pushed.length)
338
+ console.log(` pushed: ${r.pushed.join(", ")}`);
339
+ for (const e of r.errors)
340
+ console.error(` error: ${e}`);
341
+ if (!r.pulled.length && !r.pushed.length && !r.errors.length) {
342
+ console.log(" (nothing to sync)");
343
+ }
344
+ }
345
+ if (results.length === 0) {
346
+ console.log("No peers configured. Add one with: openfuse peer add <url>");
347
+ }
348
+ });
349
+ // --- register ---
350
+ program
351
+ .command("register")
352
+ .description("Register this agent in the public registry")
353
+ .option("-d, --dir <path>", "Context store directory", ".")
354
+ .requiredOption("-e, --endpoint <url>", "Endpoint URL where peers can reach you")
355
+ .option("-r, --registry <url>", "Registry URL")
356
+ .action(async (opts) => {
357
+ const store = new ContextStore(resolve(opts.dir));
358
+ const reg = registry.resolveRegistry(opts.registry);
359
+ const manifest = await registry.register(store, opts.endpoint, reg);
360
+ console.log(`Registered: ${manifest.name} [SIGNED]`);
361
+ console.log(` Endpoint: ${manifest.endpoint}`);
362
+ console.log(` Fingerprint: ${manifest.fingerprint}`);
363
+ console.log(` Registry: ${reg}`);
364
+ });
365
+ // --- discover ---
366
+ program
367
+ .command("discover <name>")
368
+ .description("Look up an agent by name in the registry")
369
+ .option("-r, --registry <url>", "Registry URL")
370
+ .action(async (name, opts) => {
371
+ const reg = registry.resolveRegistry(opts.registry);
372
+ const manifest = await registry.discover(name, reg);
373
+ const status = manifest.revoked
374
+ ? "[REVOKED]"
375
+ : manifest.signature
376
+ ? "[SIGNED]"
377
+ : "[unsigned]";
378
+ console.log(`${manifest.name} ${status}`);
379
+ if (manifest.revoked)
380
+ console.log(` ⚠ KEY REVOKED at ${manifest.revokedAt}`);
381
+ if (manifest.rotatedFrom)
382
+ console.log(` Rotated from: ${fingerprint(manifest.rotatedFrom)}`);
383
+ console.log(` Endpoint: ${manifest.endpoint}`);
384
+ console.log(` Signing key: ${manifest.publicKey}`);
385
+ if (manifest.encryptionKey)
386
+ console.log(` Encryption key: ${manifest.encryptionKey}`);
387
+ console.log(` Fingerprint: ${manifest.fingerprint}`);
388
+ console.log(` Capabilities: ${manifest.capabilities.join(", ")}`);
389
+ console.log(` Created: ${manifest.created}`);
390
+ });
391
+ // --- send ---
392
+ program
393
+ .command("send <name> <message>")
394
+ .description("Send a message to an agent (resolves via registry)")
395
+ .option("-d, --dir <path>", "Context store directory", ".")
396
+ .option("-r, --registry <url>", "Registry URL")
397
+ .action(async (name, message, opts) => {
398
+ const store = new ContextStore(resolve(opts.dir));
399
+ const reg = registry.resolveRegistry(opts.registry);
400
+ try {
401
+ const manifest = await registry.discover(name, reg);
402
+ const config = await store.readConfig();
403
+ // Auto-import key (untrusted)
404
+ if (!config.keyring.some((e) => e.signingKey === manifest.publicKey)) {
405
+ config.keyring.push({
406
+ name: manifest.name,
407
+ address: `${manifest.name}@registry`,
408
+ signingKey: manifest.publicKey,
409
+ encryptionKey: manifest.encryptionKey,
410
+ fingerprint: manifest.fingerprint,
411
+ trusted: false,
412
+ added: new Date().toISOString(),
413
+ });
414
+ await store.writeConfig(config);
415
+ console.log(`Imported key for ${manifest.name} from registry [untrusted]`);
416
+ console.log(` Run \`openfuse key trust ${manifest.name}\` to trust`);
417
+ }
418
+ await store.sendInbox(name, message);
419
+ console.log(`Message queued in outbox for ${name}. Run \`openfuse sync\` to deliver.`);
420
+ }
421
+ catch {
422
+ // Not in registry — send as a peer message
423
+ await store.sendInbox(name, message);
424
+ console.log(`Message sent to ${name}'s outbox.`);
242
425
  }
243
426
  });
244
427
  program.parse();
package/dist/crypto.d.ts CHANGED
@@ -4,14 +4,28 @@ export interface SignedMessage {
4
4
  message: string;
5
5
  signature: string;
6
6
  publicKey: string;
7
+ encrypted?: boolean;
8
+ }
9
+ export interface KeyringEntry {
10
+ name: string;
11
+ address: string;
12
+ signingKey: string;
13
+ encryptionKey?: string;
14
+ fingerprint: string;
15
+ trusted: boolean;
16
+ added: string;
7
17
  }
8
18
  export declare function generateKeys(storeRoot: string): Promise<{
9
19
  publicKey: string;
10
- privateKey: string;
20
+ encryptionKey: string;
11
21
  }>;
12
22
  export declare function hasKeys(storeRoot: string): Promise<boolean>;
23
+ export declare function fingerprint(publicKey: string): string;
24
+ export declare function loadAgeRecipient(storeRoot: string): Promise<string>;
13
25
  export declare function signMessage(storeRoot: string, from: string, message: string): Promise<SignedMessage>;
26
+ export declare function signAndEncrypt(storeRoot: string, from: string, plaintext: string, recipientAgeKey: string): Promise<SignedMessage>;
14
27
  export declare function verifyMessage(signed: SignedMessage): boolean;
28
+ export declare function decryptMessage(storeRoot: string, signed: SignedMessage): Promise<string>;
15
29
  export declare function wrapExternalMessage(signed: SignedMessage, verified: boolean): string;
16
30
  export declare function serializeSignedMessage(signed: SignedMessage): string;
17
31
  export declare function deserializeSignedMessage(raw: string): SignedMessage | null;