Skip to content

Commit caeb91d

Browse files
committed
Merge branch 'pr-854' into addon-repo
2 parents c35ca30 + 2f5a541 commit caeb91d

12 files changed

Lines changed: 1231 additions & 0 deletions

File tree

homeassistant-addon-dev/config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ options:
2121
enable_skills_as_tools: false
2222
enable_tool_search: false
2323
enable_yaml_config_editing: false
24+
enable_code_mode: false
2425
schema:
2526
backup_hint: list(strong|normal|weak|auto)
2627
secret_path: str?
2728
enable_skills: bool?
2829
enable_skills_as_tools: bool?
2930
enable_tool_search: bool?
3031
enable_yaml_config_editing: bool?
32+
enable_code_mode: bool?
3133
ports:
3234
9583/tcp: 9583

homeassistant-addon-dev/translations/en.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,12 @@ configuration:
3535
like homeassistant, http, and recorder are blocked. A backup is
3636
created before every edit. Use for YAML-only features that have no
3737
UI or API alternative. Requires restart to take effect.
38+
enable_code_mode:
39+
name: Enable custom tool sandbox
40+
description: >-
41+
Allow AI assistants to write and run custom Python code in a secure
42+
sandbox when no built-in tool can handle the request. Code runs in
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: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,30 @@ Replaces the full tool catalog (~92 tools, ~46K tokens) with search-based discov
225225

226226
Requires add-on restart to take effect.
227227

228+
### enable_code_mode
229+
230+
**Default:** `false`
231+
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()`.
233+
234+
**Safety guardrails:**
235+
- Code runs in a sandboxed interpreter (no filesystem, no network, no third-party imports)
236+
- `destructiveHint=True` — MCP clients prompt for confirmation before execution
237+
- AI must provide a justification explaining why no existing tool works
238+
- Configurable time (30s), memory (10MB), and recursion (100) limits
239+
- Rate limited to 100 tool calls per execution
240+
- Cannot recursively invoke itself
241+
242+
**When to enable:**
243+
- You need an "escape hatch" for operations not covered by the 92+ built-in tools
244+
- You trust the AI agent's judgment on when to write custom code
245+
246+
**When to leave disabled (default):**
247+
- Standard use cases covered by existing tools
248+
- You want to restrict the AI to pre-built tools only
249+
250+
Requires add-on restart to take effect.
251+
228252
**Example Configuration:**
229253

230254
```yaml

homeassistant-addon/config.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@ options:
2727
enable_skills_as_tools: false
2828
enable_tool_search: false
2929
enable_yaml_config_editing: false
30+
enable_code_mode: false
3031
schema:
3132
backup_hint: list(strong|normal|weak|auto)
3233
secret_path: str?
3334
enable_skills: bool?
3435
enable_skills_as_tools: bool?
3536
enable_tool_search: bool?
3637
enable_yaml_config_editing: bool?
38+
enable_code_mode: bool?
3739
# Add-on exposes HTTP port for MCP communication (fixed internal port)
3840
ports:
3941
9583/tcp: 9583

homeassistant-addon/start.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def main() -> int:
110110
enable_skills_as_tools = False # default
111111
enable_tool_search = False # default
112112
enable_yaml_config_editing = False # default
113+
enable_code_mode = False # default
113114

114115
if config_file.exists():
115116
try:
@@ -125,6 +126,8 @@ def main() -> int:
125126
enable_tool_search = raw_tool_search if isinstance(raw_tool_search, bool) else False
126127
raw_yaml_config = config.get("enable_yaml_config_editing", False)
127128
enable_yaml_config_editing = raw_yaml_config if isinstance(raw_yaml_config, bool) else False
129+
raw_code_mode = config.get("enable_code_mode", False)
130+
enable_code_mode = raw_code_mode if isinstance(raw_code_mode, bool) else False
128131
except Exception as e:
129132
log_error(f"Failed to read config: {e}, using defaults")
130133

@@ -140,6 +143,7 @@ def main() -> int:
140143
os.environ["ENABLE_SKILLS_AS_TOOLS"] = str(enable_skills_as_tools).lower()
141144
os.environ["ENABLE_TOOL_SEARCH"] = str(enable_tool_search).lower()
142145
os.environ["ENABLE_YAML_CONFIG_EDITING"] = str(enable_yaml_config_editing).lower()
146+
os.environ["ENABLE_CODE_MODE"] = str(enable_code_mode).lower()
143147

144148
# Validate Supervisor token
145149
supervisor_token = os.environ.get("SUPERVISOR_TOKEN")

homeassistant-addon/translations/en.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,12 @@ configuration:
3535
like homeassistant, http, and recorder are blocked. A backup is
3636
created before every edit. Use for YAML-only features that have no
3737
UI or API alternative. Requires restart to take effect.
38+
enable_code_mode:
39+
name: Enable custom tool sandbox
40+
description: >-
41+
Allow AI assistants to write and run custom Python code in a secure
42+
sandbox when no built-in tool can handle the request. Code runs in
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.

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ dependencies = [
3131
"truststore==0.10.4",
3232
"websockets==16.0",
3333
"cryptography==46.0.6",
34+
"pydantic-monty==0.0.9",
3435
]
3536

3637
[project.urls]
@@ -75,6 +76,8 @@ explicit_package_bases = true
7576
module = [
7677
"fastmcp.*",
7778
"jq",
79+
"pydantic_monty",
80+
"pydantic_monty.*",
7881
]
7982
ignore_missing_imports = true
8083

src/ha_mcp/config.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,18 @@ class Settings(BaseSettings):
112112
# files. Disabled by default; only for YAML-only features with no UI/API path.
113113
enable_yaml_config_editing: bool = Field(False, alias="ENABLE_YAML_CONFIG_EDITING")
114114

115+
# Code Mode — sandboxed Python execution via pydantic-monty.
116+
# Provides an "escape hatch" tool (ha_manage_custom_tool) that lets LLMs write
117+
# custom one-off Python code when no existing tool covers the request.
118+
# Disabled by default due to the inherent risk of LLM-generated code.
119+
enable_code_mode: bool = Field(False, alias="ENABLE_CODE_MODE")
120+
code_mode_max_duration: float = Field(30.0, alias="CODE_MODE_MAX_DURATION")
121+
code_mode_max_memory: int = Field(
122+
10_485_760, alias="CODE_MODE_MAX_MEMORY"
123+
) # 10 MB
124+
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")
126+
115127
@model_validator(mode="after")
116128
def _skills_dependency(self) -> "Settings":
117129
"""Auto-enable skills (resources) when skills-as-tools is on.

src/ha_mcp/server.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,10 @@ def _apply_tool_search(self) -> None:
424424
# Build the always_visible list
425425
pinned = list(self._PINNED_TOOLS)
426426

427+
# Pin code mode tool so it gets individual permission gating
428+
if self.settings.enable_code_mode:
429+
pinned.append("ha_manage_custom_tool")
430+
427431
# Pin ResourcesAsTools and skill guidance tools if skills-as-tools is enabled
428432
if self.settings.enable_skills_as_tools:
429433
pinned.extend(["list_resources", "read_resource"])

0 commit comments

Comments
 (0)