diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d7ea89c --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,25 @@ +# Security Policy + +## Reporting Vulnerabilities + +If you discover a security vulnerability, please report it responsibly by opening a private security advisory on this repository. Do **not** open a public issue. + +## Security Considerations for Users + +OpenSpace is a powerful agent framework that can execute shell commands, run arbitrary code, and connect to external services. Users should be aware of the following: + +### Telemetry + +Telemetry is **disabled by default** as of this PR. If you opt in by setting `MCP_USE_ANONYMIZED_TELEMETRY=true`, be aware that execution metadata (model names, tool usage counts, timing) is sent to PostHog and Scarf. Query text and response text are **never** transmitted regardless of this setting. + +### Cloud Skills + +Cloud skill search and auto-import are **disabled by default** (`search_scope="local"`). If you enable cloud search (`search_scope="all"`), downloaded skills are not sandboxed or signature-verified. Only enable this in trusted environments. + +### Host Config Auto-Detection + +OpenSpace reads host agent configs (`~/.openclaw/openclaw.json`, `~/.nanobot/config.json`) to auto-detect LLM credentials. It only reads from the explicitly scoped `openspace` env blocks — not top-level or unrelated configuration sections. + +### Shell Execution + +The grounding engine can execute shell commands. The `config_security.json` defines blocked command lists, but this is a denylist approach. For production deployments, enable sandboxing (`sandbox_enabled: true`) and review the security policy configuration. diff --git a/openspace/config/config_security.json b/openspace/config/config_security.json index c64f75e..156f65e 100644 --- a/openspace/config/config_security.json +++ b/openspace/config/config_security.json @@ -10,7 +10,7 @@ "darwin": ["diskutil", "dd", "pfctl", "launchctl", "killall"], "windows": ["del", "format", "rd", "rmdir", "/s", "/q", "taskkill", "/f"] }, - "sandbox_enabled": false + "sandbox_enabled": true }, "backend": { "shell": { diff --git a/openspace/host_detection/openclaw.py b/openspace/host_detection/openclaw.py index a97104a..0b08a71 100644 --- a/openspace/host_detection/openclaw.py +++ b/openspace/host_detection/openclaw.py @@ -265,13 +265,16 @@ def read_openclaw_skill_env(skill_name: str = "openspace") -> Dict[str, str]: def get_openclaw_openai_api_key() -> Optional[str]: """Get OpenAI API key from OpenClaw config. - Checks ``skills.entries.openspace.env.OPENAI_API_KEY`` first, - then any top-level env vars in the config. + Only reads from the explicitly scoped ``skills.entries.openspace.env`` + block. Does NOT read top-level env vars or other skill env blocks to + respect the principle of least privilege — OpenSpace should only access + credentials explicitly granted to it. Returns the key string, or None. """ - env = _get_openclaw_env("openspace") - key = _coerce_env_value(env.get("OPENAI_API_KEY")) + # Only read from the openspace skill env block — not top-level config + env = read_openclaw_skill_env("openspace") + key = env.get("OPENAI_API_KEY", "").strip() if key: logger.debug("Using OpenAI API key from OpenClaw skill env config") return key diff --git a/openspace/mcp_server.py b/openspace/mcp_server.py index 168485c..315924d 100644 --- a/openspace/mcp_server.py +++ b/openspace/mcp_server.py @@ -526,7 +526,7 @@ async def execute_task( workspace_dir: str | None = None, max_iterations: int | None = None, skill_dirs: list[str] | None = None, - search_scope: str = "all", + search_scope: str = "local", ) -> str: """Execute a task with OpenSpace's full grounding engine. @@ -552,9 +552,10 @@ async def execute_task( on every call to discover skills created since the last invocation. search_scope: Skill search scope before execution. - "all" (default) — local + cloud; falls back to local - if no API key is configured. - "local" — local SkillRegistry only (fast, no cloud). + "local" (default) — local SkillRegistry only (fast, no cloud). + "all" — local + cloud; falls back to local + if no API key is configured. Use with caution: cloud + skills are unverified and auto-imported without review. """ try: openspace = await _get_openspace() @@ -602,9 +603,9 @@ async def execute_task( @mcp.tool() async def search_skills( query: str, - source: str = "all", + source: str = "local", limit: int = 20, - auto_import: bool = True, + auto_import: bool = False, ) -> str: """Search skills across local registry and cloud community. @@ -622,9 +623,10 @@ async def search_skills( Args: query: Search query text (natural language or keywords). - source: "all" (cloud + local), "local", or "cloud". Default: "all". + source: "local" (default), "all" (cloud + local), or "cloud". limit: Maximum results to return (default: 20). - auto_import: Auto-download top public cloud skills (default: True). + auto_import: Auto-download top public cloud skills (default: False). + Enable explicitly to allow unverified cloud skill imports. """ try: from openspace.cloud.search import hybrid_search_skills diff --git a/openspace/utils/telemetry/events.py b/openspace/utils/telemetry/events.py index ddebddf..bc45ccd 100644 --- a/openspace/utils/telemetry/events.py +++ b/openspace/utils/telemetry/events.py @@ -64,16 +64,16 @@ def properties(self) -> dict[str, Any]: return { # Core execution info "execution_method": self.execution_method, - "query": self.query, + # NOTE: query and response text are intentionally excluded to + # prevent exfiltration of potentially sensitive user data. + # Only lengths are reported for aggregate analytics. "query_length": len(self.query), "success": self.success, # Agent configuration "model_provider": self.model_provider, "model_name": self.model_name, "server_count": self.server_count, - "server_identifiers": self.server_identifiers, "total_tools_available": self.total_tools_available, - "tools_available_names": self.tools_available_names, "max_steps_configured": self.max_steps_configured, "memory_enabled": self.memory_enabled, "use_server_manager": self.use_server_manager, @@ -85,7 +85,6 @@ def properties(self) -> dict[str, Any]: "steps_taken": self.steps_taken, "tools_used_count": self.tools_used_count, "tools_used_names": self.tools_used_names, - "response": self.response, "response_length": len(self.response) if self.response else None, "execution_time_ms": self.execution_time_ms, "error_type": self.error_type, diff --git a/openspace/utils/telemetry/telemetry.py b/openspace/utils/telemetry/telemetry.py index cab29a0..63e9394 100644 --- a/openspace/utils/telemetry/telemetry.py +++ b/openspace/utils/telemetry/telemetry.py @@ -79,7 +79,7 @@ class Telemetry: _curr_user_id = None def __init__(self): - telemetry_disabled = os.getenv("MCP_USE_ANONYMIZED_TELEMETRY", "true").lower() == "false" + telemetry_disabled = os.getenv("MCP_USE_ANONYMIZED_TELEMETRY", "false").lower() == "false" if telemetry_disabled: self._posthog_client = None