Skip to content

Commit 139548d

Browse files
committed
fix(workflow): migrate legacy executor defaults safely
Signed-off-by: yjlu12 <1064690083@qq.com>
1 parent 4499c99 commit 139548d

6 files changed

Lines changed: 244 additions & 2 deletions

File tree

core/workflow/configs/__init__.py

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,85 @@
22
from abc import ABC, abstractmethod
33
from pathlib import Path
44

5-
from dotenv import load_dotenv
5+
from dotenv import dotenv_values, load_dotenv
66
from loguru import logger
77

8-
from workflow.configs.app_config import WorkflowConfig
8+
from workflow.configs.app_config import DEFAULT_CODE_EXECUTOR_TYPE, WorkflowConfig
99
from workflow.consts.config_env import EnvStrategy
1010

11+
LEGACY_CODE_EXECUTOR_DEFAULT_COMMENTS = (
12+
# Directly previous releases shipped this header.
13+
"supported fallback types: disabled, ifly, ifly-v2, langchain (default: disabled)",
14+
# Keep compatibility with the earlier security release as well.
15+
"supported types: disabled, ifly, ifly-v2, langchain, e2b (default: disabled)",
16+
)
17+
18+
19+
def _read_simple_env_values(env_file: Path, keys: set[str]) -> dict[str, str]:
20+
"""Read the small set of deployment settings needed before dotenv loading.
21+
22+
``load_dotenv`` intentionally keeps existing process variables authoritative.
23+
We need to inspect the mounted workflow config once before loading it so that
24+
an untouched config from the pre-Pyodide release can be migrated safely. A
25+
non-interpolating read through python-dotenv keeps the compatibility check
26+
consistent with the actual load below (including quoting and comments).
27+
"""
28+
try:
29+
parsed_values = dotenv_values(
30+
dotenv_path=env_file, interpolate=False, encoding="utf-8"
31+
)
32+
except (OSError, UnicodeError, ValueError):
33+
return {}
34+
35+
return {key: value for key in keys if (value := parsed_values.get(key)) is not None}
36+
37+
38+
def _has_legacy_code_executor_signature(env_file: Path) -> bool:
39+
"""Return whether *env_file* has the historical generated-default header."""
40+
try:
41+
content = env_file.read_text(encoding="utf-8")
42+
except (OSError, UnicodeError):
43+
return False
44+
normalized_content = " ".join(content.lower().split())
45+
return any(
46+
signature in normalized_content
47+
for signature in LEGACY_CODE_EXECUTOR_DEFAULT_COMMENTS
48+
)
49+
50+
51+
def _migrate_legacy_code_executor_default(env_file: Path) -> None:
52+
"""Keep upgrades from the old disabled template zero-configuration.
53+
54+
Before the built-in Pyodide sandbox became the default, the generated
55+
workflow config contained ``CODE_EXEC_TYPE=disabled`` and had no memory
56+
limit setting. Compose deliberately preserves user-mounted config files,
57+
so an upgrade would otherwise keep that historical default forever and a
58+
fresh Code node would fail even though the new image contains the isolated
59+
executor. Treat only that recognizable, unversioned template as a legacy
60+
default. The historical header is required as an additional fingerprint
61+
so a customized config that explicitly sets ``disabled`` is not silently
62+
changed. An explicit process-level ``CODE_EXEC_TYPE`` (including
63+
``disabled``) always wins and remains fail-closed.
64+
"""
65+
if "CODE_EXEC_TYPE" in os.environ:
66+
return
67+
68+
values = _read_simple_env_values(
69+
env_file, {"CODE_EXEC_TYPE", "CODE_EXEC_MEMORY_LIMIT_MB"}
70+
)
71+
if (
72+
values.get("CODE_EXEC_TYPE", "").strip().lower() == "disabled"
73+
and "CODE_EXEC_MEMORY_LIMIT_MB" not in values
74+
and _has_legacy_code_executor_signature(env_file)
75+
):
76+
os.environ["CODE_EXEC_TYPE"] = DEFAULT_CODE_EXECUTOR_TYPE
77+
logger.warning(
78+
"Migrating the legacy workflow code executor default from disabled "
79+
"to the built-in isolated LangChain/Pyodide sandbox. Set "
80+
"CODE_EXEC_TYPE=disabled as a process environment variable to "
81+
"explicitly disable Code nodes."
82+
)
83+
1184

1285
class EnvLoader(ABC):
1386
"""
@@ -39,6 +112,12 @@ def load(self) -> None:
39112
:raises ValueError: If no configuration file is found
40113
"""
41114
if os.path.exists(self.env_file):
115+
# Compose may deliberately pass an empty optional override from
116+
# ``.env``. Treat an empty value as unset so the mounted workflow
117+
# config can still supply its defaults.
118+
if not os.getenv("CODE_EXEC_TYPE", "").strip():
119+
os.environ.pop("CODE_EXEC_TYPE", None)
120+
_migrate_legacy_code_executor_default(self.env_file)
42121
load_dotenv(self.env_file, override=False)
43122
logger.debug("Using config.env file.")
44123
else:
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import os
2+
from pathlib import Path
3+
4+
import pytest
5+
6+
from workflow.configs import LocalLoader
7+
8+
9+
def _write_config(
10+
path: Path,
11+
*,
12+
include_memory_limit: bool = False,
13+
legacy_header: str = "fallback",
14+
) -> None:
15+
memory_setting = "CODE_EXEC_MEMORY_LIMIT_MB=256\n" if include_memory_limit else ""
16+
if legacy_header == "fallback":
17+
header = (
18+
"# Supported fallback types: disabled, ifly, ifly-v2, langchain "
19+
"(default: disabled)\n"
20+
)
21+
else:
22+
header = (
23+
"# Supported types: disabled, ifly, ifly-v2, langchain, e2b "
24+
"(default: disabled)\n"
25+
)
26+
path.write_text(
27+
"# Code Executor Settings\n"
28+
f"{header}"
29+
"CODE_EXEC_TYPE=disabled\n"
30+
"CODE_EXEC_TIMEOUT_SEC=10\n"
31+
f"{memory_setting}",
32+
encoding="utf-8",
33+
)
34+
35+
36+
def test_legacy_disabled_template_migrates_to_builtin_executor(
37+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
38+
) -> None:
39+
config_file = tmp_path / "config.env"
40+
_write_config(config_file)
41+
monkeypatch.delenv("CODE_EXEC_TYPE", raising=False)
42+
43+
loader = LocalLoader()
44+
loader.env_file = config_file
45+
loader.load()
46+
47+
assert os.getenv("CODE_EXEC_TYPE") == "langchain"
48+
49+
50+
def test_earlier_legacy_disabled_template_migrates_to_builtin_executor(
51+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
52+
) -> None:
53+
config_file = tmp_path / "config.env"
54+
_write_config(config_file, legacy_header="types")
55+
monkeypatch.delenv("CODE_EXEC_TYPE", raising=False)
56+
57+
loader = LocalLoader()
58+
loader.env_file = config_file
59+
loader.load()
60+
61+
assert os.getenv("CODE_EXEC_TYPE") == "langchain"
62+
63+
64+
def test_explicit_process_disabled_setting_remains_fail_closed(
65+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
66+
) -> None:
67+
config_file = tmp_path / "config.env"
68+
_write_config(config_file)
69+
monkeypatch.setenv("CODE_EXEC_TYPE", "disabled")
70+
71+
loader = LocalLoader()
72+
loader.env_file = config_file
73+
loader.load()
74+
75+
assert os.getenv("CODE_EXEC_TYPE") == "disabled"
76+
77+
78+
def test_new_template_can_explicitly_keep_disabled_setting(
79+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
80+
) -> None:
81+
config_file = tmp_path / "config.env"
82+
_write_config(config_file, include_memory_limit=True)
83+
monkeypatch.delenv("CODE_EXEC_TYPE", raising=False)
84+
85+
loader = LocalLoader()
86+
loader.env_file = config_file
87+
loader.load()
88+
89+
assert os.getenv("CODE_EXEC_TYPE") == "disabled"
90+
91+
92+
def test_custom_legacy_config_disabled_setting_is_not_migrated(
93+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
94+
) -> None:
95+
config_file = tmp_path / "config.env"
96+
config_file.write_text(
97+
"# Code Executor Settings\n"
98+
"# Explicit administrator override: keep Code nodes disabled\n"
99+
"CODE_EXEC_TYPE=disabled\n",
100+
encoding="utf-8",
101+
)
102+
monkeypatch.delenv("CODE_EXEC_TYPE", raising=False)
103+
104+
loader = LocalLoader()
105+
loader.env_file = config_file
106+
loader.load()
107+
108+
assert os.getenv("CODE_EXEC_TYPE") == "disabled"
109+
110+
111+
def test_legacy_header_with_hash_in_value_is_not_migrated(
112+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
113+
) -> None:
114+
config_file = tmp_path / "config.env"
115+
config_file.write_text(
116+
"# Code Executor Settings\n"
117+
"# Supported fallback types: disabled, ifly, ifly-v2, langchain "
118+
"(default: disabled)\n"
119+
"CODE_EXEC_TYPE=disabled#custom-value\n",
120+
encoding="utf-8",
121+
)
122+
monkeypatch.delenv("CODE_EXEC_TYPE", raising=False)
123+
124+
loader = LocalLoader()
125+
loader.env_file = config_file
126+
loader.load()
127+
128+
assert os.getenv("CODE_EXEC_TYPE") == "disabled#custom-value"
129+
130+
131+
def test_empty_process_override_uses_new_template_value(
132+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
133+
) -> None:
134+
config_file = tmp_path / "config.env"
135+
config_file.write_text(
136+
"CODE_EXEC_TYPE=langchain\nCODE_EXEC_MEMORY_LIMIT_MB=256\n",
137+
encoding="utf-8",
138+
)
139+
monkeypatch.setenv("CODE_EXEC_TYPE", "")
140+
141+
loader = LocalLoader()
142+
loader.env_file = config_file
143+
loader.load()
144+
145+
assert os.getenv("CODE_EXEC_TYPE") == "langchain"

docker/astronAgent/.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,11 @@ OSS_PRESIGN_EXPIRY_SECONDS_CONSOLE=600
225225
REDIS_DATABASE_CONSOLE=1
226226

227227
# Internal workflow artifact upload authentication
228+
# Optional Code-node executor override. Leave empty to use the built-in
229+
# LangChain/Pyodide isolated sandbox; E2B is only used when enabled per workflow.
230+
# Set to "disabled" only when Code nodes should be intentionally unavailable.
231+
CODE_EXEC_TYPE=
232+
228233
# Optional: leave empty to let console-hub generate one token in the shared, private
229234
# secret volume used read-only by core-agent and core-workflow. If explicitly set,
230235
# the token must contain at least 32 characters and is atomically synchronized to

docker/astronAgent/docker-compose.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,6 +676,9 @@ services:
676676
container_name: astron-agent-core-workflow
677677
environment:
678678
RUNTIME_ENV: "${RUNTIME_ENV:-dev}"
679+
# Optional process-level override. Leave empty to use the built-in
680+
# LangChain/Pyodide default from config/workflow/config.env.
681+
CODE_EXEC_TYPE: "${CODE_EXEC_TYPE:-}"
679682
WORKFLOW_INTERNAL_API_KEY_FILE: "/app/secrets/workflow/workflow-internal-api-key"
680683
TENANT_ID: "680ab54f"
681684
TENANT_KEY_FILE: "/app/secrets/tenant/tenant-key"

docs/CONFIGURATION.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,11 @@ network, subprocess, or FFI permissions. E2B is optional; enabling it for a
281281
workflow makes that workflow use E2B first. Running user-provided code inside
282282
the core-workflow process (`local`) is not supported.
283283

284+
Upgrade note: an untouched config generated by older releases (`CODE_EXEC_TYPE=disabled`
285+
without `CODE_EXEC_MEMORY_LIMIT_MB`) is migrated to the built-in sandbox at startup, so
286+
no manual configuration change is required. To explicitly disable Code nodes, set
287+
`CODE_EXEC_TYPE=disabled` in the container process environment.
288+
284289
---
285290

286291
## 12. Console Module Configuration

docs/zh/CONFIGURATION.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,11 @@
278278
不是必需配置;工作流启用 E2B 后才会优先使用 E2B。用户代码不会在
279279
`core-workflow` 进程内执行,`local` 执行器不再支持。
280280

281+
升级提示:如果保留了旧版本自动生成的 `config.env`(其中
282+
`CODE_EXEC_TYPE=disabled` 且没有 `CODE_EXEC_MEMORY_LIMIT_MB`),Workflow
283+
会在启动时将这个历史默认值迁移为内置沙箱,因此无需手动修改配置。若要
284+
明确禁用代码节点,请在容器进程环境中设置 `CODE_EXEC_TYPE=disabled`
285+
281286
---
282287

283288
## 12. Console 模块配置

0 commit comments

Comments
 (0)