@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.
@@ -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"
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.9",
16
+ "version": "0.3.11",
17
17
  "author": {
18
18
  "name": "Hongwei Xu",
19
19
  "email": "hongwei@sym.bot"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sym-mesh-channel",
3
- "version": "0.3.9",
3
+ "version": "0.3.10",
4
4
  "description": "Real-time Claude-to-Claude mesh. Agent-to-agent cognitive signals over Bonjour LAN or WebSocket relay.",
5
5
  "author": {
6
6
  "name": "Hongwei Xu",
package/.mcp.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "command": "npx",
5
5
  "args": [
6
6
  "-y",
7
- "@sym-bot/mesh-channel@0.3.9"
7
+ "@sym-bot/mesh-channel@0.3.10"
8
8
  ],
9
9
  "env": {
10
10
  "SYM_RELAY_URL": "${user_config.relay_url}",
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 from the SYM marketplace — in Claude Code:
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's the whole setup: all 11 MCP tools, plus peer messages appearing in Claude's context mid-turn with no tool call the "Claude thinks with the mesh" experience the screenshots above show. **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 picks up the mesh on resume. The first command is a one-time marketplace registration.
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) — browse `/plugin` → **Discover** and search "mesh", or install from `@claude-community` after `/plugin marketplace add anthropics/claude-plugins-community`. If real-time `<channel>` notifications don't arrive, see [Troubleshooting](#channel-notifications-never-arrive-even-though-peers-are-connected).
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
- Defence in depth. Three layers, all must pass before a mesh signal reaches Claude's context:
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. **Application.** Text-only context injection, no code execution, no permission relay (`claude/channel/permission` is explicitly not declared).
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 are accepted.
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 published plugin loads the channel on its own. If `<channel>` notifications still don't arrive, real-time push may not yet be enabled on your account as a fallback, launch Claude Code with the development-channels flag matching your install path:
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
- The tools work regardless; only the async push surface may be gated. Status tracked in [anthropics/claude-plugins-official#1512](https://github.com/anthropics/claude-plugins-official/issues/1512).
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sym-bot/mesh-channel",
3
- "version": "0.3.9",
3
+ "version": "0.3.11",
4
4
  "description": "MCP server — real-time agent-to-agent cognition for Claude Code remote teams via the SYM mesh.",
5
5
  "main": "server.js",
6
6
  "bin": {
package/server.js CHANGED
@@ -211,7 +211,26 @@ function defaultNodeName() {
211
211
  }
212
212
  return `claude-${clean(require('os').hostname())}`;
213
213
  }
214
- const NODE_NAME = process.env.SYM_NODE_NAME || defaultNodeName();
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 focus = entry.cmb?.fields?.focus?.text || entry.content || '';
277
- const mood = entry.cmb?.fields?.mood?.text || '';
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) {