From 9630b96e39f0edf8bf981115dd27f7c6974fedc7 Mon Sep 17 00:00:00 2001 From: theAfish Date: Tue, 28 Jul 2026 14:35:44 +0800 Subject: [PATCH 1/2] feat: figures error --- .../execution_agent/step_executor_runner.py | 35 +++++++++++++++- tests/test_step_executor_runner.py | 19 +++++++++ web/vite-frontend/src/main.js | 41 +++++++++++++++---- web/vite-frontend/src/styles/overlays.css | 16 ++++++++ 4 files changed, 102 insertions(+), 9 deletions(-) diff --git a/src/matcreator/agents/execution_agent/step_executor_runner.py b/src/matcreator/agents/execution_agent/step_executor_runner.py index ce74ea3e..701a37ec 100644 --- a/src/matcreator/agents/execution_agent/step_executor_runner.py +++ b/src/matcreator/agents/execution_agent/step_executor_runner.py @@ -258,14 +258,30 @@ def _split_verified_artifacts( def _verify_step_result_artifacts( result: StepExecutorResult, allowed_roots: Optional[list[Path]] = None, + additional_artifacts: Optional[list[str]] = None, ) -> tuple[StepExecutorResult, list[str]]: - """Prevent successful step results from claiming nonexistent artifacts.""" + """Prevent successful steps from claiming nonexistent output artifacts. + + ``additional_artifacts`` covers paths returned by tools during the step + (notably ``plot_path``). Those paths are surfaced by the frontend too, so + they must be held to the same existence and containment checks as the + executor's final artifact list. + """ existing_artifacts, missing_artifacts = _split_verified_artifacts( result.artifacts, allowed_roots=allowed_roots, ) result.artifacts = existing_artifacts + if additional_artifacts: + _, additional_missing_artifacts = _split_verified_artifacts( + additional_artifacts, + allowed_roots=allowed_roots, + ) + for artifact in additional_missing_artifacts: + if artifact not in missing_artifacts: + missing_artifacts.append(artifact) + if result.status == "success" and missing_artifacts: missing_text = ", ".join(missing_artifacts) result.status = "needs_replanning" @@ -776,9 +792,24 @@ async def run_step_executor( if step_result_data: tool_context.state["_step_result"] = None # State has no pop(); reset instead result = StepExecutorResult.model_validate(step_result_data) + allowed_artifact_roots = _artifact_allowed_roots( + step_workspace, suggested_skills, output_dir, + ) result, missing_artifacts = _verify_step_result_artifacts( result, - allowed_roots=_artifact_allowed_roots(step_workspace, suggested_skills, output_dir), + allowed_roots=allowed_artifact_roots, + additional_artifacts=[*artifact_paths, *plot_paths], + ) + # Do not persist or return unverified tool outputs. In particular, + # this prevents a stale plot_path from being rendered as a broken image + # while the step is being replanned. + artifact_paths, _ = _split_verified_artifacts( + artifact_paths, + allowed_roots=allowed_artifact_roots, + ) + plot_paths, _ = _split_verified_artifacts( + plot_paths, + allowed_roots=allowed_artifact_roots, ) await asyncio.to_thread( graph.log_node_complete, diff --git a/tests/test_step_executor_runner.py b/tests/test_step_executor_runner.py index 98393346..f6acea75 100644 --- a/tests/test_step_executor_runner.py +++ b/tests/test_step_executor_runner.py @@ -98,6 +98,25 @@ def test_success_with_missing_artifact_requires_replanning(tmp_path, caplog): assert "claimed artifact path(s)" in caplog.text +def test_success_with_missing_tool_plot_requires_replanning(tmp_path): + result = StepExecutorResult( + status="success", + key_results="Generated a plot.", + concise_summary="Generated a plot.", + ) + missing_plot = tmp_path / "missing-plot.png" + + verified, missing_artifacts = _verify_step_result_artifacts( + result, + allowed_roots=[tmp_path], + additional_artifacts=[str(missing_plot)], + ) + + assert verified.status == "needs_replanning" + assert missing_artifacts == [str(missing_plot)] + assert str(missing_plot) in (verified.replan_reason or "") + + def test_success_accepts_existing_file_and_directory_artifacts(tmp_path): file_artifact = tmp_path / "result.txt" file_artifact.write_text("ok", encoding="utf-8") diff --git a/web/vite-frontend/src/main.js b/web/vite-frontend/src/main.js index 9168efb6..d25fabc1 100644 --- a/web/vite-frontend/src/main.js +++ b/web/vite-frontend/src/main.js @@ -2548,6 +2548,39 @@ function createArtifactListItem(path) { return li; } +function createImageLoadFallback(path) { + const fallback = document.createElement("div"); + fallback.className = "timeline-image-error"; + fallback.setAttribute("role", "alert"); + fallback.textContent = `⚠ Image preview unavailable: ${path.split("/").pop()}`; + return fallback; +} + +function createTimelineImage(path) { + const wrap = document.createElement("div"); + wrap.className = "timeline-image-wrap"; + const loading = document.createElement("div"); + loading.className = "timeline-image-loading"; + loading.textContent = `Loading image: ${path.split("/").pop()}`; + const img = document.createElement("img"); + img.className = "timeline-image"; + img.alt = path.split("/").pop(); + img.hidden = true; + img.style.cursor = "zoom-in"; + img.addEventListener("load", () => { + loading.remove(); + img.hidden = false; + }); + img.addEventListener("error", () => { + img.remove(); + loading.replaceWith(createImageLoadFallback(path)); + }, { once: true }); + img.addEventListener("click", () => lightbox.open(img.src)); + img.src = pathToApiUrl(path); + wrap.append(loading, img); + return wrap; +} + function isExecutorLauncherTool(name) { return ["run_flash_step", "run_node_executor", "run_sub_agent"].includes(name || ""); } @@ -2610,13 +2643,7 @@ function renderTimeline(container, timeline, shownPlotPaths = null) { continue; } visiblePlotPaths.add(plotPath); - const img = document.createElement("img"); - img.src = pathToApiUrl(plotPath); - img.className = "timeline-image"; - img.alt = plotPath.split("/").pop(); - img.style.cursor = "zoom-in"; - img.addEventListener("click", () => lightbox.open(img.src)); - container.appendChild(img); + container.appendChild(createTimelineImage(plotPath)); } getStructurePaths(item.response).forEach((path) => { container.appendChild(createStructureViewButton(path)); diff --git a/web/vite-frontend/src/styles/overlays.css b/web/vite-frontend/src/styles/overlays.css index ddd37c22..965dda98 100644 --- a/web/vite-frontend/src/styles/overlays.css +++ b/web/vite-frontend/src/styles/overlays.css @@ -386,6 +386,22 @@ margin-top: 8px; display: block; } +.timeline-image-loading, +.timeline-image-error { + margin-top: 8px; + padding: 8px 10px; + border-radius: 8px; + font-size: 12px; +} +.timeline-image-loading { + color: var(--muted); + background: rgba(148, 163, 184, 0.12); +} +.timeline-image-error { + color: var(--danger, #dc2626); + background: rgba(220, 38, 38, 0.1); + border: 1px solid rgba(220, 38, 38, 0.35); +} .artifact-image-wrap { display: flex; flex-direction: column; From 6ded0015c3b0f83b4bb3981476a3b0607622bb96 Mon Sep 17 00:00:00 2001 From: theAfish Date: Tue, 28 Jul 2026 15:06:03 +0800 Subject: [PATCH 2/2] feat: frontend settings for matcreator --- docs/getting-started.md | 12 +++++ src/matcreator/config.py | 10 +++- src/matcreator/constants.py | 4 +- src/matcreator/tools/workspace_tools.py | 40 +++++++++++----- tests/test_matcreator_constants.py | 3 ++ tests/test_web_session_access.py | 47 +++++++++++++++++- tests/test_workspace_tools.py | 16 ++++++- web/main.py | 29 ++++++++++- web/vite-frontend/index.html | 48 +++++++++++++++++++ .../features/settings/SettingsController.js | 22 ++++++++- web/vite-frontend/src/styles/overlays.css | 1 + 11 files changed, 212 insertions(+), 20 deletions(-) diff --git a/docs/getting-started.md b/docs/getting-started.md index 1f7b102a..b1b0cf64 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -66,6 +66,18 @@ export MATCREATOR_MEMORIZATION_FREQUENCY=0 export MATCREATOR_REVIEW_FREQUENCY=20 ``` +The web **Settings → MatCreator** tab exposes these knowledge frequencies, +benchmark settings, and the tool-execution timeout. Python, Bash, and skill +scripts time out after 3600 seconds by default; set a positive value there or +persist the equivalent configuration: + +```yaml +runtime: + execution_timeout_seconds: 3600 +``` + +`MATCREATOR_EXEC_TIMEOUT_SECONDS` is the corresponding environment override. + Start an interactive session in the current project workspace: ```bash diff --git a/src/matcreator/config.py b/src/matcreator/config.py index 25693bd5..e6524da2 100644 --- a/src/matcreator/config.py +++ b/src/matcreator/config.py @@ -81,6 +81,9 @@ "compute.deepmd_machine": "BOHRIUM_DEEPMD_MACHINE", "compute.deepmd_model_path": "DEEPMD_MODEL_PATH", "benchmark.server_url": "MAT_BENCH_SERVER_URL", + "benchmark.token": "MAT_BENCH_TOKEN", + "benchmark.question_bank_root": "MAT_BENCH_QUESTION_BANK_ROOT", + "runtime.execution_timeout_seconds": "MATCREATOR_EXEC_TIMEOUT_SECONDS", "skills.module_root": "MATCREATOR_MODULE_SKILLS_ROOT", "knowledge.memorization_frequency": "MATCREATOR_MEMORIZATION_FREQUENCY", "knowledge.review_frequency": "MATCREATOR_REVIEW_FREQUENCY", @@ -89,7 +92,12 @@ ENV_TO_YAML: dict[str, str] = {v: k for k, v in YAML_TO_ENV.items()} # Fields whose values should be masked when displayed. -SENSITIVE_YAML_KEYS = frozenset({"llm.api_key", "bohrium.password", "bohrium.access_key"}) +SENSITIVE_YAML_KEYS = frozenset({ + "llm.api_key", + "bohrium.password", + "bohrium.access_key", + "benchmark.token", +}) _USER_ENV_KEY_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*") _PROTECTED_USER_ENV_KEYS = frozenset({ "HOME", diff --git a/src/matcreator/constants.py b/src/matcreator/constants.py index eb4dfe6d..a082e49d 100644 --- a/src/matcreator/constants.py +++ b/src/matcreator/constants.py @@ -41,6 +41,7 @@ _bohrium_cfg = get_bohrium_config() _compute_cfg = get_compute_config() _knowledge_cfg = load_config().get("knowledge", {}) +_runtime_cfg = load_config().get("runtime", {}) _yaml_to_env: dict[str, str | None] = { "LLM_MODEL": _llm_cfg.get("model"), @@ -61,11 +62,12 @@ "DEEPMD_MODEL_PATH": _compute_cfg.get("deepmd_model_path"), "MATCREATOR_MEMORIZATION_FREQUENCY": _knowledge_cfg.get("memorization_frequency"), "MATCREATOR_REVIEW_FREQUENCY": _knowledge_cfg.get("review_frequency"), + "MATCREATOR_EXEC_TIMEOUT_SECONDS": _runtime_cfg.get("execution_timeout_seconds"), } for _env_key, _yaml_val in _yaml_to_env.items(): if _yaml_val and (_CONFIG_OVERRIDES_PRE_ENV or _env_key not in _pre_env): - os.environ[_env_key] = _yaml_val + os.environ[_env_key] = str(_yaml_val) for _env_key, _yaml_val in get_env_overrides().items(): if not _USER_ENV_KEY_RE.fullmatch(_env_key) or _env_key in _PROTECTED_USER_ENV_KEYS: diff --git a/src/matcreator/tools/workspace_tools.py b/src/matcreator/tools/workspace_tools.py index dddb49c5..18b51e6e 100644 --- a/src/matcreator/tools/workspace_tools.py +++ b/src/matcreator/tools/workspace_tools.py @@ -8,7 +8,7 @@ ----------------- * ``write_workspace_file`` only writes inside the workspace root (path traversal is rejected). -* ``run_python`` / ``run_bash`` execute in a subprocess with a 60-second +* ``run_python`` / ``run_bash`` execute in a subprocess with a configurable timeout. The agent must always present the code/command to the user and obtain explicit approval before calling these tools (enforced by the thinking_agent instruction). @@ -274,14 +274,26 @@ def create_skill( # Script execution tools # --------------------------------------------------------------------------- -_EXEC_TIMEOUT = 3600 # seconds +_DEFAULT_EXEC_TIMEOUT_SECONDS = 3600 + + +def _execution_timeout_seconds() -> int: + """Return the user-configured subprocess timeout, falling back safely.""" + raw_value = os.environ.get("MATCREATOR_EXEC_TIMEOUT_SECONDS", "").strip() + if not raw_value: + return _DEFAULT_EXEC_TIMEOUT_SECONDS + try: + timeout = int(raw_value) + except ValueError: + return _DEFAULT_EXEC_TIMEOUT_SECONDS + return timeout if timeout > 0 else _DEFAULT_EXEC_TIMEOUT_SECONDS async def run_python(code: str, tool_context: ToolContext) -> str: """Execute a Python code snippet and return its stdout/stderr. IMPORTANT: Only call this tool after the user has explicitly approved the - code to be run. The code runs in a subprocess with a 60-second timeout. + code to be run. The timeout is configured in Settings (default: 3600s). Args: code: Python source code to execute. @@ -301,11 +313,12 @@ async def run_python(code: str, tool_context: ToolContext) -> str: cwd=cwd, ) try: - stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=_EXEC_TIMEOUT) + timeout = _execution_timeout_seconds() + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) except asyncio.TimeoutError: proc.kill() stdout, stderr = await proc.communicate() - output = f"[TimeoutExpired after {_EXEC_TIMEOUT}s]\n" + stdout.decode("utf-8", errors="replace") + stderr.decode("utf-8", errors="replace") + output = f"[TimeoutExpired after {timeout}s]\n" + stdout.decode("utf-8", errors="replace") + stderr.decode("utf-8", errors="replace") except asyncio.CancelledError: proc.kill() await proc.communicate() @@ -321,7 +334,7 @@ async def run_bash(script: str, tool_context: ToolContext) -> str: """Execute a bash script snippet and return its stdout/stderr. IMPORTANT: Only call this tool after the user has explicitly approved the - script to be run. The script runs in a subprocess with a 60-second timeout. + script to be run. The timeout is configured in Settings (default: 3600s). Args: script: Bash script content to execute. @@ -341,11 +354,12 @@ async def run_bash(script: str, tool_context: ToolContext) -> str: cwd=cwd, ) try: - stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=_EXEC_TIMEOUT) + timeout = _execution_timeout_seconds() + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) except asyncio.TimeoutError: proc.kill() stdout, stderr = await proc.communicate() - output = f"[TimeoutExpired after {_EXEC_TIMEOUT}s]\n" + stdout.decode("utf-8", errors="replace") + stderr.decode("utf-8", errors="replace") + output = f"[TimeoutExpired after {timeout}s]\n" + stdout.decode("utf-8", errors="replace") + stderr.decode("utf-8", errors="replace") except asyncio.CancelledError: proc.kill() await proc.communicate() @@ -379,11 +393,12 @@ async def run_python_file(relative_path: str) -> str: stderr=asyncio.subprocess.PIPE, ) try: - stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=_EXEC_TIMEOUT) + timeout = _execution_timeout_seconds() + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) except asyncio.TimeoutError: proc.kill() stdout, stderr = await proc.communicate() - output = f"[TimeoutExpired after {_EXEC_TIMEOUT}s]\n" + stdout.decode("utf-8", errors="replace") + stderr.decode("utf-8", errors="replace") + output = f"[TimeoutExpired after {timeout}s]\n" + stdout.decode("utf-8", errors="replace") + stderr.decode("utf-8", errors="replace") except asyncio.CancelledError: proc.kill() await proc.communicate() @@ -471,11 +486,12 @@ async def run_skill_script( env=env, ) try: - stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=_EXEC_TIMEOUT) + timeout = _execution_timeout_seconds() + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) except asyncio.TimeoutError: proc.kill() stdout, stderr = await proc.communicate() - output = f"[TimeoutExpired after {_EXEC_TIMEOUT}s]\n" + stdout.decode("utf-8", errors="replace") + stderr.decode("utf-8", errors="replace") + output = f"[TimeoutExpired after {timeout}s]\n" + stdout.decode("utf-8", errors="replace") + stderr.decode("utf-8", errors="replace") except asyncio.CancelledError: proc.kill() await proc.communicate() diff --git a/tests/test_matcreator_constants.py b/tests/test_matcreator_constants.py index 8ff512dd..f6b8b4b9 100644 --- a/tests/test_matcreator_constants.py +++ b/tests/test_matcreator_constants.py @@ -77,6 +77,8 @@ def test_server_mode_config_overrides_container_defaults(monkeypatch, tmp_path: (matcreator_home / "config.yaml").write_text( "llm:\n" " model: openai/user-model\n" + "runtime:\n" + " execution_timeout_seconds: 7200\n" "env:\n" " MP_API_KEY: user-mp-key\n", encoding="utf-8", @@ -93,6 +95,7 @@ def test_server_mode_config_overrides_container_defaults(monkeypatch, tmp_path: assert constants.LLM_MODEL == "openai/user-model" assert constants.os.environ["LLM_MODEL"] == "openai/user-model" + assert constants.os.environ["MATCREATOR_EXEC_TIMEOUT_SECONDS"] == "7200" assert constants.os.environ["MP_API_KEY"] == "user-mp-key" diff --git a/tests/test_web_session_access.py b/tests/test_web_session_access.py index 35e0d4e5..9268908c 100644 --- a/tests/test_web_session_access.py +++ b/tests/test_web_session_access.py @@ -8,6 +8,9 @@ from pathlib import Path from types import SimpleNamespace +import pytest +from fastapi import HTTPException + class _FakeImage: def __init__(self, image_id: str): @@ -287,6 +290,48 @@ def test_local_adk_restart_env_uses_frontend_config(monkeypatch, tmp_path): assert "FRONTEND_SET_FLAG: visible-locally" in config_text +def test_server_runtime_and_benchmark_settings_are_persisted(monkeypatch, tmp_path): + control_home = tmp_path / "control-plane" / ".matcreator" + control_home.mkdir(parents=True) + web_main = _load_web_main_server(monkeypatch, control_home, tmp_path / "server-data") + + body = SimpleNamespace(values={ + "MATCREATOR_EXEC_TIMEOUT_SECONDS": "7200", + "MATCREATOR_MEMORIZATION_FREQUENCY": "0", + "MATCREATOR_REVIEW_FREQUENCY": "25", + "MAT_BENCH_SERVER_URL": "http://127.0.0.1:8080/bench", + "MAT_BENCH_TOKEN": "benchmark-secret", + "MAT_BENCH_QUESTION_BANK_ROOT": "/tmp/question-bank", + }) + asyncio.run(web_main.update_env_config(body, user_id="alice")) + + config = web_main._load_config_for_user("alice") + assert config["runtime"]["execution_timeout_seconds"] == "7200" + assert config["knowledge"] == { + "memorization_frequency": "0", + "review_frequency": "25", + } + assert config["benchmark"] == { + "server_url": "http://127.0.0.1:8080/bench", + "token": "benchmark-secret", + "question_bank_root": "/tmp/question-bank", + } + + response = asyncio.run(web_main.get_env_config(user_id="alice")) + values = json.loads(response.body) + assert values["MAT_BENCH_TOKEN"] == "***" + + +def test_runtime_timeout_must_be_positive(monkeypatch, tmp_path): + control_home = tmp_path / "control-plane" / ".matcreator" + control_home.mkdir(parents=True) + web_main = _load_web_main_server(monkeypatch, control_home, tmp_path / "server-data") + + body = SimpleNamespace(values={"MATCREATOR_EXEC_TIMEOUT_SECONDS": "0"}) + with pytest.raises(HTTPException, match="positive integer"): + asyncio.run(web_main.update_env_config(body, user_id="alice")) + + def test_local_mode_lists_sessions_regardless_of_requested_user(monkeypatch, tmp_path): web_main = _load_web_main(monkeypatch) db_path = tmp_path / "session.db" @@ -312,4 +357,4 @@ def test_local_mode_reads_session_detail_regardless_of_requested_user(monkeypatc assert payload["userId"] == "legacy-display-name" assert payload["state"] == {"answer": 42} - assert payload["events"] == [{"event": "persisted"}] \ No newline at end of file + assert payload["events"] == [{"event": "persisted"}] diff --git a/tests/test_workspace_tools.py b/tests/test_workspace_tools.py index 4f03bd52..0e35e7a8 100644 --- a/tests/test_workspace_tools.py +++ b/tests/test_workspace_tools.py @@ -11,6 +11,20 @@ def __init__(self): self.state = {} +def test_execution_timeout_uses_valid_user_setting(monkeypatch): + monkeypatch.delenv("MATCREATOR_EXEC_TIMEOUT_SECONDS", raising=False) + assert workspace_tools._execution_timeout_seconds() == 3600 + + monkeypatch.setenv("MATCREATOR_EXEC_TIMEOUT_SECONDS", "7200") + assert workspace_tools._execution_timeout_seconds() == 7200 + + +def test_execution_timeout_rejects_invalid_user_setting(monkeypatch): + for value in ("0", "-10", "not-a-number"): + monkeypatch.setenv("MATCREATOR_EXEC_TIMEOUT_SECONDS", value) + assert workspace_tools._execution_timeout_seconds() == 3600 + + def test_set_session_output_dir_sets_output_state_under_workspace(tmp_path, monkeypatch): monkeypatch.setenv("MATCLAW_WORKSPACE", str(tmp_path)) tool_context = _FakeToolContext() @@ -104,4 +118,4 @@ def test_set_session_workdir_rejects_paths_outside_workspace(tmp_path, monkeypat assert absolute_result["status"] == "error" assert traversal_result["status"] == "error" assert root_result["status"] == "error" - assert tool_context.state == {} \ No newline at end of file + assert tool_context.state == {} diff --git a/web/main.py b/web/main.py index 6dc23386..743c81f2 100644 --- a/web/main.py +++ b/web/main.py @@ -191,9 +191,11 @@ class EvaluationQuestionTemplateBody(BaseModel): _ENV_FIELDS = [ "LLM_MODEL", "LLM_API_KEY", "LLM_BASE_URL", "EMBEDDING_MODEL", "GRAPH_AGENT_MODEL", "REVIEW_AGENT_MODEL", - "BOHRIUM_EMAIL", "BOHRIUM_PASSWORD", "BOHRIUM_ACCESS_KEY", "BOHRIUM_API_URL", "BOHRIUM_PROJECT_ID", + "BOHRIUM_USERNAME", "BOHRIUM_PASSWORD", "BOHRIUM_ACCESS_KEY", "BOHRIUM_API_URL", "BOHRIUM_PROJECT_ID", "BOHRIUM_VASP_IMAGE", "BOHRIUM_VASP_MACHINE", "BOHRIUM_DEEPMD_IMAGE", "BOHRIUM_DEEPMD_MACHINE", "DEEPMD_MODEL_PATH", + "MATCREATOR_EXEC_TIMEOUT_SECONDS", "MATCREATOR_MEMORIZATION_FREQUENCY", "MATCREATOR_REVIEW_FREQUENCY", + "MAT_BENCH_SERVER_URL", "MAT_BENCH_TOKEN", "MAT_BENCH_QUESTION_BANK_ROOT", ] _CUSTOM_ENV_CONFIG_KEY = "CUSTOM_ENV" _ENV_VALUE_MASK = "***" @@ -546,6 +548,28 @@ def _masked_env_value(env_key: str, value: str) -> str: return _ENV_VALUE_MASK if (_is_sensitive_env_key(env_key) and value) else value +def _validate_user_setting_value(env_key: str, value: str) -> None: + """Validate user-controlled numeric runtime settings before persisting.""" + if env_key == "MATCREATOR_EXEC_TIMEOUT_SECONDS" and value: + try: + if int(value) <= 0: + raise ValueError + except ValueError: + raise HTTPException( + status_code=400, + detail="MATCREATOR_EXEC_TIMEOUT_SECONDS must be a positive integer.", + ) from None + if env_key in {"MATCREATOR_MEMORIZATION_FREQUENCY", "MATCREATOR_REVIEW_FREQUENCY"} and value: + try: + if int(value) < 0: + raise ValueError + except ValueError: + raise HTTPException( + status_code=400, + detail=f"{env_key} must be a non-negative integer.", + ) from None + + def _custom_env_from_config(config: dict[str, Any]) -> dict[str, str]: env_cfg = config.get("env", {}) if not isinstance(env_cfg, dict): @@ -4659,6 +4683,7 @@ async def update_env_config(body: EnvConfigBody, user_id: str = Query(default="" sensitive = yaml_key in SENSITIVE_YAML_KEYS or _is_sensitive_env_key(key) if sensitive and value == _ENV_VALUE_MASK: continue + _validate_user_setting_value(key, value) _set_nested_config_value(config, yaml_key, value) custom_env_raw = body.values.get(_CUSTOM_ENV_CONFIG_KEY) @@ -4692,7 +4717,7 @@ async def update_env_config(body: EnvConfigBody, user_id: str = Query(default="" yaml_key = ENV_TO_YAML.get(key) if yaml_key is None: continue - sensitive = yaml_key in SENSITIVE_YAML_KEYS + sensitive = yaml_key in SENSITIVE_YAML_KEYS or _is_sensitive_env_key(key) if sensitive and value == _ENV_VALUE_MASK: continue if value: diff --git a/web/vite-frontend/index.html b/web/vite-frontend/index.html index bdc01db3..6f3dd2ba 100644 --- a/web/vite-frontend/index.html +++ b/web/vite-frontend/index.html @@ -476,6 +476,8 @@

Questions

+ +
@@ -532,6 +534,52 @@

Questions

+ +