"""不可变 run 目录读写。不认识 Muse 业务对象。""" from __future__ import annotations import json import os import re from collections.abc import Iterator, Mapping from contextlib import contextmanager from pathlib import Path from typing import Any, BinaryIO DEFAULT_RUN_DIR_ROOT = Path("/tmp/muse-agent-runs") _RUN_ID_PATTERN = re.compile(r"^[A-Za-z0-9_.-]{1,64}$") class RunDirectoryError(OSError): """run 目录创建或写入失败。""" def validate_run_id(run_id: str) -> bool: return isinstance(run_id, str) and _RUN_ID_PATTERN.fullmatch(run_id) is not None def prepare_run_dir(run_id: str, run_dir: str | Path | None = None) -> Path: """创建仅当前用户可访问的 run 目录;默认根目录可复用,本次目录不可覆盖。""" run_dir_path = Path(run_dir) if run_dir is not None else DEFAULT_RUN_DIR_ROOT / run_id if run_dir is None: DEFAULT_RUN_DIR_ROOT.mkdir(parents=True, mode=0o700, exist_ok=True) DEFAULT_RUN_DIR_ROOT.chmod(0o700) run_dir_path.mkdir(parents=True, mode=0o700, exist_ok=False) run_dir_path.chmod(0o700) return run_dir_path def write_private_text(path: Path, text: str) -> None: fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) with os.fdopen(fd, "w", encoding="utf-8") as handle: handle.write(text) path.chmod(0o600) def write_private_json(path: Path, value: Mapping[str, Any]) -> None: write_private_text(path, json.dumps(value, ensure_ascii=False, indent=2)) def seed_run_inputs( run_dir: Path, *, spec_text: str, system_prompt: str, user_message: str, ) -> None: write_private_text(run_dir / "task-spec.json", spec_text) write_private_text(run_dir / "system-prompt.txt", system_prompt) write_private_text(run_dir / "user-message.txt", user_message) @contextmanager def open_transcript(run_dir: Path) -> Iterator[BinaryIO]: path = run_dir / "transcript.jsonl" fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) handle = os.fdopen(fd, "wb") try: yield handle path.chmod(0o600) finally: handle.close() def read_transcript_text(run_dir: Path) -> str: return (run_dir / "transcript.jsonl").read_text(encoding="utf-8") def remove_local_raw(run_dir: Path) -> None: for name in ("transcript.jsonl", "final-message.txt"): try: (run_dir / name).unlink(missing_ok=True) except OSError: pass