diff --git a/Agent.md b/Agent.md index 12c4196c..60c634f0 100644 --- a/Agent.md +++ b/Agent.md @@ -118,7 +118,7 @@ Community needs voiced in HN agent-UI discussions map directly to EMRG's design: pkill -f "emrg.server"; rm -f ~/.emrg/emrgd.token; python -m emrg ``` -Python: `uv run pytest tests/ -v` (982) — import check: `uv run python -c "from emrg.client.app import run_client"` +Python: `uv run pytest tests/ -v` (983) — import check: `uv run python -c "from emrg.client.app import run_client"` GUI: `cd emrg/gui && npm test` (260: 45 daemon_client + 19 conn-manager + 22 app-commands + 131 renderer smoke + 16 i18n + 7 integration + 3 commands + 8 build-config + 7 gui-state + 2 tool-group) — syntax: `node --check main.js preload.js daemon_client.js renderer/js/*.js` CI: `uv run pytest` (ubuntu + **windows-2025 matrix** — Windows pytest 回归在 PR CI 即失败,v0.2.29 教训 #725) + GUI tests + **actionlint workflow lint** (`rhysd/actionlint@v1.7.12` gate, #444 — workflow 解析错误在 PR CI 即失败,如 `if:` secrets 上下文) Re-trigger: `scripts/re-trigger-ci.sh [branch]` (workflow_dispatch, #527 — 替代空 commit 重触发:Actions outage 会整段丢弃 push 事件,dispatch 走 API 路径不受影响) diff --git a/emrg/server/scheduler.py b/emrg/server/scheduler.py index 25301873..af62aa90 100644 --- a/emrg/server/scheduler.py +++ b/emrg/server/scheduler.py @@ -284,32 +284,30 @@ def __init__( self._repo_configured = project_name == "emrg" self._session_id = f"emrg-evolution-{name}" self._source_dir = path or name - # Sandbox tier for this task's bash tool (rant 2026-08-20T15:46:50): - # explicit config wins; builtin tasks get suggested defaults; None = - # danger-full-access (current behavior). - self._sandbox = self._resolve_sandbox(name, config, sandbox) + # Sandbox tier for this task's bash tool (rant 2026-08-20T15:46:50 + + # 18:05:20): explicit config wins, otherwise unified default + # workspace-write. No task-name builtin defaults, no implicit + # danger-full-access fallback. + self._sandbox = self._resolve_sandbox(config, sandbox) if self._sandbox: self._logger.info( "TaskHandler[%s]: bash sandbox tier = %s", name, self._sandbox ) @staticmethod - def _resolve_sandbox(name: str, config: dict, explicit: str | None) -> str | None: + def _resolve_sandbox(config: dict, explicit: str | None) -> str: """Effective bash sandbox tier for a task. - Order: tasks.yml top-level ``sandbox:`` field → ``config.sandbox`` → - builtin defaults by task name → None (= danger-full-access, the - existing un-sandboxed behavior). Invalid values fall through to the - defaults rather than breaking the task. + Rant 2026-08-20T18:05:20: unified rule — configured value wins, + otherwise ``"workspace-write"``. No task-name builtin defaults + (emrg-task/opensource special cases removed), no implicit + danger-full-access fallback. Invalid values fall through to the + default rather than breaking the task. """ for cand in (explicit, config.get("sandbox")): - if cand in SANDBOX_MODES and cand != "danger-full-access": + if cand in SANDBOX_MODES: return cand - if name == "emrg-task": - return "workspace-write" # evolution writes its own repo - if name.endswith("-opensource-task"): - return "read-only" # community work in host-owned repos - return None + return "workspace-write" # ── Saturation state (restored from disk across daemon restarts) ── diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index baa379e2..7a1ef128 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -2075,3 +2075,37 @@ def test_evolution_template_renders_dual_project_match(): assert "emrg" in out, "task.project 值应渲染" assert "argszero/emrg" in out, "owner/repo 形式应渲染" assert "ignore rants without a `project` field entirely" in out + + +# ── sandbox tier resolution (rant 2026-08-20T18:05:20) ───────────── + + +def test_sandbox_resolution_unified_default_rule(): + """Rant 2026-08-20T18:05:20: no name-based builtin defaults — the + configured value wins, otherwise the unified default is workspace-write. + There is no implicit danger-full-access fallback.""" + # explicit tasks.yml top-level sandbox wins + assert TaskHandler._resolve_sandbox({}, "read-only") == "read-only" + # config.sandbox used when no explicit value + assert TaskHandler._resolve_sandbox({"sandbox": "read-only"}, None) == "read-only" + # explicit beats config + assert ( + TaskHandler._resolve_sandbox({"sandbox": "workspace-write"}, "danger-full-access") + == "danger-full-access" + ) + # no name-based defaults: emrg-task no longer implies workspace-write + # via the old special case — it is the unified default now + handler = TaskHandler( + name="emrg-task", config={"project": "emrg"}, interval=60, + identity=InstanceIdentity(), + ) + assert handler._sandbox == "workspace-write" + # plain task name also gets the unified default, not danger-full-access + handler2 = TaskHandler( + name="generic-task", config={}, interval=60, + identity=InstanceIdentity(), + ) + assert handler2._sandbox == "workspace-write" + # invalid values fall through to the default + assert TaskHandler._resolve_sandbox({"sandbox": "bogus"}, None) == "workspace-write" + assert TaskHandler._resolve_sandbox({}, "bogus") == "workspace-write"