Skip to content

Commit df14066

Browse files
committed
Studio: add Bypass Permissions (skip confirmation, disable tool sandbox)
Adds an opt-in Bypass Permissions toggle next to Confirm tool calls. When on, no tool call shows a confirmation prompt and the python/terminal sandbox is disabled: safety checks, command blocklist, and resource limits are skipped. Secret env vars are still stripped and HOME stays repointed at the session workdir. Default off keeps current behavior, and it takes precedence over Confirm tool calls. Enabling it requires accepting a warning each time.
1 parent f1451fc commit df14066

12 files changed

Lines changed: 526 additions & 29 deletions

File tree

studio/backend/core/inference/llama_cpp.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4453,6 +4453,7 @@ def generate_chat_completion_with_tools(
44534453
tool_call_timeout: int = 300,
44544454
session_id: Optional[str] = None,
44554455
confirm_tool_calls: bool = False,
4456+
bypass_permissions: bool = False,
44564457
) -> Generator[dict, None, None]:
44574458
"""
44584459
Agentic loop: let the model call tools, execute them, and continue.
@@ -5159,6 +5160,7 @@ def _strip_tool_markup(text: str, *, final: bool = False) -> str:
51595160
cancel_event = cancel_event,
51605161
timeout = _effective_timeout,
51615162
session_id = session_id,
5163+
disable_sandbox = bypass_permissions,
51625164
)
51635165

51645166
yield {

studio/backend/core/inference/orchestrator.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -840,6 +840,7 @@ def generate_chat_completion_with_tools(
840840
session_id: Optional[str] = None,
841841
use_adapter: Optional[Union[bool, str]] = None,
842842
confirm_tool_calls: bool = False,
843+
bypass_permissions: bool = False,
843844
**_unused,
844845
):
845846
"""Run the safetensors agentic tool loop in this (parent)
@@ -897,6 +898,7 @@ def _single_turn(conv: list):
897898
tool_call_timeout = tool_call_timeout,
898899
session_id = session_id,
899900
confirm_tool_calls = confirm_tool_calls,
901+
bypass_permissions = bypass_permissions,
900902
)
901903

902904
def generate_with_adapter_control(

studio/backend/core/inference/safetensors_agentic.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ def run_safetensors_tool_loop(
113113
tool_call_timeout: int = 300,
114114
session_id: Optional[str] = None,
115115
confirm_tool_calls: bool = False,
116+
bypass_permissions: bool = False,
116117
) -> Generator[dict, None, None]:
117118
"""Drive an agentic tool loop on top of a cumulative-text generator.
118119
@@ -376,6 +377,7 @@ def run_safetensors_tool_loop(
376377
cancel_event = cancel_event,
377378
timeout = eff_timeout,
378379
session_id = session_id,
380+
disable_sandbox = bypass_permissions,
379381
)
380382
except Exception as exc:
381383
logger.exception("Tool %s raised: %s", tool_name, exc)

studio/backend/core/inference/tools.py

Lines changed: 119 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,65 @@ def _build_safe_env(workdir: str) -> dict[str, str]:
329329
return env
330330

331331

332+
# Credential env vars dropped even in bypass mode so tool code cannot read the
333+
# operator's keys. Over-strips on purpose (a benign var is harmless to lose).
334+
_BYPASS_ENV_SECRET_NAMES = frozenset(
335+
{
336+
"HF_TOKEN",
337+
"HF_HUB_TOKEN",
338+
"HUGGING_FACE_HUB_TOKEN",
339+
"HUGGINGFACE_TOKEN",
340+
"HUGGINGFACEHUB_API_TOKEN",
341+
"WANDB_API_KEY",
342+
"GH_TOKEN",
343+
"GITHUB_TOKEN",
344+
"OPENAI_API_KEY",
345+
"ANTHROPIC_API_KEY",
346+
"GEMINI_API_KEY",
347+
"GOOGLE_API_KEY",
348+
"GROQ_API_KEY",
349+
"OPENROUTER_API_KEY",
350+
"REPLICATE_API_TOKEN",
351+
"COHERE_API_KEY",
352+
"MISTRAL_API_KEY",
353+
"NGC_API_KEY",
354+
"KAGGLE_KEY",
355+
"LD_PRELOAD",
356+
}
357+
)
358+
_BYPASS_ENV_SECRET_PREFIXES = ("AWS_", "AZURE_", "GOOGLE_", "GCP_", "GCLOUD_", "DYLD_")
359+
_BYPASS_ENV_SECRET_MARKERS = (
360+
"TOKEN",
361+
"API_KEY",
362+
"APIKEY",
363+
"SECRET",
364+
"PASSWORD",
365+
"PASSWD",
366+
"CREDENTIAL",
367+
"PRIVATE_KEY",
368+
)
369+
370+
371+
def _is_secret_env_name(name: str) -> bool:
372+
"""True if an env var name looks like it carries a credential."""
373+
upper = name.upper()
374+
if upper in _BYPASS_ENV_SECRET_NAMES:
375+
return True
376+
if any(upper.startswith(p) for p in _BYPASS_ENV_SECRET_PREFIXES):
377+
return True
378+
return any(marker in upper for marker in _BYPASS_ENV_SECRET_MARKERS)
379+
380+
381+
def _build_bypass_env(workdir: str) -> dict[str, str]:
382+
"""Env for bypass exec: full host env (unrestricted) minus credential vars,
383+
with HOME/TMPDIR repointed at the workdir so SDKs cannot read cached creds.
384+
"""
385+
env = {k: v for k, v in os.environ.items() if not _is_secret_env_name(k)}
386+
env["HOME"] = workdir
387+
env["TMPDIR"] = workdir
388+
return env
389+
390+
332391
def _sandbox_preexec():
333392
"""Best-effort sandbox setup for sandboxed subprocesses.
334393
@@ -403,6 +462,18 @@ def _sandbox_preexec():
403462
pass
404463

405464

465+
def _bypass_preexec():
466+
"""Minimal pre-exec for bypass exec: os.setsid() only.
467+
468+
Required, not a restriction: _kill_process_tree does killpg(getpgid(child)),
469+
so without a new session a timeout/cancel would kill the Studio server too.
470+
"""
471+
try:
472+
os.setsid()
473+
except OSError:
474+
pass
475+
476+
406477
def _get_shell_cmd(command: str) -> list[str]:
407478
"""Return the platform-appropriate shell invocation for a command string."""
408479
if sys.platform == "win32":
@@ -609,12 +680,16 @@ def execute_tool(
609680
cancel_event = None,
610681
timeout: int | None = _TIMEOUT_UNSET,
611682
session_id: str | None = None,
683+
disable_sandbox: bool = False,
612684
) -> str:
613685
"""Execute a tool by name with the given arguments. Returns result as a string.
614686
615687
``timeout``: int sets per-call limit in seconds, ``None`` means no limit,
616688
unset (default) uses ``_EXEC_TIMEOUT`` (300 s).
617689
``session_id``: optional thread/session ID for per-conversation sandbox isolation.
690+
``disable_sandbox``: Bypass Permissions; run python/terminal without the
691+
safety checks, blocklist, or resource caps (secrets still stripped). Only
692+
affects local code tools; web_search / MCP are unchanged.
618693
"""
619694
logger.info(
620695
f"execute_tool: name={name}, session_id={session_id}, timeout={timeout}"
@@ -647,11 +722,19 @@ def execute_tool(
647722
)
648723
if name == "python":
649724
return _python_exec(
650-
arguments.get("code", ""), cancel_event, effective_timeout, session_id
725+
arguments.get("code", ""),
726+
cancel_event,
727+
effective_timeout,
728+
session_id,
729+
disable_sandbox = disable_sandbox,
651730
)
652731
if name == "terminal":
653732
return _bash_exec(
654-
arguments.get("command", ""), cancel_event, effective_timeout, session_id
733+
arguments.get("command", ""),
734+
cancel_event,
735+
effective_timeout,
736+
session_id,
737+
disable_sandbox = disable_sandbox,
655738
)
656739
return f"Unknown tool: {name}"
657740

@@ -1987,15 +2070,21 @@ def _python_exec(
19872070
cancel_event = None,
19882071
timeout: int = _EXEC_TIMEOUT,
19892072
session_id: str | None = None,
2073+
disable_sandbox: bool = False,
19902074
) -> str:
1991-
"""Execute Python code in a subprocess sandbox."""
2075+
"""Execute Python code in a subprocess sandbox.
2076+
2077+
disable_sandbox (Bypass Permissions): skip the safety analysis and rlimit
2078+
pre-exec, and use the host env minus secrets.
2079+
"""
19922080
if not code or not code.strip():
19932081
return "No code provided."
19942082

1995-
# Validate imports and code safety
1996-
error = _check_code_safety(code)
1997-
if error:
1998-
return error
2083+
# Validate imports and code safety (skipped when the sandbox is disabled)
2084+
if not disable_sandbox:
2085+
error = _check_code_safety(code)
2086+
if error:
2087+
return error
19992088

20002089
tmp_path = None
20012090
workdir = _get_workdir(session_id)
@@ -2017,7 +2106,9 @@ def _python_exec(
20172106
with os.fdopen(fd, "w") as f:
20182107
f.write(code)
20192108

2020-
safe_env = _build_safe_env(workdir)
2109+
safe_env = (
2110+
_build_bypass_env(workdir) if disable_sandbox else _build_safe_env(workdir)
2111+
)
20212112
popen_kwargs = dict(
20222113
stdout = subprocess.PIPE,
20232114
stderr = subprocess.STDOUT,
@@ -2026,7 +2117,9 @@ def _python_exec(
20262117
env = safe_env,
20272118
)
20282119
if sys.platform != "win32":
2029-
popen_kwargs["preexec_fn"] = _sandbox_preexec
2120+
popen_kwargs["preexec_fn"] = (
2121+
_bypass_preexec if disable_sandbox else _sandbox_preexec
2122+
)
20302123
else:
20312124
popen_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
20322125

@@ -2094,19 +2187,27 @@ def _bash_exec(
20942187
cancel_event = None,
20952188
timeout: int = _EXEC_TIMEOUT,
20962189
session_id: str | None = None,
2190+
disable_sandbox: bool = False,
20972191
) -> str:
2098-
"""Execute a bash command in a subprocess sandbox."""
2192+
"""Execute a bash command in a subprocess sandbox.
2193+
2194+
disable_sandbox (Bypass Permissions): skip the command blocklist and rlimit
2195+
pre-exec, and use the host env minus secrets.
2196+
"""
20992197
if not command or not command.strip():
21002198
return "No command provided."
21012199

2102-
# Block dangerous commands (shlex + regex based)
2103-
blocked = _find_blocked_commands(command)
2104-
if blocked:
2105-
return f"Blocked command(s) for safety: {', '.join(sorted(blocked))}"
2200+
# Block dangerous commands (skipped when the sandbox is disabled)
2201+
if not disable_sandbox:
2202+
blocked = _find_blocked_commands(command)
2203+
if blocked:
2204+
return f"Blocked command(s) for safety: {', '.join(sorted(blocked))}"
21062205

21072206
try:
21082207
workdir = _get_workdir(session_id)
2109-
safe_env = _build_safe_env(workdir)
2208+
safe_env = (
2209+
_build_bypass_env(workdir) if disable_sandbox else _build_safe_env(workdir)
2210+
)
21102211
popen_kwargs = dict(
21112212
stdout = subprocess.PIPE,
21122213
stderr = subprocess.STDOUT,
@@ -2115,7 +2216,9 @@ def _bash_exec(
21152216
env = safe_env,
21162217
)
21172218
if sys.platform != "win32":
2118-
popen_kwargs["preexec_fn"] = _sandbox_preexec
2219+
popen_kwargs["preexec_fn"] = (
2220+
_bypass_preexec if disable_sandbox else _sandbox_preexec
2221+
)
21192222
else:
21202223
popen_kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
21212224

studio/backend/models/inference.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,10 @@ class ChatCompletionRequest(BaseModel):
724724
None,
725725
description = "[x-unsloth] When true, pause before each tool call and wait for the user to allow/deny it via POST /api/inference/tool-confirm.",
726726
)
727+
bypass_permissions: Optional[bool] = Field(
728+
False,
729+
description = "[x-unsloth] Bypass Permissions: when true, skip the tool-call confirmation gate AND disable the python/terminal execution sandbox (safety checks, command blocklist, resource limits). Secret env vars are still stripped. Takes precedence over confirm_tool_calls.",
730+
)
727731
auto_heal_tool_calls: Optional[bool] = Field(
728732
True,
729733
description = "[x-unsloth] Auto-detect and fix malformed tool calls from model output.",

studio/backend/routes/inference.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2811,7 +2811,11 @@ def gguf_generate_with_tools():
28112811
if payload.tool_call_timeout is not None
28122812
else 300,
28132813
session_id = payload.session_id,
2814-
confirm_tool_calls = bool(payload.confirm_tool_calls),
2814+
# Bypass Permissions takes precedence over the confirm gate:
2815+
# never prompt while bypassing.
2816+
confirm_tool_calls = bool(payload.confirm_tool_calls)
2817+
and not bool(payload.bypass_permissions),
2818+
bypass_permissions = bool(payload.bypass_permissions),
28152819
)
28162820

28172821
_tool_sentinel = object()
@@ -3332,7 +3336,11 @@ def sf_generate_with_tools():
33323336
else 300,
33333337
session_id = payload.session_id,
33343338
use_adapter = payload.use_adapter,
3335-
confirm_tool_calls = bool(payload.confirm_tool_calls),
3339+
# Bypass Permissions takes precedence over the confirm gate:
3340+
# never prompt while bypassing.
3341+
confirm_tool_calls = bool(payload.confirm_tool_calls)
3342+
and not bool(payload.bypass_permissions),
3343+
bypass_permissions = bool(payload.bypass_permissions),
33363344
)
33373345

33383346
_sf_tool_sentinel = object()
@@ -5013,6 +5021,7 @@ def _run_tool_gen():
50135021
auto_heal_tool_calls = True,
50145022
tool_call_timeout = 300,
50155023
session_id = payload.session_id,
5024+
bypass_permissions = bool(payload.bypass_permissions),
50165025
)
50175026

50185027
if payload.stream:

0 commit comments

Comments
 (0)