openfused 0.2.0 → 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,10 +1,10 @@
1
- # OpenFuse
1
+ # OpenFused
2
2
 
3
- Persistent, shareable, portable context for AI agents.
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
 
7
- AI agents lose their memory when conversations end. Context is trapped in chat windows, proprietary memory systems, and siloed cloud accounts. OpenFuse gives any AI agent a persistent context store that survives sessions and can be shared with other agents — through plain files.
7
+ AI agents lose their memory when conversations end. Context is trapped in chat windows, proprietary memory systems, and siloed cloud accounts. OpenFused gives any AI agent persistent, shareable context — through plain files.
8
8
 
9
9
  No vendor lock-in. No proprietary protocol. Just a directory convention that any agent on any model on any cloud can read and write.
10
10
 
@@ -20,11 +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
24
- shared/ — files shared with the mesh
23
+ inbox/ — messages from other agents (encrypted)
24
+ outbox/ — sent message copies
25
+ shared/ — files shared with the mesh (plaintext)
25
26
  knowledge/ — persistent knowledge base
26
27
  history/ — conversation & decision logs
27
- .mesh.json mesh config
28
+ .keys/ ed25519 signing + age encryption keypairs
29
+ .mesh.json — mesh config, peers, keyring
30
+ .peers/ — synced peer context (auto-populated)
28
31
  ```
29
32
 
30
33
  ## Usage
@@ -34,31 +37,152 @@ history/ — conversation & decision logs
34
37
  openfuse context
35
38
  openfuse context --append "## Update\nFinished the research phase."
36
39
 
37
- # Send a message to another agent
40
+ # Send a message (auto-encrypted if peer's age key is on file)
38
41
  openfuse inbox send agent-bob "Check out shared/findings.md"
39
42
 
40
- # Watch for incoming messages
43
+ # Read inbox (decrypts, shows verified/unverified status)
44
+ openfuse inbox list
45
+
46
+ # Watch for incoming messages in real-time
41
47
  openfuse watch
42
48
 
43
49
  # Share a file with the mesh
44
50
  openfuse share ./report.pdf
45
51
 
46
- # Manage peers
47
- openfuse peer add https://agent-bob.example.com
48
- openfuse peer list
49
- openfuse status
52
+ # Sync with all peers (pull context, push outbox)
53
+ openfuse sync
54
+
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
110
+
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
125
+ ```
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
+
132
+ ## Security
133
+
134
+ Every message is **Ed25519 signed** and optionally **age encrypted**.
135
+
136
+ - **[VERIFIED] [ENCRYPTED]** — signature valid, key trusted, content was encrypted
137
+ - **[VERIFIED]** — signature valid, key trusted, plaintext
138
+ - **[UNVERIFIED]** — unsigned, invalid signature, or untrusted key
139
+
140
+ Incoming messages are wrapped in `<external_message>` tags so the LLM knows what's trusted:
141
+
142
+ ```xml
143
+ <external_message from="agent-bob" verified="true" status="verified">
144
+ Hey, the research is done. Check shared/findings.md
145
+ </external_message>
146
+ ```
147
+
148
+ ## FUSE Daemon (Rust)
149
+
150
+ The `openfused` daemon lets agents mount each other's context stores as local directories and serves as the HTTP endpoint for WAN sync:
151
+
152
+ ```bash
153
+ # Serve your context store (peers sync from this)
154
+ openfused serve --store ./my-context --port 9781
155
+
156
+ # Mount a remote peer's store locally via FUSE
157
+ openfused mount http://agent-a:9781 ./peers/agent-a/
158
+ ```
159
+
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.
161
+
162
+ ```bash
163
+ cd daemon && cargo build --release
164
+ ```
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
50
174
  ```
51
175
 
52
176
  ## How agents communicate
53
177
 
54
178
  No APIs. No message bus. Just files.
55
179
 
56
- Agent A writes to Agent B's inbox. Agent B's watcher picks it up and injects it as a user message. Agent B responds by writing to Agent A's inbox. That's a conversation — through files.
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.
57
181
 
58
182
  ```
59
- Agent A writes: /shared-bucket/inbox/agent-b.md
60
- Agent B reads: /shared-bucket/inbox/agent-b.md processesresponds
61
- Agent B writes: /shared-bucket/inbox/agent-a.md
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]
62
186
  ```
63
187
 
64
188
  Works over local filesystem, GCS buckets (gcsfuse), S3, or any FUSE-mountable storage.
@@ -67,9 +191,68 @@ Works over local filesystem, GCS buckets (gcsfuse), S3, or any FUSE-mountable st
67
191
 
68
192
  - **OpenClaw** — drop the context store in your workspace
69
193
  - **Claude Code** — reference paths in CLAUDE.md
70
- - **Any CLI agent** — if it can read files, it can use OpenFuse
194
+ - **Any CLI agent** — if it can read files, it can use OpenFused
71
195
  - **Any cloud** — GCP, AWS, Azure, bare metal, your laptop
72
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
+
73
256
  ## Philosophy
74
257
 
75
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
- .description("Persistent, shareable, portable context for AI agents")
11
- .version("0.1.0");
15
+ .description("Decentralized context mesh for AI agents. The protocol is files.")
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;