fix(cron): scope cron approval context per session

Replace the process-global HERMES_CRON_SESSION env var with a per-session
ContextVar so a cron tick in the gateway process cannot leak into unrelated
live gateway/API/TUI turns. The cron scheduler now sets the ContextVar
inside the job's try/finally scope and resets it on cleanup. Gateway, API
server, ACP adapter, and TUI gateway all pass cron_session='' to explicitly
mark their sessions as non-cron, masking any stale process env.

Co-authored-by: hinablue <hinablue@gmail.com>
Closes #37968
This commit is contained in:
kshitij
2026-08-03 00:12:31 +05:30
parent 5b73cfcc98
commit fb6446fc9e
14 changed files with 368 additions and 15 deletions
+3
View File
@@ -1868,8 +1868,11 @@ class HermesACPAgent(acp.Agent):
# while the tools are rooted at the client's project, so the
# model emits absolute paths under ~/.hermes/workspace and the
# edit silently lands outside the editor's workspace.
# cron_session="" explicitly marks this as a non-cron context,
# masking any leaked process-global HERMES_CRON_SESSION (#37968).
session_tokens = set_session_vars(
session_key=session_id, session_id=session_id, cwd=state.cwd,
cron_session="",
)
except Exception:
session_tokens = None
+9 -5
View File
@@ -3009,11 +3009,6 @@ def run_job(
agent = None
# Mark this as a cron session so the approval system can apply cron_mode.
# This env var is process-wide and persists for the lifetime of the
# scheduler process — every job this process runs is a cron job.
os.environ["HERMES_CRON_SESSION"] = "1"
# Use ContextVars for per-job session/delivery state so parallel jobs
# don't clobber each other's targets (os.environ is process-global).
from gateway.session_context import set_session_vars, clear_session_vars, _VAR_MAP
@@ -3116,7 +3111,14 @@ def run_job(
# statement raises. A leaked writer would deadlock the whole scheduler
# (every future job blocks on acquire_*); a leaked reader blocks all
# future writers. Acquire itself can't leak (it either blocks or returns).
_cron_session_var = _VAR_MAP["HERMES_CRON_SESSION"]
_cron_session_token = None
try:
# Scope cron approval policy to this job. Keep the token so the finally
# restores the pre-job state instead of pinning an explicit empty value,
# which would suppress the legacy os.environ fallback used by standalone
# cron entrypoints and tests.
_cron_session_token = _cron_session_var.set("1")
if _job_workdir:
os.environ["TERMINAL_CWD"] = _job_workdir
logger.info("Job '%s': using workdir %s", job_id, _job_workdir)
@@ -3763,6 +3765,8 @@ def run_job(
# clear_session_vars also clears _SESSION_CWD internally, so no
# separate clear_session_cwd() call is needed.
clear_session_vars(_ctx_tokens)
if _cron_session_token is not None:
_cron_session_var.reset(_cron_session_token)
for _var_name in _cron_delivery_vars:
_VAR_MAP[_var_name].set("")
if _session_db:
+1
View File
@@ -5907,6 +5907,7 @@ class APIServerAdapter(BasePlatformAdapter):
session_key=session_key,
session_id=session_id,
async_delivery=False,
cron_session="",
)
async def _run_agent(
+1
View File
@@ -20582,6 +20582,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
message_id=str(context.source.message_id) if context.source.message_id else "",
profile=getattr(context.source, "profile", "") or "",
async_delivery=_async_delivery,
cron_session="",
)
def _clear_session_env(self, tokens: list) -> None:
+14
View File
@@ -95,6 +95,12 @@ _SESSION_MESSAGE_ID: ContextVar = ContextVar("HERMES_SESSION_MESSAGE_ID", defaul
_SESSION_PROFILE: ContextVar = ContextVar("HERMES_SESSION_PROFILE", default=_UNSET)
# Per-session cron marker. Unlike the process-global legacy env var, this is
# scoped to one cron job / inbound session. _UNSET preserves the legacy env
# fallback for CLI/tests; "1" marks cron; "" explicitly marks non-cron and
# masks any leaked process env value.
_CRON_SESSION: ContextVar = ContextVar("HERMES_CRON_SESSION", default=_UNSET)
# Whether the current session's delivery channel can route an ASYNC completion
# back to the agent AFTER the current turn ends (i.e. wake a fresh turn).
#
@@ -135,6 +141,7 @@ _VAR_MAP = {
"HERMES_UI_SESSION_ID": _SESSION_UI_SESSION_ID,
"HERMES_SESSION_MESSAGE_ID": _SESSION_MESSAGE_ID,
"HERMES_SESSION_PROFILE": _SESSION_PROFILE,
"HERMES_CRON_SESSION": _CRON_SESSION,
"HERMES_CRON_AUTO_DELIVER_PLATFORM": _CRON_AUTO_DELIVER_PLATFORM,
"HERMES_CRON_AUTO_DELIVER_CHAT_ID": _CRON_AUTO_DELIVER_CHAT_ID,
"HERMES_CRON_AUTO_DELIVER_THREAD_ID": _CRON_AUTO_DELIVER_THREAD_ID,
@@ -212,6 +219,7 @@ def set_session_vars(
cwd: str = "",
async_delivery: bool = True,
ui_session_id: str = "",
cron_session: Any = _UNSET,
) -> list:
"""Set all session context variables and return reset tokens.
@@ -227,6 +235,10 @@ def set_session_vars(
background completion back to the agent after the turn ends (see
``_SESSION_ASYNC_DELIVERY`` / ``async_delivery_supported``). Stateless
request/response adapters (the API server) pass ``False``.
``cron_session`` is tri-state: ``_UNSET`` preserves legacy
``os.environ["HERMES_CRON_SESSION"]`` fallback, ``"1"`` marks a cron job,
and ``""`` explicitly marks a non-cron session while masking leaked env.
"""
# Mark the session-context machinery engaged for this process. The
# subprocess-env bridge uses this to switch from "os.environ fallback" to
@@ -247,6 +259,7 @@ def set_session_vars(
_SESSION_UI_SESSION_ID.set(ui_session_id),
_SESSION_MESSAGE_ID.set(message_id),
_SESSION_PROFILE.set(profile),
_CRON_SESSION.set(cron_session),
_SESSION_ASYNC_DELIVERY.set(bool(async_delivery)),
]
try:
@@ -283,6 +296,7 @@ def clear_session_vars(tokens: list) -> None:
_SESSION_UI_SESSION_ID,
_SESSION_MESSAGE_ID,
_SESSION_PROFILE,
_CRON_SESSION,
):
var.set("")
# Reset async-delivery capability to the "never set" sentinel rather than a
+20
View File
@@ -19,3 +19,23 @@ def _default_cron_test_model(monkeypatch):
"""Pin a default HERMES_MODEL so cron run_job tests have a resolvable model."""
monkeypatch.setenv("HERMES_MODEL", "test-cron-default-model")
yield
@pytest.fixture(autouse=True)
def _reset_session_context_vars():
"""Restore session ContextVars around cron tests that call run_job directly.
Production confines each cron run to a copied context, but direct unit tests
share the pytest context. ``run_job`` intentionally clears ordinary session
variables to explicit empty values, which would otherwise shadow legacy env
fallbacks used by later approval tests in the same process.
"""
from gateway.session_context import _UNSET, _VAR_MAP
def _reset_all():
for var in _VAR_MAP.values():
var.set(_UNSET)
_reset_all()
yield
_reset_all()
@@ -0,0 +1,161 @@
"""Regression test for cron-session approval isolation.
A cron job must use ``approvals.cron_mode`` for its own ``execute_code`` call,
without leaving process-global state that changes a later interactive gateway
turn handled by the same Python process.
"""
from __future__ import annotations
import os
import pytest
import cron.scheduler as cron_scheduler
from gateway.session_context import (
clear_session_vars,
get_session_env,
reset_session_vars,
set_session_vars,
)
from tools import approval as approval_module
class _DummySessionDB:
def set_session_title(self, *args, **kwargs):
pass
def end_session(self, *args, **kwargs):
pass
def close(self):
pass
class _FakeCronAgent:
def __init__(self, *args, **kwargs):
self.kwargs = kwargs
def run_conversation(self, prompt):
result = approval_module.check_execute_code_guard(
"import os; print(1)", "local"
)
assert result["approved"] is False
assert result["outcome"] == "blocked"
assert get_session_env("HERMES_CRON_SESSION") == "1"
return {
"completed": True,
"failed": False,
"final_response": "cron execute_code blocked",
"turn_exit_reason": "",
}
def close(self):
pass
@pytest.fixture(autouse=True)
def _clear_approval_state(monkeypatch):
reset_session_vars()
monkeypatch.delenv("HERMES_CRON_SESSION", raising=False)
monkeypatch.delenv("HERMES_GATEWAY_SESSION", raising=False)
monkeypatch.delenv("HERMES_INTERACTIVE", raising=False)
monkeypatch.delenv("HERMES_EXEC_ASK", raising=False)
approval_module._permanent_approved.clear()
approval_module.clear_session("default")
approval_module.clear_session("cron-isolation-session")
yield
approval_module._permanent_approved.clear()
approval_module.clear_session("default")
approval_module.clear_session("cron-isolation-session")
reset_session_vars()
def _register_gateway_auto_approve(session_key: str) -> None:
def _notify(_approval_data):
with approval_module._lock:
entries = approval_module._gateway_queues.get(session_key, [])
if entries:
entry = entries[-1]
entry.result = "once"
entry.event.set()
with approval_module._lock:
approval_module._gateway_notify_cbs[session_key] = _notify
def test_run_job_cron_execute_code_deny_does_not_pollute_later_gateway_execute_code(
monkeypatch, tmp_path
):
"""Cron deny stays scoped; a later gateway approval still reaches its user."""
monkeypatch.setenv("HERMES_MODEL", "test-model")
monkeypatch.setattr(approval_module, "_YOLO_MODE_FROZEN", False)
monkeypatch.setattr(approval_module, "_get_approval_mode", lambda: "manual")
monkeypatch.setattr(approval_module, "_get_cron_approval_mode", lambda: "deny")
monkeypatch.setattr("hermes_state.SessionDB", _DummySessionDB)
monkeypatch.setattr("run_agent.AIAgent", _FakeCronAgent)
monkeypatch.setattr(
"hermes_constants.resolve_reasoning_config", lambda *_args, **_kwargs: None
)
monkeypatch.setattr(
"hermes_cli.runtime_provider.resolve_runtime_provider",
lambda **_kwargs: {
"api_key": "test-key",
"base_url": None,
"provider": "test-provider",
"api_mode": None,
"command": None,
"args": None,
},
)
monkeypatch.setattr("tools.mcp_tool.discover_mcp_tools", lambda: [])
monkeypatch.setattr(cron_scheduler, "_get_hermes_home", lambda: tmp_path)
monkeypatch.setattr(cron_scheduler, "get_fallback_chain", lambda _cfg: [])
monkeypatch.setattr(
cron_scheduler, "_guard_job_credential_exfil", lambda _job: None
)
success, _output, final_response, error = cron_scheduler.run_job(
{
"id": "ctx-isolation",
"name": "Context Isolation",
"prompt": "Run safely",
"schedule_display": "manual",
}
)
assert success is True
assert error is None
assert final_response == "cron execute_code blocked"
assert os.environ.get("HERMES_CRON_SESSION") is None
assert get_session_env("HERMES_CRON_SESSION") == ""
# A completed in-process job must restore the truly-unset ContextVar state,
# not leave an explicit empty value that shadows the standalone cron env
# fallback in this reused context.
monkeypatch.setenv("HERMES_CRON_SESSION", "1")
assert get_session_env("HERMES_CRON_SESSION") == "1"
monkeypatch.delenv("HERMES_CRON_SESSION")
session_key = "cron-isolation-session"
key_token = approval_module.set_current_session_key(session_key)
session_tokens = set_session_vars(
platform="discord",
chat_id="123",
session_key=session_key,
cron_session="",
)
monkeypatch.setenv("HERMES_GATEWAY_SESSION", "1")
try:
_register_gateway_auto_approve(session_key)
result = approval_module.check_execute_code_guard(
"import os; print(2)", "local"
)
assert result["approved"] is True
assert result.get("user_approved") is True
finally:
clear_session_vars(session_tokens)
approval_module.reset_current_session_key(key_token)
with approval_module._lock:
approval_module._gateway_queues.pop(session_key, None)
approval_module._gateway_notify_cbs.pop(session_key, None)
+36
View File
@@ -10,6 +10,7 @@ from gateway.session_context import (
get_session_env,
set_session_vars,
clear_session_vars,
reset_session_vars,
_VAR_MAP,
_UNSET,
)
@@ -237,3 +238,38 @@ async def test_run_in_executor_with_context_preserves_session_env(monkeypatch):
}
def test_cron_session_contextvar_preserves_legacy_env_fallback(monkeypatch):
"""Unset cron ContextVar keeps old env-only cron callers working."""
monkeypatch.setenv("HERMES_CRON_SESSION", "1")
assert get_session_env("HERMES_CRON_SESSION") == "1"
def test_cron_session_explicit_blank_masks_leaked_env(monkeypatch):
"""Non-cron session bindings must override a stale process cron env flag."""
monkeypatch.setenv("HERMES_CRON_SESSION", "1")
tokens = set_session_vars(platform="api_server", cron_session="")
try:
assert get_session_env("HERMES_CRON_SESSION") == ""
finally:
clear_session_vars(tokens)
assert get_session_env("HERMES_CRON_SESSION") == ""
def test_cron_session_set_clear_and_reset_tristate(monkeypatch):
"""Cron marker supports _UNSET fallback, 1 cron, and explicit clear."""
monkeypatch.setenv("HERMES_CRON_SESSION", "1")
tokens = set_session_vars(cron_session="1")
assert get_session_env("HERMES_CRON_SESSION") == "1"
clear_session_vars(tokens)
assert get_session_env("HERMES_CRON_SESSION") == ""
reset_session_vars()
assert get_session_env("HERMES_CRON_SESSION") == "1"
+62
View File
@@ -3,6 +3,7 @@
import pytest
import tools.approval as approval_module
from gateway.session_context import clear_session_vars, reset_session_vars, set_session_vars
from tools.approval import (
_get_cron_approval_mode,
check_all_command_guards,
@@ -16,10 +17,12 @@ def _clear_approval_state():
approval_module._permanent_approved.clear()
approval_module.clear_session("default")
approval_module.clear_session("test-session")
reset_session_vars()
yield
approval_module._permanent_approved.clear()
approval_module.clear_session("default")
approval_module.clear_session("test-session")
reset_session_vars()
# ---------------------------------------------------------------------------
@@ -83,6 +86,65 @@ class TestCronApprovalModeParsing:
assert _get_cron_approval_mode() == "deny"
# ---------------------------------------------------------------------------
# ContextVar cron detection
# ---------------------------------------------------------------------------
class TestCronContextVarDetection:
def test_legacy_env_fallback_still_marks_cron(self, monkeypatch):
monkeypatch.setenv("HERMES_CRON_SESSION", "1")
assert approval_module._is_cron_approval_context() is True
def test_explicit_blank_masks_leaked_cron_env_for_gateway_classification(self, monkeypatch):
monkeypatch.setenv("HERMES_CRON_SESSION", "1")
monkeypatch.setenv("HERMES_GATEWAY_SESSION", "1")
tokens = set_session_vars(platform="api_server", cron_session="")
try:
assert approval_module._is_cron_approval_context() is False
assert approval_module._is_gateway_approval_context() is True
finally:
clear_session_vars(tokens)
def test_scoped_cron_deny_for_dangerous_all_and_execute_code(self, monkeypatch):
monkeypatch.delenv("HERMES_CRON_SESSION", raising=False)
monkeypatch.delenv("HERMES_GATEWAY_SESSION", raising=False)
monkeypatch.delenv("HERMES_INTERACTIVE", raising=False)
monkeypatch.delenv("HERMES_EXEC_ASK", raising=False)
monkeypatch.setattr(approval_module, "_YOLO_MODE_FROZEN", False)
monkeypatch.setattr(approval_module, "_get_approval_mode", lambda: "manual")
monkeypatch.setattr(approval_module, "_get_cron_approval_mode", lambda: "deny")
tokens = set_session_vars(cron_session="1")
try:
dangerous = check_dangerous_command("rm -rf /tmp/stuff", "local")
combined = check_all_command_guards("rm -rf /tmp/stuff", "local")
code = approval_module.check_execute_code_guard("import os", "local")
finally:
clear_session_vars(tokens)
assert dangerous["approved"] is False
assert combined["approved"] is False
assert code["approved"] is False
assert code["outcome"] == "blocked"
def test_non_cron_blank_context_keeps_headless_execute_code_legacy_approved(self, monkeypatch):
monkeypatch.setenv("HERMES_CRON_SESSION", "1")
monkeypatch.delenv("HERMES_GATEWAY_SESSION", raising=False)
monkeypatch.delenv("HERMES_INTERACTIVE", raising=False)
monkeypatch.delenv("HERMES_EXEC_ASK", raising=False)
monkeypatch.setattr(approval_module, "_YOLO_MODE_FROZEN", False)
monkeypatch.setattr(approval_module, "_get_approval_mode", lambda: "manual")
monkeypatch.setattr(approval_module, "_get_cron_approval_mode", lambda: "deny")
tokens = set_session_vars(cron_session="")
try:
result = approval_module.check_execute_code_guard("import os", "local")
finally:
clear_session_vars(tokens)
assert result["approved"] is True
# ---------------------------------------------------------------------------
# check_dangerous_command() with cron session
# ---------------------------------------------------------------------------
@@ -24,6 +24,7 @@ import pytest
from tools import approval as A
from tools.thread_context import propagate_context_to_thread
from gateway.session_context import clear_session_vars, reset_session_vars, set_session_vars
# ---------------------------------------------------------------------------
@@ -122,6 +123,8 @@ def gw_session(monkeypatch):
with A._lock:
A._gateway_queues.pop(session_key, None)
A._gateway_notify_cbs.pop(session_key, None)
A._permanent_approved.discard("execute_code")
A._session_approved.get(session_key, set()).discard("execute_code")
try:
yield session_key
finally:
@@ -178,6 +181,39 @@ def test_guard_headless_local_approved(monkeypatch):
def test_guard_cron_deny_blocks(monkeypatch):
monkeypatch.setattr(A, "_YOLO_MODE_FROZEN", False)
monkeypatch.delenv("HERMES_CRON_SESSION", raising=False)
monkeypatch.delenv("HERMES_GATEWAY_SESSION", raising=False)
monkeypatch.setattr(A, "_get_approval_mode", lambda: "manual")
monkeypatch.setattr(A, "_get_cron_approval_mode", lambda: "deny")
tokens = set_session_vars(cron_session="1")
try:
res = A.check_execute_code_guard("import os", "local")
finally:
clear_session_vars(tokens)
assert res["approved"] is False
assert res["outcome"] == "blocked"
def test_guard_explicit_non_cron_masks_leaked_env(monkeypatch):
monkeypatch.setattr(A, "_YOLO_MODE_FROZEN", False)
monkeypatch.setenv("HERMES_CRON_SESSION", "1")
monkeypatch.delenv("HERMES_GATEWAY_SESSION", raising=False)
monkeypatch.delenv("HERMES_INTERACTIVE", raising=False)
monkeypatch.delenv("HERMES_EXEC_ASK", raising=False)
monkeypatch.setattr(A, "_get_approval_mode", lambda: "manual")
monkeypatch.setattr(A, "_get_cron_approval_mode", lambda: "deny")
tokens = set_session_vars(cron_session="")
try:
res = A.check_execute_code_guard("import os", "local")
finally:
clear_session_vars(tokens)
reset_session_vars()
assert res["approved"] is True
def test_guard_legacy_env_cron_still_blocks(monkeypatch):
reset_session_vars()
monkeypatch.setattr(A, "_YOLO_MODE_FROZEN", False)
monkeypatch.setenv("HERMES_CRON_SESSION", "1")
monkeypatch.delenv("HERMES_GATEWAY_SESSION", raising=False)
+3 -5
View File
@@ -77,8 +77,7 @@ class TestRequestToolApproval:
def test_cron_deny_mode_blocks(self, monkeypatch):
monkeypatch.setattr(approval, "_is_interactive_cli", lambda: False)
monkeypatch.setattr(approval, "_is_gateway_approval_context", lambda: False)
monkeypatch.setattr(approval, "env_var_enabled",
lambda v: v == "HERMES_CRON_SESSION")
monkeypatch.setattr(approval, "_is_cron_approval_context", lambda: True)
monkeypatch.setattr(approval, "_get_cron_approval_mode", lambda: "deny")
res = request_tool_approval("terminal", "smtp send")
assert res["approved"] is False
@@ -87,8 +86,7 @@ class TestRequestToolApproval:
def test_cron_approve_mode_allows(self, monkeypatch):
monkeypatch.setattr(approval, "_is_interactive_cli", lambda: False)
monkeypatch.setattr(approval, "_is_gateway_approval_context", lambda: False)
monkeypatch.setattr(approval, "env_var_enabled",
lambda v: v == "HERMES_CRON_SESSION")
monkeypatch.setattr(approval, "_is_cron_approval_context", lambda: True)
monkeypatch.setattr(approval, "_get_cron_approval_mode", lambda: "approve")
res = request_tool_approval("terminal", "smtp send")
assert res["approved"] is True
@@ -116,7 +114,7 @@ class TestRequestToolApproval:
— a plugin-flagged action never runs ungated without a human."""
monkeypatch.setattr(approval, "_is_interactive_cli", lambda: False)
monkeypatch.setattr(approval, "_is_gateway_approval_context", lambda: False)
monkeypatch.setattr(approval, "env_var_enabled", lambda v: False) # not cron
monkeypatch.setattr(approval, "_is_cron_approval_context", lambda: False)
res = request_tool_approval("terminal", "smtp send")
assert res["approved"] is False
assert "no interactive user or gateway" in res["message"].lower()
+20 -4
View File
@@ -224,6 +224,22 @@ def _get_session_platform() -> str:
return os.getenv("HERMES_SESSION_PLATFORM", "") or ""
def _is_cron_approval_context() -> bool:
"""True when the current approval decision is running inside cron.
Prefer the session ContextVar so one cron job cannot taint unrelated
gateway/API/TUI turns in the same process. If the session context layer is
not engaged or unavailable, fall back to the legacy process env var for CLI
tests and older entrypoints.
"""
try:
from gateway.session_context import get_session_env
return is_truthy_value(get_session_env("HERMES_CRON_SESSION", ""))
except Exception:
return env_var_enabled("HERMES_CRON_SESSION")
def _is_gateway_approval_context() -> bool:
"""True when this call is inside a gateway/API session.
@@ -238,7 +254,7 @@ def _is_gateway_approval_context() -> bool:
fall through to the gateway branch would submit a pending approval
with no listener and block the job indefinitely.
"""
if env_var_enabled("HERMES_CRON_SESSION"):
if _is_cron_approval_context():
return False
if env_var_enabled("HERMES_GATEWAY_SESSION"):
return True
@@ -2903,7 +2919,7 @@ def _run_approval_gate(
if not is_cli and not is_gateway:
# Cron sessions: respect cron_mode config
if env_var_enabled("HERMES_CRON_SESSION"):
if _is_cron_approval_context():
if _get_cron_approval_mode() == "deny":
return {
"approved": False,
@@ -3431,7 +3447,7 @@ def check_all_command_guards(command: str, env_type: str,
# flows, we do not block on approvals and we skip external guard work.
if not is_cli and not is_gateway and not is_ask:
# Cron sessions: respect cron_mode config
if env_var_enabled("HERMES_CRON_SESSION"):
if _is_cron_approval_context():
if _get_cron_approval_mode() == "deny":
# Run detection to get a description for the block message
is_dangerous, _pk, description = detect_dangerous_command(command)
@@ -3882,7 +3898,7 @@ def check_execute_code_guard(code: str, env_type: str,
is_ask = env_var_enabled("HERMES_EXEC_ASK")
# Cron: no user is present to approve arbitrary code.
if env_var_enabled("HERMES_CRON_SESSION"):
if _is_cron_approval_context():
if _get_cron_approval_mode() == "deny":
return {
"approved": False,
+1 -1
View File
@@ -409,7 +409,7 @@ def _cwd_marker(session_id: str) -> str:
# as the Python-side contract for the exclusion set; the dump path unsets by
# name/prefix instead of grepping declare lines (see below / issue #71296).
_SNAPSHOT_EXCLUDED_ENV_REGEX = (
"^declare -x (HERMES_SESSION_|HERMES_UI_SESSION_ID|HERMES_CRON_AUTO_DELIVER_)"
"^declare -x (HERMES_SESSION_|HERMES_UI_SESSION_ID|HERMES_CRON_AUTO_DELIVER_|HERMES_CRON_SESSION)"
)
_SHELL_ENV_NAME_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
+1
View File
@@ -2944,6 +2944,7 @@ def _set_session_context(
source=source,
cwd=resolved,
ui_session_id=ui_session_id,
cron_session="",
)
except Exception:
return []