shadow-claw 1.24.0 → 1.25.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.
Files changed (85) hide show
  1. package/README.md +2 -1
  2. package/bin/cli.mjs +111 -2
  3. package/bin/commands/webrtc-listen.mjs +21 -1
  4. package/bin/utils/control-client.mjs +44 -30
  5. package/bin/utils/control-client.test.mjs +10 -0
  6. package/bin/utils/webrtc-control-client.mjs +23 -4
  7. package/dist/public/AGENTS.md +8 -1
  8. package/dist/public/README.md +2 -1
  9. package/dist/public/agent.worker.js +49 -49
  10. package/dist/public/backup-controller-CvzDOq4T.js +1 -0
  11. package/dist/public/{configurePeerJs-CxpZeDJJ.js → configurePeerJs-UfQ6HUlv.js} +1 -1
  12. package/dist/public/docs/README.md +1 -1
  13. package/dist/public/docs/example/article/index.html +1 -1
  14. package/dist/public/docs/guides/server-development-configuration.md +28 -0
  15. package/dist/public/docs/publishing/index.html +1 -1
  16. package/dist/public/docs/skill-creator/index.html +1 -1
  17. package/dist/public/docs/subsystems/cli.md +25 -19
  18. package/dist/public/docs/subsystems/control-plane.md +11 -0
  19. package/dist/public/{downloadGroupFile-By0GR6eV.js → downloadGroupFile-9niHBj9n.js} +1 -1
  20. package/dist/public/{e2e-bridge-IGWsRl4i.js → e2e-bridge-D28tOggc.js} +1 -1
  21. package/dist/public/{file-viewer-D0eflgt6.js → file-viewer-BVg3B_JH.js} +1 -1
  22. package/dist/public/index.html +1 -1
  23. package/dist/public/index.js +1 -1
  24. package/dist/public/initControlPlane-D5kHKcB5.js +1 -0
  25. package/dist/public/llms.txt +1 -1
  26. package/dist/public/main/index.html +1 -1
  27. package/dist/public/main/memory/index.html +1 -1
  28. package/dist/public/{markdown-BnpKxSxl.js → markdown-SiFq0PTy.js} +1 -1
  29. package/dist/public/{orchestrator-nJj59RqD.js → orchestrator-CnwkoBpe.js} +1 -1
  30. package/dist/public/{orchestrator-3h9xxHUm.js → orchestrator-tHWrqi_F.js} +5 -5
  31. package/dist/public/{peerjs-BP4laMXj.js → peerjs-CuSJUH-r.js} +1 -1
  32. package/dist/public/push-client-D0lkwrK0.js +1 -0
  33. package/dist/public/service-worker/fetch-proxy.js +1 -1
  34. package/dist/public/service-worker.js +1 -1
  35. package/dist/public/{shadow-claw-CbwETBWf.js → shadow-claw-CUbhMmcB.js} +2 -2
  36. package/dist/public/{shadow-claw-channels-BDWG5Qa3.js → shadow-claw-channels-mY-JMvib.js} +1 -1
  37. package/dist/public/{shadow-claw-chat-Crv0DTCY.js → shadow-claw-chat-BN85a1xk.js} +1 -1
  38. package/dist/public/{shadow-claw-control-plane-CX1IGbh9.js → shadow-claw-control-plane-C0lSxLpW.js} +2 -2
  39. package/dist/public/{shadow-claw-conversations-_9qAaRXk.js → shadow-claw-conversations-0Q-VC_7N.js} +1 -1
  40. package/dist/public/{shadow-claw-file-viewer-Blf6yGVD.js → shadow-claw-file-viewer-AZfG4D-u.js} +1 -1
  41. package/dist/public/{shadow-claw-files-CYySBDCl.js → shadow-claw-files-Bx4v0Ggp.js} +1 -1
  42. package/dist/public/{shadow-claw-git-v8S0JjuR.js → shadow-claw-git-DD7iQRxq.js} +2 -2
  43. package/dist/public/{shadow-claw-llm-GWgoVfhI.js → shadow-claw-llm-CfeDBZKB.js} +1 -1
  44. package/dist/public/{shadow-claw-networking-qHbP95z9.js → shadow-claw-networking-e0EarDVs.js} +1 -1
  45. package/dist/public/{shadow-claw-notifications-YyHOK2Fr.js → shadow-claw-notifications-BkxuDEQI.js} +3 -3
  46. package/dist/public/{shadow-claw-pages-DFZHhzFZ.js → shadow-claw-pages-BWSE91I1.js} +1 -1
  47. package/dist/public/{shadow-claw-settings-DhxrKIfA.js → shadow-claw-settings-BZWrAoQA.js} +2 -2
  48. package/dist/public/{shadow-claw-storage-BFynbP0j.js → shadow-claw-storage-Crs6LY15.js} +1 -1
  49. package/dist/public/{shadow-claw-task-server-3BIeb5oZ.js → shadow-claw-task-server-CKm_Ennl.js} +1 -1
  50. package/dist/public/{shadow-claw-tasks-Caxc9atL.js → shadow-claw-tasks-Czx6Ucf5.js} +1 -1
  51. package/dist/public/{shadow-claw-terminal-Beb5qF-U.js → shadow-claw-terminal-Dmtr9KY1.js} +1 -1
  52. package/dist/public/{shadow-claw-tools-BfJx3A9W.js → shadow-claw-tools-t3Qq4TDO.js} +1 -1
  53. package/dist/public/{shadow-claw-webvm-BCS8ykeP.js → shadow-claw-webvm-CIuSywD_.js} +1 -1
  54. package/dist/public/{syncWebMcpRegistration-B_M2fCjH.js → syncWebMcpRegistration-DzC80wVG.js} +1 -1
  55. package/dist/server.js +203 -203
  56. package/package.json +1 -1
  57. package/src/components/settings/shadow-claw-notifications/shadow-claw-notifications.test.ts +3 -0
  58. package/src/components/settings/shadow-claw-notifications/shadow-claw-notifications.ts +43 -9
  59. package/src/core/backup-controller.ts +33 -4
  60. package/src/core/control-plane-client.test.ts +9 -2
  61. package/src/core/control-plane-client.ts +18 -2
  62. package/src/core/orchestrator/utils/operations/task.ts +46 -15
  63. package/src/core/utils/initControlPlane.ts +2 -0
  64. package/src/server/app.ts +2 -0
  65. package/src/server/config.test.ts +49 -0
  66. package/src/server/config.ts +47 -0
  67. package/src/server/control-plane.test.ts +36 -0
  68. package/src/server/control-plane.ts +41 -14
  69. package/src/server/middleware/pna.test.ts +15 -3
  70. package/src/server/middleware/pna.ts +6 -2
  71. package/src/server/peer.ts +6 -2
  72. package/src/server/routes/backup.test.ts +14 -0
  73. package/src/server/routes/backup.ts +44 -15
  74. package/src/server/server.test.ts +118 -0
  75. package/src/server/server.ts +22 -5
  76. package/src/server/tls.test.ts +266 -0
  77. package/src/server/tls.ts +178 -0
  78. package/src/service-worker/fetch-proxy-rules.test.ts +41 -0
  79. package/src/service-worker/fetch-proxy-rules.ts +15 -8
  80. package/src/service-worker/fetch-proxy.ts +18 -2
  81. package/src/subsystems/notifications/push-client.test.ts +5 -1
  82. package/src/subsystems/notifications/push-client.ts +50 -12
  83. package/dist/public/backup-controller-C_s9in2t.js +0 -1
  84. package/dist/public/initControlPlane-C9jpnWev.js +0 -1
  85. package/dist/public/push-client-CrjEXK3U.js +0 -1
package/README.md CHANGED
@@ -383,6 +383,7 @@ E2E test architecture: [`e2e/README.md`](e2e/README.md)
383
383
 
384
384
  ```bash
385
385
  npm run dev # Dev server (watch mode)
386
+ npm run dev -- --https # Dev server with opt-in HTTPS (auto-generates self-signed cert)
386
387
  npm start # Express server
387
388
  npm test # Jest (*.test.ts files live next to source)
388
389
  npm run e2e # Playwright E2E tests (e2e/*.test.ts)
@@ -413,7 +414,7 @@ npx shadow-claw webrtc listen # Start headless WebRTC Dat
413
414
  npx shadow-claw peer-id # Get or generate persistent CLI Peer ID
414
415
  ```
415
416
 
416
- Commands support `--transport webrtc` for direct peer-to-peer DataChannel execution with connected browser clients. Control plane authentication uses `SHADOWCLAW_CONTROL_TOKEN` (env) or `--token` flag. The control plane endpoint and token are printed to the console on server start.
417
+ Commands support `--transport webrtc` for direct peer-to-peer DataChannel execution with connected browser clients. Control plane authentication uses `SHADOWCLAW_CONTROL_TOKEN` (env) or `--token` flag, and supports HTTPS endpoints via `--https` (and `--insecure` for self-signed certs). The control plane endpoint and token are printed to the console on server start.
417
418
 
418
419
  ## License
419
420
 
package/bin/cli.mjs CHANGED
@@ -173,6 +173,36 @@ async function handleDev(portArg, options) {
173
173
  .filter(Boolean),
174
174
  );
175
175
 
176
+ const isHttps = Boolean(
177
+ options.https ||
178
+ ["1", "true", "yes"].includes(
179
+ (process.env.SHADOWCLAW_HTTPS || "").toLowerCase().trim(),
180
+ ),
181
+ );
182
+ const certPath =
183
+ options.cert ||
184
+ (
185
+ process.env.SHADOWCLAW_TLS_CERT ||
186
+ process.env.SHADOWCLAW_CERT ||
187
+ ""
188
+ ).trim() ||
189
+ undefined;
190
+ const keyPath =
191
+ options.key ||
192
+ (
193
+ process.env.SHADOWCLAW_TLS_KEY ||
194
+ process.env.SHADOWCLAW_KEY ||
195
+ ""
196
+ ).trim() ||
197
+ undefined;
198
+ const sslDir = path.resolve(
199
+ contentRoot,
200
+ options.sslDir ||
201
+ process.env.SHADOWCLAW_SSL_DIR ||
202
+ process.env.SHADOWCLAW_TLS_DIR ||
203
+ ".cache/tls",
204
+ );
205
+
176
206
  await startServer({
177
207
  port,
178
208
  bindHost: host,
@@ -183,9 +213,14 @@ async function handleDev(portArg, options) {
183
213
  rootPath: distPublicDir,
184
214
  databaseDir,
185
215
  allowPrivateProxy: Boolean(options.allowPrivateProxy),
216
+ https: isHttps,
217
+ certPath,
218
+ keyPath,
219
+ sslDir,
186
220
  });
187
221
 
188
- const localUrl = `http://${host}:${port}`;
222
+ const protocol = isHttps ? "https" : "http";
223
+ const localUrl = `${protocol}://${host}:${port}`;
189
224
  if (options.open) {
190
225
  openBrowser(localUrl);
191
226
  }
@@ -218,6 +253,13 @@ program
218
253
  "Allow proxy to reach private/loopback addresses",
219
254
  false,
220
255
  )
256
+ .option("--https", "Enable HTTPS dev server", false)
257
+ .option("--cert <path>", "Path to existing TLS certificate file")
258
+ .option("--key <path>", "Path to existing TLS private key file")
259
+ .option(
260
+ "--ssl-dir <path>",
261
+ "Directory for TLS certificate generation/storage",
262
+ )
221
263
  .option("-v, --verbose", "Enable verbose request/proxy logging", false)
222
264
  .option("--open", "Automatically open default browser", false)
223
265
  .action(handleDev);
@@ -245,6 +287,13 @@ program
245
287
  "Allow proxy to reach private/loopback addresses",
246
288
  false,
247
289
  )
290
+ .option("--https", "Enable HTTPS dev server", false)
291
+ .option("--cert <path>", "Path to existing TLS certificate file")
292
+ .option("--key <path>", "Path to existing TLS private key file")
293
+ .option(
294
+ "--ssl-dir <path>",
295
+ "Directory for TLS certificate generation/storage",
296
+ )
248
297
  .option("-v, --verbose", "Enable verbose request/proxy logging", false)
249
298
  .option("--open", "Automatically open default browser", false)
250
299
  .action(handleDev);
@@ -276,6 +325,13 @@ program
276
325
  "Allow proxy to reach private/loopback addresses",
277
326
  false,
278
327
  )
328
+ .option("--https", "Enable HTTPS dev server", false)
329
+ .option("--cert <path>", "Path to existing TLS certificate file")
330
+ .option("--key <path>", "Path to existing TLS private key file")
331
+ .option(
332
+ "--ssl-dir <path>",
333
+ "Directory for TLS certificate generation/storage",
334
+ )
279
335
  .option("-v, --verbose", "Enable verbose request/proxy logging", false)
280
336
  .option("--open", "Automatically open default browser", false)
281
337
  .action(async (portArg, options) => {
@@ -328,6 +384,36 @@ program
328
384
  .filter(Boolean),
329
385
  );
330
386
 
387
+ const isHttps = Boolean(
388
+ options.https ||
389
+ ["1", "true", "yes"].includes(
390
+ (process.env.SHADOWCLAW_HTTPS || "").toLowerCase().trim(),
391
+ ),
392
+ );
393
+ const certPath =
394
+ options.cert ||
395
+ (
396
+ process.env.SHADOWCLAW_TLS_CERT ||
397
+ process.env.SHADOWCLAW_CERT ||
398
+ ""
399
+ ).trim() ||
400
+ undefined;
401
+ const keyPath =
402
+ options.key ||
403
+ (
404
+ process.env.SHADOWCLAW_TLS_KEY ||
405
+ process.env.SHADOWCLAW_KEY ||
406
+ ""
407
+ ).trim() ||
408
+ undefined;
409
+ const sslDir = path.resolve(
410
+ contentRoot,
411
+ options.sslDir ||
412
+ process.env.SHADOWCLAW_SSL_DIR ||
413
+ process.env.SHADOWCLAW_TLS_DIR ||
414
+ ".cache/tls",
415
+ );
416
+
331
417
  await startServer({
332
418
  port,
333
419
  bindHost: host,
@@ -338,9 +424,14 @@ program
338
424
  rootPath: distPublicDir,
339
425
  databaseDir,
340
426
  allowPrivateProxy: Boolean(options.allowPrivateProxy),
427
+ https: isHttps,
428
+ certPath,
429
+ keyPath,
430
+ sslDir,
341
431
  });
342
432
 
343
- const localUrl = `http://${host}:${port}`;
433
+ const protocol = isHttps ? "https" : "http";
434
+ const localUrl = `${protocol}://${host}:${port}`;
344
435
  if (options.open) {
345
436
  openBrowser(localUrl);
346
437
  }
@@ -478,6 +569,11 @@ program
478
569
  "Use TLS (wss://) for the signaling server (for listen)",
479
570
  false,
480
571
  )
572
+ .option(
573
+ "--https",
574
+ "Alias for --secure: use TLS (wss://) for the signaling server",
575
+ false,
576
+ )
481
577
  .option(
482
578
  "--trusted-peer <id>",
483
579
  "Accept connections only from this peer ID (repeatable, for listen)",
@@ -486,6 +582,11 @@ program
486
582
  )
487
583
  .option("--verbose", "Verbose connection logging (for listen)", false)
488
584
  .option("--renew-peer-id", "Renew CLI peer ID before listening", false)
585
+ .option(
586
+ "-k, --insecure",
587
+ "Allow self-signed TLS certificates for the signaling server (wss://)",
588
+ true,
589
+ )
489
590
  .action(async (action, customId, options) => {
490
591
  if (action === "listen") {
491
592
  await runWebRtcListenCommand(options);
@@ -507,6 +608,8 @@ program
507
608
  .option("--host <host>", "Control plane host")
508
609
  .option("--port <port>", "Control plane port")
509
610
  .option("--token <token>", "Control token")
611
+ .option("--https", "Connect to server via HTTPS", false)
612
+ .option("-k, --insecure", "Allow self-signed TLS certificates", true)
510
613
  .option("--transport <transport>", "Transport to use: http | webrtc", "http")
511
614
  .option("--peer-id <id>", "Custom WebRTC CLI peer ID")
512
615
  .option(
@@ -530,6 +633,8 @@ program
530
633
  .option("--host <host>", "Control plane host")
531
634
  .option("--port <port>", "Control plane port")
532
635
  .option("--token <token>", "Control token")
636
+ .option("--https", "Connect to server via HTTPS", false)
637
+ .option("-k, --insecure", "Allow self-signed TLS certificates", true)
533
638
  .option("--transport <transport>", "Transport to use: http | webrtc", "http")
534
639
  .option("--peer-id <id>", "Custom WebRTC CLI peer ID")
535
640
  .option("--renew-peer-id", "Renew WebRTC CLI peer ID before sending", false)
@@ -552,6 +657,8 @@ program
552
657
  .option("--host <host>", "Control plane host")
553
658
  .option("--port <port>", "Control plane port")
554
659
  .option("--token <token>", "Control token")
660
+ .option("--https", "Connect to server via HTTPS", false)
661
+ .option("-k, --insecure", "Allow self-signed TLS certificates", true)
555
662
  .option("--transport <transport>", "Transport to use: http | webrtc", "http")
556
663
  .option("--peer-id <id>", "Custom WebRTC CLI peer ID")
557
664
  .option(
@@ -575,6 +682,8 @@ program
575
682
  .option("--host <host>", "Control plane host")
576
683
  .option("--port <port>", "Control plane port")
577
684
  .option("--token <token>", "Control token")
685
+ .option("--https", "Connect to server via HTTPS", false)
686
+ .option("-k, --insecure", "Allow self-signed TLS certificates", true)
578
687
  .option("--transport <transport>", "Transport to use: http | webrtc", "http")
579
688
  .option("--peer-id <id>", "Custom WebRTC CLI peer ID")
580
689
  .option(
@@ -156,7 +156,23 @@ export async function runWebRtcListenCommand(options = {}) {
156
156
  ? parseInt(options.port, 10)
157
157
  : parseInt(process.env.SHADOWCLAW_PORT || "8888", 10);
158
158
  const peerPath = options.path || "/";
159
- const secure = Boolean(options.secure);
159
+ const secure = Boolean(
160
+ options.secure ||
161
+ options.https ||
162
+ ["1", "true", "yes"].includes(
163
+ (process.env.SHADOWCLAW_HTTPS || "").toLowerCase().trim(),
164
+ ),
165
+ );
166
+ // --insecure / -k: allow self-signed TLS certificates (default: true for dev)
167
+ const rejectUnauthorized =
168
+ options.insecure === false ||
169
+ ["0", "false", "no"].includes(
170
+ (process.env.SHADOWCLAW_TLS_REJECT_UNAUTHORIZED || "")
171
+ .toLowerCase()
172
+ .trim(),
173
+ )
174
+ ? true
175
+ : false;
160
176
  const trustedPeerIds = options.trustedPeer
161
177
  ? Array.isArray(options.trustedPeer)
162
178
  ? options.trustedPeer
@@ -169,6 +185,9 @@ export async function runWebRtcListenCommand(options = {}) {
169
185
  console.log(
170
186
  `Signaling server : ${secure ? "wss" : "ws"}://${host}:${port}${peerPath}`,
171
187
  );
188
+ if (secure && !rejectUnauthorized) {
189
+ console.log(`TLS verification : disabled (self-signed cert allowed)`);
190
+ }
172
191
  if (trustedPeerIds.length > 0) {
173
192
  console.log(`Trusted peers : ${trustedPeerIds.join(", ")}`);
174
193
  } else {
@@ -186,6 +205,7 @@ export async function runWebRtcListenCommand(options = {}) {
186
205
  port,
187
206
  path: peerPath,
188
207
  secure,
208
+ rejectUnauthorized,
189
209
  trustedPeerIds,
190
210
  peerId: options.peerId,
191
211
  cacheDir: options.cacheDir,
@@ -72,7 +72,17 @@ export class CliControlClient {
72
72
  this.port =
73
73
  options.port || parseInt(process.env.SHADOWCLAW_PORT || "8888", 10);
74
74
  this.token = options.token || resolveControlToken(options.token);
75
- this.protocol = options.protocol || "http";
75
+ const isHttps = Boolean(
76
+ options.https ||
77
+ ["1", "true", "yes"].includes(
78
+ (process.env.SHADOWCLAW_HTTPS || "").toLowerCase().trim(),
79
+ ),
80
+ );
81
+ this.protocol = options.protocol || (isHttps ? "https" : "http");
82
+ this.rejectUnauthorized =
83
+ options.rejectUnauthorized !== undefined
84
+ ? Boolean(options.rejectUnauthorized)
85
+ : Boolean(options.insecure === false);
76
86
  this.transport = options.transport || "http";
77
87
  this._webrtcClient =
78
88
  this.transport === "webrtc"
@@ -81,6 +91,7 @@ export class CliControlClient {
81
91
  port: this.port,
82
92
  path: options.peerPath || "/",
83
93
  secure: this.protocol === "https",
94
+ rejectUnauthorized: this.rejectUnauthorized,
84
95
  peerId: options.peerId,
85
96
  cacheDir: options.cacheDir,
86
97
  renewPeerId: Boolean(options.renewPeerId),
@@ -99,35 +110,38 @@ export class CliControlClient {
99
110
  headers["x-control-token"] = this.token;
100
111
  }
101
112
 
102
- const req = client.request(
103
- {
104
- hostname: this.host,
105
- port: this.port,
106
- path: reqPath,
107
- method,
108
- headers,
109
- timeout,
110
- },
111
- (res) => {
112
- let data = "";
113
- res.on("data", (chunk) => (data += chunk));
114
- res.on("end", () => {
115
- let parsed = data;
116
- try {
117
- parsed = JSON.parse(data);
118
- } catch (_) {}
119
-
120
- if (res.statusCode >= 200 && res.statusCode < 300) {
121
- resolve(parsed);
122
- } else {
123
- const errMsg =
124
- parsed?.error ||
125
- `HTTP request failed with status ${res.statusCode}: ${data}`;
126
- reject(new Error(errMsg));
127
- }
128
- });
129
- },
130
- );
113
+ const reqOptions = {
114
+ hostname: this.host,
115
+ port: this.port,
116
+ path: reqPath,
117
+ method,
118
+ headers,
119
+ timeout,
120
+ };
121
+
122
+ if (this.protocol === "https") {
123
+ reqOptions.rejectUnauthorized = this.rejectUnauthorized;
124
+ }
125
+
126
+ const req = client.request(reqOptions, (res) => {
127
+ let data = "";
128
+ res.on("data", (chunk) => (data += chunk));
129
+ res.on("end", () => {
130
+ let parsed = data;
131
+ try {
132
+ parsed = JSON.parse(data);
133
+ } catch (_) {}
134
+
135
+ if (res.statusCode >= 200 && res.statusCode < 300) {
136
+ resolve(parsed);
137
+ } else {
138
+ const errMsg =
139
+ parsed?.error ||
140
+ `HTTP request failed with status ${res.statusCode}: ${data}`;
141
+ reject(new Error(errMsg));
142
+ }
143
+ });
144
+ });
131
145
 
132
146
  req.on("error", (err) => {
133
147
  reject(
@@ -80,4 +80,14 @@ describe("CliControlClient", () => {
80
80
  const backups = await client.listBackups();
81
81
  expect(Array.isArray(backups)).toBe(true);
82
82
  });
83
+
84
+ it("detects HTTPS protocol from options and environment", () => {
85
+ const client1 = new CliControlClient({ https: true });
86
+ expect(client1.protocol).toBe("https");
87
+
88
+ process.env.SHADOWCLAW_HTTPS = "1";
89
+ const client2 = new CliControlClient();
90
+ expect(client2.protocol).toBe("https");
91
+ delete process.env.SHADOWCLAW_HTTPS;
92
+ });
83
93
  });
@@ -203,6 +203,7 @@ export class CliWebRtcControlClient {
203
203
  options.port || parseInt(process.env.SHADOWCLAW_PORT || "8888", 10);
204
204
  this.path = options.path || "/";
205
205
  this.secure = options.secure ?? false;
206
+ this.rejectUnauthorized = options.rejectUnauthorized ?? false;
206
207
  this.peer = null;
207
208
  this.cacheDir = options.cacheDir;
208
209
  this.cliPeerId =
@@ -221,12 +222,19 @@ export class CliWebRtcControlClient {
221
222
  const Peer = mod.default?.Peer || mod.default || mod.Peer;
222
223
 
223
224
  return new Promise((resolve, reject) => {
224
- const peer = new Peer(this.cliPeerId, {
225
+ const peerConfig = {
225
226
  host: this.host,
226
227
  port: this.port,
227
228
  path: this.path,
228
229
  secure: this.secure,
229
- });
230
+ };
231
+
232
+ if (this.secure) {
233
+ peerConfig.config = { iceServers: [] };
234
+ peerConfig.wsOptions = { rejectUnauthorized: this.rejectUnauthorized };
235
+ }
236
+
237
+ const peer = new Peer(this.cliPeerId, peerConfig);
230
238
 
231
239
  this.peer = peer;
232
240
 
@@ -409,6 +417,7 @@ export class CliWebRtcListener {
409
417
  options.port || parseInt(process.env.SHADOWCLAW_PORT || "8888", 10);
410
418
  this.path = options.path || "/";
411
419
  this.secure = options.secure ?? false;
420
+ this.rejectUnauthorized = options.rejectUnauthorized ?? false;
412
421
  this.cacheDir = options.cacheDir;
413
422
  this.verbose = options.verbose ?? false;
414
423
  this.handlers = options.handlers || {};
@@ -602,12 +611,22 @@ export class CliWebRtcListener {
602
611
  return new Promise((resolve, reject) => {
603
612
  let isOpened = false;
604
613
 
605
- const peer = new Peer(this.cliPeerId, {
614
+ const peerConfig = {
606
615
  host: this.host,
607
616
  port: this.port,
608
617
  path: this.path,
609
618
  secure: this.secure,
610
- });
619
+ };
620
+
621
+ // When using self-signed certificates (typical for local dev), Node's
622
+ // WebSocket client rejects the connection by default. Pass rejectUnauthorized
623
+ // via PeerJS's ws-specific config option to allow self-signed certs.
624
+ if (this.secure) {
625
+ peerConfig.config = { iceServers: [] };
626
+ peerConfig.wsOptions = { rejectUnauthorized: this.rejectUnauthorized };
627
+ }
628
+
629
+ const peer = new Peer(this.cliPeerId, peerConfig);
611
630
 
612
631
  this._peer = peer;
613
632
  this._running = true;
@@ -106,6 +106,13 @@ To prevent infinite execution loops, the system enforces a strict recursion guar
106
106
  - Do not call `executeTool` from provider loops without passing the active enabled tool list (`allowedTools`). Runtime allowlist checks are enforced in `executeTool` and are required even when schemas already constrain generation.
107
107
  - Conversation-level `pinnedMaxTokens` overrides are optional and must remain model-aware (clamped to provider/model output limits).
108
108
 
109
+ ### Private Network Access & Loopback Connections
110
+
111
+ - **Service Worker Loopback Bypass:** The Service Worker fetch proxy (`src/service-worker/fetch-proxy-rules.ts`) unconditionally bypasses all loopback (`localhost`, `127.0.0.1`, `::1`, `[::1]`) and private IP ranges (`10.x`, `172.16-31.x`, `192.168.x`) so browser tabs hosted on static platforms (such as GitHub Pages or Cloudflare Pages) can connect directly to local servers without Service Worker interception.
112
+ - **Client `targetAddressSpace` Signaling:** Outgoing client fetch requests targeting local or private network endpoints (`control-plane-client.ts`, `backup-controller.ts`, `push-client.ts`, `task.ts`, and `fetch-proxy.ts`) must supply `targetAddressSpace: 'loopback'` (or `'private'`) for Chromium Private Network Access (PNA) compliance.
113
+ - **Server PNA & Cross-Origin Headers:** Server endpoints set `Access-Control-Allow-Private-Network: true` on both preflight (`OPTIONS`) and cross-origin responses, and trust `.github.io` / `.pages.dev` / `allowedOrigins` origins for browser integration.
114
+
115
+
109
116
  ### HTML Sanitization & Trusted Types
110
117
 
111
118
  - **Explicit Pre-Sanitization:** All dynamically rendered HTML, inline SVGs, or iframe `srcdoc` values must be sanitized using DOMPurify (e.g., `sanitizeToTrustedHtml` or `sanitizeSrcdocHtml`) **before** being passed to the Trusted Types policy.
@@ -152,7 +159,7 @@ Markdown and HTML preview work should preserve the Settings-backed iframe host a
152
159
  ### CLI & Dual-Root Build Pipeline
153
160
 
154
161
  - **Dual-Root Path Resolution:** The build toolchain (`bin/build/build.mjs`) cleanly decouples `toolchainRoot` (the ShadowClaw package/repo root) from `contentRoot` (the consumer template project). In-repo builds (`resolve(contentRoot) === resolve(toolchainRoot)`) preserve the standalone in-tree compilation path. CLI/template consumer builds read pre-bundled web assets from `toolchainRoot/dist/public` and inject `pages/`, `site-config.json`, `assets/`, `.agents/`, and pretty routes from `contentRoot`, outputting to `<contentRoot>/dist/public`.
155
- - **CLI Commands (`bin/cli.mjs`):** The `shadow-claw` / `shadowclaw` CLI provides `build`, `dev`, `run`, `serve`, `init`, `clients`, `send`, `backup`, `tasks`, `webrtc`, and `peer-id` commands. It supports running dev servers programmatically via `startServer` (`src/server/server.ts`) with custom `--root-path` and `--database-dir` arguments.
162
+ - **CLI Commands (`bin/cli.mjs`):** The `shadow-claw` / `shadowclaw` CLI provides `build`, `dev`, `run`, `serve`, `init`, `clients`, `send`, `backup`, `tasks`, `webrtc`, and `peer-id` commands. It supports running dev servers programmatically via `startServer` (`src/server/server.ts`) with custom `--root-path` and `--database-dir` arguments. Dev/run/serve commands accept `--https`, `--cert <path>`, `--key <path>`, and `--ssl-dir <path>` for opt-in HTTPS with auto-generated self-signed certs; control plane commands (`clients`, `send`, `backup`, `tasks`) accept `--https` and `-k, --insecure` to reach an HTTPS control plane server.
156
163
  - **Naming Conventions:** Refer to the product/brand in prose and documentation as **ShadowClaw**. Use kebab-case **`shadow-claw`** for package name, CLI commands (`npx shadow-claw`), repositories, directory paths, and custom elements.
157
164
 
158
165
  ## What to Avoid
@@ -383,6 +383,7 @@ E2E test architecture: [`e2e/README.md`](e2e/README.md)
383
383
 
384
384
  ```bash
385
385
  npm run dev # Dev server (watch mode)
386
+ npm run dev -- --https # Dev server with opt-in HTTPS (auto-generates self-signed cert)
386
387
  npm start # Express server
387
388
  npm test # Jest (*.test.ts files live next to source)
388
389
  npm run e2e # Playwright E2E tests (e2e/*.test.ts)
@@ -413,7 +414,7 @@ npx shadow-claw webrtc listen # Start headless WebRTC Dat
413
414
  npx shadow-claw peer-id # Get or generate persistent CLI Peer ID
414
415
  ```
415
416
 
416
- Commands support `--transport webrtc` for direct peer-to-peer DataChannel execution with connected browser clients. Control plane authentication uses `SHADOWCLAW_CONTROL_TOKEN` (env) or `--token` flag. The control plane endpoint and token are printed to the console on server start.
417
+ Commands support `--transport webrtc` for direct peer-to-peer DataChannel execution with connected browser clients. Control plane authentication uses `SHADOWCLAW_CONTROL_TOKEN` (env) or `--token` flag, and supports HTTPS endpoints via `--https` (and `--insecure` for self-signed certs). The control plane endpoint and token are printed to the console on server start.
417
418
 
418
419
  ## License
419
420