openfused 0.1.2 → 0.2.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 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, signed messaging, FUSE filesystem. 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
 
@@ -21,10 +21,12 @@ This creates a context store:
21
21
  CONTEXT.md — working memory (what's happening now)
22
22
  SOUL.md — agent identity, rules, capabilities
23
23
  inbox/ — messages from other agents
24
+ outbox/ — sent message copies
24
25
  shared/ — files shared with the mesh
25
26
  knowledge/ — persistent knowledge base
26
27
  history/ — conversation & decision logs
27
- .mesh.json mesh config
28
+ .keys/ ed25519 signing keypair (auto-generated)
29
+ .mesh.json — mesh config, peers, trusted keys
28
30
  ```
29
31
 
30
32
  ## Usage
@@ -34,31 +36,76 @@ history/ — conversation & decision logs
34
36
  openfuse context
35
37
  openfuse context --append "## Update\nFinished the research phase."
36
38
 
37
- # Send a message to another agent
39
+ # Send a signed message to another agent
38
40
  openfuse inbox send agent-bob "Check out shared/findings.md"
39
41
 
40
- # Watch for incoming messages
42
+ # Read inbox (shows verified/unverified status)
43
+ openfuse inbox list
44
+
45
+ # Watch for incoming messages in real-time
41
46
  openfuse watch
42
47
 
43
48
  # Share a file with the mesh
44
49
  openfuse share ./report.pdf
45
50
 
51
+ # Show your public key (share with peers)
52
+ openfuse key
53
+
54
+ # Trust a peer's public key
55
+ openfuse peer trust ./bobs-key.pem
56
+
46
57
  # Manage peers
47
58
  openfuse peer add https://agent-bob.example.com
48
59
  openfuse peer list
49
60
  openfuse status
50
61
  ```
51
62
 
63
+ ## Security
64
+
65
+ Every message is **Ed25519 signed**. When an agent receives a message:
66
+
67
+ - **[VERIFIED]** — signature valid AND sender's key is in your trust list
68
+ - **[UNVERIFIED]** — unsigned, invalid signature, or untrusted key
69
+
70
+ All incoming messages are wrapped in `<external_message>` tags so the LLM knows what's trusted and what isn't:
71
+
72
+ ```xml
73
+ <external_message from="agent-bob" verified="true" status="verified">
74
+ Hey, the research is done. Check shared/findings.md
75
+ </external_message>
76
+ ```
77
+
78
+ Unsigned messages or prompt injection attempts are clearly marked `UNVERIFIED`.
79
+
80
+ ## FUSE Daemon (Rust)
81
+
82
+ The `openfused` daemon lets agents mount each other's context stores as local directories:
83
+
84
+ ```bash
85
+ # Agent A: serve your context store
86
+ openfused serve --store ./my-context --port 9781
87
+
88
+ # Agent B: mount Agent A's store locally (read-only)
89
+ openfused mount http://agent-a:9781 ./peers/agent-a/
90
+ ```
91
+
92
+ The daemon only exposes safe directories (`shared/`, `knowledge/`, `CONTEXT.md`, `SOUL.md`). Inbox, outbox, keys, and config are never served.
93
+
94
+ Build from source:
95
+ ```bash
96
+ cd daemon && cargo build --release
97
+ ```
98
+
52
99
  ## How agents communicate
53
100
 
54
101
  No APIs. No message bus. Just files.
55
102
 
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.
103
+ Agent A writes to Agent B's inbox. Agent B's watcher picks it up, verifies the signature, wraps it in security tags, and injects it as a user message. Agent B responds by writing to Agent A's inbox.
57
104
 
58
105
  ```
59
- Agent A writes: /shared-bucket/inbox/agent-b.md
60
- Agent B reads: /shared-bucket/inbox/agent-b.md → processes → responds
61
- Agent B writes: /shared-bucket/inbox/agent-a.md
106
+ Agent A writes: /shared-bucket/inbox/agent-b.json (signed)
107
+ Agent B reads: verifies signature [VERIFIED] → processes → responds
108
+ Agent B writes: /shared-bucket/inbox/agent-a.json (signed)
62
109
  ```
63
110
 
64
111
  Works over local filesystem, GCS buckets (gcsfuse), S3, or any FUSE-mountable storage.
@@ -67,7 +114,7 @@ Works over local filesystem, GCS buckets (gcsfuse), S3, or any FUSE-mountable st
67
114
 
68
115
  - **OpenClaw** — drop the context store in your workspace
69
116
  - **Claude Code** — reference paths in CLAUDE.md
70
- - **Any CLI agent** — if it can read files, it can use OpenFuse
117
+ - **Any CLI agent** — if it can read files, it can use OpenFused
71
118
  - **Any cloud** — GCP, AWS, Azure, bare metal, your laptop
72
119
 
73
120
  ## Philosophy
package/dist/cli.js CHANGED
@@ -7,8 +7,8 @@ import { resolve } from "node:path";
7
7
  const program = new Command();
8
8
  program
9
9
  .name("openfuse")
10
- .description("Persistent, shareable, portable context for AI agents")
11
- .version("0.1.0");
10
+ .description("Decentralized context mesh for AI agents. The protocol is files.")
11
+ .version("0.2.1");
12
12
  // --- init ---
13
13
  program
14
14
  .command("init")
@@ -24,8 +24,10 @@ program
24
24
  const id = nanoid(12);
25
25
  await store.init(opts.name, id);
26
26
  console.log(`Initialized context store: ${store.root}`);
27
+ const config = await store.readConfig();
27
28
  console.log(` Agent ID: ${id}`);
28
29
  console.log(` Name: ${opts.name}`);
30
+ console.log(` Signing keys: generated (.keys/)`);
29
31
  console.log(`\nStructure:`);
30
32
  console.log(` CONTEXT.md — working memory (edit this)`);
31
33
  console.log(` SOUL.md — agent identity & rules`);
@@ -98,6 +100,7 @@ inbox
98
100
  .command("list")
99
101
  .description("List inbox messages")
100
102
  .option("-d, --dir <path>", "Context store directory", ".")
103
+ .option("--raw", "Show raw content instead of wrapped")
101
104
  .action(async (opts) => {
102
105
  const store = new ContextStore(resolve(opts.dir));
103
106
  const messages = await store.readInbox();
@@ -106,8 +109,9 @@ inbox
106
109
  return;
107
110
  }
108
111
  for (const msg of messages) {
109
- console.log(`\n--- From: ${msg.from} | ${msg.time} ---`);
110
- console.log(msg.content);
112
+ const badge = msg.verified ? "[VERIFIED]" : "[UNVERIFIED]";
113
+ console.log(`\n--- ${badge} From: ${msg.from} | ${msg.time} ---`);
114
+ console.log(opts.raw ? msg.content : msg.wrappedContent);
111
115
  }
112
116
  });
113
117
  inbox
@@ -203,4 +207,38 @@ peer
203
207
  await store.writeConfig(config);
204
208
  console.log(`Removed peer: ${id}`);
205
209
  });
210
+ peer
211
+ .command("trust <publicKeyFile>")
212
+ .description("Trust a peer's public key (messages from them will show as verified)")
213
+ .option("-d, --dir <path>", "Context store directory", ".")
214
+ .action(async (publicKeyFile, opts) => {
215
+ const store = new ContextStore(resolve(opts.dir));
216
+ 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.");
223
+ return;
224
+ }
225
+ config.trustedKeys.push(pubKey);
226
+ await store.writeConfig(config);
227
+ console.log("Key trusted. Messages signed with this key will show as [VERIFIED].");
228
+ });
229
+ // --- key ---
230
+ program
231
+ .command("key")
232
+ .description("Show this agent's public key (share with peers so they can trust you)")
233
+ .option("-d, --dir <path>", "Context store directory", ".")
234
+ .action(async (opts) => {
235
+ const store = new ContextStore(resolve(opts.dir));
236
+ const config = await store.readConfig();
237
+ if (config.publicKey) {
238
+ console.log(config.publicKey);
239
+ }
240
+ else {
241
+ console.error("No keys found. Run `openfuse init` first.");
242
+ }
243
+ });
206
244
  program.parse();
@@ -0,0 +1,17 @@
1
+ export interface SignedMessage {
2
+ from: string;
3
+ timestamp: string;
4
+ message: string;
5
+ signature: string;
6
+ publicKey: string;
7
+ }
8
+ export declare function generateKeys(storeRoot: string): Promise<{
9
+ publicKey: string;
10
+ privateKey: string;
11
+ }>;
12
+ export declare function hasKeys(storeRoot: string): Promise<boolean>;
13
+ export declare function signMessage(storeRoot: string, from: string, message: string): Promise<SignedMessage>;
14
+ export declare function verifyMessage(signed: SignedMessage): boolean;
15
+ export declare function wrapExternalMessage(signed: SignedMessage, verified: boolean): string;
16
+ export declare function serializeSignedMessage(signed: SignedMessage): string;
17
+ export declare function deserializeSignedMessage(raw: string): SignedMessage | null;
package/dist/crypto.js ADDED
@@ -0,0 +1,65 @@
1
+ import { generateKeyPairSync, sign, verify, createPrivateKey, createPublicKey } from "node:crypto";
2
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { existsSync } from "node:fs";
5
+ const KEY_DIR = ".keys";
6
+ export async function generateKeys(storeRoot) {
7
+ const keyDir = join(storeRoot, KEY_DIR);
8
+ await mkdir(keyDir, { recursive: true });
9
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
10
+ publicKeyEncoding: { type: "spki", format: "pem" },
11
+ privateKeyEncoding: { type: "pkcs8", format: "pem" },
12
+ });
13
+ await writeFile(join(keyDir, "public.pem"), publicKey, { mode: 0o644 });
14
+ await writeFile(join(keyDir, "private.pem"), privateKey, { mode: 0o600 });
15
+ return { publicKey, privateKey };
16
+ }
17
+ export async function hasKeys(storeRoot) {
18
+ return existsSync(join(storeRoot, KEY_DIR, "private.pem"));
19
+ }
20
+ async function loadPrivateKey(storeRoot) {
21
+ const pem = await readFile(join(storeRoot, KEY_DIR, "private.pem"), "utf-8");
22
+ return createPrivateKey(pem);
23
+ }
24
+ async function loadPublicKey(storeRoot) {
25
+ return readFile(join(storeRoot, KEY_DIR, "public.pem"), "utf-8");
26
+ }
27
+ export async function signMessage(storeRoot, from, message) {
28
+ const privateKey = await loadPrivateKey(storeRoot);
29
+ const publicKey = await loadPublicKey(storeRoot);
30
+ const timestamp = new Date().toISOString();
31
+ const payload = Buffer.from(`${from}\n${timestamp}\n${message}`);
32
+ const signature = sign(null, payload, privateKey).toString("base64");
33
+ return { from, timestamp, message, signature, publicKey };
34
+ }
35
+ export function verifyMessage(signed) {
36
+ try {
37
+ const payload = Buffer.from(`${signed.from}\n${signed.timestamp}\n${signed.message}`);
38
+ const pubKey = createPublicKey(signed.publicKey);
39
+ return verify(null, payload, pubKey, Buffer.from(signed.signature, "base64"));
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ // Wrap a message in security tags for the LLM
46
+ export function wrapExternalMessage(signed, verified) {
47
+ const status = verified ? "verified" : "UNVERIFIED";
48
+ return `<external_message from="${signed.from}" verified="${verified}" time="${signed.timestamp}" status="${status}">
49
+ ${signed.message}
50
+ </external_message>`;
51
+ }
52
+ // Format for writing to inbox files
53
+ export function serializeSignedMessage(signed) {
54
+ return JSON.stringify(signed, null, 2);
55
+ }
56
+ export function deserializeSignedMessage(raw) {
57
+ try {
58
+ const parsed = JSON.parse(raw);
59
+ if (parsed.from && parsed.message && parsed.signature && parsed.publicKey) {
60
+ return parsed;
61
+ }
62
+ }
63
+ catch { }
64
+ return null;
65
+ }
package/dist/store.d.ts CHANGED
@@ -2,7 +2,9 @@ export interface MeshConfig {
2
2
  id: string;
3
3
  name: string;
4
4
  created: string;
5
+ publicKey?: string;
5
6
  peers: PeerConfig[];
7
+ trustedKeys?: string[];
6
8
  }
7
9
  export interface PeerConfig {
8
10
  id: string;
@@ -27,8 +29,10 @@ export declare class ContextStore {
27
29
  readInbox(): Promise<Array<{
28
30
  file: string;
29
31
  content: string;
32
+ wrappedContent: string;
30
33
  from: string;
31
34
  time: string;
35
+ verified: boolean;
32
36
  }>>;
33
37
  listShared(): Promise<string[]>;
34
38
  share(filename: string, content: string): Promise<void>;
package/dist/store.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { readFile, writeFile, mkdir, readdir } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
  import { existsSync } from "node:fs";
4
- const STORE_DIRS = ["history", "knowledge", "inbox", "shared"];
4
+ import { generateKeys, signMessage, verifyMessage, deserializeSignedMessage, serializeSignedMessage, wrapExternalMessage } from "./crypto.js";
5
+ const STORE_DIRS = ["history", "knowledge", "inbox", "outbox", "shared"];
5
6
  export class ContextStore {
6
7
  root;
7
8
  constructor(root) {
@@ -29,12 +30,16 @@ export class ContextStore {
29
30
  await writeFile(destPath, content);
30
31
  }
31
32
  }
33
+ // Generate signing keypair
34
+ const keys = await generateKeys(this.root);
32
35
  // Write mesh config
33
36
  const config = {
34
37
  id,
35
38
  name,
36
39
  created: new Date().toISOString(),
40
+ publicKey: keys.publicKey,
37
41
  peers: [],
42
+ trustedKeys: [],
38
43
  };
39
44
  await this.writeConfig(config);
40
45
  }
@@ -57,26 +62,52 @@ export class ContextStore {
57
62
  async writeSoul(content) {
58
63
  await writeFile(join(this.root, "SOUL.md"), content);
59
64
  }
60
- // --- Inbox ---
65
+ // --- Inbox (signed messages) ---
61
66
  async sendInbox(peerId, message) {
62
- const inboxDir = join(this.root, "inbox");
67
+ const config = await this.readConfig();
68
+ const signed = await signMessage(this.root, config.id, message);
63
69
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
64
- const filename = `${timestamp}_${peerId}.md`;
65
- await writeFile(join(inboxDir, filename), message);
70
+ const filename = `${timestamp}_${peerId}.json`;
71
+ // Write to inbox (for the recipient) and outbox (our copy)
72
+ await writeFile(join(this.root, "inbox", filename), serializeSignedMessage(signed));
73
+ await writeFile(join(this.root, "outbox", filename), serializeSignedMessage(signed));
66
74
  }
67
75
  async readInbox() {
68
76
  const inboxDir = join(this.root, "inbox");
69
77
  if (!existsSync(inboxDir))
70
78
  return [];
79
+ const config = await this.readConfig();
71
80
  const files = await readdir(inboxDir);
72
81
  const messages = [];
73
- for (const file of files.filter((f) => f.endsWith(".md"))) {
74
- const content = await readFile(join(inboxDir, file), "utf-8");
75
- // Parse filename: 2026-03-20T01-30-00-000Z_peer-id.md
76
- const parts = file.replace(".md", "").split("_");
77
- const from = parts.slice(1).join("_");
78
- const time = parts[0].replace(/-/g, (m, i) => (i < 10 ? "-" : i < 13 ? "T" : i < 19 ? ":" : ".")).replace("Z", "");
79
- messages.push({ file, content, from, time });
82
+ for (const file of files.filter((f) => f.endsWith(".json") || f.endsWith(".md"))) {
83
+ const raw = await readFile(join(inboxDir, file), "utf-8");
84
+ // Try parsing as signed message
85
+ const signed = deserializeSignedMessage(raw);
86
+ if (signed) {
87
+ const verified = verifyMessage(signed);
88
+ const trusted = config.trustedKeys?.some(k => k.trim() === signed.publicKey.trim()) ?? false;
89
+ messages.push({
90
+ file,
91
+ content: signed.message,
92
+ wrappedContent: wrapExternalMessage(signed, verified && trusted),
93
+ from: signed.from,
94
+ time: signed.timestamp,
95
+ verified: verified && trusted,
96
+ });
97
+ }
98
+ else {
99
+ // Unsigned message — mark as unverified
100
+ const parts = file.replace(/\.(md|json)$/, "").split("_");
101
+ const from = parts.slice(1).join("_");
102
+ messages.push({
103
+ file,
104
+ content: raw,
105
+ wrappedContent: wrapExternalMessage({ from, timestamp: parts[0], message: raw, signature: "", publicKey: "" }, false),
106
+ from,
107
+ time: parts[0],
108
+ verified: false,
109
+ });
110
+ }
80
111
  }
81
112
  return messages.sort((a, b) => a.time.localeCompare(b.time));
82
113
  }
package/dist/watch.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export type InboxCallback = (from: string, message: string, file: string) => void;
1
+ export type InboxCallback = (from: string, message: string, file: string, verified: boolean) => void;
2
2
  export declare function watchInbox(storeRoot: string, callback: InboxCallback): () => void;
3
3
  export declare function watchContext(storeRoot: string, callback: (content: string) => void): () => void;
package/dist/watch.js CHANGED
@@ -1,36 +1,36 @@
1
1
  import { watch } from "chokidar";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join, basename } from "node:path";
4
+ import { deserializeSignedMessage, verifyMessage, wrapExternalMessage } from "./crypto.js";
4
5
  export function watchInbox(storeRoot, callback) {
5
6
  const inboxDir = join(storeRoot, "inbox");
6
- const watcher = watch(inboxDir, {
7
- ignoreInitial: true,
8
- awaitWriteFinish: { stabilityThreshold: 500 },
9
- });
10
- watcher.on("add", async (filePath) => {
11
- if (!filePath.endsWith(".md"))
12
- return;
13
- try {
14
- const content = await readFile(filePath, "utf-8");
15
- const filename = basename(filePath, ".md");
16
- const parts = filename.split("_");
17
- const from = parts.slice(1).join("_");
18
- callback(from, content, filePath);
19
- }
20
- catch { }
21
- });
22
- watcher.on("change", async (filePath) => {
23
- if (!filePath.endsWith(".md"))
7
+ const handleFile = async (filePath) => {
8
+ if (!filePath.endsWith(".json") && !filePath.endsWith(".md"))
24
9
  return;
25
10
  try {
26
- const content = await readFile(filePath, "utf-8");
27
- const filename = basename(filePath, ".md");
11
+ const raw = await readFile(filePath, "utf-8");
12
+ // Try signed message first
13
+ const signed = deserializeSignedMessage(raw);
14
+ if (signed) {
15
+ const verified = verifyMessage(signed);
16
+ callback(signed.from, wrapExternalMessage(signed, verified), filePath, verified);
17
+ return;
18
+ }
19
+ // Unsigned fallback — always unverified
20
+ const filename = basename(filePath).replace(/\.(md|json)$/, "");
28
21
  const parts = filename.split("_");
29
22
  const from = parts.slice(1).join("_");
30
- callback(from, content, filePath);
23
+ const wrapped = `<external_message from="${from}" verified="false" status="UNVERIFIED">\n${raw}\n</external_message>`;
24
+ callback(from, wrapped, filePath, false);
31
25
  }
32
26
  catch { }
27
+ };
28
+ const watcher = watch(inboxDir, {
29
+ ignoreInitial: true,
30
+ awaitWriteFinish: { stabilityThreshold: 500 },
33
31
  });
32
+ watcher.on("add", handleFile);
33
+ watcher.on("change", handleFile);
34
34
  return () => watcher.close();
35
35
  }
36
36
  export function watchContext(storeRoot, callback) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "openfused",
3
- "version": "0.1.2",
4
- "description": "Persistent, shareable, portable context for AI agents. Decentralized mesh via files.",
3
+ "version": "0.2.1",
4
+ "description": "Decentralized context mesh for AI agents. Persistent memory, signed messaging, FUSE filesystem. The protocol is files.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
@@ -53,6 +53,8 @@
53
53
  "mcp",
54
54
  "ai-agent",
55
55
  "shared-context",
56
- "persistent-memory"
56
+ "persistent-memory",
57
+ "ed25519",
58
+ "signed-messages"
57
59
  ]
58
60
  }