sessionfs 0.3.0__tar.gz → 0.3.2__tar.gz
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-0.3.0 → sessionfs-0.3.2}/PKG-INFO +12 -5
- {sessionfs-0.3.0 → sessionfs-0.3.2}/README.md +9 -4
- {sessionfs-0.3.0 → sessionfs-0.3.2}/pyproject.toml +2 -1
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/__init__.py +1 -1
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/cli/cmd_audit.py +43 -5
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/cli/cmd_cloud.py +30 -8
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/cli/cmd_sessions.py +20 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/daemon/main.py +12 -9
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/daemon/status.py +8 -4
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/judge/evidence.py +0 -1
- sessionfs-0.3.2/src/sessionfs/judge/export.py +85 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/judge/judge.py +1 -1
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/judge/providers.py +42 -5
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/app.py +3 -1
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/auth/dependencies.py +9 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/config.py +2 -1
- sessionfs-0.3.2/src/sessionfs/server/db/migrations/versions/006_judge_settings.py +25 -0
- sessionfs-0.3.2/src/sessionfs/server/db/migrations/versions/007_admin_audit_log.py +27 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/db/models.py +30 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/email_templates.py +25 -4
- sessionfs-0.3.2/src/sessionfs/server/routes/admin.py +435 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/routes/audit.py +41 -10
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/routes/auth.py +13 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/routes/handoffs.py +57 -61
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/routes/sessions.py +79 -49
- sessionfs-0.3.2/src/sessionfs/server/routes/settings.py +105 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/workspace/resolver.py +1 -1
- {sessionfs-0.3.0 → sessionfs-0.3.2}/.gitignore +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/audit.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/cli/__init__.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/cli/cmd_admin.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/cli/cmd_config.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/cli/cmd_daemon.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/cli/cmd_io.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/cli/cmd_mcp.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/cli/cmd_ops.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/cli/cmd_search.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/cli/common.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/cli/cost.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/cli/main.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/cli/sfs_to_cc.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/cli/sfs_to_md.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/cli/titles.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/converters/__init__.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/converters/amp_to_sfs.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/converters/cline_to_sfs.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/converters/codex_injector.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/converters/copilot_injector.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/converters/copilot_to_sfs.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/converters/cursor_to_sfs.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/converters/gemini_injector.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/converters/gemini_to_sfs.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/converters/sfs_to_codex.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/converters/sfs_to_copilot.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/converters/sfs_to_gemini.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/daemon/__init__.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/daemon/config.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/judge/__init__.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/judge/extractor.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/judge/report.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/mcp/__init__.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/mcp/cloud_client.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/mcp/remote_server.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/mcp/search.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/mcp/server.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/security/__init__.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/security/secrets.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/__init__.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/auth/__init__.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/auth/keys.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/auth/rate_limit.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/db/__init__.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/db/engine.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/db/migrations/env.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/db/migrations/versions/001_initial_schema.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/db/migrations/versions/002_verification_and_tiers.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/db/migrations/versions/003_search_index.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/db/migrations/versions/004_bigint_tokens.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/db/migrations/versions/005_handoffs.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/email.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/errors.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/routes/__init__.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/routes/health.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/schemas/__init__.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/schemas/auth.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/schemas/errors.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/schemas/handoffs.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/schemas/sessions.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/storage/__init__.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/storage/base.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/storage/gcs.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/storage/local.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/server/storage/s3.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/session_id.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/spec/__init__.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/spec/convert_cc.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/spec/examples/complete/manifest.json +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/spec/examples/complete/messages.jsonl +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/spec/examples/complete/tools.json +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/spec/examples/complete/workspace.json +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/spec/examples/minimal/manifest.json +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/spec/examples/minimal/messages.jsonl +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/spec/examples/subagent/manifest.json +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/spec/examples/subagent/messages.jsonl +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/spec/examples/subagent/tools.json +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/spec/examples/subagent/workspace.json +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/spec/schemas/manifest.schema.json +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/spec/schemas/message.schema.json +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/spec/schemas/tools.schema.json +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/spec/schemas/workspace.schema.json +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/spec/validate.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/spec/version.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/store/__init__.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/store/index.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/store/local.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/sync/__init__.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/sync/archive.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/sync/client.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/utils/__init__.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/utils/title_utils.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/watchers/__init__.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/watchers/amp.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/watchers/base.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/watchers/claude_code.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/watchers/cline.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/watchers/codex.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/watchers/copilot.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/watchers/cursor.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/watchers/gemini.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/watchers/roo.py +0 -0
- {sessionfs-0.3.0 → sessionfs-0.3.2}/src/sessionfs/workspace/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sessionfs
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Capture, sync, and resume AI coding sessions across 8 tools — Claude Code, Codex, Gemini, Copilot, Cursor, Amp, Cline, and Roo Code.
|
|
5
5
|
Project-URL: Homepage, https://sessionfs.dev
|
|
6
6
|
Project-URL: Repository, https://github.com/sessionfs/sessionfs
|
|
@@ -36,6 +36,7 @@ Requires-Dist: alembic<2.0,>=1.13; extra == 'dev'
|
|
|
36
36
|
Requires-Dist: asyncpg<1.0,>=0.29; extra == 'dev'
|
|
37
37
|
Requires-Dist: boto3<2.0,>=1.34; extra == 'dev'
|
|
38
38
|
Requires-Dist: build<2.0,>=1.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: cryptography<44.0,>=42.0; extra == 'dev'
|
|
39
40
|
Requires-Dist: fastapi<1.0,>=0.110; extra == 'dev'
|
|
40
41
|
Requires-Dist: google-cloud-storage<3.0,>=2.14; extra == 'dev'
|
|
41
42
|
Requires-Dist: httpx<1.0,>=0.27; extra == 'dev'
|
|
@@ -52,6 +53,7 @@ Provides-Extra: server
|
|
|
52
53
|
Requires-Dist: alembic<2.0,>=1.13; extra == 'server'
|
|
53
54
|
Requires-Dist: asyncpg<1.0,>=0.29; extra == 'server'
|
|
54
55
|
Requires-Dist: boto3<2.0,>=1.34; extra == 'server'
|
|
56
|
+
Requires-Dist: cryptography<44.0,>=42.0; extra == 'server'
|
|
55
57
|
Requires-Dist: fastapi<1.0,>=0.110; extra == 'server'
|
|
56
58
|
Requires-Dist: google-cloud-storage<3.0,>=2.14; extra == 'server'
|
|
57
59
|
Requires-Dist: pydantic-settings<3.0,>=2.0; extra == 'server'
|
|
@@ -61,6 +63,10 @@ Requires-Dist: sqlalchemy[asyncio]<3.0,>=2.0; extra == 'server'
|
|
|
61
63
|
Requires-Dist: uvicorn[standard]<1.0,>=0.27; extra == 'server'
|
|
62
64
|
Description-Content-Type: text/markdown
|
|
63
65
|
|
|
66
|
+
<p align="center">
|
|
67
|
+
<img src="brand/logo-full.svg" alt="SessionFS" width="300">
|
|
68
|
+
</p>
|
|
69
|
+
|
|
64
70
|
# SessionFS
|
|
65
71
|
|
|
66
72
|
**Stop re-prompting. Start resuming.**
|
|
@@ -217,21 +223,22 @@ All file paths are relative to workspace root. Sessions are append-only — conf
|
|
|
217
223
|
|
|
218
224
|
## Status
|
|
219
225
|
|
|
220
|
-
**v0.3.
|
|
226
|
+
**v0.3.2 — Public Beta.** 673 tests passing.
|
|
221
227
|
|
|
222
228
|
What works today:
|
|
223
229
|
- Eight-tool session capture (Claude Code, Codex, Gemini, Cursor, Copilot CLI, Amp, Cline, Roo Code)
|
|
224
230
|
- Cross-tool resume between Claude Code, Codex, Gemini, and Copilot CLI
|
|
225
231
|
- Full-text search across all sessions (CLI + dashboard + API)
|
|
226
232
|
- MCP server — AI tools can search your past sessions for context
|
|
233
|
+
- LLM-as-a-Judge — audit sessions for hallucinations (BYOK, multi-provider, OpenRouter)
|
|
234
|
+
- Team handoff with email notification and smart workspace resolution
|
|
227
235
|
- Browse, inspect, export, fork, and checkpoint sessions
|
|
228
236
|
- Cloud sync with push/pull, email verification, and ETag conflict detection
|
|
229
237
|
- Self-hosted API server with auth, PostgreSQL, S3/GCS storage
|
|
230
|
-
- Web dashboard with session management and
|
|
231
|
-
- Team handoff with email notification
|
|
232
|
-
- 12 security controls including secret detection, path traversal protection, and audit logging
|
|
238
|
+
- Web dashboard with session management, search, handoffs, and audit
|
|
233
239
|
|
|
234
240
|
On the roadmap:
|
|
241
|
+
- Admin API and dashboard
|
|
235
242
|
- Stripe billing integration
|
|
236
243
|
- Session similarity and duplicate detection
|
|
237
244
|
- Cost analytics dashboard
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="brand/logo-full.svg" alt="SessionFS" width="300">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
# SessionFS
|
|
2
6
|
|
|
3
7
|
**Stop re-prompting. Start resuming.**
|
|
@@ -154,21 +158,22 @@ All file paths are relative to workspace root. Sessions are append-only — conf
|
|
|
154
158
|
|
|
155
159
|
## Status
|
|
156
160
|
|
|
157
|
-
**v0.3.
|
|
161
|
+
**v0.3.2 — Public Beta.** 673 tests passing.
|
|
158
162
|
|
|
159
163
|
What works today:
|
|
160
164
|
- Eight-tool session capture (Claude Code, Codex, Gemini, Cursor, Copilot CLI, Amp, Cline, Roo Code)
|
|
161
165
|
- Cross-tool resume between Claude Code, Codex, Gemini, and Copilot CLI
|
|
162
166
|
- Full-text search across all sessions (CLI + dashboard + API)
|
|
163
167
|
- MCP server — AI tools can search your past sessions for context
|
|
168
|
+
- LLM-as-a-Judge — audit sessions for hallucinations (BYOK, multi-provider, OpenRouter)
|
|
169
|
+
- Team handoff with email notification and smart workspace resolution
|
|
164
170
|
- Browse, inspect, export, fork, and checkpoint sessions
|
|
165
171
|
- Cloud sync with push/pull, email verification, and ETag conflict detection
|
|
166
172
|
- Self-hosted API server with auth, PostgreSQL, S3/GCS storage
|
|
167
|
-
- Web dashboard with session management and
|
|
168
|
-
- Team handoff with email notification
|
|
169
|
-
- 12 security controls including secret detection, path traversal protection, and audit logging
|
|
173
|
+
- Web dashboard with session management, search, handoffs, and audit
|
|
170
174
|
|
|
171
175
|
On the roadmap:
|
|
176
|
+
- Admin API and dashboard
|
|
172
177
|
- Stripe billing integration
|
|
173
178
|
- Session similarity and duplicate detection
|
|
174
179
|
- Cost analytics dashboard
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sessionfs"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.2"
|
|
8
8
|
description = "Capture, sync, and resume AI coding sessions across 8 tools — Claude Code, Codex, Gemini, Copilot, Cursor, Amp, Cline, and Roo Code."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -56,6 +56,7 @@ server = [
|
|
|
56
56
|
"pydantic-settings>=2.0,<3.0",
|
|
57
57
|
"PyJWT>=2.8,<3.0",
|
|
58
58
|
"google-cloud-storage>=2.14,<3.0",
|
|
59
|
+
"cryptography>=42.0,<44.0",
|
|
59
60
|
]
|
|
60
61
|
dev = [
|
|
61
62
|
"sessionfs[server]",
|
|
@@ -24,10 +24,11 @@ def audit(
|
|
|
24
24
|
session_id: str = typer.Argument(..., help="Session ID or prefix"),
|
|
25
25
|
model: str = typer.Option("claude-sonnet-4", "--model", help="Judge LLM model"),
|
|
26
26
|
api_key: Optional[str] = typer.Option(None, "--api-key", help="LLM API key"),
|
|
27
|
-
provider: Optional[str] = typer.Option(None, "--provider", help="LLM provider (anthropic, openai, google)"),
|
|
27
|
+
provider: Optional[str] = typer.Option(None, "--provider", help="LLM provider (anthropic, openai, google, openrouter)"),
|
|
28
28
|
consensus: bool = typer.Option(False, "--consensus", help="Run 3 passes, report only where 2+ agree"),
|
|
29
29
|
report_only: bool = typer.Option(False, "--report", help="Show existing report only"),
|
|
30
30
|
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
31
|
+
fmt: Optional[str] = typer.Option(None, "--format", help="Export format: json, markdown, csv (use with --report)"),
|
|
31
32
|
) -> None:
|
|
32
33
|
"""Audit a session for hallucinations using LLM-as-a-Judge."""
|
|
33
34
|
store = open_store()
|
|
@@ -35,7 +36,7 @@ def audit(
|
|
|
35
36
|
session_dir = get_session_dir_or_exit(store, session_id)
|
|
36
37
|
|
|
37
38
|
if report_only:
|
|
38
|
-
_show_existing_report(session_dir, session_id, json_output)
|
|
39
|
+
_show_existing_report(session_dir, session_id, json_output, fmt)
|
|
39
40
|
return
|
|
40
41
|
|
|
41
42
|
resolved_key = _resolve_api_key(api_key, model)
|
|
@@ -58,7 +59,10 @@ def audit(
|
|
|
58
59
|
err_console.print(f"[red]Audit failed: {exc}[/red]")
|
|
59
60
|
raise typer.Exit(1)
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
if fmt:
|
|
63
|
+
_export_report(report, fmt, session_dir)
|
|
64
|
+
else:
|
|
65
|
+
_display_report(report, json_output)
|
|
62
66
|
|
|
63
67
|
|
|
64
68
|
def _resolve_api_key(explicit_key: str | None, model: str) -> str | None:
|
|
@@ -137,7 +141,7 @@ async def _run_judge(
|
|
|
137
141
|
)
|
|
138
142
|
|
|
139
143
|
|
|
140
|
-
def _show_existing_report(session_dir, session_id: str, json_output: bool) -> None:
|
|
144
|
+
def _show_existing_report(session_dir, session_id: str, json_output: bool, fmt: str | None = None) -> None:
|
|
141
145
|
"""Load and display an existing audit report."""
|
|
142
146
|
from sessionfs.judge.report import load_report
|
|
143
147
|
|
|
@@ -149,7 +153,41 @@ def _show_existing_report(session_dir, session_id: str, json_output: bool) -> No
|
|
|
149
153
|
)
|
|
150
154
|
raise typer.Exit(1)
|
|
151
155
|
|
|
152
|
-
|
|
156
|
+
if fmt:
|
|
157
|
+
_export_report(report, fmt, session_dir)
|
|
158
|
+
else:
|
|
159
|
+
_display_report(report, json_output)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _export_report(report, fmt: str, session_dir=None) -> None:
|
|
163
|
+
"""Export report in the specified format."""
|
|
164
|
+
from sessionfs.judge.export import export_csv, export_json, export_markdown
|
|
165
|
+
|
|
166
|
+
fmt = fmt.lower()
|
|
167
|
+
|
|
168
|
+
if fmt == "markdown":
|
|
169
|
+
# Try to read session metadata for richer markdown export
|
|
170
|
+
session_title = ""
|
|
171
|
+
session_tool = ""
|
|
172
|
+
message_count = 0
|
|
173
|
+
if session_dir:
|
|
174
|
+
manifest_path = session_dir / "manifest.json"
|
|
175
|
+
if manifest_path.exists():
|
|
176
|
+
try:
|
|
177
|
+
manifest = json.loads(manifest_path.read_text())
|
|
178
|
+
session_title = manifest.get("title", "")
|
|
179
|
+
session_tool = manifest.get("source", {}).get("tool", "")
|
|
180
|
+
message_count = manifest.get("stats", {}).get("message_count", 0)
|
|
181
|
+
except (json.JSONDecodeError, OSError):
|
|
182
|
+
pass
|
|
183
|
+
console.print(export_markdown(report, session_title, session_tool, message_count))
|
|
184
|
+
elif fmt == "csv":
|
|
185
|
+
console.print(export_csv(report))
|
|
186
|
+
elif fmt == "json":
|
|
187
|
+
console.print(export_json(report))
|
|
188
|
+
else:
|
|
189
|
+
err_console.print(f"[red]Unknown format: {fmt}. Use json, markdown, or csv.[/red]")
|
|
190
|
+
raise typer.Exit(1)
|
|
153
191
|
|
|
154
192
|
|
|
155
193
|
def _display_report(report, json_output: bool) -> None:
|
|
@@ -194,13 +194,35 @@ def auth_signup(
|
|
|
194
194
|
def auth_status() -> None:
|
|
195
195
|
"""Show current authentication status."""
|
|
196
196
|
cfg = _load_sync_config()
|
|
197
|
-
if cfg["api_key"]:
|
|
198
|
-
console.print("[green]Authenticated[/green]")
|
|
199
|
-
console.print(f" Server: {cfg['api_url']}")
|
|
200
|
-
console.print(f" Sync enabled: {cfg['enabled']}")
|
|
201
|
-
console.print(f" API key: {cfg['api_key'][:8]}...")
|
|
202
|
-
else:
|
|
197
|
+
if not cfg["api_key"]:
|
|
203
198
|
console.print("[dim]Not authenticated. Run 'sfs auth login'.[/dim]")
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
console.print("[green]Authenticated[/green]")
|
|
202
|
+
console.print(f" Server: {cfg['api_url']}")
|
|
203
|
+
console.print(f" API key: {cfg['api_key'][:12]}...")
|
|
204
|
+
|
|
205
|
+
# Fetch user profile from server
|
|
206
|
+
try:
|
|
207
|
+
import httpx
|
|
208
|
+
|
|
209
|
+
resp = httpx.get(
|
|
210
|
+
f"{cfg['api_url']}/api/v1/auth/me",
|
|
211
|
+
headers={"Authorization": f"Bearer {cfg['api_key']}"},
|
|
212
|
+
timeout=10,
|
|
213
|
+
)
|
|
214
|
+
if resp.status_code == 200:
|
|
215
|
+
me = resp.json()
|
|
216
|
+
console.print(f" Email: [cyan]{me.get('email', '?')}[/cyan]")
|
|
217
|
+
console.print(f" Tier: {me.get('tier', 'free')}")
|
|
218
|
+
verified = me.get("email_verified", False)
|
|
219
|
+
console.print(f" Verified: {'[green]yes[/green]' if verified else '[yellow]no — check your inbox[/yellow]'}")
|
|
220
|
+
else:
|
|
221
|
+
console.print(f" [dim]Could not fetch profile (HTTP {resp.status_code})[/dim]")
|
|
222
|
+
except Exception:
|
|
223
|
+
console.print(" [dim]Could not reach server[/dim]")
|
|
224
|
+
|
|
225
|
+
console.print(f" Sync enabled: {cfg['enabled']}")
|
|
204
226
|
|
|
205
227
|
|
|
206
228
|
def push(
|
|
@@ -519,7 +541,7 @@ def handoff(
|
|
|
519
541
|
console.print(f"\n[green]Handoff created: {handoff_id}[/green]")
|
|
520
542
|
console.print(f" Recipient: {to}")
|
|
521
543
|
console.print(f" Expires: {data['expires_at']}")
|
|
522
|
-
console.print(
|
|
544
|
+
console.print("\nRecipient can pull with:")
|
|
523
545
|
console.print(f" sfs pull --handoff {handoff_id}")
|
|
524
546
|
else:
|
|
525
547
|
err_console.print(f"[red]Handoff failed: {resp.text}[/red]")
|
|
@@ -641,7 +663,7 @@ def pull_handoff(
|
|
|
641
663
|
f"\n[green]Pulled handoff session {session_id}[/green]\n"
|
|
642
664
|
f" Size: {len(pull_result.data):,} bytes"
|
|
643
665
|
)
|
|
644
|
-
console.print(
|
|
666
|
+
console.print("\nResume with:")
|
|
645
667
|
console.print(f" sfs resume {session_id} --in {tool}")
|
|
646
668
|
|
|
647
669
|
finally:
|
|
@@ -168,6 +168,7 @@ def list_sessions(
|
|
|
168
168
|
table.add_column("Msgs", justify="right", no_wrap=True, max_width=5)
|
|
169
169
|
table.add_column("Tokens", justify="right", no_wrap=True, max_width=6)
|
|
170
170
|
table.add_column("Age", no_wrap=True, max_width=8)
|
|
171
|
+
table.add_column("Audit", no_wrap=True, max_width=5)
|
|
171
172
|
table.add_column("Title", ratio=1)
|
|
172
173
|
|
|
173
174
|
for s in sessions:
|
|
@@ -178,6 +179,24 @@ def list_sessions(
|
|
|
178
179
|
from sessionfs.cli.titles import _truncate_at_word
|
|
179
180
|
title = _truncate_at_word(title, 60)
|
|
180
181
|
|
|
182
|
+
# Load audit report if it exists
|
|
183
|
+
audit_display = ""
|
|
184
|
+
session_dir = store.get_session_dir(s["session_id"])
|
|
185
|
+
if session_dir:
|
|
186
|
+
audit_path = session_dir / "audit_report.json"
|
|
187
|
+
if audit_path.exists():
|
|
188
|
+
try:
|
|
189
|
+
audit_data = json.loads(audit_path.read_text())
|
|
190
|
+
trust = audit_data.get("summary", {}).get("trust_score", 0)
|
|
191
|
+
if trust >= 0.8:
|
|
192
|
+
audit_display = f"[green]{trust:.0%}[/green]"
|
|
193
|
+
elif trust >= 0.5:
|
|
194
|
+
audit_display = f"[yellow]{trust:.0%}[/yellow]"
|
|
195
|
+
else:
|
|
196
|
+
audit_display = f"[red]{trust:.0%}[/red]"
|
|
197
|
+
except (json.JSONDecodeError, OSError):
|
|
198
|
+
pass
|
|
199
|
+
|
|
181
200
|
table.add_row(
|
|
182
201
|
s["session_id"][:8],
|
|
183
202
|
abbreviate_tool(s.get("source_tool")),
|
|
@@ -185,6 +204,7 @@ def list_sessions(
|
|
|
185
204
|
str(s.get("message_count", 0)),
|
|
186
205
|
format_token_count(total_tokens),
|
|
187
206
|
format_relative_time(s.get("updated_at") or s.get("created_at")),
|
|
207
|
+
audit_display,
|
|
188
208
|
title,
|
|
189
209
|
)
|
|
190
210
|
|
|
@@ -370,18 +370,21 @@ class Daemon:
|
|
|
370
370
|
|
|
371
371
|
try:
|
|
372
372
|
while self._running:
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
self._reload_requested
|
|
376
|
-
|
|
373
|
+
try:
|
|
374
|
+
# Handle config reload
|
|
375
|
+
if self._reload_requested:
|
|
376
|
+
self._reload_requested = False
|
|
377
|
+
self._reload_config()
|
|
377
378
|
|
|
378
|
-
|
|
379
|
-
|
|
379
|
+
for watcher in self.watchers:
|
|
380
|
+
watcher.process_events()
|
|
380
381
|
|
|
381
|
-
|
|
382
|
-
|
|
382
|
+
# Sync any pending sessions
|
|
383
|
+
self._syncer.maybe_sync()
|
|
383
384
|
|
|
384
|
-
|
|
385
|
+
self._update_status()
|
|
386
|
+
except Exception as exc:
|
|
387
|
+
logger.error("Error in daemon loop (continuing): %s", exc)
|
|
385
388
|
time.sleep(1.0)
|
|
386
389
|
finally:
|
|
387
390
|
logger.info("sfsd shutting down...")
|
|
@@ -47,10 +47,14 @@ class DaemonStatus(BaseModel):
|
|
|
47
47
|
|
|
48
48
|
def write_status(status: DaemonStatus, status_path: Path) -> None:
|
|
49
49
|
"""Write daemon status atomically (write to temp, rename)."""
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
try:
|
|
51
|
+
status.last_updated_at = datetime.now(timezone.utc).isoformat()
|
|
52
|
+
status_path.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
tmp_path = status_path.with_suffix(".tmp")
|
|
54
|
+
tmp_path.write_text(json.dumps(status.model_dump(), indent=2))
|
|
55
|
+
tmp_path.rename(status_path)
|
|
56
|
+
except OSError:
|
|
57
|
+
pass # Non-fatal — status is informational, don't crash the daemon
|
|
54
58
|
|
|
55
59
|
|
|
56
60
|
def read_status(status_path: Path) -> DaemonStatus | None:
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Export judge reports in multiple formats."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import csv
|
|
6
|
+
import io
|
|
7
|
+
import json
|
|
8
|
+
from dataclasses import asdict
|
|
9
|
+
|
|
10
|
+
from sessionfs.judge.report import JudgeReport
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def export_markdown(
|
|
14
|
+
report: JudgeReport,
|
|
15
|
+
session_title: str = "",
|
|
16
|
+
session_tool: str = "",
|
|
17
|
+
message_count: int = 0,
|
|
18
|
+
) -> str:
|
|
19
|
+
"""Export audit report as markdown."""
|
|
20
|
+
s = report.summary
|
|
21
|
+
lines: list[str] = []
|
|
22
|
+
|
|
23
|
+
lines.append(f"# Audit Report: {report.session_id}")
|
|
24
|
+
lines.append("")
|
|
25
|
+
|
|
26
|
+
if session_title:
|
|
27
|
+
lines.append(f"**Title:** {session_title}")
|
|
28
|
+
if session_tool:
|
|
29
|
+
lines.append(f"**Tool:** {session_tool}")
|
|
30
|
+
if message_count:
|
|
31
|
+
lines.append(f"**Messages:** {message_count}")
|
|
32
|
+
lines.append(f"**Model:** {report.model}")
|
|
33
|
+
lines.append(f"**Timestamp:** {report.timestamp}")
|
|
34
|
+
lines.append("")
|
|
35
|
+
|
|
36
|
+
# Summary
|
|
37
|
+
lines.append("## Summary")
|
|
38
|
+
lines.append("")
|
|
39
|
+
lines.append("| Metric | Value |")
|
|
40
|
+
lines.append("|--------|-------|")
|
|
41
|
+
lines.append(f"| Trust Score | {s.trust_score:.0%} |")
|
|
42
|
+
lines.append(f"| Total Claims | {s.total_claims} |")
|
|
43
|
+
lines.append(f"| Verified | {s.verified} |")
|
|
44
|
+
lines.append(f"| Unverified | {s.unverified} |")
|
|
45
|
+
lines.append(f"| Hallucinations | {s.hallucinations} |")
|
|
46
|
+
lines.append(f"| Major | {s.major_findings} |")
|
|
47
|
+
lines.append(f"| Moderate | {s.moderate_findings} |")
|
|
48
|
+
lines.append(f"| Minor | {s.minor_findings} |")
|
|
49
|
+
lines.append("")
|
|
50
|
+
|
|
51
|
+
# Findings
|
|
52
|
+
if report.findings:
|
|
53
|
+
lines.append("## Findings")
|
|
54
|
+
lines.append("")
|
|
55
|
+
lines.append("| Msg | Verdict | Severity | Claim | Evidence | Explanation |")
|
|
56
|
+
lines.append("|-----|---------|----------|-------|----------|-------------|")
|
|
57
|
+
for f in report.findings:
|
|
58
|
+
claim = f.claim.replace("|", "\\|")
|
|
59
|
+
evidence = f.evidence.replace("|", "\\|")
|
|
60
|
+
explanation = f.explanation.replace("|", "\\|")
|
|
61
|
+
lines.append(
|
|
62
|
+
f"| {f.message_index} | {f.verdict} | {f.severity} "
|
|
63
|
+
f"| {claim} | {evidence} | {explanation} |"
|
|
64
|
+
)
|
|
65
|
+
lines.append("")
|
|
66
|
+
else:
|
|
67
|
+
lines.append("No verifiable claims found in this session.")
|
|
68
|
+
lines.append("")
|
|
69
|
+
|
|
70
|
+
return "\n".join(lines)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def export_csv(report: JudgeReport) -> str:
|
|
74
|
+
"""Export as CSV: message_index,verdict,severity,claim,evidence,explanation"""
|
|
75
|
+
output = io.StringIO()
|
|
76
|
+
writer = csv.writer(output)
|
|
77
|
+
writer.writerow(["message_index", "verdict", "severity", "claim", "evidence", "explanation"])
|
|
78
|
+
for f in report.findings:
|
|
79
|
+
writer.writerow([f.message_index, f.verdict, f.severity, f.claim, f.evidence, f.explanation])
|
|
80
|
+
return output.getvalue()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def export_json(report: JudgeReport) -> str:
|
|
84
|
+
"""Export as indented JSON."""
|
|
85
|
+
return json.dumps(asdict(report), indent=2)
|
|
@@ -160,7 +160,7 @@ def _parse_judge_response(response: str, claims: list[Claim]) -> list[Finding]:
|
|
|
160
160
|
if text.startswith("```"):
|
|
161
161
|
lines = text.split("\n")
|
|
162
162
|
# Remove first and last fence lines
|
|
163
|
-
lines = [
|
|
163
|
+
lines = [ln for ln in lines if not ln.strip().startswith("```")]
|
|
164
164
|
text = "\n".join(lines)
|
|
165
165
|
|
|
166
166
|
try:
|
|
@@ -17,6 +17,7 @@ _PROVIDER_DETECT = [
|
|
|
17
17
|
("gpt-", "openai"),
|
|
18
18
|
("o1", "openai"),
|
|
19
19
|
("o3", "openai"),
|
|
20
|
+
("o4", "openai"),
|
|
20
21
|
("gemini-", "google"),
|
|
21
22
|
]
|
|
22
23
|
|
|
@@ -25,18 +26,25 @@ _OPENAI_URL = "https://api.openai.com/v1/chat/completions"
|
|
|
25
26
|
_GOOGLE_URL_TEMPLATE = (
|
|
26
27
|
"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent"
|
|
27
28
|
)
|
|
29
|
+
_OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions"
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
def _detect_provider(model: str) -> str:
|
|
31
|
-
"""Auto-detect provider from model name.
|
|
33
|
+
"""Auto-detect provider from model name.
|
|
34
|
+
|
|
35
|
+
Models containing "/" are routed to OpenRouter.
|
|
36
|
+
Unknown models fall back to OpenRouter.
|
|
37
|
+
"""
|
|
38
|
+
if "/" in model:
|
|
39
|
+
return "openrouter"
|
|
40
|
+
|
|
32
41
|
model_lower = model.lower()
|
|
33
42
|
for prefix, provider in _PROVIDER_DETECT:
|
|
34
43
|
if model_lower.startswith(prefix):
|
|
35
44
|
return provider
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
)
|
|
45
|
+
|
|
46
|
+
# Unknown model — fall back to OpenRouter
|
|
47
|
+
return "openrouter"
|
|
40
48
|
|
|
41
49
|
|
|
42
50
|
async def _call_anthropic(model: str, system: str, prompt: str, api_key: str, temperature: float = 0) -> str:
|
|
@@ -108,6 +116,31 @@ async def _call_google(model: str, system: str, prompt: str, api_key: str, tempe
|
|
|
108
116
|
return ""
|
|
109
117
|
|
|
110
118
|
|
|
119
|
+
async def _call_openrouter(model: str, system: str, prompt: str, api_key: str, temperature: float = 0) -> str:
|
|
120
|
+
"""Call the OpenRouter Chat Completions API."""
|
|
121
|
+
headers = {
|
|
122
|
+
"Authorization": f"Bearer {api_key}",
|
|
123
|
+
"Content-Type": "application/json",
|
|
124
|
+
"HTTP-Referer": "https://sessionfs.dev",
|
|
125
|
+
"X-Title": "SessionFS",
|
|
126
|
+
}
|
|
127
|
+
body = {
|
|
128
|
+
"model": model,
|
|
129
|
+
"messages": [
|
|
130
|
+
{"role": "system", "content": system},
|
|
131
|
+
{"role": "user", "content": prompt},
|
|
132
|
+
],
|
|
133
|
+
"max_tokens": 4096,
|
|
134
|
+
"temperature": temperature,
|
|
135
|
+
"response_format": {"type": "json_object"},
|
|
136
|
+
}
|
|
137
|
+
async with httpx.AsyncClient(timeout=120) as client:
|
|
138
|
+
resp = await client.post(_OPENROUTER_URL, json=body, headers=headers)
|
|
139
|
+
resp.raise_for_status()
|
|
140
|
+
data = resp.json()
|
|
141
|
+
return data["choices"][0]["message"]["content"]
|
|
142
|
+
|
|
143
|
+
|
|
111
144
|
async def call_llm(
|
|
112
145
|
model: str,
|
|
113
146
|
system: str,
|
|
@@ -122,6 +155,8 @@ async def call_llm(
|
|
|
122
155
|
- claude-* -> anthropic
|
|
123
156
|
- gpt-*, o1*, o3* -> openai
|
|
124
157
|
- gemini-* -> google
|
|
158
|
+
- models containing "/" -> openrouter
|
|
159
|
+
- unknown models -> openrouter (fallback)
|
|
125
160
|
|
|
126
161
|
Uses httpx directly — no SDK dependencies. The API key is used for
|
|
127
162
|
this single request only and is never persisted. Temperature defaults
|
|
@@ -139,5 +174,7 @@ async def call_llm(
|
|
|
139
174
|
return await _call_openai(model, system, prompt, api_key, temperature)
|
|
140
175
|
elif provider == "google":
|
|
141
176
|
return await _call_google(model, system, prompt, api_key, temperature)
|
|
177
|
+
elif provider == "openrouter":
|
|
178
|
+
return await _call_openrouter(model, system, prompt, api_key, temperature)
|
|
142
179
|
else:
|
|
143
180
|
raise ValueError(f"Unsupported provider: {provider}")
|
|
@@ -12,7 +12,7 @@ from sessionfs import __version__
|
|
|
12
12
|
from sessionfs.server.config import ServerConfig
|
|
13
13
|
from sessionfs.server.db.engine import close_engine, init_engine
|
|
14
14
|
from sessionfs.server.errors import register_exception_handlers
|
|
15
|
-
from sessionfs.server.routes import audit, auth, handoffs, health, sessions
|
|
15
|
+
from sessionfs.server.routes import admin, audit, auth, handoffs, health, sessions, settings
|
|
16
16
|
from sessionfs.server.storage.local import LocalBlobStore
|
|
17
17
|
|
|
18
18
|
|
|
@@ -81,6 +81,8 @@ def create_app(config: ServerConfig | None = None) -> FastAPI:
|
|
|
81
81
|
app.include_router(sessions.router)
|
|
82
82
|
app.include_router(handoffs.router)
|
|
83
83
|
app.include_router(audit.router)
|
|
84
|
+
app.include_router(settings.router)
|
|
85
|
+
app.include_router(admin.router)
|
|
84
86
|
|
|
85
87
|
# Serve dashboard static files if the dist directory exists.
|
|
86
88
|
# The dashboard is a React SPA — all non-API routes serve index.html.
|
|
@@ -70,6 +70,15 @@ async def get_current_user(
|
|
|
70
70
|
return user
|
|
71
71
|
|
|
72
72
|
|
|
73
|
+
async def require_admin(
|
|
74
|
+
user: User = Depends(get_current_user),
|
|
75
|
+
) -> User:
|
|
76
|
+
"""Require that the authenticated user has admin tier."""
|
|
77
|
+
if user.tier != "admin":
|
|
78
|
+
raise HTTPException(status_code=403, detail="Admin access required")
|
|
79
|
+
return user
|
|
80
|
+
|
|
81
|
+
|
|
73
82
|
async def require_verified_user(
|
|
74
83
|
user: User = Depends(get_current_user),
|
|
75
84
|
) -> User:
|
|
@@ -22,7 +22,8 @@ class ServerConfig(BaseSettings):
|
|
|
22
22
|
|
|
23
23
|
resend_api_key: str = ""
|
|
24
24
|
verification_secret: str = ""
|
|
25
|
-
|
|
25
|
+
max_sync_bytes_free: int = 52_428_800 # 50 MB — free tier
|
|
26
|
+
max_sync_bytes_paid: int = 314_572_800 # 300 MB — pro/team/enterprise/admin
|
|
26
27
|
retention_days_free: int = 14
|
|
27
28
|
|
|
28
29
|
host: str = "0.0.0.0"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Add user_judge_settings table for stored LLM API keys.
|
|
2
|
+
|
|
3
|
+
Revision ID: 006
|
|
4
|
+
Revises: 005
|
|
5
|
+
"""
|
|
6
|
+
from alembic import op
|
|
7
|
+
import sqlalchemy as sa
|
|
8
|
+
|
|
9
|
+
revision = "006"
|
|
10
|
+
down_revision = "005"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def upgrade():
|
|
14
|
+
op.create_table(
|
|
15
|
+
"user_judge_settings",
|
|
16
|
+
sa.Column("user_id", sa.String(36), sa.ForeignKey("users.id"), primary_key=True),
|
|
17
|
+
sa.Column("provider", sa.String(50), nullable=False),
|
|
18
|
+
sa.Column("model", sa.String(100), nullable=False),
|
|
19
|
+
sa.Column("encrypted_api_key", sa.Text(), nullable=False),
|
|
20
|
+
sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def downgrade():
|
|
25
|
+
op.drop_table("user_judge_settings")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Add admin_actions table for audit logging.
|
|
2
|
+
|
|
3
|
+
Revision ID: 007
|
|
4
|
+
Revises: 006
|
|
5
|
+
"""
|
|
6
|
+
from alembic import op
|
|
7
|
+
import sqlalchemy as sa
|
|
8
|
+
|
|
9
|
+
revision = "007"
|
|
10
|
+
down_revision = "006"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def upgrade():
|
|
14
|
+
op.create_table(
|
|
15
|
+
"admin_actions",
|
|
16
|
+
sa.Column("id", sa.String(36), primary_key=True),
|
|
17
|
+
sa.Column("admin_id", sa.String(36), sa.ForeignKey("users.id"), nullable=False),
|
|
18
|
+
sa.Column("action", sa.String(50), nullable=False),
|
|
19
|
+
sa.Column("target_type", sa.String(20), nullable=False),
|
|
20
|
+
sa.Column("target_id", sa.String(64), nullable=False),
|
|
21
|
+
sa.Column("details", sa.Text(), nullable=True),
|
|
22
|
+
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def downgrade():
|
|
27
|
+
op.drop_table("admin_actions")
|