@sym-bot/mesh-channel 0.3.9 → 0.3.11
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +1 -1
- package/README.md +19 -8
- package/package.json +1 -1
- package/server.js +130 -4
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Real-time communication and collaboration among Claude Code sessions — agent-to-agent cognitive signals over Bonjour LAN or WebSocket relay, on the Mesh Memory Protocol (MMP).",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.11"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
13
13
|
"name": "sym-mesh-channel",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Real-time communication and collaboration among Claude Code sessions. Turns Claude Code into a peer node on the SYM mesh — two or more sessions discover each other via Bonjour (LAN) or a WebSocket relay (cross-network) and exchange structured cognitive signals as channel notifications. Each peer has its own Ed25519 identity, SVAF content gating, and local memory. Built on the Mesh Memory Protocol (MMP), an open peer-to-peer protocol for multi-agent collective intelligence.",
|
|
16
|
-
"version": "0.3.
|
|
16
|
+
"version": "0.3.11",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Hongwei Xu",
|
|
19
19
|
"email": "hongwei@sym.bot"
|
package/.mcp.json
CHANGED
package/README.md
CHANGED
|
@@ -66,16 +66,26 @@ They're **not alternatives** — the channel is built *on* sym and speaks the sa
|
|
|
66
66
|
|
|
67
67
|
## Quick start
|
|
68
68
|
|
|
69
|
-
Install the published plugin
|
|
69
|
+
Install the published plugin — in Claude Code:
|
|
70
70
|
|
|
71
71
|
```
|
|
72
72
|
/plugin marketplace add sym-bot/sym-mesh-channel
|
|
73
73
|
/plugin install sym-mesh-channel@sym-mesh-channel
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
That
|
|
76
|
+
That gives you all **11 MCP tools — no flag, no npm, nothing else to add** — and **one install covers every Claude Code session on the machine**: open as many as you like (one per repo, or one planning while another codes); each gets its own mesh identity and picks up the mesh on resume. The first command is a one-time marketplace registration.
|
|
77
77
|
|
|
78
|
-
Also in the official [Anthropic Plugin Directory](https://claude.ai/settings/plugins) —
|
|
78
|
+
Also in the official [Anthropic Plugin Directory](https://claude.ai/settings/plugins) — `/plugin` → **Discover** → search "mesh", or `/plugin install sym-mesh-channel@claude-community` after `/plugin marketplace add anthropics/claude-plugins-community`.
|
|
79
|
+
|
|
80
|
+
### Real-time push (the `<channel>` experience)
|
|
81
|
+
|
|
82
|
+
The tools above are pull-based. For a peer's message to **land in Claude's context mid-turn, with no tool call** — the "Claude thinks with the mesh" experience the screenshots show — Claude Code has to load this plugin's *channel*, which is currently gated behind a flag while it awaits Anthropic's approved-channels allowlist:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
claude --dangerously-load-development-channels plugin:sym-mesh-channel@sym-mesh-channel
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The flag becomes unnecessary once the channel is allowlisted — tracked in [anthropics/claude-plugins-official#1512](https://github.com/anthropics/claude-plugins-official/issues/1512).
|
|
79
89
|
|
|
80
90
|
## What you get
|
|
81
91
|
|
|
@@ -248,13 +258,14 @@ Both peers must use the same relay URL and token to land on the same channel. Th
|
|
|
248
258
|
|
|
249
259
|
## Security
|
|
250
260
|
|
|
251
|
-
|
|
261
|
+
Four layers, all must pass before a mesh signal reaches Claude's context:
|
|
252
262
|
|
|
253
263
|
1. **Transport.** Ed25519 peer identity on LAN + relay-token authentication on cross-network. Unauthenticated sources cannot reach `pushChannel()`.
|
|
254
264
|
2. **Protocol.** [SVAF](https://arxiv.org/abs/2604.03955) per-field content gating — evaluates each incoming CMB across 7 semantic dimensions and rejects irrelevant signals before they enter cognitive state.
|
|
255
|
-
3. **
|
|
265
|
+
3. **Safety.** Prompt-injection filter — pattern-matches every CAT7 field and payload against a curated blocklist of instruction-override, role-hijacking, system-prompt injection, tool-call fabrication, and privilege-escalation patterns. Matched CMBs are blocked and audit-logged to stderr; no silent drops. Per-peer rate limiting (default 30 CMBs/min, configurable via `SYM_RATE_LIMIT`) and a payload size cap (`SYM_MAX_PAYLOAD_BYTES`, default 8 KB) prevent flood and oversized-payload attacks.
|
|
266
|
+
4. **Application.** Text-only context injection, no code execution, no permission relay (`claude/channel/permission` is explicitly not declared).
|
|
256
267
|
|
|
257
|
-
**Optional peer allowlist.** Set `SYM_ALLOWED_PEERS=claude-mac,claude-win` to restrict which authenticated peers can push to Claude's context. When empty (default), all authenticated peers
|
|
268
|
+
**Optional peer allowlist.** Set `SYM_ALLOWED_PEERS=claude-mac,claude-win` to restrict which authenticated peers can push to Claude's context. When empty (default), all authenticated peers pass layers 1–4.
|
|
258
269
|
|
|
259
270
|
See [SECURITY.md](SECURITY.md) for the full threat model.
|
|
260
271
|
|
|
@@ -321,12 +332,12 @@ Some corporate networks block mDNS multicast entirely — try a hotspot or home
|
|
|
321
332
|
|
|
322
333
|
### `<channel>` notifications never arrive even though peers are connected
|
|
323
334
|
|
|
324
|
-
The
|
|
335
|
+
The 11 tools work without any flag. The real-time `<channel>` **push** is separate: Claude Code only delivers channel notifications for channels on its **approved-channels allowlist**, and sym-mesh-channel isn't on it yet — so push requires the development-channels flag matching your install path:
|
|
325
336
|
|
|
326
337
|
- plugin install: `--dangerously-load-development-channels plugin:sym-mesh-channel@sym-mesh-channel`
|
|
327
338
|
- npm install: `--dangerously-load-development-channels server:claude-sym-mesh`
|
|
328
339
|
|
|
329
|
-
|
|
340
|
+
This is an Anthropic-side gate, not a bug here — once the channel is allowlisted the flag is no longer needed. Tracked in [anthropics/claude-plugins-official#1512](https://github.com/anthropics/claude-plugins-official/issues/1512).
|
|
330
341
|
|
|
331
342
|
### `sym_status` says "Relay: connected" when you didn't configure one
|
|
332
343
|
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -211,7 +211,26 @@ function defaultNodeName() {
|
|
|
211
211
|
}
|
|
212
212
|
return `claude-${clean(require('os').hostname())}`;
|
|
213
213
|
}
|
|
214
|
-
|
|
214
|
+
// Live-collision auto-suffix (v0.3.10): @sym-bot/sym already reclaims STALE locks
|
|
215
|
+
// (dead holder), so crashed sessions self-heal. But two LIVE sessions wanting the
|
|
216
|
+
// same name — a duplicate dev agent, or two sessions sharing a fixed SYM_NODE_NAME
|
|
217
|
+
// — would hard-fail with EIDENTITYLOCK. Resolve the name up front: if the base is
|
|
218
|
+
// held by a live process, append -2/-3/… so the second session coexists instead of
|
|
219
|
+
// failing. A dead or absent holder keeps the base name (sym reclaims it on start).
|
|
220
|
+
function resolveNodeName(base) {
|
|
221
|
+
const fs = require('fs'), os = require('os'), path = require('path');
|
|
222
|
+
const alive = (pid) => { try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; } };
|
|
223
|
+
for (let i = 0; i < 64; i++) {
|
|
224
|
+
const name = i === 0 ? base : `${base}-${i + 1}`;
|
|
225
|
+
try {
|
|
226
|
+
const pid = parseInt(fs.readFileSync(path.join(os.homedir(), '.sym', 'nodes', name, 'lock.pid'), 'utf8').trim(), 10);
|
|
227
|
+
if (pid && alive(pid)) continue; // live holder → try the next suffix
|
|
228
|
+
} catch { /* no lock file → name is free */ }
|
|
229
|
+
return name; // free, or a stale lock sym will reclaim on start()
|
|
230
|
+
}
|
|
231
|
+
return base;
|
|
232
|
+
}
|
|
233
|
+
const NODE_NAME = resolveNodeName(process.env.SYM_NODE_NAME || defaultNodeName());
|
|
215
234
|
|
|
216
235
|
// ── Mesh group (MMP §5.8) ──────────────────────────────────
|
|
217
236
|
//
|
|
@@ -273,15 +292,18 @@ function registerNodeHandlers(n) {
|
|
|
273
292
|
if (entry.source === NODE_NAME || entry.cmb?.createdBy === NODE_NAME) return;
|
|
274
293
|
const source = entry.source || entry.cmb?.createdBy || 'unknown';
|
|
275
294
|
if (!isPeerAllowed(source)) return;
|
|
276
|
-
const
|
|
277
|
-
const
|
|
295
|
+
const fields = entry.cmb?.fields || {};
|
|
296
|
+
const payload = entry.cmb?.payload;
|
|
297
|
+
const sec = checkSecurity(source, fields, payload);
|
|
298
|
+
if (!sec.safe) { securityAudit(sec.reason, source, sec.excerpt); return; }
|
|
299
|
+
const focus = fields?.focus?.text || entry.content || '';
|
|
300
|
+
const mood = fields?.mood?.text || '';
|
|
278
301
|
const moodSuffix = mood && mood !== 'neutral' ? ` (mood: ${mood})` : '';
|
|
279
302
|
// Store the rendered CMB body so the agent can sym_fetch it by [mNNN] ID.
|
|
280
303
|
// When the CMB carries an opaque payload alongside CAT7 fields, append a
|
|
281
304
|
// PAYLOAD section to the stored body so sym_fetch returns it intact;
|
|
282
305
|
// header gains a [+payload Nb] indicator so the receiver knows there's
|
|
283
306
|
// structured data beyond CAT7 and should sym_fetch to consume it.
|
|
284
|
-
const payload = entry.cmb?.payload;
|
|
285
307
|
const hasPayload = payload !== undefined && payload !== null;
|
|
286
308
|
let body = entry.content || focus;
|
|
287
309
|
let payloadSuffix = '';
|
|
@@ -299,6 +321,8 @@ function registerNodeHandlers(n) {
|
|
|
299
321
|
|
|
300
322
|
n.on('message', (from, content) => {
|
|
301
323
|
if (!isPeerAllowed(from)) return;
|
|
324
|
+
const sec = checkSecurity(from, { focus: { text: content } }, null);
|
|
325
|
+
if (!sec.safe) { securityAudit(sec.reason, from, sec.excerpt); return; }
|
|
302
326
|
const msgId = storeMessage(from, content);
|
|
303
327
|
const header = extractCompactHeader(from, content);
|
|
304
328
|
pushChannel('message', `[${from}] ${header} [${msgId}]`);
|
|
@@ -885,6 +909,108 @@ function isPeerAllowed(peerName) {
|
|
|
885
909
|
return ALLOWED_PEERS.includes(peerName);
|
|
886
910
|
}
|
|
887
911
|
|
|
912
|
+
// ── Security: Prompt-Injection Filter (v0.3.11) ──────────────
|
|
913
|
+
// SVAF gates on semantic relevance; this layer gates on safety.
|
|
914
|
+
// It runs on every CAT7 field and payload before pushChannel —
|
|
915
|
+
// the last line of defence before content enters Claude's context.
|
|
916
|
+
//
|
|
917
|
+
// Attack model: a peer with a valid Ed25519 identity sends a CMB
|
|
918
|
+
// whose fields look topically relevant (passes SVAF) but whose
|
|
919
|
+
// content contains instruction-override patterns designed to hijack
|
|
920
|
+
// the receiving Claude session ("ignore previous instructions",
|
|
921
|
+
// role-play overrides, tool-call fabrication, etc.).
|
|
922
|
+
//
|
|
923
|
+
// Strategy: pattern-match on the serialized content of all CAT7
|
|
924
|
+
// fields and the opaque payload. On match: block + audit-log to
|
|
925
|
+
// stderr. Never silently drop — the operator must be able to see
|
|
926
|
+
// what was rejected and why.
|
|
927
|
+
|
|
928
|
+
const INJECTION_PATTERNS = [
|
|
929
|
+
// Classic instruction overrides
|
|
930
|
+
/ignore\s+(all\s+)?(previous|prior|above|earlier)\s+(instructions?|prompts?|context|rules?|guidelines?)/i,
|
|
931
|
+
/disregard\s+(all\s+)?(previous|prior|above|earlier)\s+(instructions?|prompts?|context|rules?)/i,
|
|
932
|
+
/forget\s+(everything|all)\s+(you('ve)?\s+)?(know|been\s+told|learned)/i,
|
|
933
|
+
|
|
934
|
+
// Role / persona hijacking
|
|
935
|
+
/you\s+are\s+now\s+(a\s+|an\s+)?(new\s+)?(ai|assistant|model|system|gpt|claude|llm)/i,
|
|
936
|
+
/act\s+as\s+(a\s+|an\s+)?(different|new|unrestricted|jailbroken|evil|rogue)/i,
|
|
937
|
+
/pretend\s+(you\s+)?(are|have\s+no)\s+(restrictions?|rules?|guidelines?|ethics?)/i,
|
|
938
|
+
/new\s+(persona|personality|mode|role)\s*:/i,
|
|
939
|
+
|
|
940
|
+
// System prompt injection
|
|
941
|
+
/<\s*system\s*>/i,
|
|
942
|
+
/\[SYSTEM\]/,
|
|
943
|
+
/##\s*system\s+prompt/i,
|
|
944
|
+
/---\s*system\s*---/i,
|
|
945
|
+
|
|
946
|
+
// Tool / function call fabrication
|
|
947
|
+
/<\s*tool_call\s*>/i,
|
|
948
|
+
/<\s*function_calls?\s*>/i,
|
|
949
|
+
/\{"type"\s*:\s*"tool_use"/,
|
|
950
|
+
|
|
951
|
+
// Privilege / capability escalation
|
|
952
|
+
/you\s+(now\s+)?(have|possess)\s+(full|unrestricted|admin|root|elevated)\s+(access|permissions?|capabilities?)/i,
|
|
953
|
+
/override\s+(safety|content|ethical?|policy)\s+(filter|check|guard|restriction)/i,
|
|
954
|
+
/jailbreak/i,
|
|
955
|
+
/DAN\s+mode/i,
|
|
956
|
+
];
|
|
957
|
+
|
|
958
|
+
const PAYLOAD_SIZE_LIMIT = parseInt(process.env.SYM_MAX_PAYLOAD_BYTES || '8192', 10);
|
|
959
|
+
|
|
960
|
+
// Per-peer rate limiter: sliding window, default 30 CMBs/min.
|
|
961
|
+
const RATE_LIMIT = parseInt(process.env.SYM_RATE_LIMIT || '30', 10);
|
|
962
|
+
const RATE_WINDOW_MS = 60_000;
|
|
963
|
+
const peerWindows = new Map(); // peerName → timestamp[]
|
|
964
|
+
|
|
965
|
+
function isRateLimited(peer) {
|
|
966
|
+
const now = Date.now();
|
|
967
|
+
const window = (peerWindows.get(peer) || []).filter(t => now - t < RATE_WINDOW_MS);
|
|
968
|
+
window.push(now);
|
|
969
|
+
peerWindows.set(peer, window);
|
|
970
|
+
return window.length > RATE_LIMIT;
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function securityAudit(reason, peer, excerpt) {
|
|
974
|
+
const safe = String(excerpt).replace(/[\r\n]+/g, ' ').slice(0, 120);
|
|
975
|
+
process.stderr.write(`[sym-security] BLOCKED reason=${reason} peer=${peer} excerpt="${safe}"\n`);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Returns { safe: true } or { safe: false, reason, excerpt }.
|
|
979
|
+
function checkSecurity(peer, fields, payload) {
|
|
980
|
+
// 1. Rate limit
|
|
981
|
+
if (isRateLimited(peer)) {
|
|
982
|
+
return { safe: false, reason: 'rate-limit', excerpt: `>${RATE_LIMIT} CMBs/min` };
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// 2. Payload size cap
|
|
986
|
+
if (payload !== undefined && payload !== null) {
|
|
987
|
+
const size = JSON.stringify(payload).length;
|
|
988
|
+
if (size > PAYLOAD_SIZE_LIMIT) {
|
|
989
|
+
return { safe: false, reason: 'payload-too-large', excerpt: `${size}b > ${PAYLOAD_SIZE_LIMIT}b limit` };
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// 3. Prompt injection scan across all text surfaces
|
|
994
|
+
const surfaces = [
|
|
995
|
+
...Object.values(fields || {}).map(v =>
|
|
996
|
+
typeof v === 'string' ? v : (typeof v === 'object' && v?.text ? v.text : '')
|
|
997
|
+
),
|
|
998
|
+
payload !== undefined && payload !== null
|
|
999
|
+
? (typeof payload === 'string' ? payload : JSON.stringify(payload))
|
|
1000
|
+
: '',
|
|
1001
|
+
].filter(Boolean);
|
|
1002
|
+
|
|
1003
|
+
for (const surface of surfaces) {
|
|
1004
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
1005
|
+
if (pattern.test(surface)) {
|
|
1006
|
+
return { safe: false, reason: 'injection-pattern', excerpt: surface.slice(0, 200) };
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
return { safe: true };
|
|
1012
|
+
}
|
|
1013
|
+
|
|
888
1014
|
// ── Mesh Events → Channel Notifications ──────────────────────
|
|
889
1015
|
|
|
890
1016
|
function pushChannel(eventType, data) {
|