From 212495c8e2c794d728d12aa7325c25793680b4da Mon Sep 17 00:00:00 2001 From: bitmaster162 <115934939+bitmaster162@users.noreply.github.com> Date: Wed, 16 Sep 2026 22:45:05 +0700 Subject: [PATCH 1/5] feat(mcp): add governed remote commander read-only surface --- continuityos/remote_mcp_server.py | 370 ++++++++++++++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 continuityos/remote_mcp_server.py diff --git a/continuityos/remote_mcp_server.py b/continuityos/remote_mcp_server.py new file mode 100644 index 0000000..4797b4c --- /dev/null +++ b/continuityos/remote_mcp_server.py @@ -0,0 +1,370 @@ +"""Governed Remote Commander extension for the ContinuityOS MCP server. + +R1 intentionally adds only bounded read-only host inspection. Mutating command +execution is not reimplemented here: callers must use the inherited +``preflight_exec`` -> ``execute_preflight`` GateBroker path. + +The remote surface is fail-closed. Enable it explicitly with +``CONTINUITYOS_REMOTE_ENABLED=1`` or ``--enable-remote`` and scope filesystem +access with ``CONTINUITYOS_REMOTE_ROOTS`` or one or more ``--remote-root`` +arguments. +""" +from __future__ import annotations + +import argparse +import fnmatch +import json +import os +import platform +from pathlib import Path +import socket +import sys +from typing import Iterable + +from . import __version__ +from .mcp_server import PROTOCOL, TOOLS as BASE_TOOLS, Server as BaseServer + +MAX_READ_BYTES = 256 * 1024 +MAX_LIST_ENTRIES = 500 + +_SENSITIVE_DIRS = { + ".ssh", + ".gnupg", + ".aws", + ".azure", + ".kube", +} +_SENSITIVE_FILE_PATTERNS = ( + ".env", + ".env.*", + "*.pem", + "*.key", + "*.p12", + "*.pfx", + "id_rsa", + "id_rsa.*", + "id_ed25519", + "id_ed25519.*", + "credentials", + "credentials.*", + "secrets", + "secrets.*", +) + +REMOTE_TOOLS = [ + { + "name": "capability_status", + "description": ( + "Report the ContinuityOS Remote Commander capability boundary: " + "enabled state, allowed roots, read limits, and governed execution path." + ), + "inputSchema": {"type": "object", "additionalProperties": False, "properties": {}}, + }, + { + "name": "system_info", + "description": ( + "Read-only host identity and runtime information. Does not return environment " + "variables, credentials, or process contents." + ), + "inputSchema": {"type": "object", "additionalProperties": False, "properties": {}}, + }, + { + "name": "fs_list", + "description": ( + "List one directory inside an explicitly allowed remote root. Sensitive files " + "and credential directories are omitted. Read-only." + ), + "inputSchema": { + "type": "object", + "additionalProperties": False, + "properties": { + "path": {"type": "string", "default": "."}, + "limit": {"type": "integer", "minimum": 1, "maximum": MAX_LIST_ENTRIES, "default": 200}, + }, + }, + }, + { + "name": "fs_read", + "description": ( + "Read a bounded UTF-8 text file inside an explicitly allowed remote root. " + "Known credential/key paths are denied. Read-only." + ), + "inputSchema": { + "type": "object", + "additionalProperties": False, + "properties": { + "path": {"type": "string", "minLength": 1, "maxLength": 32768}, + "max_bytes": { + "type": "integer", + "minimum": 1, + "maximum": MAX_READ_BYTES, + "default": 65536, + }, + }, + "required": ["path"], + }, + }, +] + +TOOLS = [*BASE_TOOLS, *REMOTE_TOOLS] + + +def _env_enabled(value: str | None) -> bool: + return (value or "").strip().lower() in {"1", "true", "yes", "on"} + + +def _canonical_roots(values: Iterable[str | os.PathLike[str]] | None) -> list[Path]: + raw = list(values or []) + if not raw: + configured = os.environ.get("CONTINUITYOS_REMOTE_ROOTS", "") + raw = [item for item in configured.split(os.pathsep) if item] + if not raw: + raw = [os.getcwd()] + + roots: list[Path] = [] + for value in raw: + root = Path(value).expanduser().resolve(strict=True) + if not root.is_dir(): + raise ValueError(f"remote root is not a directory: {root}") + if root not in roots: + roots.append(root) + return roots + + +class RemoteSurface: + """Fail-closed read-only host surface with root containment and secret denial.""" + + def __init__(self, *, enabled: bool | None = None, roots: Iterable[str | os.PathLike[str]] | None = None): + self.enabled = _env_enabled(os.environ.get("CONTINUITYOS_REMOTE_ENABLED")) if enabled is None else bool(enabled) + self.roots = _canonical_roots(roots) + + def _require_enabled(self) -> None: + if not self.enabled: + raise PermissionError( + "remote commander disabled; set CONTINUITYOS_REMOTE_ENABLED=1 or pass --enable-remote" + ) + + @staticmethod + def _sensitive_reason(path: Path) -> str | None: + lowered_parts = [part.lower() for part in path.parts] + for part in lowered_parts: + if part in _SENSITIVE_DIRS: + return f"sensitive directory denied: {part}" + name = path.name.lower() + for pattern in _SENSITIVE_FILE_PATTERNS: + if fnmatch.fnmatch(name, pattern): + return f"sensitive file denied: {pattern}" + return None + + def _resolve(self, value: str | os.PathLike[str]) -> Path: + self._require_enabled() + candidate = Path(value).expanduser() + if not candidate.is_absolute(): + candidate = self.roots[0] / candidate + resolved = candidate.resolve(strict=True) + if not any(resolved == root or resolved.is_relative_to(root) for root in self.roots): + raise PermissionError("path is outside configured remote roots") + reason = self._sensitive_reason(resolved) + if reason: + raise PermissionError(reason) + return resolved + + def status(self) -> dict: + return { + "enabled": self.enabled, + "mode": "read_only_host_surface_plus_governed_exec", + "roots": [str(root) for root in self.roots], + "max_read_bytes": MAX_READ_BYTES, + "max_list_entries": MAX_LIST_ENTRIES, + "mutating_execution": { + "direct_shell": False, + "path": ["preflight_exec", "execute_preflight"], + "governor": "ContinuityOS GateBroker", + }, + "secret_policy": "deny known credential/key files and sensitive credential directories", + } + + def system_info(self) -> dict: + self._require_enabled() + return { + "hostname": socket.gethostname(), + "platform": platform.system(), + "platform_release": platform.release(), + "machine": platform.machine(), + "python": platform.python_version(), + "cwd": os.getcwd(), + } + + def list_dir(self, path: str = ".", *, limit: int = 200) -> dict: + directory = self._resolve(path) + if not directory.is_dir(): + raise NotADirectoryError(str(directory)) + if not isinstance(limit, int) or isinstance(limit, bool) or not (1 <= limit <= MAX_LIST_ENTRIES): + raise ValueError(f"limit must be an integer in 1..{MAX_LIST_ENTRIES}") + + entries = [] + omitted_sensitive = 0 + for child in sorted(directory.iterdir(), key=lambda item: item.name.lower()): + if self._sensitive_reason(child): + omitted_sensitive += 1 + continue + if len(entries) >= limit: + break + stat = child.lstat() + if child.is_symlink(): + kind = "symlink" + elif child.is_dir(): + kind = "directory" + elif child.is_file(): + kind = "file" + else: + kind = "other" + entries.append({"name": child.name, "type": kind, "size": stat.st_size}) + return { + "path": str(directory), + "entries": entries, + "returned": len(entries), + "limit": limit, + "omitted_sensitive": omitted_sensitive, + } + + def read_text(self, path: str, *, max_bytes: int = 65536) -> dict: + target = self._resolve(path) + if not target.is_file(): + raise FileNotFoundError(f"not a regular file: {target}") + if not isinstance(max_bytes, int) or isinstance(max_bytes, bool) or not (1 <= max_bytes <= MAX_READ_BYTES): + raise ValueError(f"max_bytes must be an integer in 1..{MAX_READ_BYTES}") + + with target.open("rb") as handle: + data = handle.read(max_bytes + 1) + truncated = len(data) > max_bytes + data = data[:max_bytes] + if b"\x00" in data: + raise ValueError("binary file refused") + try: + text = data.decode("utf-8") + except UnicodeDecodeError as exc: + raise ValueError("non-UTF-8 file refused") from exc + return { + "path": str(target), + "text": text, + "bytes": len(data), + "truncated": truncated, + } + + +class RemoteServer(BaseServer): + def __init__(self, db=None, policy_path: str = "", db_source: str = "", *, remote_enabled: bool | None = None, remote_roots=None): + super().__init__(db, policy_path, db_source) + self.remote = RemoteSurface(enabled=remote_enabled, roots=remote_roots) + + def call(self, name, args): + if name == "capability_status": + self.turns += 1 + return json.dumps(self.remote.status(), ensure_ascii=False, indent=2) + if name == "system_info": + self.turns += 1 + return json.dumps(self.remote.system_info(), ensure_ascii=False, indent=2) + if name == "fs_list": + self.turns += 1 + return json.dumps( + self.remote.list_dir(args.get("path", "."), limit=args.get("limit", 200)), + ensure_ascii=False, + indent=2, + ) + if name == "fs_read": + self.turns += 1 + return json.dumps( + self.remote.read_text(args["path"], max_bytes=args.get("max_bytes", 65536)), + ensure_ascii=False, + indent=2, + ) + return super().call(name, args) + + +def _send(obj) -> None: + sys.stdout.write(json.dumps(obj) + "\n") + sys.stdout.flush() + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--db", default=None) + parser.add_argument("--policy", default="", help="Path to one JSON policy, or YAML when PyYAML is installed") + parser.add_argument("--enable-remote", action="store_true", default=None) + parser.add_argument( + "--remote-root", + action="append", + default=None, + help="Allowed read-only filesystem root; repeat for multiple roots.", + ) + args = parser.parse_args() + server = RemoteServer( + args.db, + args.policy, + remote_enabled=args.enable_remote, + remote_roots=args.remote_root, + ) + + for line in sys.stdin: + line = line.strip() + if not line: + continue + try: + request = json.loads(line) + except Exception: + continue + message_id = request.get("id") + method = request.get("method") + if method == "initialize": + _send( + { + "jsonrpc": "2.0", + "id": message_id, + "result": { + "protocolVersion": PROTOCOL, + "capabilities": {"tools": {}}, + "serverInfo": {"name": "continuityos-remote", "version": __version__}, + }, + } + ) + elif method == "notifications/initialized": + continue + elif method == "tools/list": + _send({"jsonrpc": "2.0", "id": message_id, "result": {"tools": TOOLS}}) + elif method == "tools/call": + params = request.get("params", {}) or {} + try: + output = server.call(params.get("name"), params.get("arguments", {}) or {}) + _send( + { + "jsonrpc": "2.0", + "id": message_id, + "result": {"content": [{"type": "text", "text": str(output)}]}, + } + ) + except Exception as exc: + _send( + { + "jsonrpc": "2.0", + "id": message_id, + "result": { + "isError": True, + "content": [{"type": "text", "text": f"error: {exc}"}], + }, + } + ) + elif method == "ping": + _send({"jsonrpc": "2.0", "id": message_id, "result": {}}) + elif message_id is not None: + _send( + { + "jsonrpc": "2.0", + "id": message_id, + "error": {"code": -32601, "message": f"method not found: {method}"}, + } + ) + + +if __name__ == "__main__": + main() From b892c1155f12191ac1b0a190a1acc781412c36a6 Mon Sep 17 00:00:00 2001 From: bitmaster162 <115934939+bitmaster162@users.noreply.github.com> Date: Wed, 16 Sep 2026 22:45:23 +0700 Subject: [PATCH 2/5] test(mcp): cover remote commander containment and secret denial --- tests/test_remote_mcp_server.py | 74 +++++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 tests/test_remote_mcp_server.py diff --git a/tests/test_remote_mcp_server.py b/tests/test_remote_mcp_server.py new file mode 100644 index 0000000..8e8536c --- /dev/null +++ b/tests/test_remote_mcp_server.py @@ -0,0 +1,74 @@ +import json + +import pytest + +from continuityos.remote_mcp_server import RemoteSurface, TOOLS + + +def _tool_names(): + return {tool["name"] for tool in TOOLS} + + +def test_remote_tools_extend_existing_governed_exec_surface(): + names = _tool_names() + assert {"capability_status", "system_info", "fs_list", "fs_read"} <= names + assert {"preflight_exec", "execute_preflight"} <= names + + +def test_remote_surface_is_disabled_by_default_when_explicitly_disabled(tmp_path): + surface = RemoteSurface(enabled=False, roots=[tmp_path]) + with pytest.raises(PermissionError, match="remote commander disabled"): + surface.list_dir(".") + + +def test_list_and_read_are_root_bounded_and_hide_sensitive_files(tmp_path): + (tmp_path / "notes.txt").write_text("hello continuity", encoding="utf-8") + (tmp_path / ".env").write_text("TOKEN=secret", encoding="utf-8") + nested = tmp_path / "nested" + nested.mkdir() + + surface = RemoteSurface(enabled=True, roots=[tmp_path]) + listing = surface.list_dir(".") + names = {entry["name"] for entry in listing["entries"]} + assert "notes.txt" in names + assert "nested" in names + assert ".env" not in names + assert listing["omitted_sensitive"] == 1 + + read = surface.read_text("notes.txt") + assert read["text"] == "hello continuity" + assert read["truncated"] is False + + with pytest.raises(PermissionError, match="sensitive file denied"): + surface.read_text(".env") + + +def test_path_escape_is_denied(tmp_path): + root = tmp_path / "root" + root.mkdir() + outside = tmp_path / "outside.txt" + outside.write_text("outside", encoding="utf-8") + + surface = RemoteSurface(enabled=True, roots=[root]) + with pytest.raises(PermissionError, match="outside configured remote roots"): + surface.read_text(outside) + + +def test_binary_and_oversized_limits_are_rejected(tmp_path): + (tmp_path / "binary.dat").write_bytes(b"abc\x00def") + (tmp_path / "text.txt").write_text("abcdef", encoding="utf-8") + surface = RemoteSurface(enabled=True, roots=[tmp_path]) + + with pytest.raises(ValueError, match="binary file refused"): + surface.read_text("binary.dat") + with pytest.raises(ValueError, match="max_bytes"): + surface.read_text("text.txt", max_bytes=0) + + +def test_status_declares_no_direct_shell(tmp_path): + surface = RemoteSurface(enabled=True, roots=[tmp_path]) + status = surface.status() + assert status["enabled"] is True + assert status["mutating_execution"]["direct_shell"] is False + assert status["mutating_execution"]["path"] == ["preflight_exec", "execute_preflight"] + json.dumps(status) From ea5d8e86470646e06beb01905a24756796f6b71f Mon Sep 17 00:00:00 2001 From: bitmaster162 <115934939+bitmaster162@users.noreply.github.com> Date: Wed, 16 Sep 2026 22:45:40 +0700 Subject: [PATCH 3/5] docs(mcp): document remote commander R1 boundary --- docs/REMOTE_COMMANDER_R1.md | 79 +++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/REMOTE_COMMANDER_R1.md diff --git a/docs/REMOTE_COMMANDER_R1.md b/docs/REMOTE_COMMANDER_R1.md new file mode 100644 index 0000000..861323c --- /dev/null +++ b/docs/REMOTE_COMMANDER_R1.md @@ -0,0 +1,79 @@ +# ContinuityOS Remote Commander R1 + +## Purpose + +R1 extends the existing ContinuityOS MCP server with a fail-closed, bounded, +read-only host inspection surface. It deliberately does **not** add a second +shell implementation. + +The existing ContinuityOS execution path remains authoritative for mutating +commands: + +`preflight_exec -> GateBroker -> execute_preflight` + +This preserves policy evaluation, durable request identity, witness validation, +and execution receipts instead of creating an ungoverned subprocess bypass. + +## R1 tools + +- `capability_status` — reports the effective boundary and allowed roots. +- `system_info` — OS/runtime identity only; no environment-variable dump. +- `fs_list` — bounded directory listing inside configured roots. +- `fs_read` — bounded UTF-8 text reads inside configured roots. +- Existing `preflight_exec` / `execute_preflight` — the only mutation-capable + command path exposed by this extension. + +## Fail-closed defaults + +Remote host inspection is disabled unless explicitly enabled. + +PowerShell example: + +```powershell +$env:CONTINUITYOS_REMOTE_ENABLED = "1" +$env:CONTINUITYOS_REMOTE_ROOTS = "C:\Repos;C:\Work" +python -m continuityos.remote_mcp_server --db "$HOME\.continuityos\memory.db" +``` + +Equivalent CLI configuration: + +```powershell +python -m continuityos.remote_mcp_server ` + --enable-remote ` + --remote-root C:\Repos ` + --remote-root C:\Work ` + --db "$HOME\.continuityos\memory.db" +``` + +If no root is configured, the process working directory is the only root. A +path must resolve inside one configured root. Symlink/path traversal that +resolves outside the roots is denied. + +## Secret boundary + +R1 refuses known credential/key locations and file patterns, including common +SSH/GPG/cloud credential directories, `.env*`, private-key formats, and +credential/secret files. Reads are UTF-8 text only and capped at 256 KiB. +Directory listings are capped at 500 entries and omit sensitive entries. + +This is defense in depth, not a claim that file-name filtering can classify all +secrets. Remote roots should therefore be narrow and purpose-specific. + +## Transport boundary + +R1 is a **stdio MCP extension**. It prepares the governed host-side capability +but does not expose a public network listener and does not open an inbound port. + +The next transport layer should use an outbound authenticated tunnel/agent to a +remote MCP endpoint suitable for ChatGPT. Authentication material must not be +stored in the repository. The remote endpoint must preserve the same tool +boundary and must not introduce a direct shell route around GateBroker. + +## Baseline evidence + +- Repository: `bitmaster162/continuityos` +- Baseline branch: `master` +- Baseline commit: `502df1efe0741f9681b7a2cb7988240113bda266` +- Implementation branch: `agent/r23-remote-commander-mcp-r1` + +No change in this R1 is intended to modify `master` directly. From b9d02b5eb43bb8938d243da576808242c2a7b4cc Mon Sep 17 00:00:00 2001 From: bitmaster162 <115934939+bitmaster162@users.noreply.github.com> Date: Wed, 16 Sep 2026 22:48:22 +0700 Subject: [PATCH 4/5] fix(mcp): harden remote secret boundary --- continuityos/remote_mcp_server.py | 163 ++++++++++++++++++++++++------ 1 file changed, 132 insertions(+), 31 deletions(-) diff --git a/continuityos/remote_mcp_server.py b/continuityos/remote_mcp_server.py index 4797b4c..429e1f8 100644 --- a/continuityos/remote_mcp_server.py +++ b/continuityos/remote_mcp_server.py @@ -1,10 +1,10 @@ """Governed Remote Commander extension for the ContinuityOS MCP server. -R1 intentionally adds only bounded read-only host inspection. Mutating command -execution is not reimplemented here: callers must use the inherited +R1 adds bounded read-only host inspection. Mutation-capable command execution is +not reimplemented here: callers must use the inherited ``preflight_exec`` -> ``execute_preflight`` GateBroker path. -The remote surface is fail-closed. Enable it explicitly with +The host surface is fail-closed. Enable it explicitly with ``CONTINUITYOS_REMOTE_ENABLED=1`` or ``--enable-remote`` and scope filesystem access with ``CONTINUITYOS_REMOTE_ROOTS`` or one or more ``--remote-root`` arguments. @@ -27,16 +27,25 @@ MAX_READ_BYTES = 256 * 1024 MAX_LIST_ENTRIES = 500 +# Defense in depth only. Narrow allow-roots remain the primary boundary. _SENSITIVE_DIRS = { + ".git", ".ssh", ".gnupg", ".aws", ".azure", ".kube", + ".docker", + ".terraform.d", } _SENSITIVE_FILE_PATTERNS = ( ".env", ".env.*", + ".netrc", + "_netrc", + ".npmrc", + ".pypirc", + ".git-credentials", "*.pem", "*.key", "*.p12", @@ -58,7 +67,11 @@ "Report the ContinuityOS Remote Commander capability boundary: " "enabled state, allowed roots, read limits, and governed execution path." ), - "inputSchema": {"type": "object", "additionalProperties": False, "properties": {}}, + "inputSchema": { + "type": "object", + "additionalProperties": False, + "properties": {}, + }, }, { "name": "system_info", @@ -66,7 +79,11 @@ "Read-only host identity and runtime information. Does not return environment " "variables, credentials, or process contents." ), - "inputSchema": {"type": "object", "additionalProperties": False, "properties": {}}, + "inputSchema": { + "type": "object", + "additionalProperties": False, + "properties": {}, + }, }, { "name": "fs_list", @@ -79,7 +96,12 @@ "additionalProperties": False, "properties": { "path": {"type": "string", "default": "."}, - "limit": {"type": "integer", "minimum": 1, "maximum": MAX_LIST_ENTRIES, "default": 200}, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": MAX_LIST_ENTRIES, + "default": 200, + }, }, }, }, @@ -113,7 +135,9 @@ def _env_enabled(value: str | None) -> bool: return (value or "").strip().lower() in {"1", "true", "yes", "on"} -def _canonical_roots(values: Iterable[str | os.PathLike[str]] | None) -> list[Path]: +def _canonical_roots( + values: Iterable[str | os.PathLike[str]] | None, +) -> list[Path]: raw = list(values or []) if not raw: configured = os.environ.get("CONTINUITYOS_REMOTE_ROOTS", "") @@ -134,14 +158,24 @@ def _canonical_roots(values: Iterable[str | os.PathLike[str]] | None) -> list[Pa class RemoteSurface: """Fail-closed read-only host surface with root containment and secret denial.""" - def __init__(self, *, enabled: bool | None = None, roots: Iterable[str | os.PathLike[str]] | None = None): - self.enabled = _env_enabled(os.environ.get("CONTINUITYOS_REMOTE_ENABLED")) if enabled is None else bool(enabled) + def __init__( + self, + *, + enabled: bool | None = None, + roots: Iterable[str | os.PathLike[str]] | None = None, + ): + self.enabled = ( + _env_enabled(os.environ.get("CONTINUITYOS_REMOTE_ENABLED")) + if enabled is None + else bool(enabled) + ) self.roots = _canonical_roots(roots) def _require_enabled(self) -> None: if not self.enabled: raise PermissionError( - "remote commander disabled; set CONTINUITYOS_REMOTE_ENABLED=1 or pass --enable-remote" + "remote commander disabled; set CONTINUITYOS_REMOTE_ENABLED=1 " + "or pass --enable-remote" ) @staticmethod @@ -162,7 +196,10 @@ def _resolve(self, value: str | os.PathLike[str]) -> Path: if not candidate.is_absolute(): candidate = self.roots[0] / candidate resolved = candidate.resolve(strict=True) - if not any(resolved == root or resolved.is_relative_to(root) for root in self.roots): + if not any( + resolved == root or resolved.is_relative_to(root) + for root in self.roots + ): raise PermissionError("path is outside configured remote roots") reason = self._sensitive_reason(resolved) if reason: @@ -181,7 +218,9 @@ def status(self) -> dict: "path": ["preflight_exec", "execute_preflight"], "governor": "ContinuityOS GateBroker", }, - "secret_policy": "deny known credential/key files and sensitive credential directories", + "secret_policy": ( + "deny known credential/key files and sensitive credential directories" + ), } def system_info(self) -> dict: @@ -199,12 +238,20 @@ def list_dir(self, path: str = ".", *, limit: int = 200) -> dict: directory = self._resolve(path) if not directory.is_dir(): raise NotADirectoryError(str(directory)) - if not isinstance(limit, int) or isinstance(limit, bool) or not (1 <= limit <= MAX_LIST_ENTRIES): - raise ValueError(f"limit must be an integer in 1..{MAX_LIST_ENTRIES}") + if ( + not isinstance(limit, int) + or isinstance(limit, bool) + or not (1 <= limit <= MAX_LIST_ENTRIES) + ): + raise ValueError( + f"limit must be an integer in 1..{MAX_LIST_ENTRIES}" + ) entries = [] omitted_sensitive = 0 - for child in sorted(directory.iterdir(), key=lambda item: item.name.lower()): + for child in sorted( + directory.iterdir(), key=lambda item: item.name.lower() + ): if self._sensitive_reason(child): omitted_sensitive += 1 continue @@ -219,7 +266,9 @@ def list_dir(self, path: str = ".", *, limit: int = 200) -> dict: kind = "file" else: kind = "other" - entries.append({"name": child.name, "type": kind, "size": stat.st_size}) + entries.append( + {"name": child.name, "type": kind, "size": stat.st_size} + ) return { "path": str(directory), "entries": entries, @@ -232,8 +281,14 @@ def read_text(self, path: str, *, max_bytes: int = 65536) -> dict: target = self._resolve(path) if not target.is_file(): raise FileNotFoundError(f"not a regular file: {target}") - if not isinstance(max_bytes, int) or isinstance(max_bytes, bool) or not (1 <= max_bytes <= MAX_READ_BYTES): - raise ValueError(f"max_bytes must be an integer in 1..{MAX_READ_BYTES}") + if ( + not isinstance(max_bytes, int) + or isinstance(max_bytes, bool) + or not (1 <= max_bytes <= MAX_READ_BYTES) + ): + raise ValueError( + f"max_bytes must be an integer in 1..{MAX_READ_BYTES}" + ) with target.open("rb") as handle: data = handle.read(max_bytes + 1) @@ -254,28 +309,49 @@ def read_text(self, path: str, *, max_bytes: int = 65536) -> dict: class RemoteServer(BaseServer): - def __init__(self, db=None, policy_path: str = "", db_source: str = "", *, remote_enabled: bool | None = None, remote_roots=None): + def __init__( + self, + db=None, + policy_path: str = "", + db_source: str = "", + *, + remote_enabled: bool | None = None, + remote_roots=None, + ): super().__init__(db, policy_path, db_source) - self.remote = RemoteSurface(enabled=remote_enabled, roots=remote_roots) + self.remote = RemoteSurface( + enabled=remote_enabled, + roots=remote_roots, + ) def call(self, name, args): if name == "capability_status": self.turns += 1 - return json.dumps(self.remote.status(), ensure_ascii=False, indent=2) + return json.dumps( + self.remote.status(), ensure_ascii=False, indent=2 + ) if name == "system_info": self.turns += 1 - return json.dumps(self.remote.system_info(), ensure_ascii=False, indent=2) + return json.dumps( + self.remote.system_info(), ensure_ascii=False, indent=2 + ) if name == "fs_list": self.turns += 1 return json.dumps( - self.remote.list_dir(args.get("path", "."), limit=args.get("limit", 200)), + self.remote.list_dir( + args.get("path", "."), + limit=args.get("limit", 200), + ), ensure_ascii=False, indent=2, ) if name == "fs_read": self.turns += 1 return json.dumps( - self.remote.read_text(args["path"], max_bytes=args.get("max_bytes", 65536)), + self.remote.read_text( + args["path"], + max_bytes=args.get("max_bytes", 65536), + ), ensure_ascii=False, indent=2, ) @@ -290,7 +366,11 @@ def _send(obj) -> None: def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--db", default=None) - parser.add_argument("--policy", default="", help="Path to one JSON policy, or YAML when PyYAML is installed") + parser.add_argument( + "--policy", + default="", + help="Path to one JSON policy, or YAML when PyYAML is installed", + ) parser.add_argument("--enable-remote", action="store_true", default=None) parser.add_argument( "--remote-root", @@ -324,23 +404,39 @@ def main() -> None: "result": { "protocolVersion": PROTOCOL, "capabilities": {"tools": {}}, - "serverInfo": {"name": "continuityos-remote", "version": __version__}, + "serverInfo": { + "name": "continuityos-remote", + "version": __version__, + }, }, } ) elif method == "notifications/initialized": continue elif method == "tools/list": - _send({"jsonrpc": "2.0", "id": message_id, "result": {"tools": TOOLS}}) + _send( + { + "jsonrpc": "2.0", + "id": message_id, + "result": {"tools": TOOLS}, + } + ) elif method == "tools/call": params = request.get("params", {}) or {} try: - output = server.call(params.get("name"), params.get("arguments", {}) or {}) + output = server.call( + params.get("name"), + params.get("arguments", {}) or {}, + ) _send( { "jsonrpc": "2.0", "id": message_id, - "result": {"content": [{"type": "text", "text": str(output)}]}, + "result": { + "content": [ + {"type": "text", "text": str(output)} + ] + }, } ) except Exception as exc: @@ -350,7 +446,9 @@ def main() -> None: "id": message_id, "result": { "isError": True, - "content": [{"type": "text", "text": f"error: {exc}"}], + "content": [ + {"type": "text", "text": f"error: {exc}"} + ], }, } ) @@ -361,7 +459,10 @@ def main() -> None: { "jsonrpc": "2.0", "id": message_id, - "error": {"code": -32601, "message": f"method not found: {method}"}, + "error": { + "code": -32601, + "message": f"method not found: {method}", + }, } ) From 07dab23138b14827a5547b14d72207db9a7c9b8e Mon Sep 17 00:00:00 2001 From: bitmaster162 <115934939+bitmaster162@users.noreply.github.com> Date: Wed, 16 Sep 2026 22:48:33 +0700 Subject: [PATCH 5/5] test(mcp): regress credential-path denial --- tests/test_remote_mcp_secret_boundary.py | 31 ++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 tests/test_remote_mcp_secret_boundary.py diff --git a/tests/test_remote_mcp_secret_boundary.py b/tests/test_remote_mcp_secret_boundary.py new file mode 100644 index 0000000..d0cc95e --- /dev/null +++ b/tests/test_remote_mcp_secret_boundary.py @@ -0,0 +1,31 @@ +import pytest + +from continuityos.remote_mcp_server import RemoteSurface + + +def test_git_metadata_and_common_credential_files_are_denied(tmp_path): + git_dir = tmp_path / ".git" + git_dir.mkdir() + (git_dir / "config").write_text( + "[remote \"origin\"]\nurl = https://token@example.invalid/repo.git\n", + encoding="utf-8", + ) + (tmp_path / ".netrc").write_text("machine example.invalid login x password y", encoding="utf-8") + (tmp_path / ".npmrc").write_text("//registry.example.invalid/:_authToken=secret", encoding="utf-8") + (tmp_path / "safe.txt").write_text("ok", encoding="utf-8") + + surface = RemoteSurface(enabled=True, roots=[tmp_path]) + listing = surface.list_dir(".") + names = {entry["name"] for entry in listing["entries"]} + + assert ".git" not in names + assert ".netrc" not in names + assert ".npmrc" not in names + assert "safe.txt" in names + + with pytest.raises(PermissionError, match="sensitive directory denied"): + surface.read_text(".git/config") + with pytest.raises(PermissionError, match="sensitive file denied"): + surface.read_text(".netrc") + with pytest.raises(PermissionError, match="sensitive file denied"): + surface.read_text(".npmrc")