dsh-agy-link 0.4.76 → 0.4.77
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/CHANGELOG.md +16 -0
- package/dist/index.js +39 -8
- package/docs/KNOWN-GAPS.md +4 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.77 (2026-10-29)
|
|
4
|
+
|
|
5
|
+
### English
|
|
6
|
+
|
|
7
|
+
- **Security(mcp): harden the loopback endpoint against a token-holding, prompt-injected child.**
|
|
8
|
+
- **Bounded request body** — `/call` now caps the buffered body at 8 MiB and returns **413 `request body too large`** instead of buffering an unbounded payload, so a compromised/injected agy child cannot OOM the host process by streaming huge tool arguments.
|
|
9
|
+
- **Constant-time bearer check** — the shared-secret token is compared with `crypto.timingSafeEqual` (length-guarded) rather than `!==`, removing the timing side-channel.
|
|
10
|
+
|
|
11
|
+
### 中文
|
|
12
|
+
|
|
13
|
+
- **安全(MCP):加固 loopback 端点,防御持有 token、被提示词注入的子进程。**
|
|
14
|
+
- **请求体上限**:`/call` 现在把缓冲上限设为 8 MiB,超限直接返回 **413 `request body too large`**,避免被攻陷/注入的 agy 子进程用超大工具参数把宿主进程内存打爆。
|
|
15
|
+
- **常量时间 token 校验**:共享密钥改用 `crypto.timingSafeEqual`(先校验长度)比对,消除 `!==` 的时序侧信道。
|
|
16
|
+
|
|
17
|
+
**Full Changelog**: https://github.com/amlyczz/dsh-agy-link/compare/v0.4.76...v0.4.77
|
|
18
|
+
|
|
3
19
|
## 0.4.76 (2026-10-29)
|
|
4
20
|
|
|
5
21
|
### English
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
6
6
|
import { mkdir, mkdtemp, readFile, readdir, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
7
7
|
import * as dshLlm from "@deepseek-ai/dsh-llm";
|
|
8
8
|
import { LlmAdapter, LlmError } from "@deepseek-ai/dsh-llm";
|
|
9
|
-
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
9
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
10
10
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
11
11
|
import { EventEmitter } from "node:events";
|
|
12
12
|
import { createServer } from "node:http";
|
|
@@ -34660,14 +34660,41 @@ const INTERNAL_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
34660
34660
|
function toMcpName(name) {
|
|
34661
34661
|
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
34662
34662
|
}
|
|
34663
|
-
|
|
34663
|
+
/** Loopback request-body cap. Tool arguments are tiny; this only exists so a
|
|
34664
|
+
* token-holding (possibly prompt-injected) agy child cannot OOM the host by
|
|
34665
|
+
* streaming an unbounded body at /call. */
|
|
34666
|
+
const DEFAULT_MAX_BODY_BYTES = 8388608;
|
|
34667
|
+
function readBody(req, maxBytes) {
|
|
34664
34668
|
return new Promise((resolve) => {
|
|
34665
34669
|
const chunks = [];
|
|
34666
|
-
|
|
34667
|
-
|
|
34668
|
-
req.on("
|
|
34670
|
+
let size = 0;
|
|
34671
|
+
let tooLarge = false;
|
|
34672
|
+
req.on("data", (c) => {
|
|
34673
|
+
size += c.length;
|
|
34674
|
+
if (size > maxBytes) {
|
|
34675
|
+
tooLarge = true;
|
|
34676
|
+
chunks.length = 0;
|
|
34677
|
+
return;
|
|
34678
|
+
}
|
|
34679
|
+
chunks.push(c);
|
|
34680
|
+
});
|
|
34681
|
+
req.on("end", () => resolve({
|
|
34682
|
+
tooLarge,
|
|
34683
|
+
text: tooLarge ? "" : Buffer.concat(chunks).toString("utf8")
|
|
34684
|
+
}));
|
|
34685
|
+
req.on("error", () => resolve({
|
|
34686
|
+
tooLarge,
|
|
34687
|
+
text: ""
|
|
34688
|
+
}));
|
|
34669
34689
|
});
|
|
34670
34690
|
}
|
|
34691
|
+
/** Constant-time bearer comparison (the token is a shared secret). */
|
|
34692
|
+
function bearerMatches(provided, expected) {
|
|
34693
|
+
const a = Buffer.from(provided, "utf8");
|
|
34694
|
+
const b = Buffer.from(expected, "utf8");
|
|
34695
|
+
if (a.length !== b.length) return false;
|
|
34696
|
+
return timingSafeEqual(a, b);
|
|
34697
|
+
}
|
|
34671
34698
|
function sendJson(res, status, body) {
|
|
34672
34699
|
const text = JSON.stringify(body);
|
|
34673
34700
|
res.writeHead(status, {
|
|
@@ -34706,7 +34733,7 @@ function startMcpBridge(opts) {
|
|
|
34706
34733
|
const server = createServer((req, res) => {
|
|
34707
34734
|
(async () => {
|
|
34708
34735
|
const url = (req.url ?? "").split("?")[0];
|
|
34709
|
-
if (String(req.headers["authorization"] ?? "")
|
|
34736
|
+
if (!bearerMatches(String(req.headers["authorization"] ?? ""), "Bearer " + token)) {
|
|
34710
34737
|
sendJson(res, 401, { error: "unauthorized" });
|
|
34711
34738
|
return;
|
|
34712
34739
|
}
|
|
@@ -34737,10 +34764,14 @@ function startMcpBridge(opts) {
|
|
|
34737
34764
|
return;
|
|
34738
34765
|
}
|
|
34739
34766
|
if (req.method === "POST" && (url === "/call" || url === "/mcp/call")) {
|
|
34740
|
-
const body = await readBody(req);
|
|
34767
|
+
const body = await readBody(req, opts.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES);
|
|
34768
|
+
if (body.tooLarge) {
|
|
34769
|
+
sendJson(res, 413, { error: "request body too large" });
|
|
34770
|
+
return;
|
|
34771
|
+
}
|
|
34741
34772
|
let parsed = {};
|
|
34742
34773
|
try {
|
|
34743
|
-
parsed = JSON.parse(body);
|
|
34774
|
+
parsed = JSON.parse(body.text);
|
|
34744
34775
|
} catch {
|
|
34745
34776
|
sendJson(res, 400, { error: "invalid JSON" });
|
|
34746
34777
|
return;
|
package/docs/KNOWN-GAPS.md
CHANGED
|
@@ -116,7 +116,10 @@ reference-counted slot is the strongest isolation the plugin can provide.
|
|
|
116
116
|
restricts the served set **and is
|
|
117
117
|
enforced at execution time** — a `/call` for a tool outside the allowlist is
|
|
118
118
|
refused (403) even when the caller holds the bridge token, so a prompt-injected
|
|
119
|
-
agy child cannot silently reach tools the operator did not expose.
|
|
119
|
+
agy child cannot silently reach tools the operator did not expose. The
|
|
120
|
+
loopback endpoint also caps request bodies (8 MiB → HTTP 413) and compares the
|
|
121
|
+
bearer token in constant time, so a token-holding but prompt-injected child
|
|
122
|
+
cannot OOM the host with an unbounded body or probe the token via timing.
|
|
120
123
|
Every bridged call is audit-logged as `mcp call <tool> [<outcome>] <ms>` with
|
|
121
124
|
the caller class and the failure text (never the arguments), so a failed run
|
|
122
125
|
is visible in journald instead of only in transcripts.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agy-link",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.77",
|
|
4
4
|
"description": "Google Antigravity (agy CLI) models for DeepSeek Harness — stream Gemini/Claude/GPT-OSS subscriptions into DSH with thinking, tool activity, token usage and in-GUI Google OAuth login.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|