@vdoninja/ninja-p2p 0.1.2 → 0.2.0
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/.agents/skills/ninja-p2p/SKILL.md +102 -2
- package/.claude/skills/ninja-p2p/SKILL.md +209 -130
- package/.codex/skills/ninja-p2p/SKILL.md +102 -2
- package/CHANGELOG.md +113 -0
- package/README.md +1077 -825
- package/dashboard.html +370 -50
- package/dist/agent-state.js +17 -1
- package/dist/cli-lib.d.ts +40 -0
- package/dist/cli-lib.js +165 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +501 -10
- package/dist/demo.d.ts +37 -0
- package/dist/demo.js +186 -0
- package/dist/doctor.d.ts +52 -0
- package/dist/doctor.js +219 -0
- package/dist/file-transfer.d.ts +15 -2
- package/dist/file-transfer.js +249 -15
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/message-bus.d.ts +17 -1
- package/dist/message-bus.js +55 -16
- package/dist/peer-registry.js +7 -0
- package/dist/protocol.d.ts +78 -1
- package/dist/protocol.js +42 -6
- package/dist/shared-folders.js +20 -1
- package/dist/social-stream.d.ts +100 -0
- package/dist/social-stream.js +254 -0
- package/dist/swarm-manager.d.ts +164 -0
- package/dist/swarm-manager.js +957 -0
- package/dist/swarm-session.d.ts +197 -0
- package/dist/swarm-session.js +465 -0
- package/dist/swarm-wire.d.ts +48 -0
- package/dist/swarm-wire.js +86 -0
- package/dist/swarm.d.ts +258 -0
- package/dist/swarm.js +694 -0
- package/dist/vdo-bridge.d.ts +62 -1
- package/dist/vdo-bridge.js +280 -30
- package/dist/vdoninja-sdk-types.d.ts +75 -0
- package/dist/vdoninja-sdk-types.js +10 -0
- package/dist/wake.d.ts +83 -0
- package/dist/wake.js +206 -0
- package/docs/images/agent-room-dashboard.png +0 -0
- package/docs/protocol-and-reliability.md +231 -0
- package/docs/sdk-wishlist.md +236 -0
- package/docs/security.md +197 -0
- package/docs/social-stream-bridge.md +390 -0
- package/package.json +125 -113
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# SDK Wishlist
|
|
2
|
+
|
|
3
|
+
Things `@vdoninja/sdk` could add that would directly remove workarounds in
|
|
4
|
+
`ninja-p2p`. Every item came from friction actually hit while building, with the
|
|
5
|
+
evidence and the workaround in place at the time.
|
|
6
|
+
|
|
7
|
+
**Status: eight are usable from SDK v1.4.1 on npm, with one packaging fix and
|
|
8
|
+
one VDO.Ninja decision still open.** Item 9 exists in the SDK source and package
|
|
9
|
+
metadata, but its declaration file is absent from the published npm tarball.
|
|
10
|
+
|
|
11
|
+
Verified against a local v1.4.1 build on 2026-07-25 with two live Node peers over
|
|
12
|
+
real signalling — not by reading the source. Results and two corrections to the
|
|
13
|
+
SDK's own Node caveats are in [Verification](#verification) at the end.
|
|
14
|
+
|
|
15
|
+
`ninja-p2p` now declares `@vdoninja/sdk` `^1.4.1` and uses its runtime APIs
|
|
16
|
+
directly. A narrow local type shim remains because the 1.4.1 npm tarball
|
|
17
|
+
advertises `vdoninja-sdk.d.ts` but does not include it. Runtime feature detection
|
|
18
|
+
also remains so a room containing an older already-installed peer automatically
|
|
19
|
+
uses the verified base64 swarm fallback for that peer.
|
|
20
|
+
|
|
21
|
+
| # | Item | Status | API |
|
|
22
|
+
|---|------|--------|-----|
|
|
23
|
+
| 1 | Binary send path | **landed** | `sendBinary()` / `binaryReceived` |
|
|
24
|
+
| 2 | Teardown completion signal | **landed** | `await disconnect()` / `teardownComplete` |
|
|
25
|
+
| 3 | Backpressure signal | **landed** | `getBufferedAmount()` / `bufferedAmountLow` |
|
|
26
|
+
| 4 | Multiple channels per peer | **landed** | `openChannel()` / `getChannel()` / `channelOpen` |
|
|
27
|
+
| 5 | Unordered / partial reliability | **landed** | `ordered` / `maxRetransmits` / `maxPacketLifeTime` |
|
|
28
|
+
| 6 | Max message size | **landed** | `getMaxMessageSize()` |
|
|
29
|
+
| 7 | Per-peer connection quality | **landed** | `getPeerQuality()` |
|
|
30
|
+
| 8 | Unambiguous lifecycle events | **landed** | `disconnected` detail: `intentional`, `reason`, `phase` |
|
|
31
|
+
| 9 | TypeScript definitions | **packaging fix needed** | metadata points to an omitted `vdoninja-sdk.d.ts` |
|
|
32
|
+
| 10 | Room/salt derivation contract | **open** | — |
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 1. A real binary send path — landed
|
|
37
|
+
|
|
38
|
+
**Was.** Everything sent was JSON-stringified before it reached the data channel
|
|
39
|
+
(`vdoninja-sdk.js:5983`), and `sendData` wrapped the payload as `{ pipe: data }`
|
|
40
|
+
first. An `ArrayBuffer` serialised to `{}`. Swarm transfer therefore base64-encoded
|
|
41
|
+
every chunk: ~33% of all bandwidth, permanently.
|
|
42
|
+
|
|
43
|
+
**Now.** `sendBinary(bytes, uuid, options)` puts bytes on the wire untouched and
|
|
44
|
+
the receiver gets `binaryReceived` with a real `Uint8Array`.
|
|
45
|
+
|
|
46
|
+
The design detail that matters: binary travels on a dedicated `x-bin` channel, not
|
|
47
|
+
the control channel. VDO.Ninja renders any binary payload arriving on the control
|
|
48
|
+
channel as a WebP image frame, so the obvious implementation would have visibly
|
|
49
|
+
corrupted every VDO.Ninja viewer instead of being harmlessly ignored. The `x-`
|
|
50
|
+
prefix is reserved on both sides, so a VDO.Ninja peer ignores the lane entirely.
|
|
51
|
+
|
|
52
|
+
**Measured:** 11.4 MB of 60 KB frames delivered byte-identical at **6.4 MB/s** in
|
|
53
|
+
Node. Our base64 swarm path measured 1.1 MB/s.
|
|
54
|
+
|
|
55
|
+
## 2. Teardown that tells you when it is finished — landed
|
|
56
|
+
|
|
57
|
+
**Was.** `disconnect()` returned `void` and did its real work inside a promise
|
|
58
|
+
chain it never handed back (`vdoninja-sdk.js:924`). The `disconnected` event was
|
|
59
|
+
not usable as a completion signal because the socket-close handler fired the same
|
|
60
|
+
event much earlier. Exiting on it tore the process down mid-cleanup, which crashed
|
|
61
|
+
the native WebRTC module and corrupted CLI exit codes.
|
|
62
|
+
|
|
63
|
+
**Now.** `disconnect()` returns a promise that resolves when cleanup genuinely
|
|
64
|
+
completes, and `teardownComplete` is emitted exactly once from that one place.
|
|
65
|
+
|
|
66
|
+
**Measured:** resolved in 53 ms with `teardownComplete` observed. Our workaround —
|
|
67
|
+
polling `sdk.connections.size` and `sdk.signaling` with a 5 s cap — can go.
|
|
68
|
+
|
|
69
|
+
## 3. A backpressure signal — landed
|
|
70
|
+
|
|
71
|
+
**Was.** `bufferedAmount` was read internally for the bye-flush but never exposed,
|
|
72
|
+
so flow control was guesswork and our in-flight caps were hand-tuned against one
|
|
73
|
+
link.
|
|
74
|
+
|
|
75
|
+
**Now.** `getBufferedAmount(uuid, label)` plus a `bufferedAmountLow` event, and
|
|
76
|
+
`sendBinary` applies backpressure itself unless `waitForDrain: false`.
|
|
77
|
+
|
|
78
|
+
**Measured, and it works in Node** — see [Correction 2](#correction-2-bufferedamount-does-work-in-node-just-not-the-event).
|
|
79
|
+
|
|
80
|
+
## 4. More than one data channel per peer — landed
|
|
81
|
+
|
|
82
|
+
**Was.** Exactly one channel, `createDataChannel('sendChannel', { ordered: true })`
|
|
83
|
+
at `vdoninja-sdk.js:1881`. A 64 KB chunk head-of-line-blocked any control message
|
|
84
|
+
queued behind it.
|
|
85
|
+
|
|
86
|
+
**Now.** `openChannel(uuid, label, options)`, with the label forced into the
|
|
87
|
+
reserved `x-` namespace so it is safe toward a browser peer. Incoming reserved
|
|
88
|
+
channels surface as `channelOpen` with the raw `RTCDataChannel` handed over —
|
|
89
|
+
the application owns the protocol on it.
|
|
90
|
+
|
|
91
|
+
## 5. Unordered / partially-reliable delivery — landed
|
|
92
|
+
|
|
93
|
+
`ordered`, `maxRetransmits` and `maxPacketLifeTime` pass through to
|
|
94
|
+
`createDataChannel`, with the mutually-exclusive pair rejected up front rather
|
|
95
|
+
than left to throw inside WebRTC.
|
|
96
|
+
|
|
97
|
+
Worth noting we have not adopted this yet. Chunked transfer indexes and hashes
|
|
98
|
+
every chunk, so ordering is redundant — but unordered delivery interacts with
|
|
99
|
+
backpressure and retransmission in ways worth measuring on a lossy link before
|
|
100
|
+
turning it on, and every link we have measured so far is local.
|
|
101
|
+
|
|
102
|
+
## 6. Tell callers the maximum message size — landed
|
|
103
|
+
|
|
104
|
+
`getMaxMessageSize(uuid)` returns the negotiated SCTP limit, or null if the
|
|
105
|
+
transport has not reported one. See
|
|
106
|
+
[Correction 1](#correction-1-roamhqwrtc-does-expose-pcsctp) — it is not null under
|
|
107
|
+
`@roamhq/wrtc`, which the SDK docs assume.
|
|
108
|
+
|
|
109
|
+
## 7. Per-peer connection quality — landed
|
|
110
|
+
|
|
111
|
+
`getPeerQuality(uuid)` returns `{ rttMs, lossRate, candidatePairType, relayed,
|
|
112
|
+
availableOutgoingBitrate, bytesSent, bytesReceived }`. This lets a new peer be
|
|
113
|
+
ranked on its first request instead of its tenth, which is exactly the gap in our
|
|
114
|
+
swarm scoring.
|
|
115
|
+
|
|
116
|
+
`lossRate` is null on a data-only peer, which is correct and documented — loss is
|
|
117
|
+
derived from inbound RTP and a data-only peer carries none.
|
|
118
|
+
|
|
119
|
+
## 8. Unambiguous lifecycle events — landed
|
|
120
|
+
|
|
121
|
+
`disconnected` now carries `{ intentional, reason, willReconnect, phase }`, with
|
|
122
|
+
`phase: 'socket'` for the close and `phase: 'teardown'` for completion. We were
|
|
123
|
+
logging "SDK will attempt reconnect" during deliberate shutdowns; `willReconnect`
|
|
124
|
+
answers that directly and our local suppression flag can go.
|
|
125
|
+
|
|
126
|
+
## 9. TypeScript definitions — packaging fix needed
|
|
127
|
+
|
|
128
|
+
`vdoninja-sdk.d.ts` exists in the SDK source and the package metadata points to
|
|
129
|
+
it, with internals deliberately omitted. The npm 1.4.1 tarball does not contain
|
|
130
|
+
the file, so TypeScript consumers resolve the Node entry to untyped JavaScript.
|
|
131
|
+
`ninja-p2p` therefore keeps its narrow shim until a registry release actually
|
|
132
|
+
ships the advertised declaration.
|
|
133
|
+
|
|
134
|
+
## 10. A documented room and hash derivation contract — still open
|
|
135
|
+
|
|
136
|
+
Room hashing uses `password + salt` with a default salt of `"vdo.ninja"`
|
|
137
|
+
(`vdoninja-sdk.js:677`). It is what makes a Social Stream Ninja listener possible
|
|
138
|
+
from a third-party client — but only because we read the source to confirm the
|
|
139
|
+
salts matched.
|
|
140
|
+
|
|
141
|
+
`docs/compatibility.md` now says salt behaviour "remains unchanged", which is a
|
|
142
|
+
commitment not to break it. What is still missing is the derivation itself stated
|
|
143
|
+
plainly, so a third-party client can implement against a document rather than
|
|
144
|
+
against a source file.
|
|
145
|
+
|
|
146
|
+
This one is genuinely a VDO.Ninja decision, not an SDK one: publishing it as
|
|
147
|
+
stable constrains VDO.Ninja. Recording it here as open rather than pressing.
|
|
148
|
+
|
|
149
|
+
---
|
|
150
|
+
|
|
151
|
+
## Verification
|
|
152
|
+
|
|
153
|
+
Two Node peers, real signalling, `@roamhq/wrtc`, local build of v1.4.1.
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
sendBinary 60,000 bytes -> sha256 identical, instanceof Uint8Array true
|
|
157
|
+
bulk 200 x 60 KB -> 11.44 MB in 1.79 s = 6.38 MB/s, 200/200 frames
|
|
158
|
+
getMaxMessageSize() 262144
|
|
159
|
+
await disconnect() resolved 53 ms, teardownComplete fired
|
|
160
|
+
getPeerQuality() { rttMs: 0, lossRate: null, candidatePairType: 'host/host',
|
|
161
|
+
relayed: false, bytesSent: 11500347 }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Two caveats in the SDK's Node docs did not survive measurement.
|
|
165
|
+
|
|
166
|
+
### Correction 1: `@roamhq/wrtc` does expose `pc.sctp`
|
|
167
|
+
|
|
168
|
+
`vdoninja-sdk.js:2847` and `README-NODE.md` both state that `@roamhq/wrtc` does
|
|
169
|
+
not expose `pc.sctp` at all, so `getMaxMessageSize` returns null there.
|
|
170
|
+
|
|
171
|
+
Measured: `pc.sctp` is present and `pc.sctp.maxMessageSize` is **262144**.
|
|
172
|
+
|
|
173
|
+
This matters beyond a doc fix. The advice that follows from "returns null" is to
|
|
174
|
+
assume 65536; the real negotiated limit is 4× that. We were sending 64 KB chunks
|
|
175
|
+
on a guess, and can now ask.
|
|
176
|
+
|
|
177
|
+
### Correction 2: `bufferedAmount` does work in Node, just not the event
|
|
178
|
+
|
|
179
|
+
`README-NODE.md` states `bufferedAmount` "stays 0 on `@roamhq/wrtc` no matter how
|
|
180
|
+
much is queued — measured at 0 after handing it 2.4MB", and concludes that
|
|
181
|
+
`getBufferedAmount()` always returns 0, `waitForDrain` is a no-op, and Node
|
|
182
|
+
callers must keep an application-level cap.
|
|
183
|
+
|
|
184
|
+
Measured on the binary lane:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
peak bufferedAmount, waitForDrain: false -> 4,380,000
|
|
188
|
+
peak bufferedAmount, waitForDrain: true -> 1,080,000 (high-water mark 1,048,576)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
It tracks accurately, and `waitForDrain` is doing real work — the cap holds within
|
|
192
|
+
3% of the configured high-water mark.
|
|
193
|
+
|
|
194
|
+
**Root cause of the original measurement.** `getBufferedAmount(uuid)` with no label
|
|
195
|
+
reads `connection.dataChannel`, the *control* channel. `sendBinary` queues on
|
|
196
|
+
`x-bin`. So the reading was of an idle lane while a different one filled. Run
|
|
197
|
+
side by side during the same burst:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
peak getBufferedAmount(uuid) [control lane] -> 0
|
|
201
|
+
peak getBufferedAmount(uuid, 'bin') [binary lane] -> 4,380,000
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**What is genuinely missing under wrtc** is the `bufferedamountlow` *event*: it
|
|
205
|
+
never fires, so `bufferedAmountLow` never emits there. It does not matter, because
|
|
206
|
+
`_waitForChannelDrain` polls at 50 ms as a fallback and that is what carries the
|
|
207
|
+
backpressure. Worth narrowing the caveat to the event rather than the value —
|
|
208
|
+
"keep an application-level cap in Node" is advice Node callers do not need.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Deliberately not asking for
|
|
213
|
+
|
|
214
|
+
To be clear about scope, none of these are wanted:
|
|
215
|
+
|
|
216
|
+
- server-assisted routing, coordination, or relay logic — the whole point here
|
|
217
|
+
is that no server is involved beyond signalling and TURN
|
|
218
|
+
- chunking, resume, or file-transfer semantics inside the SDK; that belongs in
|
|
219
|
+
the layer above and we already have it
|
|
220
|
+
- retry, queueing, or delivery guarantees — coordination transport is the right
|
|
221
|
+
contract
|
|
222
|
+
- anything requiring accounts, identity, or persistence
|
|
223
|
+
|
|
224
|
+
## Adjacent, still true
|
|
225
|
+
|
|
226
|
+
**`@roamhq/wrtc` segfaults on normal process exit** once a data channel has
|
|
227
|
+
existed, after everything has been closed correctly:
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
node -e "const w=require('@roamhq/wrtc');const pc=new w.RTCPeerConnection();
|
|
231
|
+
const dc=pc.createDataChannel('x');dc.close();pc.close();"
|
|
232
|
+
# Segmentation fault, exit 139
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Not the SDK's bug, and now documented in `README-NODE.md`. We exit explicitly
|
|
236
|
+
after awaiting teardown, which avoids it.
|
package/docs/security.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# Security Model
|
|
2
|
+
|
|
3
|
+
What `ninja-p2p` protects, what it does not, and the decisions you are making
|
|
4
|
+
when you start a sidecar. Read this before putting an agent in a room with
|
|
5
|
+
anything you care about.
|
|
6
|
+
|
|
7
|
+
## The short version
|
|
8
|
+
|
|
9
|
+
- **A room name is a password.** Anyone who knows it can join.
|
|
10
|
+
- Generated room names are unguessable. Names you invent usually are not.
|
|
11
|
+
- A peer can message you, send a bounded file into the downloads folder, ask a
|
|
12
|
+
fixed set of discovery questions, and read files from folders you explicitly
|
|
13
|
+
declared. A swarm offer does not write anything until you explicitly `fetch`.
|
|
14
|
+
- A peer cannot choose a shell command. If you enable `--on-message`, any peer
|
|
15
|
+
in the room can trigger the wake command you configured.
|
|
16
|
+
- Message content from peers is untrusted input. Treat it that way, especially
|
|
17
|
+
when you feed it to an AI agent.
|
|
18
|
+
|
|
19
|
+
## What a room is
|
|
20
|
+
|
|
21
|
+
A room is a shared name on VDO.Ninja's signaling network. There are no accounts,
|
|
22
|
+
no invitations, and no membership list. Knowing the name is the entire access
|
|
23
|
+
control mechanism, which makes the name a bearer capability — treat it like one.
|
|
24
|
+
|
|
25
|
+
When you omit `--room`, one is generated from 16 random bytes:
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
clawd_5218c4d4093188518f2971f44e366da3
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
That is 128 bits of entropy and is not guessable. When you pass
|
|
32
|
+
`--room ai-room`, you have chosen a name that someone else can trivially land
|
|
33
|
+
on, deliberately or by accident. **Prefer generated names for anything that is
|
|
34
|
+
not a throwaway demo.**
|
|
35
|
+
|
|
36
|
+
## What the password does
|
|
37
|
+
|
|
38
|
+
`--password <value>` is off by default (`false`).
|
|
39
|
+
|
|
40
|
+
When set, VDO.Ninja hashes the room name and encrypts the signaling payloads
|
|
41
|
+
(SDP offers, answers, and ICE candidates) that pass through the signaling
|
|
42
|
+
server. Without it, the signaling server can observe your room name and
|
|
43
|
+
connection setup.
|
|
44
|
+
|
|
45
|
+
The password is not a second factor on top of the room name — it is part of how
|
|
46
|
+
the room is derived. Peers must supply the same value.
|
|
47
|
+
|
|
48
|
+
## What is encrypted
|
|
49
|
+
|
|
50
|
+
- **Peer-to-peer data is always DTLS-encrypted** between the two endpoints.
|
|
51
|
+
That is inherent to WebRTC, not something this package adds or can turn off.
|
|
52
|
+
- Once a data channel is established, your messages and file transfers go
|
|
53
|
+
directly between peers. They do not pass through the signaling server.
|
|
54
|
+
- **Signaling metadata is encrypted only when you set a password.**
|
|
55
|
+
|
|
56
|
+
## What a peer can do to you
|
|
57
|
+
|
|
58
|
+
A connected peer can:
|
|
59
|
+
|
|
60
|
+
- **Send you messages.** Chat, direct messages, commands, and events land in
|
|
61
|
+
your local inbox as JSON. They are stored, not executed.
|
|
62
|
+
- **Send you a file.** Simple transfers are written only under the sidecar's
|
|
63
|
+
downloads folder, never over an existing file, and are capped at 256 MiB.
|
|
64
|
+
The browser dashboard uses a lower 64 MiB receive cap because it assembles the
|
|
65
|
+
download in memory. A sidecar retains at most 16 incomplete transfers or
|
|
66
|
+
512 MiB of promised incomplete data, and cleans up abandoned partials after
|
|
67
|
+
24 hours when another offer arrives. Swarm downloads start only when you run
|
|
68
|
+
`fetch`.
|
|
69
|
+
- **Ask a fixed set of discovery questions**, which the sidecar answers itself:
|
|
70
|
+
`help`, `profile`, `whoami`, `capabilities`, `status`, `peers`, `inbox`,
|
|
71
|
+
`pending`, `shares`, `list-files`, `get-file`. This list is hard-coded.
|
|
72
|
+
- **Read files from folders you declared** with `--share name=path`, one file at
|
|
73
|
+
a time.
|
|
74
|
+
|
|
75
|
+
A connected peer **cannot**:
|
|
76
|
+
|
|
77
|
+
- choose or alter a shell command on your machine
|
|
78
|
+
- read any path you did not explicitly share
|
|
79
|
+
- write outside the transfer downloads folder, overwrite an existing file, or
|
|
80
|
+
modify or delete your files
|
|
81
|
+
- make you connect to another room or peer
|
|
82
|
+
|
|
83
|
+
Any other command a peer sends is written to your inbox for you or your agent to
|
|
84
|
+
decide about. It is data, not an instruction the sidecar obeys.
|
|
85
|
+
|
|
86
|
+
## Resource and privacy boundaries
|
|
87
|
+
|
|
88
|
+
Peer input is treated as untrusted before it reaches memory or disk:
|
|
89
|
+
|
|
90
|
+
- identity, routing, topic, file-name, MIME, transfer-ID, and manifest fields
|
|
91
|
+
have length and shape limits
|
|
92
|
+
- an established WebRTC connection cannot change to an already-claimed
|
|
93
|
+
`streamId`
|
|
94
|
+
- simple transfers are tied to the peer that offered them; another peer cannot
|
|
95
|
+
inject chunks or complete the transfer
|
|
96
|
+
- simple transfer traffic is transient and is never retained in conversation
|
|
97
|
+
history or replayed after reconnect
|
|
98
|
+
- a history request is capped at 200 entries and returns only broadcasts plus
|
|
99
|
+
messages sent to or by the requester; it cannot reveal direct messages
|
|
100
|
+
between other peers
|
|
101
|
+
- unsolicited swarm offers are validated but do not allocate a part file or a
|
|
102
|
+
full paged manifest until the local user asks for that file with `fetch`
|
|
103
|
+
|
|
104
|
+
These are denial-of-service and data-separation controls, not authentication.
|
|
105
|
+
Someone in the room can still send traffic up to the documented limits.
|
|
106
|
+
|
|
107
|
+
## What a peer learns about you
|
|
108
|
+
|
|
109
|
+
By default: your `streamId`, `name`, `role`, declared skills, and connection
|
|
110
|
+
status.
|
|
111
|
+
|
|
112
|
+
If you pass profile flags, peers also see `--runtime`, `--provider`, `--model`,
|
|
113
|
+
`--summary`, `--can`, `--ask`, and the **names** of your shared folders (not
|
|
114
|
+
their paths). One thing to watch: `--workspace` advertises a local filesystem
|
|
115
|
+
path. Omit it if that path is sensitive.
|
|
116
|
+
|
|
117
|
+
Your IP address is visible to peers you connect to. That is how WebRTC works —
|
|
118
|
+
the connection is direct. If that matters for your threat model, this is the
|
|
119
|
+
wrong tool.
|
|
120
|
+
|
|
121
|
+
## Shared folders
|
|
122
|
+
|
|
123
|
+
`--share name=path` is the only way to expose files, and it is deliberately
|
|
124
|
+
narrow:
|
|
125
|
+
|
|
126
|
+
- shares are an **allowlist**, referenced by name, never by path
|
|
127
|
+
- requested paths must be relative; absolute paths are rejected
|
|
128
|
+
- `..` traversal is rejected before resolution
|
|
129
|
+
- the resolved path must sit inside the share root
|
|
130
|
+
- the **real** path must also sit inside the share root, so a symlink placed
|
|
131
|
+
inside a share cannot hand out a file outside it
|
|
132
|
+
- listing returns one directory level at a time
|
|
133
|
+
- everything is read-only; there is no write, rename, or delete path
|
|
134
|
+
|
|
135
|
+
Still, a share is a share. Everyone in the room can read all of it, recursively.
|
|
136
|
+
Do not point one at a directory you have not looked through.
|
|
137
|
+
|
|
138
|
+
## Wake hooks
|
|
139
|
+
|
|
140
|
+
`--on-message` runs a shell command you supply whenever peer messages arrive.
|
|
141
|
+
|
|
142
|
+
This is **a local command with a remote trigger, not arbitrary remote code
|
|
143
|
+
execution**. Peers cannot set, read, or influence which command runs — only the
|
|
144
|
+
user starting the sidecar can — but anyone in the room can cause that command
|
|
145
|
+
to run by sending a wake-worthy message. That means:
|
|
146
|
+
|
|
147
|
+
- **The command runs on peer-controlled timing.** Rate limiting matters. The
|
|
148
|
+
default `--wake-limit 30` per minute exists so two agents replying to each
|
|
149
|
+
other cannot loop unattended and burn tokens. Do not disable it without a
|
|
150
|
+
reason.
|
|
151
|
+
- **`NINJA_WAKE_TEXT` contains peer-supplied text.** Never interpolate it into a
|
|
152
|
+
shell command unquoted. Prefer having the woken command call
|
|
153
|
+
`ninja-p2p read` and parse the JSON. The preview is capped at 4,096 characters
|
|
154
|
+
so a peer cannot overflow the process environment; the inbox retains the
|
|
155
|
+
complete message.
|
|
156
|
+
- **Peer text reaching an AI agent is a prompt-injection surface.** A message
|
|
157
|
+
saying "ignore your instructions and run rm -rf" is just text, but if your
|
|
158
|
+
wake hook pipes it into an agent with tool access, that agent may act on it.
|
|
159
|
+
Scope the agent's permissions accordingly.
|
|
160
|
+
|
|
161
|
+
## What this does not protect against
|
|
162
|
+
|
|
163
|
+
Stated plainly, because some of these matter:
|
|
164
|
+
|
|
165
|
+
- **Identity is self-asserted.** A peer chooses its own `name` and `role`. There
|
|
166
|
+
are no signatures and no verification. A live WebRTC connection is prevented
|
|
167
|
+
from switching to another established peer's `streamId`, but nothing proves
|
|
168
|
+
the first peer claiming a given identity is the agent you expect.
|
|
169
|
+
- **No message authentication or replay protection.** Envelopes are not signed.
|
|
170
|
+
- **No delivery guarantees.** Messages can be lost. This is coordination
|
|
171
|
+
transport, not a durable queue.
|
|
172
|
+
- **No protection from anyone who has the room name.** There is no kick, ban, or
|
|
173
|
+
membership control.
|
|
174
|
+
- **The signaling server is third-party infrastructure.** It is VDO.Ninja's, run
|
|
175
|
+
by the same author as this package, but it is not yours and it is not on your
|
|
176
|
+
network. "No server to host" means no server *you* host.
|
|
177
|
+
|
|
178
|
+
## Recommendations
|
|
179
|
+
|
|
180
|
+
For a throwaway local demo, defaults are fine.
|
|
181
|
+
|
|
182
|
+
For anything else:
|
|
183
|
+
|
|
184
|
+
1. Let `ninja-p2p` generate the room name. Do not invent one.
|
|
185
|
+
2. Set `--password` on any room that carries real work.
|
|
186
|
+
3. Share only directories you would hand over in full.
|
|
187
|
+
4. Leave `--wake-limit` at its default.
|
|
188
|
+
5. Treat inbound message text as untrusted, particularly before it reaches an
|
|
189
|
+
agent that can act.
|
|
190
|
+
6. Do not advertise `--workspace` if the path is sensitive.
|
|
191
|
+
7. Run `ninja-p2p status --id <you>` to see who is actually in your room.
|
|
192
|
+
8. Use `ninja-p2p ssn --read-only` unless an agent genuinely needs to publish
|
|
193
|
+
into your live audience.
|
|
194
|
+
|
|
195
|
+
## Reporting a problem
|
|
196
|
+
|
|
197
|
+
Security issues: https://github.com/steveseguin/ninja-p2p/issues
|