diff --git a/dashboard/screenshot-interact.mjs b/dashboard/screenshot-interact.mjs index 037ca53..9d3abd6 100644 --- a/dashboard/screenshot-interact.mjs +++ b/dashboard/screenshot-interact.mjs @@ -43,6 +43,7 @@ for (const action of actions) { } else if (action.startsWith('search:')) { const query = action.slice('search:'.length); const input = await page.$('input[placeholder="Search memories..."]'); + if (!input) { console.error(`search: input not found for "${query}"`); continue; } await input.type(query, { delay: 30 }); await new Promise((r) => setTimeout(r, 900)); } else if (action.startsWith('click:')) { diff --git a/dashboard/server.py b/dashboard/server.py index 9144ae5..a7fbd54 100644 --- a/dashboard/server.py +++ b/dashboard/server.py @@ -22,8 +22,8 @@ def spa(full_path: str): if not DIST_DIR.exists(): raise HTTPException(503, "dashboard/dist not built yet - run `npm run build` in dashboard/") - candidate = DIST_DIR / full_path - if full_path and candidate.is_file(): + candidate = (DIST_DIR / full_path).resolve() + if full_path and candidate.is_file() and candidate.is_relative_to(DIST_DIR.resolve()): return FileResponse(candidate) return FileResponse(DIST_DIR / "index.html") diff --git a/scripts/check_package_boundaries.py b/scripts/check_package_boundaries.py index 6475231..586bb9d 100644 --- a/scripts/check_package_boundaries.py +++ b/scripts/check_package_boundaries.py @@ -70,7 +70,7 @@ def main() -> int: problems = [ f"{path.as_posix()}: modules live in a package named for their type; only main.py sits in app/." for path in APP.glob("*.py") - if path.name != "main.py" + if path.name not in ("main.py", "__init__.py") ] allowed = allowlist() found = edges() diff --git a/services/mcp/memorygate_mcp.py b/services/mcp/memorygate_mcp.py index 16d4fe1..8cdc9e5 100644 --- a/services/mcp/memorygate_mcp.py +++ b/services/mcp/memorygate_mcp.py @@ -16,7 +16,7 @@ def redirect_request(self, request, file_pointer, code, message, headers, new_ur TOOL = { "name": "memorygate_context", "description": "Retrieve bounded, evidence-aware personal memory context. Read-only; use it before answering personal questions.", - "inputSchema": {"type": "object", "properties": {"query": {"type": "string"}, "include_evidence": {"type": "boolean"}}, "required": ["query"]}, + "inputSchema": {"type": "object", "additionalProperties": False, "properties": {"query": {"type": "string", "maxLength": 2000}, "include_evidence": {"type": "boolean"}}, "required": ["query"]}, } EXPLORE = { @@ -58,7 +58,10 @@ def exchange(path, payload): def context(arguments: dict) -> dict: - return exchange("context", {"query": arguments["query"], "compact": True, + query = arguments.get("query") + if not isinstance(query, str) or not query or len(query) > 2000: + raise ValueError("Invalid query") + return exchange("context", {"query": query, "compact": True, "max_items": 8, "include_evidence": bool(arguments.get("include_evidence", False))}) @@ -79,6 +82,9 @@ def main(): request = {} try: request = json.loads(line) + if not isinstance(request, dict): + respond(None, error="Invalid request: batch requests are not supported") + continue method = request.get("method") params = request.get("params", {}) if method == "initialize": @@ -91,8 +97,10 @@ def main(): raise RuntimeError("unknown tool") value = handler(params.get("arguments", {})) respond(request.get("id"), {"content": [{"type": "text", "text": json.dumps(value)}]}) - elif "id" in request: + elif method == "ping": respond(request.get("id"), {}) + elif "id" in request: + respond(request.get("id"), error="Method not found") except Exception: respond(request.get("id") if isinstance(request, dict) else None, error="Memory request failed; check configuration, scope and arguments.")