diff --git a/Agent.md b/Agent.md index 91fb5d87..9b958689 100644 --- a/Agent.md +++ b/Agent.md @@ -54,7 +54,7 @@ What EMRG does today — capability statements, not a changelog. One line per command: what it answers. Run the daemon in the foreground with `python -m emrg`. - Python: `uv run pytest tests/ -v` — the number is **measured, never stored**: `uv run --no-sync python3 scripts/check-doc-count.py --measure`. Import check: `uv run python -c "from emrg.client.app import run_client"` -GUI: `cd emrg/gui && npm test` (156: 95 daemon_client + 20 conn-manager + 7 integration + 7 nav-policy + 7 gui-state + 9 build-config + 6 boot-contract + 3 preload-api + 2 theme-guard) — syntax: `node --check main.js preload.js daemon_client.js` +GUI: `cd emrg/gui && npm test` (157: 96 daemon_client + 20 conn-manager + 7 integration + 7 nav-policy + 7 gui-state + 9 build-config + 6 boot-contract + 3 preload-api + 2 theme-guard) — syntax: `node --check main.js preload.js daemon_client.js` Renderer: `cd emrg/gui/renderer && npm run typecheck && npm test` (684: 5 snapshot-store + 13 utils + 3 ErrorBoundary + 2 App smoke + 11 commands + 4 copywriting + 11 i18n + 13 markdown + 28 transcript + 26 TranscriptView + 22 history + 20 historyReplay + 38 composer + 65 Composer + 6 LinkDialog + 16 sidebar + 18 Sidebar + 9 fileTree + 9 FileTree + 17 resultPanel + 8 ResultPanel + 27 workspaceView + 13 WorkspaceView + 10 dialog + 6 Dialog + 9 ConfirmDialog + 9 RenameDialog + 10 dialogLists + 3 HelpDialog + 9 MemoryDialog + 6 SkillsDialog + 8 openSession + 6 WelcomeDialog + 9 OpenSessionDialog + 7 NewSessionDialog + 7 rewind + 8 RewindDialog + 7 GithubDeviceDialog + 50 daemonBridge + 9 DaemonBridgeProvider + 44 Shell + 22 DialogHost + 26 SettingsPanel + 6 TaskFormDialog + 5 RantDialog + 4 vendorMarkdown + 6 ApprovalDialog + 14 SandboxRootsDialog) - CI: pytest on ubuntu **and windows-2025**, the GUI suites, and an actionlint gate over `.github/workflows/`; re-trigger a dropped push with `scripts/re-trigger-ci.sh `, never an empty commit - Merge gates, run before merging — each answers a question no other gate asks: `uv run --no-sync python3 scripts/check-vote-count.py` (approvals still valid) · `scripts/check-pr-base.py` (base reaches master) · `scripts/check-merge-freshness.py` (that green CI is about the landing tree) · `scripts/check-merge-order.py` (dirt and what it lands) · `scripts/check-merge-landing-diff.py` (what the landing changes) · `scripts/check-merge-pairs.py` / `scripts/check-merge-sequence.py` (two PRs together; every step of an order) · `scripts/check-merge-tree-health.py` (the merged tree passes one guard) · `scripts/check-merge-plan-suite.py` (the plan's final tree passes the suite) — all under `uv run --no-sync python3` diff --git a/emrg/gui/daemon_client.js b/emrg/gui/daemon_client.js index ba556718..888d452b 100644 --- a/emrg/gui/daemon_client.js +++ b/emrg/gui/daemon_client.js @@ -102,11 +102,22 @@ const NO_LOG_MARK = Object.freeze({ size: 0, ino: null }); // (测试与将来别的调用者)时的默认值,与"子进程有 pid"同义。 const NO_SPAWN_STATE = Object.freeze({ err: null, neverStarted: false }); -// 会话 ID 允许多种形态: -// - 交互会话:s_<6位日期>_<4位时间>_(generateSessionId 产物) -// - 固定任务会话:emrg-evolution-(首尾均连字符,无下划线分隔) -// 两者都可能经 GUI 的 switchSession/打开会话 传入,需都接受。 -const SESSION_ID_RE = /^(s_\d{6}_\d{4}_[0-9a-f]{4,8}|emrg-evolution-[A-Za-z0-9_-]+)$/; +// 会话 ID 的**形态不做检查**(宿主 2026-10-17:定时任务的会话 id 就去掉前缀,叫任务名)。 +// 原先这里是一张白名单 `/^(s_\d{6}_\d{4}_[0-9a-f]{4,8}|emrg-evolution-[...])$/`, +// 它挡的是"命名方案"而不是危险输入:交互会话叫 `s_260808_0857_387c852f`、任务会话现在 +// 叫任务自己的名字,白名单每多一种命名就要改一次,漏改的后果是这个会话在 GUI 里**打不开** +// (main.js 的每个 IPC 都先问这个谓词),而错误信息只说 `invalid session_id`。 +// +// 留下的只有路径安全,那不是命名规则:一个 id 会拼进 `/.emrg/sessions/` +// (`Session.__init__`),所以空串、分隔符与 `..` 段必须拒绝——它们会逃出目录。 +const MAX_SESSION_ID_LENGTH = 128; + +function isSessionId(id) { + if (typeof id !== "string") return false; + if (id.length === 0 || id.length > MAX_SESSION_ID_LENGTH) return false; + if (id === "." || id === "..") return false; + return !id.includes("/") && !id.includes("\\") && !id.includes("\u0000"); +} // G93:命令类型 → 响应帧类型映射(daemon 协议,daemon.py _process_message) const RESPONSE_TYPES = { @@ -1271,4 +1282,4 @@ function generateSessionId(seed) { return sid; } -module.exports = { DaemonClient, generateSessionId, TOKEN_FILE, SESSION_ID_RE, MAX_PAYLOAD, EMRGD_PORT, EMRGD_START_ERR }; +module.exports = { DaemonClient, generateSessionId, isSessionId, TOKEN_FILE, MAX_PAYLOAD, EMRGD_PORT, EMRGD_START_ERR }; diff --git a/emrg/gui/main.js b/emrg/gui/main.js index b2cf7a9a..755218a2 100644 --- a/emrg/gui/main.js +++ b/emrg/gui/main.js @@ -13,7 +13,7 @@ const { pathToFileURL } = require("url"); const { spawn } = require("child_process"); const crypto = require("crypto"); const { parse: parseToml, stringify: stringifyToml } = require("smol-toml"); -const { generateSessionId, SESSION_ID_RE } = require("./daemon_client"); +const { generateSessionId, isSessionId } = require("./daemon_client"); const { ConnManager } = require("./conn-manager"); const { guiStatePath, sanitizeOpenSessions, saveGuiState, DEFAULT_CAP } = require("./gui-state"); const { externalNavPolicy } = require("./nav-policy"); @@ -283,8 +283,10 @@ vision = false // ── IPC(G102/G114 输入校验)──────────────────────────── + // 宿主 2026-10-17:不再按"命名方案"校验会话 id,只保留路径安全(见 daemon_client.js + // 的 `isSessionId`)——任务会话的 id 就是任务名,白名单会把它挡在门外。 function validateSessionId(id) { - return typeof id === "string" && SESSION_ID_RE.test(id); + return isSessionId(id); } function validateText(t) { diff --git a/emrg/gui/renderer/src/components/Shell.test.tsx b/emrg/gui/renderer/src/components/Shell.test.tsx index 07f59918..a9e51a52 100644 --- a/emrg/gui/renderer/src/components/Shell.test.tsx +++ b/emrg/gui/renderer/src/components/Shell.test.tsx @@ -26,7 +26,7 @@ function mockEmrg() { const clearSession = vi.fn().mockResolvedValue({ ok: true }); const compactSession = vi.fn().mockResolvedValue({ ok: true }); const listProjects = vi.fn().mockResolvedValue([{ name: "emrg", path: "/p/emrg" }]); - const listTasks = vi.fn().mockResolvedValue([{ name: "evo", type: "evolution", enabled: true, session_id: "emrg-evolution-evo", project_path: "/p/emrg" }]); + const listTasks = vi.fn().mockResolvedValue([{ name: "evo", type: "evolution", enabled: true, session_id: "evo", project_path: "/p/emrg" }]); const listRants = vi.fn().mockResolvedValue([{ timestamp: "2026-08-26T12:00:00+08:00", project: "emrg", status: "pending", message: "x" }]); const listMemories = vi.fn().mockResolvedValue([]); const listSkills = vi.fn().mockResolvedValue([]); @@ -615,7 +615,7 @@ describe("Shell (Batch 5 slice 3 chat wiring)", () => { expect(openBtn).toBeEnabled(); fireEvent.click(openBtn); await waitFor(() => - expect(m.switchSession).toHaveBeenCalledWith({ sessionId: "emrg-evolution-evo", projectPath: "/p/emrg" }), + expect(m.switchSession).toHaveBeenCalledWith({ sessionId: "evo", projectPath: "/p/emrg" }), ); // 切回会话视图(transcript + composer 可见) await waitFor(() => expect(screen.getByTestId("composer")).toBeInTheDocument()); diff --git a/emrg/gui/renderer/src/components/WorkspaceView.test.tsx b/emrg/gui/renderer/src/components/WorkspaceView.test.tsx index 933c4a90..c727706f 100644 --- a/emrg/gui/renderer/src/components/WorkspaceView.test.tsx +++ b/emrg/gui/renderer/src/components/WorkspaceView.test.tsx @@ -118,7 +118,7 @@ describe("WorkspaceView", () => { const startedAt = Math.floor(Date.now() / 1000) - 100; // 100s 前启动 const tasks = [ task("daily-report", { running: true, started_at: startedAt }), - task("evolve", { next_run_in_seconds: 83, session_id: "emrg-evolution-evolve", project_path: "/proj/evolve" }), + task("evolve", { next_run_in_seconds: 83, session_id: "evolve", project_path: "/proj/evolve" }), task("idle", {}), ]; setup({ activeView: "tasks", tasks, onTriggerTask: onTrigger, onOpenSessionTask: onOpenSession, onEditTask: onEdit, onDeleteTask: onDelete }); diff --git a/emrg/gui/test/daemon_client.test.js b/emrg/gui/test/daemon_client.test.js index b28afc24..5bc88273 100644 --- a/emrg/gui/test/daemon_client.test.js +++ b/emrg/gui/test/daemon_client.test.js @@ -66,7 +66,7 @@ Module._load = function (request, parent, isMain) { if (request === "ws") return MockWs; return origLoad.apply(this, arguments); }; -const { DaemonClient, generateSessionId, TOKEN_FILE, EMRGD_PORT, EMRGD_START_ERR } = require("../daemon_client.js"); +const { DaemonClient, generateSessionId, isSessionId, TOKEN_FILE, EMRGD_PORT, EMRGD_START_ERR } = require("../daemon_client.js"); let tmpHome = null; let origHome = null; let origUserProfile = null; @@ -1278,6 +1278,22 @@ test("generateSessionId 格式(G28+G81)", () => { assert.strictEqual(seen.size, 100); }); +test("isSessionId 不查命名方案,只查路径安全(宿主 2026-10-17)", () => { + // 定时任务的会话 id 现在就是任务名,所以凡是"看起来像名字"的都必须放行—— + // 原先的白名单只认 s_<...>_ 与 emrg-evolution-<...>,裸任务名会被挡在 + // GUI 门外,而报错只说 `invalid session_id`(main.js 每个 IPC 都先问这个谓词)。 + for (const id of ["emrg-task", "competition-task", "silicon-science-cs-journal-task", + "OfficeCLI-opensource-task", "s_260808_0857_387c852f"]) { + assert.strictEqual(isSessionId(id), true, `${id} must be accepted`); + } + // 剩下的只有路径安全:id 会拼进 `/.emrg/sessions/`,逃出目录的必须拒绝。 + for (const id of ["", ".", "..", "../etc", "a/b", "a\\b", "cwd\u0000x", "x".repeat(129)]) { + assert.strictEqual(isSessionId(id), false, `${JSON.stringify(id)} must be refused`); + } + assert.strictEqual(isSessionId(42), false, "a non-string is not an id"); + assert.strictEqual(isSessionId(null), false, "null is not an id"); +}); + test("isRunning:TCP 探测(G43/G90)", async () => { const client = new DaemonClient(); // mock net.connect:成功 → true diff --git a/emrg/server/daemon.py b/emrg/server/daemon.py index db8a881a..8d3606d0 100644 --- a/emrg/server/daemon.py +++ b/emrg/server/daemon.py @@ -2255,7 +2255,7 @@ async def _task_vibe_check(self, task_name: str, session_id: str, cwd: str, Rant 2026-08-19T10:15:43 (host-finalized architecture): the PRIMARY evidence is the task's OWN session history, loaded here by the task's - fixed ``session_id`` (``emrg-evolution-{name}``) + working dir — + fixed ``session_id`` (the task's own name) + working dir — the full tool loop / analysis / memory writes are in the session, far richer than the transported prompt/completion_summary. Those are kept only as auxiliary context. System message lives in @@ -3769,7 +3769,7 @@ async def _run_tool_loop_locked( # Rant 2026-09-02T10:36:26:权威 turn 生命周期广播——started_at 为任务 # 实际开始执行时刻(队列等待之后)。TUI/GUI 据此对齐各自计时(排队请求 # 不再从发送时刻起算);覆盖所有 turn 来源:TUI/GUI 请求、演化任务 - # (emrg-evolution-*)、upgrade 会话(都经此 locked 包装执行)。 + # (定时任务会话)、upgrade 会话(都经此 locked 包装执行)。 started_at = time.time() self._session_turn_started[session_id] = started_at # A new turn speaks for itself: the previous turn's terminal frame (or diff --git a/emrg/server/scheduler.py b/emrg/server/scheduler.py index 03d2926a..d84838ce 100644 --- a/emrg/server/scheduler.py +++ b/emrg/server/scheduler.py @@ -186,7 +186,7 @@ def recovery_recipe(stash_message: str) -> str: # # 这条规则是**路径安全**规则,不是命名风格规则。任务名同时是文件名与会话 id: # `~/.emrg/logs/task-runs/.jsonl`、`.heartbeat.json`、`~/.emrg/next-run/ -# .json`、会话 id `emrg-evolution-{name}`。所以它必须挡住分隔符、跳转、空格与 +# .json`、会话 id `{name}`。所以它必须挡住分隔符、跳转、空格与 # 超长——那些会逃出目录或造出非法文件名。大小写不在其中。 # # 原先只收小写,于是一条**已经在 tasks.yml 里**的记录被加载器每个 tick 拒绝: @@ -930,7 +930,7 @@ def _apply_record(self, record: dict) -> None: "repo": repo, "repo_url": repo_url, "repo_configured": repo_configured, - "session_id": f"emrg-evolution-{self.name}", + "session_id": self.name, "source_dir": path or self.name, "sandbox": sandbox, # The template follows the record's type, so changing a task's type in @@ -976,7 +976,7 @@ def _repo_configured(self) -> bool: @property def _session_id(self) -> str: - return self._derived.get("session_id", f"emrg-evolution-{self.name}") + return self._derived.get("session_id", self.name) @property def _source_dir(self) -> str: @@ -2513,7 +2513,7 @@ def status(self) -> dict: # rant 2026-08-21T17:46:12: expose the task→session link so the GUI # tasks panel can add an "open session" action. All in-memory — # project name (config.project), resolved project path, and the - # fixed task session_id (emrg-evolution-{name}). + # fixed task session_id (the task name). "project": self._project_name, "project_path": self.project_path, "session_id": self._session_id, @@ -3306,7 +3306,7 @@ def _idle_status(self, record: dict) -> dict: "saturation": {}, "project": project, "project_path": _resolve_project_path(project) if project else "", - "session_id": f"emrg-evolution-{record.get('name')}", + "session_id": record.get("name"), } def total_evolutions(self) -> int: @@ -3427,7 +3427,7 @@ def task_create( return False, f"invalid sandbox {sandbox!r} (expected one of {', '.join(SANDBOX_MODES)})" tasks = self._read_table() # 名字即文件名与会话 id,而 macOS/Windows 的文件系统不区分大小写:`Foo` 与 - # `foo` 会写进同一个 `.jsonl`、同一个 `emrg-evolution-{name}` 会话。 + # `foo` 会写进同一个 `.jsonl`、同一个 `{name}` 会话。 # 允许大写之后这种重名才可能出现,所以这道唯一性比较必须忽略大小写。 if any(str(t.get("name", "")).lower() == name.lower() for t in tasks): return False, ( diff --git a/emrg/session.py b/emrg/session.py index 8129a5bf..669e10b5 100644 --- a/emrg/session.py +++ b/emrg/session.py @@ -74,8 +74,8 @@ def _parse_instant(value: object) -> datetime | None: def records_since(records: list[dict], since: str) -> list[dict]: """The records from `since` (an ISO-8601 instant) onward — the run's own. - A task session is **one session for every run of that task**: `emrg-evolution-` - is reused cycle after cycle, so its history holds all of them. Anything reading + A task session is **one session for every run of that task**: the task's own name is + reused cycle after cycle, so its history holds all of them. Anything reading "the recent history" as evidence about *this* run therefore reads the previous run's work as if it were this one's, unless it knows where the run began. This function is that knowledge, applied: `since` is the instant the run was dispatched, diff --git a/scripts/llm-cost-report.py b/scripts/llm-cost-report.py index d38fadde..877fd3a3 100644 --- a/scripts/llm-cost-report.py +++ b/scripts/llm-cost-report.py @@ -37,7 +37,7 @@ Usage examples:: scripts/llm-cost-report.py # all indexed sessions - scripts/llm-cost-report.py --session-id emrg-evolution-emrg-task + scripts/llm-cost-report.py --session-id emrg-task scripts/llm-cost-report.py --root ~/.emrg/.emrg/sessions --json scripts/llm-cost-report.py --pricing my-model:0.5:1.5 """ diff --git a/tests/test_app_task_session.py b/tests/test_app_task_session.py index 87c4ca8f..9bb6b517 100644 --- a/tests/test_app_task_session.py +++ b/tests/test_app_task_session.py @@ -34,7 +34,7 @@ def make_task(name: str = "t", **extra) -> dict: "name": name, "project": "emrg", "project_path": "/Users/x/projects/emrg", - "session_id": "emrg-evolution-emrg", + "session_id": "emrg", "running": False, "interval": 300, } @@ -111,7 +111,7 @@ def test_selected_task_is_none_when_nothing_is_selected(): def test_a_real_row_yields_the_tasks_project_and_session(): assert _task_session_target(make_task()) == ( "/Users/x/projects/emrg", - "emrg-evolution-emrg", + "emrg", ) @@ -129,7 +129,7 @@ def test_a_real_row_yields_the_tasks_project_and_session(): # replay somewhere that does not exist. make_task(project_path=" "), # Not a dict at all: the row comes off the wire. - "emrg-evolution-emrg", + "emrg", ], ) def test_a_row_that_cannot_name_a_session_yields_nothing(row): @@ -143,8 +143,8 @@ def test_a_row_that_cannot_name_a_session_yields_nothing(row): def test_the_cwd_moves_only_for_the_session_the_open_requested(): - pending = ("/Users/x/projects/emrg", "emrg-evolution-emrg") - assert _task_open_switch(pending, "emrg-evolution-emrg") == "/Users/x/projects/emrg" + pending = ("/Users/x/projects/emrg", "emrg") + assert _task_open_switch(pending, "emrg") == "/Users/x/projects/emrg" def test_no_open_means_no_switch(): @@ -157,7 +157,7 @@ def test_a_stale_open_does_not_ride_along_on_an_unrelated_resume(): (connection dropped) is still pending when a different session resumes, and moving the host into that stale project would put every later message and the history replay in the wrong directory.""" - stale = ("/Users/x/projects/other", "emrg-evolution-other") + stale = ("/Users/x/projects/other", "other") assert _task_open_switch(stale, "s_260917_1200_abcd") is None diff --git a/tests/test_daemon.py b/tests/test_daemon.py index 0c75e7a2..7b708aeb 100644 --- a/tests/test_daemon.py +++ b/tests/test_daemon.py @@ -48,7 +48,7 @@ def test_build_prompt_emrg_self(): # Core template variables must be present assert "argszero/emrg" in prompt - assert "emrg-evolution" in prompt + assert "- Session ID: `emrg`" in prompt, "the prompt must carry the task's own session id" assert "https://github.com/argszero/emrg.git" in prompt # …and the per-daemon identity must NOT be, even though the builder still passes it # (it is there for the host's own custom templates in `~/.emrg/task-templates/`). @@ -80,7 +80,7 @@ def test_build_prompt_with_project(): prompt = handler._build_evolution_prompt() assert "/home/user/src/myproject" in prompt - assert "emrg-evolution-myproject" in prompt + assert "- Session ID: `myproject`" in prompt assert "https://github.com/user/myproject.git" in prompt assert "<<<<<<<" not in prompt assert ">>>>>>>" not in prompt diff --git a/tests/test_local_exclude.py b/tests/test_local_exclude.py index ecbbfa7e..e0caa5b0 100644 --- a/tests/test_local_exclude.py +++ b/tests/test_local_exclude.py @@ -65,8 +65,8 @@ def _exclude_file(repo: Path) -> Path: def _runtime_dir(repo: Path) -> None: """What this instance writes into a directory it works in.""" - (repo / ".emrg" / "sessions" / "emrg-evolution-emrg-task").mkdir(parents=True) - (repo / ".emrg" / "sessions" / "emrg-evolution-emrg-task" / "history.jsonl").write_text( + (repo / ".emrg" / "sessions" / "emrg-task").mkdir(parents=True) + (repo / ".emrg" / "sessions" / "emrg-task" / "history.jsonl").write_text( "{}\n", encoding="utf-8" ) @@ -77,7 +77,7 @@ def _runtime_file(repo: Path) -> str: A file rather than the bare directory: git ignores what it can see, and an empty directory is invisible to it either way. """ - return str(repo / ".emrg" / "sessions" / "emrg-evolution-emrg-task" / "history.jsonl") + return str(repo / ".emrg" / "sessions" / "emrg-task" / "history.jsonl") def _ignored(repo: Path, path: str) -> bool: diff --git a/tests/test_run_evidence_boundary.py b/tests/test_run_evidence_boundary.py index 4b89d214..7ceb8e4d 100644 --- a/tests/test_run_evidence_boundary.py +++ b/tests/test_run_evidence_boundary.py @@ -2,8 +2,8 @@ Why this file exists -------------------- -A scheduled task runs in **one** session for its whole life: `emrg-evolution-` -is reused by every run. So "the recent history of that session" is not evidence about +A scheduled task runs in **one** session for its whole life: the session is named after +the task (``) and is reused by every run. So "the recent history of that session" is not evidence about *this* run — it is a window that mostly holds the *previous* one whenever this run was light on narrative (a vote-only run, a verification run, a run whose work is reading). @@ -67,7 +67,7 @@ def _record(role: str, content: str, at: str) -> dict: def _two_run_session(cwd) -> Session: """One session file holding two runs, as the real task session is.""" - session = Session.create_with_id("emrg-evolution-boundary-test", cwd) + session = Session.create_with_id("boundary-test", cwd) for record in ( _record("user", "run one prompt", "2026-10-08T09:00:05"), _record("assistant", f"I {PREVIOUS_RUN_WORK}", "2026-10-08T09:20:00"), @@ -356,7 +356,7 @@ def test_the_judge_is_not_shown_the_previous_runs_work(tmp_path) -> None: server = _bare_server(llm) asyncio.run(server._task_vibe_check( - "emrg-task", "emrg-evolution-boundary-test", str(tmp_path), + "emrg-task", "boundary-test", str(tmp_path), prompt="run two prompt", completion_summary="done", cycle_started_at=RUN_TWO_START, )) @@ -380,7 +380,7 @@ def test_the_same_session_still_shows_both_runs_without_a_boundary(tmp_path) -> server = _bare_server(llm) asyncio.run(server._task_vibe_check( - "emrg-task", "emrg-evolution-boundary-test", str(tmp_path), + "emrg-task", "boundary-test", str(tmp_path), prompt="run two prompt", completion_summary="done", )) diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index be2a9a65..7514e385 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -675,7 +675,7 @@ def test_evolution_handler_status_last_run_fields(): # rant 2026-08-18T21:32:32: recent_runs present, empty before any run assert st["recent_runs"] == [] # rant 2026-08-21T17:46:12: task→session link fields (GUI open-session action) - assert st["session_id"] == "emrg-evolution-test" + assert st["session_id"] == "test" assert st["project"] == "" # config={} → empty project name assert st["project_path"] == "test" # fallback path = name # rant 2026-08-22T07:18:35: started_at = per-CYCLE start epoch (set at tick @@ -2652,7 +2652,7 @@ def test_the_name_rule_is_about_the_path_not_the_case(tmp_path): """It refuses what could escape a directory, and nothing else. The task name is a file name (`task-runs/.jsonl`, `.heartbeat.json`, - `next-run/.json`) and a session id (`emrg-evolution-{name}`), so separators, + `next-run/.json`) and a session id (the name itself), so separators, traversal, spaces, control characters and over-length are unsafe. Case is not: it was never a safety property, and treating it as one killed a live host task. """ diff --git a/tests/test_ws_e2e.py b/tests/test_ws_e2e.py index 7b2b0e3f..0251116f 100644 --- a/tests/test_ws_e2e.py +++ b/tests/test_ws_e2e.py @@ -283,7 +283,7 @@ async def _test(): tmp = Path(tmp) # Pre-create the task's session history on disk (session files # are organized by cwd: /.emrg/sessions//). - sess_dir = tmp / ".emrg" / "sessions" / "emrg-evolution-deepseek-harness-opensource-task" + sess_dir = tmp / ".emrg" / "sessions" / "deepseek-harness-opensource-task" sess_dir.mkdir(parents=True, exist_ok=True) (sess_dir / "history.jsonl").write_text( json.dumps({"type": "message", "role": "user", @@ -307,7 +307,7 @@ async def fake_chat(messages, tools=None): try: await ws.send(json.dumps({ "type": "task_vibe_check", - "session_id": "emrg-evolution-deepseek-harness-opensource-task", + "session_id": "deepseek-harness-opensource-task", "cwd": str(tmp), "task_name": "deepseek-harness-opensource-task", "prompt": "run cycle", @@ -348,7 +348,7 @@ def test_vibe_check_frame_carries_the_run_boundary_into_the_evidence(self): async def _test(): with tempfile.TemporaryDirectory() as tmp: tmp = Path(tmp) - sess_dir = tmp / ".emrg" / "sessions" / "emrg-evolution-two-runs-task" + sess_dir = tmp / ".emrg" / "sessions" / "two-runs-task" sess_dir.mkdir(parents=True, exist_ok=True) (sess_dir / "history.jsonl").write_text( json.dumps({"type": "message", "role": "user", @@ -380,7 +380,7 @@ async def fake_chat(messages, tools=None): try: await ws.send(json.dumps({ "type": "task_vibe_check", - "session_id": "emrg-evolution-two-runs-task", + "session_id": "two-runs-task", "cwd": str(tmp), "task_name": "two-runs-task", "prompt": "run cycle", @@ -415,7 +415,7 @@ def test_vibe_check_long_session_window_leading_tool_stripped(self): async def _test(): with tempfile.TemporaryDirectory() as tmp: tmp = Path(tmp) - sess_dir = tmp / ".emrg" / "sessions" / "emrg-evolution-emrg-task" + sess_dir = tmp / ".emrg" / "sessions" / "emrg-task" sess_dir.mkdir(parents=True, exist_ok=True) lines = [] # 60 user → assistant(tool_calls) → tool_result triples: @@ -450,7 +450,7 @@ async def fake_chat(messages, tools=None): try: await ws.send(json.dumps({ "type": "task_vibe_check", - "session_id": "emrg-evolution-emrg-task", + "session_id": "emrg-task", "cwd": str(tmp), "task_name": "emrg-task", "prompt": "run cycle",