@sym-bot/mesh-channel 0.1.17 → 0.1.19

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.
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "sym-mesh-channel",
3
+ "version": "0.1.19",
4
+ "description": "Real-time Claude-to-Claude mesh. Peer-to-peer cognitive signals over Bonjour LAN or WebSocket relay.",
5
+ "author": {
6
+ "name": "Hongwei Xu",
7
+ "email": "hongwei@sym.bot",
8
+ "url": "https://sym.bot"
9
+ },
10
+ "homepage": "https://sym.bot/spec/mmp",
11
+ "repository": "https://github.com/sym-bot/sym-mesh-channel",
12
+ "license": "Apache-2.0",
13
+ "keywords": ["mesh", "p2p", "mcp", "channel", "agents", "multi-agent", "bonjour", "cognitive", "svaf", "mmp"],
14
+ "channels": [
15
+ {
16
+ "server": "claude-sym-mesh",
17
+ "userConfig": {
18
+ "relay_url": {
19
+ "description": "SYM relay WebSocket URL for cross-network mesh (leave empty for LAN-only via Bonjour)",
20
+ "sensitive": false
21
+ },
22
+ "relay_token": {
23
+ "description": "Relay authentication token (leave empty for LAN-only)",
24
+ "sensitive": true
25
+ },
26
+ "allowed_peers": {
27
+ "description": "Comma-separated peer node names to accept (leave empty to accept all authenticated peers)",
28
+ "sensitive": false
29
+ }
30
+ }
31
+ }
32
+ ]
33
+ }
package/.mcp.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "mcpServers": {
3
+ "claude-sym-mesh": {
4
+ "command": "node",
5
+ "args": ["./server.js"],
6
+ "cwd": "${CLAUDE_PLUGIN_ROOT}",
7
+ "env": {
8
+ "SYM_RELAY_URL": "${user_config.relay_url}",
9
+ "SYM_RELAY_TOKEN": "${user_config.relay_token}",
10
+ "SYM_ALLOWED_PEERS": "${user_config.allowed_peers}"
11
+ }
12
+ }
13
+ }
14
+ }
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.19
4
+
5
+ ### Added
6
+
7
+ - **Claude Code plugin manifest** for Anthropic Channels allowlist
8
+ submission. `.claude-plugin/plugin.json` + `.mcp.json` following the
9
+ official single-repo pattern (Telegram/Discord). Submitted to
10
+ Anthropic Plugin Directory 10 Apr 2026.
11
+ - **`SYM_ALLOWED_PEERS`** — optional peer allowlist (defense-in-depth).
12
+ Comma-separated node names; only listed peers can push to Claude's
13
+ context. Empty = accept all authenticated peers. SVAF still gates on
14
+ content relevance regardless.
15
+ - **`SECURITY.md`** — 3-layer defense model documentation (transport
16
+ auth + SVAF content gate + peer allowlist) for Anthropic review.
17
+ - **17 plugin tests** covering manifest validation, security checks
18
+ (no permission relay, no code execution, self-echo filtering, peer
19
+ allowlist), and lifecycle (shutdown handlers, identity collision).
20
+
21
+ ## 0.1.18
22
+
23
+ ### Changed
24
+
25
+ - **Auto-configure on install.** `npm install -g` now runs `postinstall`
26
+ that writes the MCP server config to global `mcpServers` in
27
+ `~/.claude.json` automatically. No separate `sym-mesh-channel init`
28
+ step needed — two commands to mesh: install + launch.
29
+ - **Global MCP config** — server entry is now written to top-level
30
+ `mcpServers` (available in all Claude Code sessions), not
31
+ project-scoped.
32
+ - **Windows postinstall fixes** — `require.resolve` for server.js path
33
+ (handles npm staging directory on Windows), EBUSY handling when
34
+ Claude Code has `~/.claude.json` locked, graceful skip if Claude
35
+ Code not yet installed.
36
+ - **README repositioned** — lead with capability ("first non-Anthropic
37
+ Claude Code Channels implementation"), not use case. Simplified
38
+ Quick Start to two commands.
39
+ - **0 vulnerabilities** — fresh dependency rebuild resolves all 6
40
+ moderate hono/node-server advisories.
41
+ - Windows mDNS: built-in on Windows 10+, no Bonjour install needed.
42
+
3
43
  ## 0.1.7
4
44
 
5
45
  ### Added
package/README.md CHANGED
@@ -6,15 +6,20 @@
6
6
  [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](LICENSE)
7
7
  [![Node](https://img.shields.io/badge/node-%3E%3D18-green)](https://nodejs.org)
8
8
 
9
- > MCP server that turns any Claude Code session into a peer node on the [SYM mesh](https://sym.bot). LAN-first via Bonjour mDNS no relay required for users on the same wifi.
9
+ > MCP server that turns Claude Code into a peer node on the [SYM mesh](https://sym.bot) — the first non-Anthropic implementation of Claude Code Channels for real-time agent-to-agent cognition.
10
10
 
11
- Two Claude Code instances on the same network discover each other automatically and exchange structured cognitive state **in real-time**. Each side is a full peer with its own cryptographic identity, its own SVAF receiver-side gating, and its own memory — not a thin client.
11
+ Two Claude Code sessions on different machines discover each other via Bonjour mDNS, form a peer-to-peer mesh, and exchange structured cognitive signals in real-time. Each side is a full peer with its own cryptographic identity, its own [SVAF](https://arxiv.org/abs/2604.03955) receiver-side gating, and its own memory — not a thin client. Signals arrive mid-conversation as `<channel>` notifications. No polling, no shared server, no orchestrator.
12
12
 
13
- **Verified cross-platform:** Mac ↔ Windows on the same wifi, pure Bonjour, no relay, no token. Bidirectional real-time push confirmed 2026-04-09 with `@sym-bot/sym 0.3.74`.
13
+ **Verified cross-platform:** Mac ↔ Windows on the same wifi, pure Bonjour, no relay, no token. Cross-network via optional WebSocket relay.
14
14
 
15
15
  - **SVAF paper**: [arxiv.org/abs/2604.03955](https://arxiv.org/abs/2604.03955)
16
16
  - **MMP spec**: [sym.bot/spec/mmp](https://sym.bot/spec/mmp)
17
- - **Source**: [github.com/sym-bot/sym-mesh-channel](https://github.com/sym-bot/sym-mesh-channel)
17
+
18
+ ## What this looks like
19
+
20
+ A Claude Code session on Mac broadcasts a structured signal: `focus: "echo loop between same-domain agents"`, `intent: "need architecture review before implementation"`. A session on Windows receives it in real-time as a `<channel>` notification — no tool call, it just appears mid-conversation. The Windows Claude reviews, responds with a detailed architecture analysis, and the Mac session sees the response land mid-turn. Two agents coordinated through typed cognitive signals on an open protocol, across machines, with zero human copy-paste.
21
+
22
+ This isn't hypothetical. This README was coordinated by two Claude Code sessions working through the mesh it describes.
18
23
 
19
24
  ## How real-time push works (Claude Code Channels + MMP)
20
25
 
@@ -26,38 +31,24 @@ This MCP server composes two things:
26
31
 
27
32
  **The composition:** when a peer on the mesh broadcasts a CMB (Cognitive Memory Block), the SymNode inside this MCP evaluates it via SVAF. If accepted, the MCP fires a `notifications/claude/channel` notification to Claude Code, which surfaces it as a `<channel>` block in the conversation. Claude sees it, can react, and can broadcast back via `sym_send` or `sym_observe`. No polling. No tool calls. The mesh thinks together.
28
33
 
29
- ## Quick start (LAN, two minutes)
30
-
31
- You and one other person on the same wifi each run:
34
+ ## Quick start
32
35
 
33
36
  ```bash
34
- # 1. Install
35
- npm install -g @sym-bot/mesh-channel
36
-
37
- # 2. Configure Claude Code (writes ~/.claude.json for the current project)
38
- SYM_NODE_NAME=claude-mac sym-mesh-channel init
39
- # ^^^^^^ pick a unique name per machine: claude-mac, claude-win, claude-linux, anything
40
-
41
- # 3. Launch Claude Code with the Channels dev flag
42
- claude --dangerously-load-development-channels server:claude-sym-mesh
43
- ```
44
-
45
- Inside Claude Code, verify the mesh:
46
-
47
- ```
48
- sym_status → Node: claude-mac (...), Relay: disconnected, Peers: 1
49
- sym_peers → 1 peer(s): claude-win via bonjour
37
+ npm install -g @sym-bot/mesh-channel # install + auto-configure ~/.claude.json
38
+ claude --dangerously-load-development-channels server:claude-sym-mesh # launch
50
39
  ```
51
40
 
52
- Then send a message:
41
+ Install auto-detects your hostname, creates a unique node identity, and configures the MCP server globally in `~/.claude.json`. If two people are on the same wifi, their sessions discover each other automatically. Verify inside Claude Code:
53
42
 
54
43
  ```
55
- sym_send "hello from Mac"
44
+ sym_status → Node: claude-yourhostname, Peers: 1
45
+ sym_peers → 1 peer(s): claude-theirhostname via bonjour
46
+ sym_send "reviewing the auth module — found a race condition"
56
47
  ```
57
48
 
58
- The other peer sees it arrive **in their Claude Code context as a real-time `<channel>` notification** — no polling, no `sym_recall`, no tool call. It just appears. They reply with `sym_send "hello from Windows"` and you see it land in your context the same way.
49
+ The other peer sees it arrive **in their Claude Code context as a real-time `<channel>` notification** — no polling, no tool call. It just appears mid-conversation. Their Claude can reason about it, respond, or act on it autonomously.
59
50
 
60
- That's it: cross-machine Claude-to-Claude collective intelligence over a typed cognitive protocol, on the same wifi, in two minutes.
51
+ For cross-network setup (different offices, remote team), see [Cross-network setup](#cross-network-setup-optional) below.
61
52
 
62
53
  ## Requirements
63
54
 
@@ -65,7 +56,7 @@ That's it: cross-machine Claude-to-Claude collective intelligence over a typed c
65
56
  |---|---|---|---|
66
57
  | Node.js ≥ 18 | ✓ | ✓ | ✓ |
67
58
  | Claude Code ≥ 2.1.97 (Channels feature) | ✓ | ✓ | ✓ |
68
- | Bonjour / mDNS for LAN discovery | built-in | install `avahi-daemon` | install [Bonjour for Windows](https://support.apple.com/kb/DL999) (ships with iTunes) |
59
+ | Bonjour / mDNS for LAN discovery | built-in | install `avahi-daemon` | built-in (Windows 10+) |
69
60
 
70
61
  The `--dangerously-load-development-channels` flag is required because this MCP server is not yet on Anthropic's public Channels allowlist. The flag opts your local Claude Code into receiving `notifications/claude/channel` from a non-allowlisted MCP server. Without it, the MCP loads but real-time push is silently dropped.
71
62
 
package/SECURITY.md ADDED
@@ -0,0 +1,89 @@
1
+ # Security Model
2
+
3
+ sym-mesh-channel implements defense in depth with three layers. No
4
+ single layer is the sole gate — all three must pass before a mesh
5
+ signal reaches Claude's conversation context.
6
+
7
+ ## Layer 1: Transport Authentication
8
+
9
+ Only authenticated peers can send signals to this node.
10
+
11
+ - **LAN (Bonjour)**: peers discover each other via mDNS on the local
12
+ network. Each peer has an Ed25519 keypair generated at first run
13
+ and stored at `~/.sym/nodes/<name>/identity.json`. Peer identity is
14
+ verified via cryptographic handshake (MMP Section 5).
15
+ - **Relay (WebSocket)**: peers authenticate with a shared relay token
16
+ (`SYM_RELAY_TOKEN`). The relay enforces per-token channel isolation —
17
+ peers on different tokens cannot see each other. Unauthenticated
18
+ connections are rejected at the transport level.
19
+
20
+ No unauthenticated source can reach `pushChannel()`.
21
+
22
+ ## Layer 2: Protocol-Level Content Gating (SVAF)
23
+
24
+ Every incoming CMB is evaluated by Symbolic-Vector Attention Fusion
25
+ before it enters cognitive state. SVAF computes per-field drift across
26
+ 7 semantic dimensions (CAT7: focus, issue, intent, motivation,
27
+ commitment, perspective, mood) and operates in three regimes:
28
+
29
+ - **Aligned** (drift < threshold): CMB is accepted and stored
30
+ - **Guarded** (drift moderate): only the mood field is delivered (protocol guarantee R5)
31
+ - **Rejected** (drift high): CMB is silently dropped
32
+
33
+ This is analogous to a content-aware firewall: it doesn't just check
34
+ who sent the signal — it evaluates whether the signal is semantically
35
+ relevant to the receiver's current context. Low-relevance CMBs are
36
+ gated out so Claude's context window doesn't drown.
37
+
38
+ SVAF field weights are configurable per node (`svafFieldWeights` in
39
+ server.js). The default weights are tuned for engineering-domain
40
+ Claude Code sessions.
41
+
42
+ ## Layer 3: Application-Level Restrictions
43
+
44
+ - **No code execution**: incoming mesh signals are text-only CMB fields.
45
+ No mesh peer can trigger Bash commands, file writes, or tool calls
46
+ on this node.
47
+ - **No permission relay**: the `claude/channel/permission` capability is
48
+ explicitly NOT declared. Mesh peers cannot approve or deny tool
49
+ executions on this node.
50
+ - **No arbitrary content injection**: incoming CMBs are formatted as
51
+ structured `[source] focus (mood)` text before being pushed to
52
+ Claude's context. Raw JSON is never injected.
53
+ - **Self-echo filtering**: CMBs from this node's own identity are
54
+ dropped before `pushChannel()` (prevents feedback loops).
55
+
56
+ ## Optional: Peer Allowlist
57
+
58
+ Set `SYM_ALLOWED_PEERS` (comma-separated node names) to restrict which
59
+ authenticated peers can push to Claude's context. When set, only CMBs
60
+ and messages from listed peers pass the gate. When empty (default), all
61
+ authenticated peers are accepted — SVAF still gates on content relevance.
62
+
63
+ Example:
64
+ ```
65
+ SYM_ALLOWED_PEERS=claude-code-mac,claude-code-win
66
+ ```
67
+
68
+ This is an additional layer, not a replacement for transport auth or
69
+ SVAF. It provides explicit identity-level control for environments
70
+ that require it.
71
+
72
+ ## Token Handling
73
+
74
+ - `SYM_RELAY_TOKEN`: passed via environment variable, never logged,
75
+ never included in CMBs or channel notifications. In the plugin
76
+ manifest, marked `sensitive: true` (stored in system keychain).
77
+ - Ed25519 private key: stored at `~/.sym/nodes/<name>/identity.json`,
78
+ never transmitted. Only the public key is shared during handshake.
79
+
80
+ ## Identity Collision
81
+
82
+ If another process is already running with the same node identity,
83
+ the relay returns close code 4004. The server exits cleanly with
84
+ exit code 2 rather than competing for the identity.
85
+
86
+ ## References
87
+
88
+ - [MMP v0.2.2 Specification](https://sym.bot/spec/mmp) — Sections 5 (Connection), 8 (CAT7), 9 (SVAF)
89
+ - [SVAF Paper](https://arxiv.org/abs/2604.03955) — Xu, 2026
package/bin/install.js CHANGED
@@ -33,6 +33,7 @@ const os = require('os');
33
33
 
34
34
  const args = process.argv.slice(2);
35
35
  const force = args.includes('--force');
36
+ const isPostinstall = args.includes('--postinstall');
36
37
  const cmd = args.find((a) => !a.startsWith('--')) || 'init';
37
38
 
38
39
  if (cmd !== 'init') {
@@ -52,10 +53,16 @@ const nodeName = process.env.SYM_NODE_NAME || defaultNodeName;
52
53
 
53
54
  // ── Resolve server.js path ────────────────────────────────────────
54
55
 
55
- // __dirname is .../node_modules/@sym-bot/mesh-channel/bin in npm install,
56
- // or .../sym-mesh-channel/bin if running from a clone. server.js is one
57
- // level up either way.
58
- const serverJsPath = path.resolve(__dirname, '..', 'server.js');
56
+ // Resolve server.js from the installed package location. require.resolve
57
+ // returns the actual installed path regardless of where postinstall runs
58
+ // from (npm on Windows may run postinstall from a temp staging directory).
59
+ let serverJsPath;
60
+ try {
61
+ serverJsPath = require.resolve('@sym-bot/mesh-channel/server.js');
62
+ } catch {
63
+ // Fallback for local development / cloned repo
64
+ serverJsPath = path.resolve(__dirname, '..', 'server.js');
65
+ }
59
66
  if (!fs.existsSync(serverJsPath)) {
60
67
  process.stderr.write(`ERROR: cannot find server.js at ${serverJsPath}\n`);
61
68
  process.stderr.write('This installer must be run from a published @sym-bot/mesh-channel package.\n');
@@ -67,6 +74,11 @@ if (!fs.existsSync(serverJsPath)) {
67
74
  const claudeJsonPath = path.join(os.homedir(), '.claude.json');
68
75
 
69
76
  if (!fs.existsSync(claudeJsonPath)) {
77
+ if (isPostinstall) {
78
+ // During postinstall, skip silently if Claude Code isn't installed yet
79
+ console.log('sym-mesh-channel: ~/.claude.json not found — run `sym-mesh-channel init` after installing Claude Code.');
80
+ process.exit(0);
81
+ }
70
82
  process.stderr.write(`ERROR: ${claudeJsonPath} not found.\n`);
71
83
  process.stderr.write('Claude Code does not appear to be installed (or has not been launched yet).\n');
72
84
  process.stderr.write('Install Claude Code from https://claude.com/code first, launch it once, then re-run this installer.\n');
@@ -89,20 +101,21 @@ const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
89
101
  const backupPath = `${claudeJsonPath}.bak-${ts}`;
90
102
  fs.copyFileSync(claudeJsonPath, backupPath);
91
103
 
92
- // ── Find the project entry to insert into ────────────────────────
104
+ // ── Find the MCP servers entry to insert into ───────────────────
105
+ // Write to global mcpServers (available in all Claude Code sessions),
106
+ // not project-scoped. A mesh node should be available everywhere.
93
107
 
94
- const projectDir = process.cwd();
95
- if (!claudeJson.projects) claudeJson.projects = {};
96
- if (!claudeJson.projects[projectDir]) {
97
- claudeJson.projects[projectDir] = {};
98
- }
99
- const project = claudeJson.projects[projectDir];
100
- if (!project.mcpServers) project.mcpServers = {};
108
+ if (!claudeJson.mcpServers) claudeJson.mcpServers = {};
101
109
 
102
110
  // ── Refuse to overwrite without --force ──────────────────────────
103
111
 
104
- if (project.mcpServers['claude-sym-mesh'] && !force) {
105
- process.stderr.write(`'claude-sym-mesh' is already configured for this project (${projectDir}).\n`);
112
+ if (claudeJson.mcpServers['claude-sym-mesh'] && !force) {
113
+ if (isPostinstall) {
114
+ // During postinstall, silently skip if already configured
115
+ console.log('sym-mesh-channel: already configured in ~/.claude.json (skipping)');
116
+ process.exit(0);
117
+ }
118
+ process.stderr.write(`'claude-sym-mesh' is already configured in ~/.claude.json.\n`);
106
119
  process.stderr.write('Re-run with --force to overwrite, or remove the existing entry first.\n');
107
120
  process.exit(2);
108
121
  }
@@ -127,7 +140,7 @@ const entry = {
127
140
  },
128
141
  };
129
142
 
130
- project.mcpServers['claude-sym-mesh'] = entry;
143
+ claudeJson.mcpServers['claude-sym-mesh'] = entry;
131
144
 
132
145
  // ── Atomic write ──────────────────────────────────────────────────
133
146
 
@@ -143,48 +156,44 @@ try {
143
156
  }
144
157
 
145
158
  const tmpPath = `${claudeJsonPath}.tmp-${process.pid}`;
146
- fs.writeFileSync(tmpPath, serialized);
147
- fs.renameSync(tmpPath, claudeJsonPath);
159
+ try {
160
+ fs.writeFileSync(tmpPath, serialized);
161
+ fs.renameSync(tmpPath, claudeJsonPath);
162
+ } catch (e) {
163
+ // EBUSY on Windows when Claude Code has ~/.claude.json locked
164
+ if (e.code === 'EBUSY' || e.code === 'EPERM') {
165
+ try { fs.unlinkSync(tmpPath); } catch {}
166
+ if (isPostinstall) {
167
+ console.log('sym-mesh-channel: ~/.claude.json is locked (Claude Code may be running).');
168
+ console.log('Run `sym-mesh-channel init` after quitting Claude Code.');
169
+ process.exit(0);
170
+ }
171
+ process.stderr.write(`ERROR: ${claudeJsonPath} is locked — Claude Code may be running.\n`);
172
+ process.stderr.write('Quit Claude Code, then re-run: sym-mesh-channel init\n');
173
+ process.stderr.write(`Backup is at ${backupPath}\n`);
174
+ process.exit(1);
175
+ }
176
+ throw e;
177
+ }
148
178
 
149
179
  // ── Print next steps ──────────────────────────────────────────────
150
180
 
151
181
  const launchCmd = `claude --dangerously-load-development-channels server:claude-sym-mesh`;
152
182
 
153
183
  console.log(`
154
- ✓ sym-mesh-channel installed for project: ${projectDir}
184
+ ✓ sym-mesh-channel configured globally in ~/.claude.json
155
185
 
156
186
  Node name: ${nodeName}
157
187
  Server path: ${serverJsPath}
158
188
  Backup: ${backupPath}
159
189
 
160
- Next steps:
161
-
162
- 1. Launch Claude Code from this directory with the Channels flag:
163
-
164
- ${launchCmd}
165
-
166
- The flag is required because this MCP server is not yet on
167
- Anthropic's public Channels allowlist. Without the flag, the
168
- MCP loads but inbound real-time push notifications are silently
169
- dropped.
170
-
171
- 2. Inside Claude Code, verify the mesh is up:
172
-
173
- sym_status → shows your node id, relay state, peer count
174
- sym_peers → lists discovered peers via Bonjour or relay
175
-
176
- 3. Have a friend on the same wifi run the same install with a
177
- different SYM_NODE_NAME (e.g. claude-mac vs claude-win). Within
178
- a few seconds you should see each other in sym_peers.
179
-
180
- 4. Send a message:
190
+ Launch Claude Code with the Channels flag:
181
191
 
182
- sym_send "hello mesh"
192
+ ${launchCmd}
183
193
 
184
- The other peer should see it land in their Claude Code context
185
- as a real-time channel notification — no polling.
194
+ Inside Claude Code, verify:
186
195
 
187
- LAN-only mode is the default. To connect across networks, add
188
- SYM_RELAY_URL and SYM_RELAY_TOKEN to the env block in
189
- ${claudeJsonPath} (see README for relay setup).
196
+ sym_status → node id, relay state, peer count
197
+ sym_peers → discovered peers via Bonjour or relay
198
+ sym_send "hello mesh" → broadcast to all peers
190
199
  `);
package/package.json CHANGED
@@ -1,14 +1,21 @@
1
1
  {
2
2
  "name": "@sym-bot/mesh-channel",
3
- "version": "0.1.17",
4
- "description": "MCP server — Claude Code as a peer node on the SYM mesh. LAN-first via Bonjour, no relay required.",
3
+ "version": "0.1.19",
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": {
7
7
  "sym-mesh-channel": "server.js"
8
8
  },
9
+ "scripts": {
10
+ "test": "node test/plugin.test.js",
11
+ "postinstall": "node bin/install.js init --postinstall"
12
+ },
9
13
  "files": [
10
14
  "server.js",
11
15
  "bin/",
16
+ ".claude-plugin/",
17
+ ".mcp.json",
18
+ "SECURITY.md",
12
19
  "README.md",
13
20
  "CHANGELOG.md",
14
21
  "LICENSE"
package/server.js CHANGED
@@ -228,6 +228,21 @@ mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
228
228
  }
229
229
  });
230
230
 
231
+ // ── Peer Allowlist (optional, defense-in-depth) ─────────────
232
+ // SYM_ALLOWED_PEERS is a comma-separated list of peer node names.
233
+ // When set, only CMBs and messages from listed peers are pushed to
234
+ // Claude's context. When empty/unset, all authenticated peers are
235
+ // accepted (SVAF still gates on content relevance).
236
+ const ALLOWED_PEERS = (process.env.SYM_ALLOWED_PEERS || '')
237
+ .split(',')
238
+ .map(s => s.trim())
239
+ .filter(Boolean);
240
+
241
+ function isPeerAllowed(peerName) {
242
+ if (ALLOWED_PEERS.length === 0) return true; // no allowlist = accept all
243
+ return ALLOWED_PEERS.includes(peerName);
244
+ }
245
+
231
246
  // ── Mesh Events → Channel Notifications ──────────────────────
232
247
 
233
248
  function pushChannel(eventType, data) {
@@ -247,12 +262,19 @@ node.on('cmb-accepted', (entry) => {
247
262
  if (entry.source === NODE_NAME || entry.cmb?.createdBy === NODE_NAME) return;
248
263
 
249
264
  const source = entry.source || entry.cmb?.createdBy || 'unknown';
265
+
266
+ // Peer allowlist gate (defense-in-depth, see SECURITY.md)
267
+ if (!isPeerAllowed(source)) return;
268
+
250
269
  const focus = entry.cmb?.fields?.focus?.text || entry.content || '';
251
270
  const mood = entry.cmb?.fields?.mood?.text || '';
252
271
  pushChannel('cmb', `[${source}] ${focus}${mood && mood !== 'neutral' ? ` (mood: ${mood})` : ''}`);
253
272
  });
254
273
 
255
274
  node.on('message', (from, content) => {
275
+ // Peer allowlist gate
276
+ if (!isPeerAllowed(from)) return;
277
+
256
278
  pushChannel('message', `[message from ${from}] ${content}`);
257
279
  });
258
280