sessionfs 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sessionfs/__init__.py +3 -0
- sessionfs/audit.py +153 -0
- sessionfs/cli/__init__.py +1 -0
- sessionfs/cli/cmd_admin.py +60 -0
- sessionfs/cli/cmd_cloud.py +522 -0
- sessionfs/cli/cmd_config.py +117 -0
- sessionfs/cli/cmd_daemon.py +161 -0
- sessionfs/cli/cmd_io.py +137 -0
- sessionfs/cli/cmd_ops.py +230 -0
- sessionfs/cli/cmd_sessions.py +321 -0
- sessionfs/cli/common.py +84 -0
- sessionfs/cli/cost.py +77 -0
- sessionfs/cli/main.py +58 -0
- sessionfs/cli/sfs_to_cc.py +459 -0
- sessionfs/cli/sfs_to_md.py +125 -0
- sessionfs/cli/titles.py +159 -0
- sessionfs/converters/__init__.py +0 -0
- sessionfs/converters/codex_injector.py +162 -0
- sessionfs/converters/cursor_to_sfs.py +324 -0
- sessionfs/converters/gemini_injector.py +92 -0
- sessionfs/converters/gemini_to_sfs.py +269 -0
- sessionfs/converters/sfs_to_codex.py +383 -0
- sessionfs/converters/sfs_to_gemini.py +204 -0
- sessionfs/daemon/__init__.py +0 -0
- sessionfs/daemon/config.py +137 -0
- sessionfs/daemon/main.py +393 -0
- sessionfs/daemon/status.py +66 -0
- sessionfs/security/__init__.py +0 -0
- sessionfs/security/secrets.py +223 -0
- sessionfs/server/__init__.py +1 -0
- sessionfs/server/app.py +95 -0
- sessionfs/server/auth/__init__.py +1 -0
- sessionfs/server/auth/dependencies.py +70 -0
- sessionfs/server/auth/keys.py +24 -0
- sessionfs/server/auth/rate_limit.py +36 -0
- sessionfs/server/config.py +28 -0
- sessionfs/server/db/__init__.py +1 -0
- sessionfs/server/db/engine.py +40 -0
- sessionfs/server/db/migrations/env.py +70 -0
- sessionfs/server/db/migrations/versions/001_initial_schema.py +78 -0
- sessionfs/server/db/models.py +77 -0
- sessionfs/server/errors.py +48 -0
- sessionfs/server/routes/__init__.py +1 -0
- sessionfs/server/routes/auth.py +132 -0
- sessionfs/server/routes/health.py +12 -0
- sessionfs/server/routes/sessions.py +822 -0
- sessionfs/server/schemas/__init__.py +1 -0
- sessionfs/server/schemas/auth.py +37 -0
- sessionfs/server/schemas/errors.py +17 -0
- sessionfs/server/schemas/sessions.py +104 -0
- sessionfs/server/storage/__init__.py +1 -0
- sessionfs/server/storage/base.py +15 -0
- sessionfs/server/storage/local.py +41 -0
- sessionfs/server/storage/s3.py +49 -0
- sessionfs/session_id.py +32 -0
- sessionfs/spec/__init__.py +0 -0
- sessionfs/spec/convert_cc.py +634 -0
- sessionfs/spec/examples/complete/manifest.json +33 -0
- sessionfs/spec/examples/complete/messages.jsonl +8 -0
- sessionfs/spec/examples/complete/tools.json +9 -0
- sessionfs/spec/examples/complete/workspace.json +33 -0
- sessionfs/spec/examples/minimal/manifest.json +28 -0
- sessionfs/spec/examples/minimal/messages.jsonl +3 -0
- sessionfs/spec/examples/subagent/manifest.json +33 -0
- sessionfs/spec/examples/subagent/messages.jsonl +7 -0
- sessionfs/spec/examples/subagent/tools.json +7 -0
- sessionfs/spec/examples/subagent/workspace.json +24 -0
- sessionfs/spec/schemas/manifest.schema.json +287 -0
- sessionfs/spec/schemas/message.schema.json +291 -0
- sessionfs/spec/schemas/tools.schema.json +138 -0
- sessionfs/spec/schemas/workspace.schema.json +152 -0
- sessionfs/spec/validate.py +353 -0
- sessionfs/store/__init__.py +0 -0
- sessionfs/store/index.py +230 -0
- sessionfs/store/local.py +147 -0
- sessionfs/sync/__init__.py +1 -0
- sessionfs/sync/archive.py +73 -0
- sessionfs/sync/client.py +313 -0
- sessionfs/utils/__init__.py +0 -0
- sessionfs/utils/title_utils.py +207 -0
- sessionfs/watchers/__init__.py +0 -0
- sessionfs/watchers/base.py +71 -0
- sessionfs/watchers/claude_code.py +641 -0
- sessionfs/watchers/codex.py +573 -0
- sessionfs/watchers/cursor.py +202 -0
- sessionfs/watchers/gemini.py +206 -0
- sessionfs-0.1.0.dist-info/METADATA +213 -0
- sessionfs-0.1.0.dist-info/RECORD +90 -0
- sessionfs-0.1.0.dist-info/WHEEL +4 -0
- sessionfs-0.1.0.dist-info/entry_points.txt +3 -0
sessionfs/__init__.py
ADDED
sessionfs/audit.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""M9: Audit logging module.
|
|
2
|
+
|
|
3
|
+
Provides structured audit logging to both local file (~/.sessionfs/audit.log)
|
|
4
|
+
and the server audit_events table. All significant events are logged in JSON
|
|
5
|
+
lines format.
|
|
6
|
+
|
|
7
|
+
IMPORTANT: Never log session content, blob data, or API key values.
|
|
8
|
+
Log metadata only.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import os
|
|
16
|
+
import stat
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("sessionfs.audit")
|
|
22
|
+
|
|
23
|
+
# Event types that can be logged
|
|
24
|
+
EVENT_TYPES = frozenset({
|
|
25
|
+
"session_captured",
|
|
26
|
+
"session_synced",
|
|
27
|
+
"session_pulled",
|
|
28
|
+
"session_resumed",
|
|
29
|
+
"session_exported",
|
|
30
|
+
"session_handoff",
|
|
31
|
+
"session_deleted",
|
|
32
|
+
"session_forked",
|
|
33
|
+
"session_checkpoint_created",
|
|
34
|
+
"api_key_created",
|
|
35
|
+
"api_key_revoked",
|
|
36
|
+
"auth_failed",
|
|
37
|
+
"auth_success",
|
|
38
|
+
"sync_conflict",
|
|
39
|
+
"sync_error",
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class AuditLogger:
|
|
44
|
+
"""Writes audit events to a JSON lines file."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, audit_log_path: Path | None = None) -> None:
|
|
47
|
+
if audit_log_path is None:
|
|
48
|
+
audit_log_path = Path.home() / ".sessionfs" / "audit.log"
|
|
49
|
+
self._path = audit_log_path
|
|
50
|
+
|
|
51
|
+
def _ensure_file(self) -> None:
|
|
52
|
+
"""Ensure the audit log file exists with correct permissions."""
|
|
53
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
54
|
+
if not self._path.exists():
|
|
55
|
+
self._path.touch()
|
|
56
|
+
os.chmod(self._path, stat.S_IRUSR | stat.S_IWUSR) # 0o600
|
|
57
|
+
|
|
58
|
+
def log(
|
|
59
|
+
self,
|
|
60
|
+
event_type: str,
|
|
61
|
+
*,
|
|
62
|
+
user_id: str | None = None,
|
|
63
|
+
session_id: str | None = None,
|
|
64
|
+
details: dict[str, Any] | None = None,
|
|
65
|
+
source_ip: str | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Write an audit event to the log file.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
event_type: One of the EVENT_TYPES constants.
|
|
71
|
+
user_id: User who performed the action (None for daemon events).
|
|
72
|
+
session_id: Session involved (if applicable).
|
|
73
|
+
details: Additional context (never include secret values).
|
|
74
|
+
source_ip: Client IP (server-side events only).
|
|
75
|
+
"""
|
|
76
|
+
entry = {
|
|
77
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
78
|
+
"event_type": event_type,
|
|
79
|
+
}
|
|
80
|
+
if user_id is not None:
|
|
81
|
+
entry["user_id"] = user_id
|
|
82
|
+
if session_id is not None:
|
|
83
|
+
entry["session_id"] = session_id
|
|
84
|
+
if details:
|
|
85
|
+
entry["details"] = details
|
|
86
|
+
if source_ip:
|
|
87
|
+
entry["source_ip"] = source_ip
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
self._ensure_file()
|
|
91
|
+
with open(self._path, "a", encoding="utf-8") as f:
|
|
92
|
+
f.write(json.dumps(entry, separators=(",", ":")) + "\n")
|
|
93
|
+
except OSError as e:
|
|
94
|
+
logger.error("Failed to write audit log: %s", e)
|
|
95
|
+
|
|
96
|
+
def read_events(
|
|
97
|
+
self,
|
|
98
|
+
event_type: str | None = None,
|
|
99
|
+
session_id: str | None = None,
|
|
100
|
+
limit: int = 100,
|
|
101
|
+
) -> list[dict[str, Any]]:
|
|
102
|
+
"""Read audit events from the log file with optional filters."""
|
|
103
|
+
if not self._path.exists():
|
|
104
|
+
return []
|
|
105
|
+
|
|
106
|
+
events: list[dict[str, Any]] = []
|
|
107
|
+
with open(self._path, "r", encoding="utf-8") as f:
|
|
108
|
+
for line in f:
|
|
109
|
+
line = line.strip()
|
|
110
|
+
if not line:
|
|
111
|
+
continue
|
|
112
|
+
try:
|
|
113
|
+
event = json.loads(line)
|
|
114
|
+
except json.JSONDecodeError:
|
|
115
|
+
continue
|
|
116
|
+
if event_type and event.get("event_type") != event_type:
|
|
117
|
+
continue
|
|
118
|
+
if session_id and event.get("session_id") != session_id:
|
|
119
|
+
continue
|
|
120
|
+
events.append(event)
|
|
121
|
+
|
|
122
|
+
# Return most recent first, limited
|
|
123
|
+
return events[-limit:][::-1]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# Module-level default instance
|
|
127
|
+
_default_logger: AuditLogger | None = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_audit_logger(audit_log_path: Path | None = None) -> AuditLogger:
|
|
131
|
+
"""Get or create the default AuditLogger instance."""
|
|
132
|
+
global _default_logger
|
|
133
|
+
if _default_logger is None or audit_log_path is not None:
|
|
134
|
+
_default_logger = AuditLogger(audit_log_path)
|
|
135
|
+
return _default_logger
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def audit_event(
|
|
139
|
+
event_type: str,
|
|
140
|
+
*,
|
|
141
|
+
user_id: str | None = None,
|
|
142
|
+
session_id: str | None = None,
|
|
143
|
+
details: dict[str, Any] | None = None,
|
|
144
|
+
source_ip: str | None = None,
|
|
145
|
+
) -> None:
|
|
146
|
+
"""Convenience function to log an audit event using the default logger."""
|
|
147
|
+
get_audit_logger().log(
|
|
148
|
+
event_type,
|
|
149
|
+
user_id=user_id,
|
|
150
|
+
session_id=session_id,
|
|
151
|
+
details=details,
|
|
152
|
+
source_ip=source_ip,
|
|
153
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""SessionFS CLI."""
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""Admin commands: sfs admin reindex."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
|
|
9
|
+
from sessionfs.cli.common import console, err_console
|
|
10
|
+
|
|
11
|
+
admin_app = typer.Typer(name="admin", help="Server administration commands.")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _get_sync_client():
|
|
15
|
+
"""Create a SyncClient from stored config."""
|
|
16
|
+
from sessionfs.cli.cmd_cloud import _get_sync_client
|
|
17
|
+
return _get_sync_client()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@admin_app.command("reindex")
|
|
21
|
+
def reindex() -> None:
|
|
22
|
+
"""Re-extract metadata from all session archives on the server.
|
|
23
|
+
|
|
24
|
+
Backfills title, source_tool, model, stats, and tags for sessions
|
|
25
|
+
that were pushed before metadata extraction was deployed.
|
|
26
|
+
"""
|
|
27
|
+
from sessionfs.sync.client import SyncClient, SyncAuthError, SyncError
|
|
28
|
+
|
|
29
|
+
client = _get_sync_client()
|
|
30
|
+
|
|
31
|
+
async def _reindex():
|
|
32
|
+
try:
|
|
33
|
+
http = await client._get_client()
|
|
34
|
+
resp = await http.post(
|
|
35
|
+
f"{client.api_url}/api/v1/sessions/admin/reindex",
|
|
36
|
+
timeout=120.0,
|
|
37
|
+
)
|
|
38
|
+
if resp.status_code in (401, 403):
|
|
39
|
+
raise SyncAuthError(f"Authentication failed: {resp.status_code}")
|
|
40
|
+
resp.raise_for_status()
|
|
41
|
+
return resp.json()
|
|
42
|
+
finally:
|
|
43
|
+
await client.close()
|
|
44
|
+
|
|
45
|
+
console.print("Reindexing all sessions on the server...")
|
|
46
|
+
try:
|
|
47
|
+
result = asyncio.run(_reindex())
|
|
48
|
+
except SyncAuthError:
|
|
49
|
+
err_console.print("[red]Authentication failed. Run 'sfs auth login' first.[/red]")
|
|
50
|
+
raise SystemExit(1)
|
|
51
|
+
except Exception as exc:
|
|
52
|
+
err_console.print(f"[red]Reindex failed: {exc}[/red]")
|
|
53
|
+
raise SystemExit(1)
|
|
54
|
+
|
|
55
|
+
console.print(
|
|
56
|
+
f"[green]Reindex complete:[/green] "
|
|
57
|
+
f"{result['reindexed']} sessions, "
|
|
58
|
+
f"{result['updated']} updated, "
|
|
59
|
+
f"{result['errors']} errors"
|
|
60
|
+
)
|