mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-04-26 12:55:58 +00:00
feat(exec): support allowed_env_keys to pass specified env vars to subprocess
Add allowed_env_keys config field to selectively forward host environment variables (e.g. GOPATH, JAVA_HOME) into the sandboxed subprocess environment, while keeping the default allow-list unchanged.
This commit is contained in:
parent
c625c0c2a7
commit
0e6331b66d
@ -242,6 +242,7 @@ class AgentLoop:
|
|||||||
restrict_to_workspace=self.restrict_to_workspace,
|
restrict_to_workspace=self.restrict_to_workspace,
|
||||||
sandbox=self.exec_config.sandbox,
|
sandbox=self.exec_config.sandbox,
|
||||||
path_append=self.exec_config.path_append,
|
path_append=self.exec_config.path_append,
|
||||||
|
allowed_env_keys=self.exec_config.allowed_env_keys,
|
||||||
))
|
))
|
||||||
if self.web_config.enable:
|
if self.web_config.enable:
|
||||||
self.tools.register(WebSearchTool(config=self.web_config.search, proxy=self.web_config.proxy))
|
self.tools.register(WebSearchTool(config=self.web_config.search, proxy=self.web_config.proxy))
|
||||||
|
|||||||
@ -46,6 +46,7 @@ class ExecTool(Tool):
|
|||||||
restrict_to_workspace: bool = False,
|
restrict_to_workspace: bool = False,
|
||||||
sandbox: str = "",
|
sandbox: str = "",
|
||||||
path_append: str = "",
|
path_append: str = "",
|
||||||
|
allowed_env_keys: list[str] | None = None,
|
||||||
):
|
):
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
self.working_dir = working_dir
|
self.working_dir = working_dir
|
||||||
@ -64,6 +65,7 @@ class ExecTool(Tool):
|
|||||||
self.allow_patterns = allow_patterns or []
|
self.allow_patterns = allow_patterns or []
|
||||||
self.restrict_to_workspace = restrict_to_workspace
|
self.restrict_to_workspace = restrict_to_workspace
|
||||||
self.path_append = path_append
|
self.path_append = path_append
|
||||||
|
self.allowed_env_keys = allowed_env_keys or []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
@ -208,7 +210,7 @@ class ExecTool(Tool):
|
|||||||
"""
|
"""
|
||||||
if _IS_WINDOWS:
|
if _IS_WINDOWS:
|
||||||
sr = os.environ.get("SYSTEMROOT", r"C:\Windows")
|
sr = os.environ.get("SYSTEMROOT", r"C:\Windows")
|
||||||
return {
|
env = {
|
||||||
"SYSTEMROOT": sr,
|
"SYSTEMROOT": sr,
|
||||||
"COMSPEC": os.environ.get("COMSPEC", f"{sr}\\system32\\cmd.exe"),
|
"COMSPEC": os.environ.get("COMSPEC", f"{sr}\\system32\\cmd.exe"),
|
||||||
"USERPROFILE": os.environ.get("USERPROFILE", ""),
|
"USERPROFILE": os.environ.get("USERPROFILE", ""),
|
||||||
@ -225,12 +227,22 @@ class ExecTool(Tool):
|
|||||||
"ProgramFiles(x86)": os.environ.get("ProgramFiles(x86)", ""),
|
"ProgramFiles(x86)": os.environ.get("ProgramFiles(x86)", ""),
|
||||||
"ProgramW6432": os.environ.get("ProgramW6432", ""),
|
"ProgramW6432": os.environ.get("ProgramW6432", ""),
|
||||||
}
|
}
|
||||||
|
for key in self.allowed_env_keys:
|
||||||
|
val = os.environ.get(key)
|
||||||
|
if val is not None:
|
||||||
|
env[key] = val
|
||||||
|
return env
|
||||||
home = os.environ.get("HOME", "/tmp")
|
home = os.environ.get("HOME", "/tmp")
|
||||||
return {
|
env = {
|
||||||
"HOME": home,
|
"HOME": home,
|
||||||
"LANG": os.environ.get("LANG", "C.UTF-8"),
|
"LANG": os.environ.get("LANG", "C.UTF-8"),
|
||||||
"TERM": os.environ.get("TERM", "dumb"),
|
"TERM": os.environ.get("TERM", "dumb"),
|
||||||
}
|
}
|
||||||
|
for key in self.allowed_env_keys:
|
||||||
|
val = os.environ.get(key)
|
||||||
|
if val is not None:
|
||||||
|
env[key] = val
|
||||||
|
return env
|
||||||
|
|
||||||
def _guard_command(self, command: str, cwd: str) -> str | None:
|
def _guard_command(self, command: str, cwd: str) -> str | None:
|
||||||
"""Best-effort safety guard for potentially destructive commands."""
|
"""Best-effort safety guard for potentially destructive commands."""
|
||||||
|
|||||||
@ -177,6 +177,7 @@ class ExecToolConfig(Base):
|
|||||||
timeout: int = 60
|
timeout: int = 60
|
||||||
path_append: str = ""
|
path_append: str = ""
|
||||||
sandbox: str = "" # sandbox backend: "" (none) or "bwrap"
|
sandbox: str = "" # sandbox backend: "" (none) or "bwrap"
|
||||||
|
allowed_env_keys: list[str] = Field(default_factory=list) # Env var names to pass through to subprocess (e.g. ["GOPATH", "JAVA_HOME"])
|
||||||
|
|
||||||
class MCPServerConfig(Base):
|
class MCPServerConfig(Base):
|
||||||
"""MCP server connection configuration (stdio or HTTP)."""
|
"""MCP server connection configuration (stdio or HTTP)."""
|
||||||
|
|||||||
@ -43,3 +43,34 @@ async def test_exec_path_append_preserves_system_path():
|
|||||||
tool = ExecTool(path_append="/opt/custom/bin")
|
tool = ExecTool(path_append="/opt/custom/bin")
|
||||||
result = await tool.execute(command="ls /")
|
result = await tool.execute(command="ls /")
|
||||||
assert "Exit code: 0" in result
|
assert "Exit code: 0" in result
|
||||||
|
|
||||||
|
|
||||||
|
@_UNIX_ONLY
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_exec_allowed_env_keys_passthrough(monkeypatch):
|
||||||
|
"""Env vars listed in allowed_env_keys should be visible to commands."""
|
||||||
|
monkeypatch.setenv("MY_CUSTOM_VAR", "hello-from-config")
|
||||||
|
tool = ExecTool(allowed_env_keys=["MY_CUSTOM_VAR"])
|
||||||
|
result = await tool.execute(command="printenv MY_CUSTOM_VAR")
|
||||||
|
assert "hello-from-config" in result
|
||||||
|
|
||||||
|
|
||||||
|
@_UNIX_ONLY
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_exec_allowed_env_keys_does_not_leak_others(monkeypatch):
|
||||||
|
"""Env vars NOT in allowed_env_keys should still be blocked."""
|
||||||
|
monkeypatch.setenv("MY_CUSTOM_VAR", "hello-from-config")
|
||||||
|
monkeypatch.setenv("MY_SECRET_VAR", "secret-value")
|
||||||
|
tool = ExecTool(allowed_env_keys=["MY_CUSTOM_VAR"])
|
||||||
|
result = await tool.execute(command="printenv MY_SECRET_VAR")
|
||||||
|
assert "secret-value" not in result
|
||||||
|
|
||||||
|
|
||||||
|
@_UNIX_ONLY
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_exec_allowed_env_keys_missing_var_ignored(monkeypatch):
|
||||||
|
"""If an allowed key is not set in the parent process, it should be silently skipped."""
|
||||||
|
monkeypatch.delenv("NONEXISTENT_VAR_12345", raising=False)
|
||||||
|
tool = ExecTool(allowed_env_keys=["NONEXISTENT_VAR_12345"])
|
||||||
|
result = await tool.execute(command="printenv NONEXISTENT_VAR_12345")
|
||||||
|
assert "Exit code: 1" in result
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user