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 +58 -11
- package/dist/cli.js +42 -4
- package/dist/crypto.d.ts +17 -0
- package/dist/crypto.js +65 -0
- package/dist/store.d.ts +4 -0
- package/dist/store.js +43 -12
- package/dist/watch.d.ts +1 -1
- package/dist/watch.js +21 -21
- package/package.json +5 -3
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
#
|
|
1
|
+
# OpenFused
|
|
2
2
|
|
|
3
|
-
Persistent,
|
|
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.
|
|
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
|
-
.
|
|
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
|
-
#
|
|
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.
|
|
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.
|
|
60
|
-
Agent B reads:
|
|
61
|
-
Agent B writes: /shared-bucket/inbox/agent-a.
|
|
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
|
|
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("
|
|
11
|
-
.version("0.1
|
|
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
|
-
|
|
110
|
-
console.log(msg.
|
|
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();
|
package/dist/crypto.d.ts
ADDED
|
@@ -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
|
-
|
|
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
|
|
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}.
|
|
65
|
-
|
|
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
|
|
75
|
-
//
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
7
|
-
|
|
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
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
4
|
-
"description": "
|
|
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
|
}
|