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 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
- function readBody(req) {
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
- req.on("data", (c) => chunks.push(c));
34667
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
34668
- req.on("error", () => resolve(""));
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"] ?? "") !== "Bearer " + token) {
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;
@@ -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.76",
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",