Release v0.51.303 — Release JS (stage-p1a — cron toggle + config var expansion + git-discard hardening) (#3756)
Some checks failed
Release & Docker / release (push) Has been cancelled
Some checks failed
Release & Docker / release (push) Has been cancelled
* fix(cron): toggle run output rows instead of re-fetching when already open (#3732) _loadRunContent() only ever expanded, so clicking an already-open cron run row re-fetched its content pointlessly. It now toggles: an open row collapses (clears the expansion state + resets the toggle button) and returns early, avoiding the redundant API call. Co-authored-by: mysoul12138 <mysoul12138@users.noreply.github.com> * feat(config): expand ${VAR} references in config.yaml at load time (#3736) hermes-agent already expands ${ENV_VAR} in config.yaml, but the WebUI's own loader stored the raw dict, leaving literal ${...} strings. Recursively expand ${VAR} against os.environ on both config load paths (reload_config and _load_yaml_config_file); unset vars are left untouched (${VAR} preserved). Co-authored-by: Carry00 <Carry00@users.noreply.github.com> * fix(security): anchor untracked-file deletes in git_discard (#3702) git_discard(delete_untracked=True) used raw shutil.rmtree / Path.unlink after a separate safe_resolve_ws validation, leaving a validation-to-use symlink-swap window. Route untracked deletes through the anchored helpers (rmtree_anchored / unlink_anchored) so a swapped path component is rejected at delete time; preserve the prior missing_ok tolerance for benign concurrent-removal races. Adds regression coverage for both the symlink-swap block and the concurrent-missing case. Co-authored-by: Hinotoi-agent <Hinotoi-agent@users.noreply.github.com> * docs(changelog): stamp v0.51.303 — Release JS (stage-p1a #3732 #3736 #3702) --------- Co-authored-by: nesquena-hermes <[email protected]> Co-authored-by: mysoul12138 <mysoul12138@users.noreply.github.com> Co-authored-by: Carry00 <Carry00@users.noreply.github.com> Co-authored-by: Hinotoi-agent <Hinotoi-agent@users.noreply.github.com>
This commit is contained in:
@@ -9,7 +9,6 @@ from __future__ import annotations
|
||||
|
||||
import difflib
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
@@ -18,7 +17,7 @@ from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
|
||||
from api.workspace import safe_resolve_ws
|
||||
from api.workspace import rmtree_anchored, safe_resolve_ws, unlink_anchored
|
||||
|
||||
|
||||
GIT_TIMEOUT = 5
|
||||
@@ -978,9 +977,16 @@ def git_discard(workspace: str | Path, paths: Iterable[str], *, delete_untracked
|
||||
raise GitWorkspaceError("Untracked files require delete_untracked=true")
|
||||
target = safe_resolve_ws(ctx.workspace, workspace_rel)
|
||||
if target.is_dir():
|
||||
shutil.rmtree(target)
|
||||
rmtree_anchored(ctx.workspace, target)
|
||||
else:
|
||||
target.unlink(missing_ok=True)
|
||||
try:
|
||||
unlink_anchored(ctx.workspace, target)
|
||||
except FileNotFoundError:
|
||||
# Preserve the previous Path.unlink(missing_ok=True)
|
||||
# behavior for benign races where another process
|
||||
# removes the untracked file after git_status() has
|
||||
# reported it but before this discard reaches unlink.
|
||||
pass
|
||||
continue
|
||||
_run_git(ctx, ["restore", "--worktree", "--", repo_rel], check=True)
|
||||
return git_status(workspace)
|
||||
|
||||
Reference in New Issue
Block a user