Skip to content

Commit 2f5a541

Browse files
kingpanther13claude
andcommitted
fix: address Gemini review — docs, configurable limits, code cleanup
Security (high): - Fix misleading docs/translations that said "only through MCP tools" — now correctly documents api_get/api_post direct API access - Add explicit warning on _saved_tools about shared state in multi-user modes Code quality (medium): - Make max invocations configurable: CODE_MODE_MAX_INVOCATIONS setting (was hardcoded _MAX_CALL_TOOL_INVOCATIONS = 100) - Combine isinstance checks for basic types - Narrow except Exception to json.JSONDecodeError in api_get/api_post - Rename kwargs to post_kwargs in _api_post to avoid shadowing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3a0c261 commit 2f5a541

5 files changed

Lines changed: 26 additions & 27 deletions

File tree

homeassistant-addon-dev/translations/en.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ configuration:
4040
description: >-
4141
Allow AI assistants to write and run custom Python code in a secure
4242
sandbox when no built-in tool can handle the request. Code runs in
43-
an isolated interpreter with no filesystem or network access — the
44-
only way to interact with Home Assistant is through registered MCP
45-
tools. Includes save/reuse for frequently-used custom tools.
46-
Requires restart to take effect.
43+
an isolated interpreter with no filesystem or network access.
44+
Sandbox code can access the HA REST API directly (api_get/api_post)
45+
or call existing MCP tools (call_tool). Includes save/reuse for
46+
frequently-used custom tools. Requires restart to take effect.

homeassistant-addon/DOCS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ Requires add-on restart to take effect.
229229

230230
**Default:** `false`
231231

232-
Enables the `ha_manage_custom_tool` — a sandboxed "escape hatch" that lets AI agents write and run custom Python code when no existing tool covers the request. Code runs in pydantic-monty, a Rust-based sandbox with no filesystem or network access. The only way to interact with Home Assistant is through registered MCP tools via `call_tool()`.
232+
Enables the `ha_manage_custom_tool` — a sandboxed "escape hatch" that lets AI agents write and run custom Python code when no existing tool covers the request. Code runs in pydantic-monty, a Rust-based sandbox with no filesystem or network access. Sandbox code can access the HA REST API directly via `api_get()`/`api_post()`, or call existing MCP tools via `call_tool()`.
233233

234234
**Safety guardrails:**
235235
- Code runs in a sandboxed interpreter (no filesystem, no network, no third-party imports)

homeassistant-addon/translations/en.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ configuration:
4040
description: >-
4141
Allow AI assistants to write and run custom Python code in a secure
4242
sandbox when no built-in tool can handle the request. Code runs in
43-
an isolated interpreter with no filesystem or network access — the
44-
only way to interact with Home Assistant is through registered MCP
45-
tools. Includes save/reuse for frequently-used custom tools.
46-
Requires restart to take effect.
43+
an isolated interpreter with no filesystem or network access.
44+
Sandbox code can access the HA REST API directly (api_get/api_post)
45+
or call existing MCP tools (call_tool). Includes save/reuse for
46+
frequently-used custom tools. Requires restart to take effect.

src/ha_mcp/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ class Settings(BaseSettings):
122122
10_485_760, alias="CODE_MODE_MAX_MEMORY"
123123
) # 10 MB
124124
code_mode_max_recursion: int = Field(100, alias="CODE_MODE_MAX_RECURSION")
125+
code_mode_max_invocations: int = Field(100, alias="CODE_MODE_MAX_INVOCATIONS")
125126

126127
@model_validator(mode="after")
127128
def _skills_dependency(self) -> "Settings":

src/ha_mcp/tools/tools_code.py

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@
2727

2828
logger = logging.getLogger(__name__)
2929

30-
# In-memory cache for saved custom tools (session-scoped, not persistent)
30+
# In-memory cache for saved custom tools (session-scoped, not persistent).
31+
# WARNING: This is shared across all clients in the same server process.
32+
# In multi-user modes (OAuth, HTTP), one user's saved tools are visible to
33+
# all other users. Scope to per-session/user before multi-user support.
3134
_saved_tools: dict[str, dict[str, str]] = {}
3235

3336
# Tools that sandbox code must not call (prevents recursive self-invocation)
3437
_BLOCKED_TOOLS = frozenset({"ha_manage_custom_tool"})
3538

36-
# Max call_tool invocations per sandbox execution (prevents API flooding)
37-
_MAX_CALL_TOOL_INVOCATIONS = 100
38-
3939
# Validation for save_as names
4040
_SAVE_NAME_PATTERN = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]{0,63}$")
4141

@@ -48,9 +48,7 @@ def _extract_tool_result(result: Any) -> Any:
4848
float, bool, list, dict, None), so we must serialize.
4949
"""
5050
# Already a basic type — pass through
51-
if isinstance(result, (str, int, float, bool, type(None))):
52-
return result
53-
if isinstance(result, dict):
51+
if isinstance(result, (str, int, float, bool, type(None), dict)):
5452
return result
5553

5654
# ToolResult or similar: extract content list
@@ -99,13 +97,13 @@ async def _api_get(endpoint: str) -> Any:
9997
"""GET request to Home Assistant REST API."""
10098
nonlocal call_count
10199
call_count += 1
102-
if call_count > _MAX_CALL_TOOL_INVOCATIONS:
103-
return {"error": f"API call limit exceeded ({_MAX_CALL_TOOL_INVOCATIONS})"}
100+
if call_count > settings.code_mode_max_invocations:
101+
return {"error": f"API call limit exceeded ({settings.code_mode_max_invocations})"}
104102
try:
105103
response = await client.httpx_client.request("GET", endpoint)
106104
try:
107105
return response.json()
108-
except Exception:
106+
except json.JSONDecodeError:
109107
return response.text
110108
except Exception as exc:
111109
return {"error": str(exc)[:200]}
@@ -114,16 +112,16 @@ async def _api_post(endpoint: str, data: dict[str, Any] | None = None) -> Any:
114112
"""POST request to Home Assistant REST API."""
115113
nonlocal call_count
116114
call_count += 1
117-
if call_count > _MAX_CALL_TOOL_INVOCATIONS:
118-
return {"error": f"API call limit exceeded ({_MAX_CALL_TOOL_INVOCATIONS})"}
115+
if call_count > settings.code_mode_max_invocations:
116+
return {"error": f"API call limit exceeded ({settings.code_mode_max_invocations})"}
119117
try:
120-
kwargs: dict[str, Any] = {}
118+
post_kwargs: dict[str, Any] = {}
121119
if data is not None:
122-
kwargs["json"] = data
123-
response = await client.httpx_client.request("POST", endpoint, **kwargs)
120+
post_kwargs["json"] = data
121+
response = await client.httpx_client.request("POST", endpoint, **post_kwargs)
124122
try:
125123
return response.json()
126-
except Exception:
124+
except json.JSONDecodeError:
127125
return response.text
128126
except Exception as exc:
129127
return {"error": str(exc)[:200]}
@@ -141,12 +139,12 @@ async def _call_tool(tool_name: str, arguments: dict[str, Any]) -> Any:
141139
}
142140

143141
call_count += 1
144-
if call_count > _MAX_CALL_TOOL_INVOCATIONS:
142+
if call_count > settings.code_mode_max_invocations:
145143
return {
146144
"success": False,
147145
"error": {
148146
"message": (
149-
f"call_tool limit exceeded ({_MAX_CALL_TOOL_INVOCATIONS} "
147+
f"call_tool limit exceeded ({settings.code_mode_max_invocations} "
150148
f"calls per execution)"
151149
)
152150
},

0 commit comments

Comments
 (0)