fix(session): store session history outside the agent workspace
Session files lived under <workspace>/sessions/ (since #713), which is the on-disk scope of the agent's filesystem tools. With restrict_to_workspace enabled, an agent could read_file / list_dir every session transcript — including other users' or channels' conversations — bypassing the scoped sessions.py access layer entirely. Move session storage to ~/.nanobot/sessions/<sha256-of-resolved-workspace>[:16]/, outside the workspace. Per-workspace isolation (the goal of #713) is preserved via a hash of the resolved workspace path, so different workspaces keep independent session stores. A one-shot, idempotent migration moves legacy in-workspace *.jsonl files into the new location at store init. Scope note: this protects sessions whenever restrict_to_workspace=true. The default restrict_to_workspace=false leaves read_file unrestricted in general (not only sessions) and is a separate concern. Refs #5278
This commit is contained in:
@@ -2,9 +2,11 @@
|
||||
|
||||
import base64
|
||||
import errno
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from collections import OrderedDict
|
||||
from contextlib import suppress
|
||||
from copy import deepcopy
|
||||
@@ -521,8 +523,43 @@ class JsonlSessionStore:
|
||||
"""JSONL implementation of session persistence."""
|
||||
|
||||
def __init__(self, workspace: Path):
|
||||
self.sessions_dir = ensure_dir(workspace / "sessions")
|
||||
self.legacy_sessions_dir = get_legacy_sessions_dir()
|
||||
root = get_legacy_sessions_dir()
|
||||
self.sessions_dir = ensure_dir(root / self._workspace_hash(workspace))
|
||||
self.legacy_sessions_dir = root
|
||||
self._write_workspace_marker(self.sessions_dir, workspace)
|
||||
self._migrate_from_workspace(workspace)
|
||||
|
||||
@staticmethod
|
||||
def _workspace_hash(workspace: Path) -> str:
|
||||
canonical = str(Path(workspace).expanduser().resolve(strict=False))
|
||||
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:16]
|
||||
|
||||
@staticmethod
|
||||
def _write_workspace_marker(sessions_dir: Path, workspace: Path) -> None:
|
||||
marker = sessions_dir / ".workspace"
|
||||
if marker.exists():
|
||||
return
|
||||
try:
|
||||
marker.write_text(
|
||||
str(Path(workspace).expanduser().resolve(strict=False)),
|
||||
encoding="utf-8",
|
||||
)
|
||||
except OSError as exc:
|
||||
logger.debug("Failed to write sessions workspace marker: {}", exc)
|
||||
|
||||
def _migrate_from_workspace(self, workspace: Path) -> None:
|
||||
"""Move legacy in-workspace session files into the out-of-workspace store."""
|
||||
old_dir = Path(workspace).expanduser() / "sessions"
|
||||
if not old_dir.is_dir():
|
||||
return
|
||||
for src in old_dir.glob("*.jsonl"):
|
||||
dst = self.sessions_dir / src.name
|
||||
if dst.exists():
|
||||
continue
|
||||
try:
|
||||
shutil.move(str(src), str(dst))
|
||||
except OSError as exc:
|
||||
logger.warning("Failed to migrate session {}: {}", src, exc)
|
||||
|
||||
@staticmethod
|
||||
def safe_key(key: str) -> str:
|
||||
|
||||
Reference in New Issue
Block a user