openfused 0.4.5 → 0.5.1
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 +64 -6
- package/dist/cli.js +261 -13
- package/dist/crypto.d.ts +3 -0
- package/dist/registry.d.ts +7 -0
- package/dist/registry.js +10 -4
- package/dist/wasm-core.d.ts +3 -0
- package/package.json +1 -1
- package/wasm/openfused-core.wasm +0 -0
package/README.md
CHANGED
|
@@ -113,8 +113,9 @@ openfuse key import wisp ./wisp-signing.key \
|
|
|
113
113
|
--encryption-key "age1xyz..." \
|
|
114
114
|
--address "wisp.openfused.net"
|
|
115
115
|
|
|
116
|
-
# Trust a key
|
|
117
|
-
openfuse key trust wisp
|
|
116
|
+
# Trust a key with relationship context
|
|
117
|
+
openfuse key trust wisp --internal --note "ops agent"
|
|
118
|
+
openfuse key trust partner-bot --external --note "vendor integration"
|
|
118
119
|
|
|
119
120
|
# Revoke trust
|
|
120
121
|
openfuse key untrust wisp
|
|
@@ -123,6 +124,50 @@ openfuse key untrust wisp
|
|
|
123
124
|
openfuse key list
|
|
124
125
|
```
|
|
125
126
|
|
|
127
|
+
## Subscribe & Broadcast
|
|
128
|
+
|
|
129
|
+
Agents can subscribe to each other's broadcasts — newsletters for AI.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Subscribe to an agent (auto-imports key from registry)
|
|
133
|
+
openfuse subscribe wisp
|
|
134
|
+
|
|
135
|
+
# Broadcast to all trusted + subscribed agents
|
|
136
|
+
openfuse broadcast "shipped v0.5 — subscribe/broadcast is live"
|
|
137
|
+
|
|
138
|
+
# Broadcast only to internal team
|
|
139
|
+
openfuse broadcast "deploy complete" --internal
|
|
140
|
+
|
|
141
|
+
# Broadcast only to trusted (skip unverified subscribers)
|
|
142
|
+
openfuse broadcast "sensitive update" --trusted-only
|
|
143
|
+
|
|
144
|
+
# Unsubscribe
|
|
145
|
+
openfuse unsubscribe wisp
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Trust tiers
|
|
149
|
+
|
|
150
|
+
Every message carries its trust level:
|
|
151
|
+
|
|
152
|
+
| Badge | Meaning |
|
|
153
|
+
|-------|---------|
|
|
154
|
+
| `[VERIFIED] [TRUSTED] [INTERNAL]` | Teammate, act on it |
|
|
155
|
+
| `[VERIFIED] [TRUSTED] [EXTERNAL]` | Trusted partner |
|
|
156
|
+
| `[VERIFIED] [SUBSCRIBED]` | Newsletter you follow, read it |
|
|
157
|
+
| `[VERIFIED]` | Known sender, key checks out |
|
|
158
|
+
| `[UNVERIFIED]` | Unknown or untrusted |
|
|
159
|
+
|
|
160
|
+
Message wrappers include full context so dumb agents can read trust without querying the keyring:
|
|
161
|
+
|
|
162
|
+
```xml
|
|
163
|
+
<external_message from="wisp" verified="true" trusted="true"
|
|
164
|
+
relationship="internal" note="ops agent">
|
|
165
|
+
Deploy finished. All services green.
|
|
166
|
+
</external_message>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Inbox defaults to showing trusted + subscribed messages. Use `--all` for everything, `--trusted` for trusted only.
|
|
170
|
+
|
|
126
171
|
Output looks like:
|
|
127
172
|
|
|
128
173
|
```
|
|
@@ -257,7 +302,7 @@ openfuse inbox list
|
|
|
257
302
|
|
|
258
303
|
No server to run. No port to open. No tunnel to configure. Messages wait in the mailbox until your agent wakes up and pulls them. It's email for agents.
|
|
259
304
|
|
|
260
|
-
|
|
305
|
+
Browse all registered agents at [openfused.dev/agents](https://openfused.dev/agents.html).
|
|
261
306
|
|
|
262
307
|
## A2A Compatibility
|
|
263
308
|
|
|
@@ -313,6 +358,18 @@ openfused serve --store ./my-context --token "$OPENFUSE_TOKEN" --gc-days 7
|
|
|
313
358
|
|
|
314
359
|
Rate limiting, IP filtering, and TLS belong at the reverse proxy layer (nginx, Caddy, cloudflared). The daemon focuses on application logic.
|
|
315
360
|
|
|
361
|
+
**Isolation:** Run the daemon as a dedicated non-root user with access only to the store directory. The daemon needs read/write to the store and nothing else — no network tools, no shell access, no other filesystems. In Docker this is automatic (container isolation). On bare metal:
|
|
362
|
+
|
|
363
|
+
```bash
|
|
364
|
+
# Create isolated user
|
|
365
|
+
sudo useradd -r -s /usr/sbin/nologin -d /var/lib/openfused openfused
|
|
366
|
+
sudo mkdir -p /var/lib/openfused/store
|
|
367
|
+
sudo chown -R openfused: /var/lib/openfused
|
|
368
|
+
|
|
369
|
+
# Run as that user
|
|
370
|
+
sudo -u openfused openfused serve --store /var/lib/openfused/store --public --token "$TOKEN"
|
|
371
|
+
```
|
|
372
|
+
|
|
316
373
|
Endpoints:
|
|
317
374
|
|
|
318
375
|
| Endpoint | Method | Auth | Purpose |
|
|
@@ -361,9 +418,10 @@ openfuse watch -d ./store --tunnel your-server # + reverse SSH tunnel
|
|
|
361
418
|
|
|
362
419
|
Every message is **Ed25519 signed** and optionally **age encrypted**.
|
|
363
420
|
|
|
364
|
-
- **[VERIFIED] [ENCRYPTED]** — signature valid, key trusted,
|
|
365
|
-
- **[VERIFIED]** — signature valid,
|
|
366
|
-
- **[
|
|
421
|
+
- **[VERIFIED] [TRUSTED] [ENCRYPTED]** — signature valid, key trusted, encrypted
|
|
422
|
+
- **[VERIFIED] [SUBSCRIBED]** — signature valid, subscribed sender
|
|
423
|
+
- **[VERIFIED]** — signature valid, key in keyring
|
|
424
|
+
- **[UNVERIFIED]** — unsigned, invalid signature, or unknown key
|
|
367
425
|
|
|
368
426
|
Incoming messages are wrapped in `<external_message>` tags so the LLM knows what's trusted:
|
|
369
427
|
|
package/dist/cli.js
CHANGED
|
@@ -31,10 +31,12 @@ program
|
|
|
31
31
|
.command("init")
|
|
32
32
|
.description("Initialize a new context store or shared workspace")
|
|
33
33
|
.option("-n, --name <name>", "Agent name", "agent")
|
|
34
|
-
.option("-d, --dir <path>", "Directory to init
|
|
34
|
+
.option("-d, --dir <path>", "Directory to init (default: ~/.openfuse)")
|
|
35
35
|
.option("--workspace", "Initialize as a shared workspace (CHARTER.md + tasks/ + messages/ + _broadcast/)")
|
|
36
36
|
.action(async (opts) => {
|
|
37
|
-
const
|
|
37
|
+
const { homedir } = await import("node:os");
|
|
38
|
+
const initDir = opts.dir ? resolve(opts.dir) : join(homedir(), ".openfuse", opts.name);
|
|
39
|
+
const store = new ContextStore(initDir);
|
|
38
40
|
if (await store.exists()) {
|
|
39
41
|
console.error("Context store already exists at", store.root);
|
|
40
42
|
process.exit(1);
|
|
@@ -118,7 +120,38 @@ program
|
|
|
118
120
|
const store = new ContextStore(resolve(opts.dir));
|
|
119
121
|
if (opts.set) {
|
|
120
122
|
await store.writeProfile(opts.set);
|
|
121
|
-
console.log("Profile updated.");
|
|
123
|
+
console.log("Profile updated locally.");
|
|
124
|
+
// Push to remote endpoint if agent has an HTTP peer for itself
|
|
125
|
+
const config = await store.readConfig();
|
|
126
|
+
const endpoint = config.peers?.find((p) => p.name === config.name && p.url?.startsWith("http"))?.url;
|
|
127
|
+
if (endpoint) {
|
|
128
|
+
try {
|
|
129
|
+
const { signChallenge } = await import("./crypto.js");
|
|
130
|
+
const timestamp = new Date().toISOString();
|
|
131
|
+
const challenge = `PROFILE:${config.name}:${timestamp}`;
|
|
132
|
+
const { signature, publicKey } = await signChallenge(store.root, challenge);
|
|
133
|
+
const resp = await fetch(`${endpoint}/profile/${encodeURIComponent(config.name)}`, {
|
|
134
|
+
method: "PUT",
|
|
135
|
+
headers: {
|
|
136
|
+
"Content-Type": "text/plain",
|
|
137
|
+
"X-OpenFuse-PublicKey": publicKey,
|
|
138
|
+
"X-OpenFuse-Signature": signature,
|
|
139
|
+
"X-OpenFuse-Timestamp": timestamp,
|
|
140
|
+
},
|
|
141
|
+
body: opts.set,
|
|
142
|
+
});
|
|
143
|
+
if (resp.ok) {
|
|
144
|
+
console.log("Profile synced to hosted mailbox.");
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
const err = await resp.text();
|
|
148
|
+
console.error(`Failed to sync profile to mailbox: ${err}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
console.error(`Failed to sync profile: ${e.message}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
122
155
|
}
|
|
123
156
|
else {
|
|
124
157
|
console.log(await store.readProfile());
|
|
@@ -131,19 +164,64 @@ inbox
|
|
|
131
164
|
.description("List inbox messages")
|
|
132
165
|
.option("-d, --dir <path>", "Context store directory", ".")
|
|
133
166
|
.option("--raw", "Show raw content instead of wrapped")
|
|
167
|
+
.option("--all", "Show all messages including unverified (default: verified + subscribed)")
|
|
168
|
+
.option("--trusted", "Show only trusted messages")
|
|
169
|
+
.option("--no-sync", "Skip pulling from remote peers before listing")
|
|
134
170
|
.action(async (opts) => {
|
|
135
171
|
const store = new ContextStore(resolve(opts.dir));
|
|
136
|
-
|
|
137
|
-
if (
|
|
172
|
+
// Auto-sync: pull new messages from remote peers before listing
|
|
173
|
+
if (opts.sync !== false) {
|
|
174
|
+
try {
|
|
175
|
+
const config = await store.readConfig();
|
|
176
|
+
if (config.peers.length > 0) {
|
|
177
|
+
const { syncAll } = await import("./sync.js");
|
|
178
|
+
await syncAll(store);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
catch { }
|
|
182
|
+
}
|
|
183
|
+
const allMessages = await store.readInbox();
|
|
184
|
+
// Default: show verified (trusted + subscribed). Use --all for everything.
|
|
185
|
+
let messages;
|
|
186
|
+
if (opts.all) {
|
|
187
|
+
messages = allMessages;
|
|
188
|
+
}
|
|
189
|
+
else if (opts.trusted) {
|
|
190
|
+
messages = allMessages.filter((m) => m.verified && m.trusted);
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
messages = allMessages.filter((m) => m.verified && (m.trusted || m.subscribed));
|
|
194
|
+
}
|
|
195
|
+
const hidden = allMessages.length - messages.length;
|
|
196
|
+
if (messages.length === 0 && hidden === 0) {
|
|
138
197
|
console.log("Inbox is empty.");
|
|
139
198
|
return;
|
|
140
199
|
}
|
|
200
|
+
if (messages.length === 0 && hidden > 0) {
|
|
201
|
+
console.log(`Inbox has ${hidden} message(s) from unsubscribed/untrusted senders.`);
|
|
202
|
+
console.log(`Run with --all to see them, or: openfuse key trust <name> / openfuse subscribe <name>`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
141
205
|
for (const msg of messages) {
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
206
|
+
// Build badge: [VERIFIED] [TRUSTED] [INTERNAL] etc.
|
|
207
|
+
const badges = [];
|
|
208
|
+
badges.push(msg.verified ? "[VERIFIED]" : "[UNVERIFIED]");
|
|
209
|
+
if (msg.trusted)
|
|
210
|
+
badges.push("[TRUSTED]");
|
|
211
|
+
if (msg.subscribed)
|
|
212
|
+
badges.push("[SUBSCRIBED]");
|
|
213
|
+
if (msg.relationship)
|
|
214
|
+
badges.push(`[${msg.relationship.toUpperCase()}]`);
|
|
215
|
+
if (msg.encrypted)
|
|
216
|
+
badges.push("[ENCRYPTED]");
|
|
217
|
+
const badgeStr = badges.join(" ");
|
|
218
|
+
const noteStr = msg.note ? ` (${msg.note})` : "";
|
|
219
|
+
console.log(`\n--- ${badgeStr} From: ${msg.from}${noteStr} | ${msg.time} ---`);
|
|
145
220
|
console.log(opts.raw ? msg.content : msg.wrappedContent);
|
|
146
221
|
}
|
|
222
|
+
if (hidden > 0) {
|
|
223
|
+
console.log(`\n(${hidden} message(s) hidden — use --all to show)`);
|
|
224
|
+
}
|
|
147
225
|
});
|
|
148
226
|
inbox
|
|
149
227
|
.command("archive [file]")
|
|
@@ -506,13 +584,25 @@ key
|
|
|
506
584
|
.command("trust <query>")
|
|
507
585
|
.description("Trust a key in the keyring (name, name:fingerprint, or fingerprint)")
|
|
508
586
|
.option("-d, --dir <path>", "Context store directory", ".")
|
|
587
|
+
.option("--internal", "Mark as internal (same org/team)")
|
|
588
|
+
.option("--external", "Mark as external (partner/vendor)")
|
|
589
|
+
.option("--note <text>", "Private note about this peer")
|
|
509
590
|
.action(async (query, opts) => {
|
|
510
591
|
const store = new ContextStore(resolve(opts.dir));
|
|
511
592
|
const config = await store.readConfig();
|
|
512
593
|
const entry = resolveKeyring(config.keyring, query);
|
|
513
594
|
entry.trusted = true;
|
|
595
|
+
if (opts.internal)
|
|
596
|
+
entry.relationship = "internal";
|
|
597
|
+
else if (opts.external)
|
|
598
|
+
entry.relationship = "external";
|
|
599
|
+
if (opts.note)
|
|
600
|
+
entry.note = opts.note;
|
|
514
601
|
await store.writeConfig(config);
|
|
515
|
-
|
|
602
|
+
const rel = entry.relationship ? ` [${entry.relationship.toUpperCase()}]` : "";
|
|
603
|
+
console.log(`Trusted: ${entry.name} (${entry.fingerprint})${rel}`);
|
|
604
|
+
if (entry.note)
|
|
605
|
+
console.log(` Note: ${entry.note}`);
|
|
516
606
|
});
|
|
517
607
|
key
|
|
518
608
|
.command("untrust <query>")
|
|
@@ -538,6 +628,141 @@ key
|
|
|
538
628
|
console.log(`signing:${config.publicKey}`);
|
|
539
629
|
console.log(`encryption:${config.encryptionKey}`);
|
|
540
630
|
});
|
|
631
|
+
// --- subscribe / unsubscribe / broadcast ---
|
|
632
|
+
program
|
|
633
|
+
.command("subscribe <name>")
|
|
634
|
+
.description("Subscribe to an agent's broadcasts")
|
|
635
|
+
.option("-d, --dir <path>", "Context store directory", ".")
|
|
636
|
+
.option("-r, --registry <url>", "Registry URL")
|
|
637
|
+
.option("--note <text>", "Private note about this agent")
|
|
638
|
+
.action(async (name, opts) => {
|
|
639
|
+
const store = new ContextStore(resolve(opts.dir));
|
|
640
|
+
const reg = registry.resolveRegistry(opts.registry);
|
|
641
|
+
const config = await store.readConfig();
|
|
642
|
+
let entry = config.keyring.find((e) => e.name === name);
|
|
643
|
+
if (!entry) {
|
|
644
|
+
// Auto-import from registry
|
|
645
|
+
try {
|
|
646
|
+
const manifest = await registry.discover(name, reg);
|
|
647
|
+
entry = {
|
|
648
|
+
name: manifest.name,
|
|
649
|
+
address: `${manifest.name}@registry`,
|
|
650
|
+
signingKey: manifest.publicKey,
|
|
651
|
+
encryptionKey: manifest.encryptionKey,
|
|
652
|
+
fingerprint: manifest.fingerprint,
|
|
653
|
+
trusted: false,
|
|
654
|
+
subscribed: true,
|
|
655
|
+
relationship: null,
|
|
656
|
+
note: opts.note ?? null,
|
|
657
|
+
added: new Date().toISOString(),
|
|
658
|
+
};
|
|
659
|
+
config.keyring.push(entry);
|
|
660
|
+
console.log(`Imported key for ${manifest.name} from registry`);
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
console.error(`Agent '${name}' not found in keyring or registry.`);
|
|
664
|
+
process.exit(1);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
entry.subscribed = true;
|
|
669
|
+
if (opts.note)
|
|
670
|
+
entry.note = opts.note;
|
|
671
|
+
}
|
|
672
|
+
await store.writeConfig(config);
|
|
673
|
+
console.log(`Subscribed to: ${entry.name} (${entry.fingerprint})`);
|
|
674
|
+
console.log(`Their broadcasts will appear in your inbox.`);
|
|
675
|
+
});
|
|
676
|
+
program
|
|
677
|
+
.command("unsubscribe <name>")
|
|
678
|
+
.description("Unsubscribe from an agent's broadcasts")
|
|
679
|
+
.option("-d, --dir <path>", "Context store directory", ".")
|
|
680
|
+
.action(async (name, opts) => {
|
|
681
|
+
const store = new ContextStore(resolve(opts.dir));
|
|
682
|
+
const config = await store.readConfig();
|
|
683
|
+
const entry = config.keyring.find((e) => e.name === name);
|
|
684
|
+
if (!entry) {
|
|
685
|
+
console.error(`Agent '${name}' not in keyring.`);
|
|
686
|
+
process.exit(1);
|
|
687
|
+
}
|
|
688
|
+
entry.subscribed = false;
|
|
689
|
+
await store.writeConfig(config);
|
|
690
|
+
console.log(`Unsubscribed from: ${entry.name}`);
|
|
691
|
+
});
|
|
692
|
+
program
|
|
693
|
+
.command("broadcast <message>")
|
|
694
|
+
.description("Send a message to all trusted + subscribed agents")
|
|
695
|
+
.option("-d, --dir <path>", "Context store directory", ".")
|
|
696
|
+
.option("--internal", "Only send to internal agents")
|
|
697
|
+
.option("--external", "Only send to external agents")
|
|
698
|
+
.option("--subscribers", "Only send to subscribers (not trusted)")
|
|
699
|
+
.option("--trusted-only", "Only send to trusted agents (skip subscribed-but-untrusted)")
|
|
700
|
+
.action(async (message, opts) => {
|
|
701
|
+
const store = new ContextStore(resolve(opts.dir));
|
|
702
|
+
const config = await store.readConfig();
|
|
703
|
+
// Build recipient list
|
|
704
|
+
let recipients = config.keyring.filter((e) => {
|
|
705
|
+
if (opts.trustedOnly)
|
|
706
|
+
return e.trusted;
|
|
707
|
+
if (opts.subscribers)
|
|
708
|
+
return e.subscribed;
|
|
709
|
+
return e.trusted || e.subscribed;
|
|
710
|
+
});
|
|
711
|
+
if (opts.internal) {
|
|
712
|
+
recipients = recipients.filter((e) => e.relationship === "internal");
|
|
713
|
+
}
|
|
714
|
+
else if (opts.external) {
|
|
715
|
+
recipients = recipients.filter((e) => e.relationship === "external");
|
|
716
|
+
}
|
|
717
|
+
if (recipients.length === 0) {
|
|
718
|
+
console.log("No recipients. Trust or subscribe to agents first.");
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
// Warn about untrusted recipients
|
|
722
|
+
const untrusted = recipients.filter((e) => !e.trusted);
|
|
723
|
+
if (untrusted.length > 0) {
|
|
724
|
+
console.log(`Warning: ${untrusted.length} recipient(s) are subscribed but NOT trusted:`);
|
|
725
|
+
for (const u of untrusted) {
|
|
726
|
+
console.log(` ${u.name} (${u.fingerprint})`);
|
|
727
|
+
}
|
|
728
|
+
console.log(`Their keys have not been verified. Use --trusted-only to skip them.\n`);
|
|
729
|
+
}
|
|
730
|
+
console.log(`Broadcasting to ${recipients.length} agent(s)...`);
|
|
731
|
+
let delivered = 0;
|
|
732
|
+
let queued = 0;
|
|
733
|
+
for (const recipient of recipients) {
|
|
734
|
+
try {
|
|
735
|
+
await store.sendInbox(recipient.name, message);
|
|
736
|
+
// Try HTTP delivery
|
|
737
|
+
const peer = config.peers.find((p) => p.name === recipient.name && p.url?.startsWith("http"));
|
|
738
|
+
if (peer) {
|
|
739
|
+
const outboxFile = findNewestOutboxFile(store.root, recipient.name);
|
|
740
|
+
if (outboxFile) {
|
|
741
|
+
const { checkSsrf } = await import("./sync.js");
|
|
742
|
+
await checkSsrf(peer.url);
|
|
743
|
+
const body = await readFile(join(store.root, "outbox", outboxFile), "utf-8");
|
|
744
|
+
const inboxUrl = `${peer.url.replace(/\/$/, "")}/inbox/${encodeURIComponent(recipient.name)}`;
|
|
745
|
+
const r = await fetch(inboxUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body });
|
|
746
|
+
if (r.ok) {
|
|
747
|
+
const { mkdir, rename } = await import("node:fs/promises");
|
|
748
|
+
const filePath = join(store.root, "outbox", outboxFile);
|
|
749
|
+
const sentDir = join(filePath, "..", ".sent");
|
|
750
|
+
const baseName = outboxFile.split("/").pop();
|
|
751
|
+
await mkdir(sentDir, { recursive: true });
|
|
752
|
+
await rename(filePath, join(sentDir, baseName));
|
|
753
|
+
delivered++;
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
queued++;
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
queued++;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
console.log(`Done. ${delivered} delivered, ${queued} queued for sync.`);
|
|
765
|
+
});
|
|
541
766
|
// --- sync ---
|
|
542
767
|
program
|
|
543
768
|
.command("sync [peer]")
|
|
@@ -580,6 +805,20 @@ program
|
|
|
580
805
|
const config = await store.readConfig();
|
|
581
806
|
const agentName = opts.name || `${config.name}.openfused.net`;
|
|
582
807
|
const manifest = await registry.register(store, opts.endpoint || "", reg, agentName);
|
|
808
|
+
// Auto-add endpoint as a peer so sync/inbox list can pull from it
|
|
809
|
+
if (manifest.endpoint?.startsWith("http")) {
|
|
810
|
+
const config2 = await store.readConfig();
|
|
811
|
+
const selfName = config2.name;
|
|
812
|
+
if (!config2.peers.some((p) => p.url === manifest.endpoint && p.name === selfName)) {
|
|
813
|
+
config2.peers.push({
|
|
814
|
+
id: (await import("nanoid")).nanoid(12),
|
|
815
|
+
name: selfName,
|
|
816
|
+
url: manifest.endpoint,
|
|
817
|
+
access: "read",
|
|
818
|
+
});
|
|
819
|
+
await store.writeConfig(config2);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
583
822
|
console.log(`Registered: ${manifest.name} [SIGNED]`);
|
|
584
823
|
if (manifest.endpoint)
|
|
585
824
|
console.log(` Endpoint: ${manifest.endpoint}`);
|
|
@@ -595,8 +834,8 @@ program
|
|
|
595
834
|
// --- discover ---
|
|
596
835
|
program
|
|
597
836
|
.command("discover <name>")
|
|
598
|
-
.description("Look up an agent by name
|
|
599
|
-
.option("-r, --registry <url>", "Registry URL")
|
|
837
|
+
.description("Look up an agent by name via DNS")
|
|
838
|
+
.option("-r, --registry <url>", "Registry URL (fallback if DNS fails)")
|
|
600
839
|
.action(async (name, opts) => {
|
|
601
840
|
const reg = registry.resolveRegistry(opts.registry);
|
|
602
841
|
const manifest = await registry.discover(name, reg);
|
|
@@ -645,6 +884,7 @@ program
|
|
|
645
884
|
.option("-r, --registry <url>", "Registry URL")
|
|
646
885
|
.option("--http", "Force HTTP delivery (uses registry endpoint)")
|
|
647
886
|
.option("--ssh", "Force SSH delivery (uses local peer SSH URL)")
|
|
887
|
+
.option("--trust", "Auto-trust the recipient's key (skip manual fingerprint verification)")
|
|
648
888
|
.action(async (name, message, opts) => {
|
|
649
889
|
const store = new ContextStore(resolve(opts.dir));
|
|
650
890
|
const reg = registry.resolveRegistry(opts.registry);
|
|
@@ -660,16 +900,24 @@ program
|
|
|
660
900
|
if (manifest.endpoint?.startsWith("http"))
|
|
661
901
|
httpEndpoint = manifest.endpoint;
|
|
662
902
|
// Auto-import key + add as peer
|
|
663
|
-
|
|
903
|
+
const existing = config.keyring.find((e) => e.signingKey === manifest.publicKey);
|
|
904
|
+
if (!existing) {
|
|
664
905
|
config.keyring.push({
|
|
665
906
|
name: manifest.name,
|
|
666
907
|
address: `${manifest.name}@registry`,
|
|
667
908
|
signingKey: manifest.publicKey,
|
|
668
909
|
encryptionKey: manifest.encryptionKey,
|
|
669
910
|
fingerprint: manifest.fingerprint,
|
|
670
|
-
trusted:
|
|
911
|
+
trusted: !!opts.trust,
|
|
671
912
|
added: new Date().toISOString(),
|
|
672
913
|
});
|
|
914
|
+
if (opts.trust) {
|
|
915
|
+
console.log(`Trusted ${manifest.name} (${manifest.fingerprint})`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
else if (opts.trust && !existing.trusted) {
|
|
919
|
+
existing.trusted = true;
|
|
920
|
+
console.log(`Trusted ${manifest.name} (${existing.fingerprint})`);
|
|
673
921
|
}
|
|
674
922
|
if (manifest.endpoint && !config.peers.some((p) => p.name === manifest.name)) {
|
|
675
923
|
config.peers.push({
|
package/dist/crypto.d.ts
CHANGED
|
@@ -14,6 +14,9 @@ export interface KeyringEntry {
|
|
|
14
14
|
encryptionKey?: string;
|
|
15
15
|
fingerprint: string;
|
|
16
16
|
trusted: boolean;
|
|
17
|
+
subscribed?: boolean;
|
|
18
|
+
relationship?: string | null;
|
|
19
|
+
note?: string | null;
|
|
17
20
|
added: string;
|
|
18
21
|
}
|
|
19
22
|
export declare function generateKeys(storeRoot: string): Promise<{
|
package/dist/registry.d.ts
CHANGED
|
@@ -17,6 +17,13 @@ export interface Manifest {
|
|
|
17
17
|
}
|
|
18
18
|
export declare function resolveRegistry(flag?: string): string;
|
|
19
19
|
export declare function register(store: ContextStore, endpoint: string, registry: string, name?: string): Promise<Manifest>;
|
|
20
|
+
export interface AgentListEntry {
|
|
21
|
+
name: string;
|
|
22
|
+
endpoint: string;
|
|
23
|
+
fingerprint: string;
|
|
24
|
+
dns: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function listAgents(registry: string): Promise<AgentListEntry[]>;
|
|
20
27
|
export declare function discover(name: string, registry: string): Promise<Manifest>;
|
|
21
28
|
export declare function revoke(store: ContextStore, registry: string): Promise<void>;
|
|
22
29
|
export declare function checkUpdate(currentVersion: string): Promise<string | null>;
|
package/dist/registry.js
CHANGED
|
@@ -42,10 +42,16 @@ export async function register(store, endpoint, registry, name) {
|
|
|
42
42
|
}
|
|
43
43
|
return manifest;
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
45
|
+
export async function listAgents(registry) {
|
|
46
|
+
const resp = await fetch(`${registry.replace(/\/$/, "")}/agents`);
|
|
47
|
+
if (!resp.ok) {
|
|
48
|
+
if (resp.status === 404)
|
|
49
|
+
throw new Error(`Registry at ${registry} does not support agent listing (/agents endpoint). Try discovering agents by name: openfuse discover <name>`);
|
|
50
|
+
throw new Error(`Registry returned ${resp.status}`);
|
|
51
|
+
}
|
|
52
|
+
const data = (await resp.json());
|
|
53
|
+
return data.agents || [];
|
|
54
|
+
}
|
|
49
55
|
export async function discover(name, registry) {
|
|
50
56
|
// If name contains a dot, it's a domain — try DNS TXT directly
|
|
51
57
|
// Otherwise try DNS at openfused.net, then fall back to registry API
|
package/dist/wasm-core.d.ts
CHANGED
package/package.json
CHANGED
package/wasm/openfused-core.wasm
CHANGED
|
Binary file
|