Skip to content

Commit 8ed3969

Browse files
committed
fix(p2): TEVARN_HOME unify, checkpoint ownership, static version guard
P2 from post-a23c95b audit + suggested improvements: 1. TEVARN_HOME: capability/wave_a/agent_ops tools, skill storage, usage ledger, run_events/inbox/snapshots, audit, encryption, process_guard, smoke_test, cli, win_boot, intent_telemetry, permissions 2. Checkpoint registry stores session_id/user_id/workspace_root; restore API enforces ownership match when bound 3. static_frontend warns if served tree looks like 0.5.4-alpha / version mismatch vs product 0.4.2 (no silent stale UI) 4. frontend build:export + write:static-version → out/version.json stamp 5. check_gates: no || true; fail if tests import deleted _next_seq doSync after_seq already landed in a23c95b. Panoramic audit clean.
1 parent a23c95b commit 8ed3969

21 files changed

Lines changed: 179 additions & 32 deletions

‎backend/agent/control_inbox.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def _inbox_dir() -> Path:
6868
return base
6969
except Exception:
7070
try:
71-
base = Path.home() / ".tevarn" / "control_inbox"
71+
base = get_tevarn_home() / "control_inbox"
7272
base.mkdir(parents=True, exist_ok=True)
7373
return base
7474
except Exception:

‎backend/agent/file_checkpoint.py‎

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -154,19 +154,33 @@ def register_checkpoint(
154154
snapshot_path: str,
155155
target_path: str,
156156
tool: str = "",
157+
session_id: str | None = None,
158+
user_id: str | None = None,
159+
workspace_id: str | None = None,
157160
) -> str:
158-
"""Register a snapshot and return opaque id (thread + cross-process locked)."""
161+
"""Register a snapshot and return opaque id (thread + cross-process locked).
162+
163+
Optional ownership fields bind restore to the creating session/user.
164+
"""
159165
root = _project_root()
160166
cid = uuid.uuid4().hex
161167

162168
def _do() -> None:
163169
reg = _load_registry(root)
164-
reg[cid] = {
170+
entry: dict[str, Any] = {
165171
"snapshot": str(snapshot_path),
166172
"target": str(target_path),
167173
"tool": tool,
168174
"created_at": datetime.now(timezone.utc).isoformat(),
175+
"workspace_root": str(root),
169176
}
177+
if session_id:
178+
entry["session_id"] = str(session_id)
179+
if user_id:
180+
entry["user_id"] = str(user_id)
181+
if workspace_id:
182+
entry["workspace_id"] = str(workspace_id)
183+
reg[cid] = entry
170184
if len(reg) > 400:
171185
items = sorted(
172186
reg.items(),
@@ -248,10 +262,22 @@ def snapshot_path_for_tool(name: str, arguments: dict[str, Any]) -> str | None:
248262
idx = dest_root / "INDEX.txt"
249263
with idx.open("a", encoding="utf-8") as f:
250264
f.write(f"{name}\t{target}\t{dest}\n")
265+
_sid = str(
266+
(arguments or {}).get("_session_id")
267+
or (arguments or {}).get("session_id")
268+
or ""
269+
).strip() or None
270+
_uid = str(
271+
(arguments or {}).get("_user_id")
272+
or (arguments or {}).get("user_id")
273+
or ""
274+
).strip() or None
251275
cid = register_checkpoint(
252276
snapshot_path=str(dest),
253277
target_path=str(target),
254278
tool=name,
279+
session_id=_sid,
280+
user_id=_uid,
255281
)
256282
# also write id into index for operators
257283
with idx.open("a", encoding="utf-8") as f:
@@ -267,12 +293,18 @@ def list_recent_checkpoints(limit: int = 20) -> list[str]:
267293
return [str(p) for p in dirs[:limit]]
268294

269295

270-
def restore_checkpoint_file(snapshot_path_or_id: str) -> dict[str, Any]:
296+
def restore_checkpoint_file(
297+
snapshot_path_or_id: str,
298+
*,
299+
session_id: str | None = None,
300+
user_id: str | None = None,
301+
) -> dict[str, Any]:
271302
"""Restore by opaque id (preferred) or legacy snapshot path.
272303
273304
Security:
274305
- registry targets must stay under project root
275306
- legacy path snapshots must live under .tevarn/checkpoints/
307+
- if registry has session_id/user_id, caller must match when provided
276308
"""
277309
raw = str(snapshot_path_or_id or "").strip()
278310
if not raw:
@@ -284,6 +316,13 @@ def restore_checkpoint_file(snapshot_path_or_id: str) -> dict[str, Any]:
284316
# Opaque id path
285317
entry = lookup_checkpoint(raw)
286318
if entry and entry.get("backend") == "python" and entry.get("snapshot"):
319+
# Ownership gate (shared-deploy safety)
320+
reg_sid = str(entry.get("session_id") or "").strip()
321+
reg_uid = str(entry.get("user_id") or "").strip()
322+
if reg_sid and session_id and str(session_id).strip() != reg_sid:
323+
return {"ok": False, "error": "checkpoint session mismatch"}
324+
if reg_uid and user_id and str(user_id).strip() != reg_uid:
325+
return {"ok": False, "error": "checkpoint owner mismatch"}
287326
snap = Path(str(entry["snapshot"])).expanduser()
288327
target = Path(str(entry.get("target") or "")).expanduser()
289328
try:

‎backend/agent/run_events.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def _spill_dir() -> Path:
3333
return p
3434
except Exception:
3535
try:
36-
p = Path.home() / ".tevarn" / "run_events"
36+
p = get_tevarn_home() / "run_events"
3737
p.mkdir(parents=True, exist_ok=True)
3838
return p
3939
except Exception:

‎backend/agent/run_snapshot_store.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
"""
1515
from __future__ import annotations
1616

17+
from backend.core.config import get_tevarn_home
18+
1719
import json
1820
import logging
1921
import os
@@ -55,7 +57,7 @@ def _dir() -> Path:
5557

5658
root = Path(os.environ.get("TEVARN_HOME") or (host_home() / ".tevarn"))
5759
except Exception:
58-
root = Path(os.environ.get("TEVARN_HOME") or Path.home() / ".tevarn")
60+
root = Path(os.environ.get("TEVARN_HOME") or get_tevarn_home())
5961
d = root / "run_snapshots"
6062
d.mkdir(parents=True, exist_ok=True)
6163
return d

‎backend/api/routes/files.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,11 @@ def _try_python(key: str) -> dict | None:
719719
else nullcontext()
720720
)
721721
with ctx:
722-
result = restore_checkpoint_file(key)
722+
result = restore_checkpoint_file(
723+
key,
724+
session_id=session_id_raw or None,
725+
user_id=str(getattr(current_user, "id", "") or "") or None,
726+
)
723727
if result.get("ok"):
724728
result["backend"] = "python"
725729
if root_for_restore:

‎backend/api/routes/smoke_test.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from backend.core.config import get_tevarn_home
12
"""
23
冒烟测试专用路由
34
提供同步 HTTP 接口,方便后端直接调用模拟用户对话,
@@ -122,7 +123,7 @@ async def smoke_chat(
122123

123124
# 读取最近日志提取上下文压缩信息
124125
try:
125-
log_dir = os.environ.get("TEVARN_LOG_DIR", "") or str(Path.home() / ".tevarn" / "logs")
126+
log_dir = os.environ.get("TEVARN_LOG_DIR", "") or str(get_tevarn_home() / "logs")
126127
log_path = str(Path(log_dir) / "tevarn.log")
127128
with open(log_path, "r", encoding="utf-8", errors="ignore") as f:
128129
lines = f.readlines()[-200:] # 只看最近 200 行

‎backend/cli.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
from __future__ import annotations
1717

18+
from backend.core.config import get_tevarn_home
19+
1820
import argparse
1921
import json
2022
import os
@@ -42,7 +44,7 @@ def _base_url(args: argparse.Namespace) -> str:
4244

4345

4446
def _token_path() -> Path:
45-
home = Path.home() / ".tevarn"
47+
home = get_tevarn_home()
4648
home.mkdir(parents=True, exist_ok=True)
4749
return home / "cli_token"
4850

‎backend/core/encryption.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ def _secrets_file_path() -> Path:
3030
from backend.core.config import get_tevarn_home
3131
return get_tevarn_home() / "secrets.json"
3232
except Exception:
33-
return Path.home() / ".tevarn" / "secrets.json"
33+
return get_tevarn_home() / "secrets.json"
3434

3535

3636
def _derive_key_from_jwt_secret() -> bytes:

‎backend/core/process_guard.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
"""
1313
from __future__ import annotations
1414

15+
from backend.core.config import get_tevarn_home
16+
1517
import atexit
1618
import json
1719
import logging
@@ -32,7 +34,7 @@
3234

3335
def breadcrumb_path() -> Path:
3436
"""Prefer ~/.tevarn/logs (user-writable); fall back next to package."""
35-
home = os.environ.get("TEVARN_HOME") or str(Path.home() / ".tevarn")
37+
home = os.environ.get("TEVARN_HOME") or str(get_tevarn_home())
3638
p = Path(home) / "logs" / "process_breadcrumb.jsonl"
3739
try:
3840
p.parent.mkdir(parents=True, exist_ok=True)

‎backend/kernel/audit_store.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def _default_audit_path() -> str:
2020
from backend.core.config import get_tevarn_home
2121
return str(get_tevarn_home() / "kernel_events.jsonl")
2222
except Exception:
23-
return os.path.expanduser("~/.tevarn/kernel_events.jsonl")
23+
return str(get_tevarn_home() / "kernel_events.jsonl")
2424

2525

2626
_DEFAULT_PATH = _default_audit_path()

0 commit comments

Comments
 (0)