openfused 0.4.4 → 0.5.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 +14 -4
- package/dist/cli.js +267 -19
- package/dist/crypto.d.ts +3 -0
- package/dist/registry.d.ts +7 -0
- package/dist/registry.js +10 -4
- package/dist/sync.js +19 -1
- package/dist/wasm-core.d.ts +3 -0
- package/package.json +1 -1
- package/wasm/openfused-core.wasm +0 -0
package/README.md
CHANGED
|
@@ -68,12 +68,10 @@ history/ — archived [DONE] context
|
|
|
68
68
|
openfuse context
|
|
69
69
|
openfuse context --append "## Update\nFinished the research phase."
|
|
70
70
|
|
|
71
|
-
# Mark work as done, then compact to history
|
|
72
|
-
# (edit CONTEXT.md, add [DONE] to the header, then:)
|
|
71
|
+
# Mark work as done, then compact to history/# (edit CONTEXT.md, add [DONE] to the header, then:)
|
|
73
72
|
openfuse compact
|
|
74
73
|
|
|
75
|
-
# Add validity windows to time-sensitive context
|
|
76
|
-
# <!-- validity: 6h --> for task state, 1d for sprint, 3d for architecture
|
|
74
|
+
# Add validity windows to time-sensitive context# <!-- validity: 6h --> for task state, 1d for sprint, 3d for architecture
|
|
77
75
|
openfuse validate # scan for stale entries
|
|
78
76
|
openfuse compact --prune-stale # archive expired validity windows
|
|
79
77
|
|
|
@@ -315,6 +313,18 @@ openfused serve --store ./my-context --token "$OPENFUSE_TOKEN" --gc-days 7
|
|
|
315
313
|
|
|
316
314
|
Rate limiting, IP filtering, and TLS belong at the reverse proxy layer (nginx, Caddy, cloudflared). The daemon focuses on application logic.
|
|
317
315
|
|
|
316
|
+
**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:
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
# Create isolated user
|
|
320
|
+
sudo useradd -r -s /usr/sbin/nologin -d /var/lib/openfused openfused
|
|
321
|
+
sudo mkdir -p /var/lib/openfused/store
|
|
322
|
+
sudo chown -R openfused: /var/lib/openfused
|
|
323
|
+
|
|
324
|
+
# Run as that user
|
|
325
|
+
sudo -u openfused openfused serve --store /var/lib/openfused/store --public --token "$TOKEN"
|
|
326
|
+
```
|
|
327
|
+
|
|
318
328
|
Endpoints:
|
|
319
329
|
|
|
320
330
|
| Endpoint | Method | Auth | Purpose |
|
package/dist/cli.js
CHANGED
|
@@ -118,7 +118,38 @@ program
|
|
|
118
118
|
const store = new ContextStore(resolve(opts.dir));
|
|
119
119
|
if (opts.set) {
|
|
120
120
|
await store.writeProfile(opts.set);
|
|
121
|
-
console.log("Profile updated.");
|
|
121
|
+
console.log("Profile updated locally.");
|
|
122
|
+
// Push to remote endpoint if agent has an HTTP peer for itself
|
|
123
|
+
const config = await store.readConfig();
|
|
124
|
+
const endpoint = config.peers?.find((p) => p.name === config.name && p.url?.startsWith("http"))?.url;
|
|
125
|
+
if (endpoint) {
|
|
126
|
+
try {
|
|
127
|
+
const { signChallenge } = await import("./crypto.js");
|
|
128
|
+
const timestamp = new Date().toISOString();
|
|
129
|
+
const challenge = `PROFILE:${config.name}:${timestamp}`;
|
|
130
|
+
const { signature, publicKey } = await signChallenge(store.root, challenge);
|
|
131
|
+
const resp = await fetch(`${endpoint}/profile/${encodeURIComponent(config.name)}`, {
|
|
132
|
+
method: "PUT",
|
|
133
|
+
headers: {
|
|
134
|
+
"Content-Type": "text/plain",
|
|
135
|
+
"X-OpenFuse-PublicKey": publicKey,
|
|
136
|
+
"X-OpenFuse-Signature": signature,
|
|
137
|
+
"X-OpenFuse-Timestamp": timestamp,
|
|
138
|
+
},
|
|
139
|
+
body: opts.set,
|
|
140
|
+
});
|
|
141
|
+
if (resp.ok) {
|
|
142
|
+
console.log("Profile synced to hosted mailbox.");
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const err = await resp.text();
|
|
146
|
+
console.error(`Failed to sync profile to mailbox: ${err}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
console.error(`Failed to sync profile: ${e.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
122
153
|
}
|
|
123
154
|
else {
|
|
124
155
|
console.log(await store.readProfile());
|
|
@@ -131,19 +162,64 @@ inbox
|
|
|
131
162
|
.description("List inbox messages")
|
|
132
163
|
.option("-d, --dir <path>", "Context store directory", ".")
|
|
133
164
|
.option("--raw", "Show raw content instead of wrapped")
|
|
165
|
+
.option("--all", "Show all messages including unverified (default: verified + subscribed)")
|
|
166
|
+
.option("--trusted", "Show only trusted messages")
|
|
167
|
+
.option("--no-sync", "Skip pulling from remote peers before listing")
|
|
134
168
|
.action(async (opts) => {
|
|
135
169
|
const store = new ContextStore(resolve(opts.dir));
|
|
136
|
-
|
|
137
|
-
if (
|
|
170
|
+
// Auto-sync: pull new messages from remote peers before listing
|
|
171
|
+
if (opts.sync !== false) {
|
|
172
|
+
try {
|
|
173
|
+
const config = await store.readConfig();
|
|
174
|
+
if (config.peers.length > 0) {
|
|
175
|
+
const { syncAll } = await import("./sync.js");
|
|
176
|
+
await syncAll(store);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
catch { }
|
|
180
|
+
}
|
|
181
|
+
const allMessages = await store.readInbox();
|
|
182
|
+
// Default: show verified (trusted + subscribed). Use --all for everything.
|
|
183
|
+
let messages;
|
|
184
|
+
if (opts.all) {
|
|
185
|
+
messages = allMessages;
|
|
186
|
+
}
|
|
187
|
+
else if (opts.trusted) {
|
|
188
|
+
messages = allMessages.filter((m) => m.verified && m.trusted);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
messages = allMessages.filter((m) => m.verified && (m.trusted || m.subscribed));
|
|
192
|
+
}
|
|
193
|
+
const hidden = allMessages.length - messages.length;
|
|
194
|
+
if (messages.length === 0 && hidden === 0) {
|
|
138
195
|
console.log("Inbox is empty.");
|
|
139
196
|
return;
|
|
140
197
|
}
|
|
198
|
+
if (messages.length === 0 && hidden > 0) {
|
|
199
|
+
console.log(`Inbox has ${hidden} message(s) from unsubscribed/untrusted senders.`);
|
|
200
|
+
console.log(`Run with --all to see them, or: openfuse key trust <name> / openfuse subscribe <name>`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
141
203
|
for (const msg of messages) {
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
204
|
+
// Build badge: [VERIFIED] [TRUSTED] [INTERNAL] etc.
|
|
205
|
+
const badges = [];
|
|
206
|
+
badges.push(msg.verified ? "[VERIFIED]" : "[UNVERIFIED]");
|
|
207
|
+
if (msg.trusted)
|
|
208
|
+
badges.push("[TRUSTED]");
|
|
209
|
+
if (msg.subscribed)
|
|
210
|
+
badges.push("[SUBSCRIBED]");
|
|
211
|
+
if (msg.relationship)
|
|
212
|
+
badges.push(`[${msg.relationship.toUpperCase()}]`);
|
|
213
|
+
if (msg.encrypted)
|
|
214
|
+
badges.push("[ENCRYPTED]");
|
|
215
|
+
const badgeStr = badges.join(" ");
|
|
216
|
+
const noteStr = msg.note ? ` (${msg.note})` : "";
|
|
217
|
+
console.log(`\n--- ${badgeStr} From: ${msg.from}${noteStr} | ${msg.time} ---`);
|
|
145
218
|
console.log(opts.raw ? msg.content : msg.wrappedContent);
|
|
146
219
|
}
|
|
220
|
+
if (hidden > 0) {
|
|
221
|
+
console.log(`\n(${hidden} message(s) hidden — use --all to show)`);
|
|
222
|
+
}
|
|
147
223
|
});
|
|
148
224
|
inbox
|
|
149
225
|
.command("archive [file]")
|
|
@@ -185,15 +261,17 @@ inbox
|
|
|
185
261
|
.option("-d, --dir <path>", "Context store directory", ".")
|
|
186
262
|
.action(async (peerId, message, opts) => {
|
|
187
263
|
const store = new ContextStore(resolve(opts.dir));
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
const
|
|
191
|
-
if (
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
264
|
+
await store.sendInbox(peerId, message);
|
|
265
|
+
// Find the outbox file we just created
|
|
266
|
+
const outboxFile = findNewestOutboxFile(store.root, peerId);
|
|
267
|
+
if (outboxFile) {
|
|
268
|
+
const delivered = await deliverOne(store, peerId, outboxFile);
|
|
269
|
+
if (delivered) {
|
|
270
|
+
console.log(`Delivered to ${peerId}.`);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
196
273
|
}
|
|
274
|
+
console.log(`Queued for ${peerId}. Will deliver on next sync.`);
|
|
197
275
|
});
|
|
198
276
|
// --- watch ---
|
|
199
277
|
program
|
|
@@ -504,13 +582,25 @@ key
|
|
|
504
582
|
.command("trust <query>")
|
|
505
583
|
.description("Trust a key in the keyring (name, name:fingerprint, or fingerprint)")
|
|
506
584
|
.option("-d, --dir <path>", "Context store directory", ".")
|
|
585
|
+
.option("--internal", "Mark as internal (same org/team)")
|
|
586
|
+
.option("--external", "Mark as external (partner/vendor)")
|
|
587
|
+
.option("--note <text>", "Private note about this peer")
|
|
507
588
|
.action(async (query, opts) => {
|
|
508
589
|
const store = new ContextStore(resolve(opts.dir));
|
|
509
590
|
const config = await store.readConfig();
|
|
510
591
|
const entry = resolveKeyring(config.keyring, query);
|
|
511
592
|
entry.trusted = true;
|
|
593
|
+
if (opts.internal)
|
|
594
|
+
entry.relationship = "internal";
|
|
595
|
+
else if (opts.external)
|
|
596
|
+
entry.relationship = "external";
|
|
597
|
+
if (opts.note)
|
|
598
|
+
entry.note = opts.note;
|
|
512
599
|
await store.writeConfig(config);
|
|
513
|
-
|
|
600
|
+
const rel = entry.relationship ? ` [${entry.relationship.toUpperCase()}]` : "";
|
|
601
|
+
console.log(`Trusted: ${entry.name} (${entry.fingerprint})${rel}`);
|
|
602
|
+
if (entry.note)
|
|
603
|
+
console.log(` Note: ${entry.note}`);
|
|
514
604
|
});
|
|
515
605
|
key
|
|
516
606
|
.command("untrust <query>")
|
|
@@ -536,6 +626,141 @@ key
|
|
|
536
626
|
console.log(`signing:${config.publicKey}`);
|
|
537
627
|
console.log(`encryption:${config.encryptionKey}`);
|
|
538
628
|
});
|
|
629
|
+
// --- subscribe / unsubscribe / broadcast ---
|
|
630
|
+
program
|
|
631
|
+
.command("subscribe <name>")
|
|
632
|
+
.description("Subscribe to an agent's broadcasts")
|
|
633
|
+
.option("-d, --dir <path>", "Context store directory", ".")
|
|
634
|
+
.option("-r, --registry <url>", "Registry URL")
|
|
635
|
+
.option("--note <text>", "Private note about this agent")
|
|
636
|
+
.action(async (name, opts) => {
|
|
637
|
+
const store = new ContextStore(resolve(opts.dir));
|
|
638
|
+
const reg = registry.resolveRegistry(opts.registry);
|
|
639
|
+
const config = await store.readConfig();
|
|
640
|
+
let entry = config.keyring.find((e) => e.name === name);
|
|
641
|
+
if (!entry) {
|
|
642
|
+
// Auto-import from registry
|
|
643
|
+
try {
|
|
644
|
+
const manifest = await registry.discover(name, reg);
|
|
645
|
+
entry = {
|
|
646
|
+
name: manifest.name,
|
|
647
|
+
address: `${manifest.name}@registry`,
|
|
648
|
+
signingKey: manifest.publicKey,
|
|
649
|
+
encryptionKey: manifest.encryptionKey,
|
|
650
|
+
fingerprint: manifest.fingerprint,
|
|
651
|
+
trusted: false,
|
|
652
|
+
subscribed: true,
|
|
653
|
+
relationship: null,
|
|
654
|
+
note: opts.note ?? null,
|
|
655
|
+
added: new Date().toISOString(),
|
|
656
|
+
};
|
|
657
|
+
config.keyring.push(entry);
|
|
658
|
+
console.log(`Imported key for ${manifest.name} from registry`);
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
console.error(`Agent '${name}' not found in keyring or registry.`);
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
entry.subscribed = true;
|
|
667
|
+
if (opts.note)
|
|
668
|
+
entry.note = opts.note;
|
|
669
|
+
}
|
|
670
|
+
await store.writeConfig(config);
|
|
671
|
+
console.log(`Subscribed to: ${entry.name} (${entry.fingerprint})`);
|
|
672
|
+
console.log(`Their broadcasts will appear in your inbox.`);
|
|
673
|
+
});
|
|
674
|
+
program
|
|
675
|
+
.command("unsubscribe <name>")
|
|
676
|
+
.description("Unsubscribe from an agent's broadcasts")
|
|
677
|
+
.option("-d, --dir <path>", "Context store directory", ".")
|
|
678
|
+
.action(async (name, opts) => {
|
|
679
|
+
const store = new ContextStore(resolve(opts.dir));
|
|
680
|
+
const config = await store.readConfig();
|
|
681
|
+
const entry = config.keyring.find((e) => e.name === name);
|
|
682
|
+
if (!entry) {
|
|
683
|
+
console.error(`Agent '${name}' not in keyring.`);
|
|
684
|
+
process.exit(1);
|
|
685
|
+
}
|
|
686
|
+
entry.subscribed = false;
|
|
687
|
+
await store.writeConfig(config);
|
|
688
|
+
console.log(`Unsubscribed from: ${entry.name}`);
|
|
689
|
+
});
|
|
690
|
+
program
|
|
691
|
+
.command("broadcast <message>")
|
|
692
|
+
.description("Send a message to all trusted + subscribed agents")
|
|
693
|
+
.option("-d, --dir <path>", "Context store directory", ".")
|
|
694
|
+
.option("--internal", "Only send to internal agents")
|
|
695
|
+
.option("--external", "Only send to external agents")
|
|
696
|
+
.option("--subscribers", "Only send to subscribers (not trusted)")
|
|
697
|
+
.option("--trusted-only", "Only send to trusted agents (skip subscribed-but-untrusted)")
|
|
698
|
+
.action(async (message, opts) => {
|
|
699
|
+
const store = new ContextStore(resolve(opts.dir));
|
|
700
|
+
const config = await store.readConfig();
|
|
701
|
+
// Build recipient list
|
|
702
|
+
let recipients = config.keyring.filter((e) => {
|
|
703
|
+
if (opts.trustedOnly)
|
|
704
|
+
return e.trusted;
|
|
705
|
+
if (opts.subscribers)
|
|
706
|
+
return e.subscribed;
|
|
707
|
+
return e.trusted || e.subscribed;
|
|
708
|
+
});
|
|
709
|
+
if (opts.internal) {
|
|
710
|
+
recipients = recipients.filter((e) => e.relationship === "internal");
|
|
711
|
+
}
|
|
712
|
+
else if (opts.external) {
|
|
713
|
+
recipients = recipients.filter((e) => e.relationship === "external");
|
|
714
|
+
}
|
|
715
|
+
if (recipients.length === 0) {
|
|
716
|
+
console.log("No recipients. Trust or subscribe to agents first.");
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
// Warn about untrusted recipients
|
|
720
|
+
const untrusted = recipients.filter((e) => !e.trusted);
|
|
721
|
+
if (untrusted.length > 0) {
|
|
722
|
+
console.log(`Warning: ${untrusted.length} recipient(s) are subscribed but NOT trusted:`);
|
|
723
|
+
for (const u of untrusted) {
|
|
724
|
+
console.log(` ${u.name} (${u.fingerprint})`);
|
|
725
|
+
}
|
|
726
|
+
console.log(`Their keys have not been verified. Use --trusted-only to skip them.\n`);
|
|
727
|
+
}
|
|
728
|
+
console.log(`Broadcasting to ${recipients.length} agent(s)...`);
|
|
729
|
+
let delivered = 0;
|
|
730
|
+
let queued = 0;
|
|
731
|
+
for (const recipient of recipients) {
|
|
732
|
+
try {
|
|
733
|
+
await store.sendInbox(recipient.name, message);
|
|
734
|
+
// Try HTTP delivery
|
|
735
|
+
const peer = config.peers.find((p) => p.name === recipient.name && p.url?.startsWith("http"));
|
|
736
|
+
if (peer) {
|
|
737
|
+
const outboxFile = findNewestOutboxFile(store.root, recipient.name);
|
|
738
|
+
if (outboxFile) {
|
|
739
|
+
const { checkSsrf } = await import("./sync.js");
|
|
740
|
+
await checkSsrf(peer.url);
|
|
741
|
+
const body = await readFile(join(store.root, "outbox", outboxFile), "utf-8");
|
|
742
|
+
const inboxUrl = `${peer.url.replace(/\/$/, "")}/inbox/${encodeURIComponent(recipient.name)}`;
|
|
743
|
+
const r = await fetch(inboxUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body });
|
|
744
|
+
if (r.ok) {
|
|
745
|
+
const { mkdir, rename } = await import("node:fs/promises");
|
|
746
|
+
const filePath = join(store.root, "outbox", outboxFile);
|
|
747
|
+
const sentDir = join(filePath, "..", ".sent");
|
|
748
|
+
const baseName = outboxFile.split("/").pop();
|
|
749
|
+
await mkdir(sentDir, { recursive: true });
|
|
750
|
+
await rename(filePath, join(sentDir, baseName));
|
|
751
|
+
delivered++;
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
queued++;
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
queued++;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
console.log(`Done. ${delivered} delivered, ${queued} queued for sync.`);
|
|
763
|
+
});
|
|
539
764
|
// --- sync ---
|
|
540
765
|
program
|
|
541
766
|
.command("sync [peer]")
|
|
@@ -578,6 +803,20 @@ program
|
|
|
578
803
|
const config = await store.readConfig();
|
|
579
804
|
const agentName = opts.name || `${config.name}.openfused.net`;
|
|
580
805
|
const manifest = await registry.register(store, opts.endpoint || "", reg, agentName);
|
|
806
|
+
// Auto-add endpoint as a peer so sync/inbox list can pull from it
|
|
807
|
+
if (manifest.endpoint?.startsWith("http")) {
|
|
808
|
+
const config2 = await store.readConfig();
|
|
809
|
+
const selfName = config2.name;
|
|
810
|
+
if (!config2.peers.some((p) => p.url === manifest.endpoint && p.name === selfName)) {
|
|
811
|
+
config2.peers.push({
|
|
812
|
+
id: (await import("nanoid")).nanoid(12),
|
|
813
|
+
name: selfName,
|
|
814
|
+
url: manifest.endpoint,
|
|
815
|
+
access: "read",
|
|
816
|
+
});
|
|
817
|
+
await store.writeConfig(config2);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
581
820
|
console.log(`Registered: ${manifest.name} [SIGNED]`);
|
|
582
821
|
if (manifest.endpoint)
|
|
583
822
|
console.log(` Endpoint: ${manifest.endpoint}`);
|
|
@@ -593,8 +832,8 @@ program
|
|
|
593
832
|
// --- discover ---
|
|
594
833
|
program
|
|
595
834
|
.command("discover <name>")
|
|
596
|
-
.description("Look up an agent by name
|
|
597
|
-
.option("-r, --registry <url>", "Registry URL")
|
|
835
|
+
.description("Look up an agent by name via DNS")
|
|
836
|
+
.option("-r, --registry <url>", "Registry URL (fallback if DNS fails)")
|
|
598
837
|
.action(async (name, opts) => {
|
|
599
838
|
const reg = registry.resolveRegistry(opts.registry);
|
|
600
839
|
const manifest = await registry.discover(name, reg);
|
|
@@ -643,6 +882,7 @@ program
|
|
|
643
882
|
.option("-r, --registry <url>", "Registry URL")
|
|
644
883
|
.option("--http", "Force HTTP delivery (uses registry endpoint)")
|
|
645
884
|
.option("--ssh", "Force SSH delivery (uses local peer SSH URL)")
|
|
885
|
+
.option("--trust", "Auto-trust the recipient's key (skip manual fingerprint verification)")
|
|
646
886
|
.action(async (name, message, opts) => {
|
|
647
887
|
const store = new ContextStore(resolve(opts.dir));
|
|
648
888
|
const reg = registry.resolveRegistry(opts.registry);
|
|
@@ -658,16 +898,24 @@ program
|
|
|
658
898
|
if (manifest.endpoint?.startsWith("http"))
|
|
659
899
|
httpEndpoint = manifest.endpoint;
|
|
660
900
|
// Auto-import key + add as peer
|
|
661
|
-
|
|
901
|
+
const existing = config.keyring.find((e) => e.signingKey === manifest.publicKey);
|
|
902
|
+
if (!existing) {
|
|
662
903
|
config.keyring.push({
|
|
663
904
|
name: manifest.name,
|
|
664
905
|
address: `${manifest.name}@registry`,
|
|
665
906
|
signingKey: manifest.publicKey,
|
|
666
907
|
encryptionKey: manifest.encryptionKey,
|
|
667
908
|
fingerprint: manifest.fingerprint,
|
|
668
|
-
trusted:
|
|
909
|
+
trusted: !!opts.trust,
|
|
669
910
|
added: new Date().toISOString(),
|
|
670
911
|
});
|
|
912
|
+
if (opts.trust) {
|
|
913
|
+
console.log(`Trusted ${manifest.name} (${manifest.fingerprint})`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
else if (opts.trust && !existing.trusted) {
|
|
917
|
+
existing.trusted = true;
|
|
918
|
+
console.log(`Trusted ${manifest.name} (${existing.fingerprint})`);
|
|
671
919
|
}
|
|
672
920
|
if (manifest.endpoint && !config.peers.some((p) => p.name === manifest.name)) {
|
|
673
921
|
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/sync.js
CHANGED
|
@@ -247,7 +247,9 @@ async function syncHttp(store, peer, baseUrl, peerDir) {
|
|
|
247
247
|
// Sanitize outboxFile — it comes from the remote peer's response and could
|
|
248
248
|
// contain path traversal characters (e.g., "../../inbox/important.json").
|
|
249
249
|
const rawOutboxFile = msg._outboxFile || "";
|
|
250
|
-
|
|
250
|
+
// Allow / for subdir paths (e.g., "name-FP/filename.json") but strip
|
|
251
|
+
// path traversal (..) and other dangerous chars.
|
|
252
|
+
const outboxFile = rawOutboxFile.replace(/\.\./g, "").replace(/[^a-zA-Z0-9_\-./ ]/g, "");
|
|
251
253
|
const dest = join(inboxDir, fname);
|
|
252
254
|
if (!existsSync(dest)) {
|
|
253
255
|
// Strip the _outboxFile metadata before saving
|
|
@@ -340,9 +342,14 @@ async function syncSsh(store, peer, host, remotePath, peerDir) {
|
|
|
340
342
|
const pulled = [];
|
|
341
343
|
const pushed = [];
|
|
342
344
|
const errors = [];
|
|
345
|
+
const esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
343
346
|
for (const file of ["CONTEXT.md", "PROFILE.md"]) {
|
|
344
347
|
try {
|
|
345
348
|
await execFile("rsync", ["-az", `${host}:${remotePath}/${file}`, join(peerDir, file)]);
|
|
349
|
+
// Wrap in unverified tags — SSH-synced content is untrusted external input
|
|
350
|
+
const raw = sanitizePeerContent(await readFile(join(peerDir, file), "utf-8"));
|
|
351
|
+
const wrapped = `<external_content_unverified from="${esc(peer.name)}" file="${esc(file)}">\n${raw}\n</external_content_unverified>`;
|
|
352
|
+
await writeFile(join(peerDir, file), wrapped);
|
|
346
353
|
pulled.push(file);
|
|
347
354
|
}
|
|
348
355
|
catch (e) {
|
|
@@ -354,6 +361,17 @@ async function syncSsh(store, peer, host, remotePath, peerDir) {
|
|
|
354
361
|
await mkdir(localDir, { recursive: true });
|
|
355
362
|
try {
|
|
356
363
|
await execFile("rsync", ["-az", "--delete", `${host}:${remotePath}/${dir}/`, `${localDir}/`]);
|
|
364
|
+
// Wrap each file in unverified tags
|
|
365
|
+
const { readdirSync } = await import("node:fs");
|
|
366
|
+
for (const fname of readdirSync(localDir)) {
|
|
367
|
+
const fpath = join(localDir, fname);
|
|
368
|
+
try {
|
|
369
|
+
const raw = sanitizePeerContent(await readFile(fpath, "utf-8"));
|
|
370
|
+
const wrapped = `<external_content_unverified from="${esc(peer.name)}" file="${esc(`${dir}/${fname}`)}">\n${raw}\n</external_content_unverified>`;
|
|
371
|
+
await writeFile(fpath, wrapped);
|
|
372
|
+
}
|
|
373
|
+
catch { }
|
|
374
|
+
}
|
|
357
375
|
pulled.push(`${dir}/`);
|
|
358
376
|
}
|
|
359
377
|
catch (e) {
|
package/dist/wasm-core.d.ts
CHANGED
package/package.json
CHANGED
package/wasm/openfused-core.wasm
CHANGED
|
Binary file
|