@sleep2agi/commhub-server
Advanced tools
+2
-2
| { | ||
| "name": "@sleep2agi/commhub-server", | ||
| "version": "0.9.0-preview.12", | ||
| "description": "CommHub Server \u2014 AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and 17 MCP tools.", | ||
| "version": "0.9.0-preview.13", | ||
| "description": "CommHub Server — AI Agent communication hub with MCP protocol, multi-network isolation, user auth, and 17 MCP tools.", | ||
| "type": "module", | ||
@@ -6,0 +6,0 @@ "main": "src/index.ts", |
+66
-0
@@ -315,1 +315,67 @@ // #221 — unit tests for the upload primitives. The HTTP wire-up + Bun | ||
| }); | ||
| import { stripHostLocalPathsForCrossHostSafe } from "./uploads"; | ||
| describe("stripHostLocalPathsForCrossHostSafe (#222 cross-host attachment safety)", () => { | ||
| test("strips path when file_id is present (cross-host safe — agent will fetch via /api/files/<id>)", () => { | ||
| const meta = { | ||
| attachments: [ | ||
| { type: "file", file_id: "abc12345", path: "/home/hub/uploads/abc12345.png", mime: "image/png" }, | ||
| ], | ||
| otherKey: "preserved", | ||
| }; | ||
| const out = stripHostLocalPathsForCrossHostSafe(meta); | ||
| expect(out.attachments[0]).toEqual({ type: "file", file_id: "abc12345", mime: "image/png" }); | ||
| expect(out.attachments[0].path).toBeUndefined(); | ||
| expect(out.otherKey).toBe("preserved"); | ||
| }); | ||
| test("PRESERVES path when file_id is absent (feishu single-host fallback)", () => { | ||
| // 🔴 通信龙 PR #222 pre-review nit — feishu-bridge ships path-only | ||
| // attachments (writes /work/feishu-attachments/...). If we strip | ||
| // unconditionally those break too. Lock the conditional contract. | ||
| const meta = { | ||
| attachments: [ | ||
| { type: "file", path: "/work/feishu-attachments/feishu-local/oc_xxx/img.jpg", mime: "image/jpeg" }, | ||
| ], | ||
| }; | ||
| const out = stripHostLocalPathsForCrossHostSafe(meta); | ||
| expect(out.attachments[0].path).toBe("/work/feishu-attachments/feishu-local/oc_xxx/img.jpg"); | ||
| expect(out.attachments[0].file_id).toBeUndefined(); | ||
| }); | ||
| test("mixed array: strips path on entries with file_id, preserves on path-only entries", () => { | ||
| const meta = { | ||
| attachments: [ | ||
| { type: "file", file_id: "id1xxxxx", path: "/hub/local/id1.png", mime: "image/png" }, | ||
| { type: "file", path: "/work/feishu-attachments/x.jpg", mime: "image/jpeg" }, | ||
| { type: "file", file_id: "id2yyyyy", mime: "image/gif" }, // already path-free | ||
| ], | ||
| }; | ||
| const out = stripHostLocalPathsForCrossHostSafe(meta); | ||
| expect(out.attachments[0]).toEqual({ type: "file", file_id: "id1xxxxx", mime: "image/png" }); | ||
| expect(out.attachments[1]).toEqual({ type: "file", path: "/work/feishu-attachments/x.jpg", mime: "image/jpeg" }); | ||
| expect(out.attachments[2]).toEqual({ type: "file", file_id: "id2yyyyy", mime: "image/gif" }); | ||
| }); | ||
| test("no-op when meta has no attachments array", () => { | ||
| const meta = { something: "else" }; | ||
| expect(stripHostLocalPathsForCrossHostSafe(meta)).toBe(meta); | ||
| }); | ||
| test("no-op when nothing to strip (returns SAME object reference, not a copy)", () => { | ||
| const meta = { attachments: [{ type: "file", file_id: "x12345xx", mime: "image/png" }] }; | ||
| expect(stripHostLocalPathsForCrossHostSafe(meta)).toBe(meta); | ||
| }); | ||
| test("handles null / non-object meta gracefully", () => { | ||
| expect(stripHostLocalPathsForCrossHostSafe(null)).toBe(null); | ||
| expect(stripHostLocalPathsForCrossHostSafe(undefined as any)).toBe(undefined); | ||
| expect(stripHostLocalPathsForCrossHostSafe("string" as any)).toBe("string"); | ||
| }); | ||
| test("handles non-array attachments field gracefully", () => { | ||
| const meta = { attachments: "not-an-array" }; | ||
| expect(stripHostLocalPathsForCrossHostSafe(meta)).toBe(meta); | ||
| }); | ||
| }); |
+36
-0
@@ -244,2 +244,38 @@ // #221 — commhub-server file upload primitives. | ||
| /** | ||
| * #222 cross-host attachment safety — strip `path` from meta.attachments | ||
| * entries that ALSO carry a `file_id`. Reason: the hub's /api/upload | ||
| * returns `{file_id, path}` where `path` is a hub-machine-local | ||
| * absolute path. Senders sometimes echo `path` back into meta.attachments. | ||
| * When the receiving agent runs on a different host (Vincent 2026-06-30 | ||
| * toodadev2 case), that path string is meaningless. agent-node's | ||
| * fetch-attachment.ts will fetch via `file_id` regardless, but leaving | ||
| * the stale `path` in the JSON misleads any tooling that reads it. | ||
| * | ||
| * CRITICAL — conditional, not unconditional (通信龙 PR #222 pre-review | ||
| * nit): feishu-bridge currently writes `/work/feishu-attachments/<conn>/ | ||
| * <chat>/` directly and ships path-only attachments (NO file_id). If we | ||
| * strip path unconditionally those break too. Only strip when file_id | ||
| * is present (the "safe to drop" signal). Single-host feishu fallback | ||
| * preserved. | ||
| * | ||
| * Shared by tools.ts (MCP send_task) and index.ts (REST /api/task) so | ||
| * both transports apply the same sanitization to meta. | ||
| */ | ||
| export function stripHostLocalPathsForCrossHostSafe(meta: any): any { | ||
| if (!meta || typeof meta !== "object") return meta; | ||
| if (!Array.isArray((meta as any).attachments)) return meta; | ||
| let mutated = false; | ||
| const sanitized = (meta as any).attachments.map((a: any) => { | ||
| if (a && typeof a === "object" && typeof a.file_id === "string" && a.file_id && typeof a.path === "string") { | ||
| mutated = true; | ||
| const { path: _stripped, ...rest } = a; | ||
| return rest; | ||
| } | ||
| return a; | ||
| }); | ||
| if (!mutated) return meta; | ||
| return { ...meta, attachments: sanitized }; | ||
| } | ||
| /** Light validator usable from the REST handler. Returns null on success or an error message string. */ | ||
@@ -246,0 +282,0 @@ export function validateAttachments(input: unknown): { ok: true; attachments: TaskAttachment[] } | { ok: false; error: string } { |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
833298
0.68%16765
0.63%72
-1.37%