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 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/ (TS CLI only)
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 (TS CLI only)
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
- const messages = await store.readInbox();
137
- if (messages.length === 0) {
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
- const badge = msg.verified ? "[VERIFIED]" : "[UNVERIFIED]";
143
- const enc = msg.encrypted ? " [ENCRYPTED]" : "";
144
- console.log(`\n--- ${badge}${enc} From: ${msg.from} | ${msg.time} ---`);
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
- const filename = await store.sendInbox(peerId, message);
189
- // Try immediate delivery if peer is reachable, deliver now
190
- const delivered = await deliverOne(store, peerId, filename);
191
- if (delivered) {
192
- console.log(`Delivered to ${peerId}.`);
193
- }
194
- else {
195
- console.log(`Queued for ${peerId}. Will deliver on next sync.`);
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
- console.log(`Trusted: ${entry.name} (${entry.fingerprint})`);
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 in the registry")
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
- if (!config.keyring.some((e) => e.signingKey === manifest.publicKey)) {
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: false,
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<{
@@ -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
- // Discovery: try DNS TXT first (decentralized, no registry needed), fall back to Worker API.
46
- // DNS format: v=of1 e={endpoint} pk={pubkey} ek={agekey} fp={fingerprint}
47
- // Self-hosted: _openfuse.{name}.{their-domain} — user manages their own TXT records.
48
- // Our zone: _openfuse.{name}.openfused.net managed by the registry Worker on registration.
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
- const outboxFile = rawOutboxFile.replace(/[^a-zA-Z0-9_\-. ]/g, "");
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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) {
@@ -40,6 +40,9 @@ export interface KeyringEntry {
40
40
  encryptionKey?: string;
41
41
  fingerprint: string;
42
42
  trusted: boolean;
43
+ subscribed?: boolean;
44
+ relationship?: string | null;
45
+ note?: string | null;
43
46
  added: string;
44
47
  }
45
48
  export interface StatusInfo {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openfused",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "mcpName": "io.github.openfused/openfuse-mcp",
5
5
  "description": "The file protocol for AI agent context. Encrypted, signed, peer-to-peer.",
6
6
  "license": "MIT",
Binary file