corvustunnel 1.0.0__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.
Files changed (52) hide show
  1. corvustunnel-1.0.0/LICENSE +21 -0
  2. corvustunnel-1.0.0/PKG-INFO +180 -0
  3. corvustunnel-1.0.0/README.md +130 -0
  4. corvustunnel-1.0.0/audit/__init__.py +1 -0
  5. corvustunnel-1.0.0/audit/deep_logger.py +270 -0
  6. corvustunnel-1.0.0/audit/logger.py +119 -0
  7. corvustunnel-1.0.0/auth/__init__.py +1 -0
  8. corvustunnel-1.0.0/auth/bearer.py +266 -0
  9. corvustunnel-1.0.0/auth/dependencies.py +77 -0
  10. corvustunnel-1.0.0/boot.py +90 -0
  11. corvustunnel-1.0.0/cli.py +747 -0
  12. corvustunnel-1.0.0/config/__init__.py +1 -0
  13. corvustunnel-1.0.0/config/settings.py +104 -0
  14. corvustunnel-1.0.0/corvustunnel.egg-info/PKG-INFO +180 -0
  15. corvustunnel-1.0.0/corvustunnel.egg-info/SOURCES.txt +50 -0
  16. corvustunnel-1.0.0/corvustunnel.egg-info/dependency_links.txt +1 -0
  17. corvustunnel-1.0.0/corvustunnel.egg-info/entry_points.txt +2 -0
  18. corvustunnel-1.0.0/corvustunnel.egg-info/requires.txt +22 -0
  19. corvustunnel-1.0.0/corvustunnel.egg-info/top_level.txt +14 -0
  20. corvustunnel-1.0.0/crypto/__init__.py +3 -0
  21. corvustunnel-1.0.0/crypto/e2e.py +249 -0
  22. corvustunnel-1.0.0/executor/__init__.py +1 -0
  23. corvustunnel-1.0.0/executor/term_session.py +519 -0
  24. corvustunnel-1.0.0/history/__init__.py +8 -0
  25. corvustunnel-1.0.0/history/parsers.py +625 -0
  26. corvustunnel-1.0.0/history/service.py +227 -0
  27. corvustunnel-1.0.0/internal_app.py +40 -0
  28. corvustunnel-1.0.0/main.py +115 -0
  29. corvustunnel-1.0.0/middleware/__init__.py +1 -0
  30. corvustunnel-1.0.0/middleware/ip_ban.py +143 -0
  31. corvustunnel-1.0.0/middleware/rate_limit.py +40 -0
  32. corvustunnel-1.0.0/middleware/security_headers.py +53 -0
  33. corvustunnel-1.0.0/public_app.py +116 -0
  34. corvustunnel-1.0.0/pyproject.toml +108 -0
  35. corvustunnel-1.0.0/routers/__init__.py +1 -0
  36. corvustunnel-1.0.0/routers/internal.py +64 -0
  37. corvustunnel-1.0.0/routers/public.py +730 -0
  38. corvustunnel-1.0.0/setup.cfg +4 -0
  39. corvustunnel-1.0.0/static/assets/index-Bk9ltKwG.js +59 -0
  40. corvustunnel-1.0.0/static/assets/index-CGLFeCfu.css +1 -0
  41. corvustunnel-1.0.0/static/icons/icon-192.png +0 -0
  42. corvustunnel-1.0.0/static/icons/icon-512.png +0 -0
  43. corvustunnel-1.0.0/static/icons/icon.svg +37 -0
  44. corvustunnel-1.0.0/static/index.html +26 -0
  45. corvustunnel-1.0.0/static/manifest.json +31 -0
  46. corvustunnel-1.0.0/static/sw.js +136 -0
  47. corvustunnel-1.0.0/tests/test_api.py +277 -0
  48. corvustunnel-1.0.0/tests/test_auth.py +210 -0
  49. corvustunnel-1.0.0/tests/test_boot.py +104 -0
  50. corvustunnel-1.0.0/tests/test_cli.py +155 -0
  51. corvustunnel-1.0.0/tests/test_config.py +175 -0
  52. corvustunnel-1.0.0/tests/test_crypto.py +263 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 KALAI (kalai-tech.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,180 @@
1
+ Metadata-Version: 2.4
2
+ Name: corvustunnel
3
+ Version: 1.0.0
4
+ Summary: Control AI coding agents from your phone — Claude Code, Codex, and Antigravity. Self-hosted, E2E encrypted, open source.
5
+ Author-email: KALAI <hello@kalai-tech.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://corvustunnel.com
8
+ Project-URL: Documentation, https://corvustunnel.com
9
+ Project-URL: Repository, https://github.com/maliozturk/CorvusTunnel
10
+ Project-URL: Issues, https://github.com/maliozturk/CorvusTunnel/issues
11
+ Project-URL: Changelog, https://github.com/maliozturk/CorvusTunnel/blob/main/CHANGELOG.md
12
+ Keywords: ai,agent,terminal,remote,claude,codex,antigravity,coding,mobile,self-hosted,pwa
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Environment :: Console
15
+ Classifier: Environment :: Web Environment
16
+ Classifier: Framework :: FastAPI
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Operating System :: POSIX :: Linux
20
+ Classifier: Operating System :: MacOS
21
+ Classifier: Operating System :: Microsoft :: Windows
22
+ Classifier: Programming Language :: Python :: 3
23
+ Classifier: Programming Language :: Python :: 3.10
24
+ Classifier: Programming Language :: Python :: 3.11
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Programming Language :: Python :: 3.13
27
+ Classifier: Topic :: Software Development
28
+ Classifier: Topic :: System :: Networking
29
+ Requires-Python: >=3.10
30
+ Description-Content-Type: text/markdown
31
+ License-File: LICENSE
32
+ Requires-Dist: fastapi>=0.115.0
33
+ Requires-Dist: uvicorn[standard]>=0.30.0
34
+ Requires-Dist: httpx>=0.27.0
35
+ Requires-Dist: pydantic>=2.0
36
+ Requires-Dist: pydantic-settings>=2.0
37
+ Requires-Dist: pexpect>=4.9.0; sys_platform != "win32"
38
+ Requires-Dist: pywinpty>=2.0.0; sys_platform == "win32"
39
+ Requires-Dist: qrcode>=7.4.0
40
+ Requires-Dist: slowapi>=0.1.9
41
+ Requires-Dist: PyNaCl>=1.5.0
42
+ Requires-Dist: websockets>=12.0
43
+ Provides-Extra: dev
44
+ Requires-Dist: pytest>=7.0; extra == "dev"
45
+ Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
46
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
47
+ Requires-Dist: httpx; extra == "dev"
48
+ Requires-Dist: ruff>=0.4.0; extra == "dev"
49
+ Dynamic: license-file
50
+
51
+ # CorvusTunnel
52
+
53
+ **Control AI Agents from Your Phone**
54
+
55
+ Self-hosted remote control for Claude Code, Codex, and Antigravity. Free, open source, E2E encrypted.
56
+
57
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
58
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
59
+ [![PyPI](https://img.shields.io/pypi/v/corvustunnel.svg)](https://pypi.org/project/corvustunnel/)
60
+
61
+ ---
62
+
63
+ ## Quick Start
64
+
65
+ ```bash
66
+ # Install
67
+ pip install corvustunnel
68
+
69
+ # Start
70
+ corvustunnel start
71
+
72
+ # With a specific workspace
73
+ corvustunnel start --workspace /path/to/your/project
74
+ ```
75
+
76
+ Scan the QR code with your phone → you're connected.
77
+
78
+ ## Features
79
+
80
+ - **Multi-agent support** — Claude Code, Codex CLI, Antigravity
81
+ - **End-to-end encryption** — NaCl/libsodium, key exchange via QR code
82
+ - **QR code connect** — scan to connect, no manual URL typing
83
+ - **Real-time streaming** — WebSocket-based terminal I/O via xterm.js
84
+ - **Push notifications** — Web Push API alerts for agent events
85
+ - **PWA support** — install on your phone like a native app
86
+ - **Smart suggestions** — context-aware action chips (approve, reject, continue)
87
+ - **Command favorites** — pin frequently used prompts for one-tap access
88
+ - **Audit logging** — JSONL forensic logs of all sessions
89
+ - **Security hardened** — IP ban, rate limiting, body size limits, CORS, security headers
90
+ - **Self-hosted** — runs on your machine, your data stays with you
91
+ - **Unlimited** — no session limits, no cooldowns, no restrictions
92
+
93
+ ## How It Works
94
+
95
+ ```
96
+ Your Phone Your Computer
97
+ ┌─────────┐ ┌──────────────────┐
98
+ │ │ WebSocket │ CorvusTunnel │
99
+ │ Scan │ ◄──────────────► │ ├── FastAPI │
100
+ │ QR │ (E2E encrypted)│ ├── PTY/pexpect │
101
+ │ Code │ │ └── Agent │
102
+ │ │ │ (claude/ │
103
+ │ Send │ │ codex/ │
104
+ │ prompts│ │ agy) │
105
+ └─────────┘ └──────────────────┘
106
+ ▲ ▲
107
+ │ Cloudflare Relay │
108
+ └──────────────────────────────┘
109
+ (TLS + E2E encryption)
110
+ ```
111
+
112
+ ## Installation
113
+
114
+ ```bash
115
+ pip install corvustunnel
116
+ corvustunnel start
117
+ ```
118
+
119
+ ## Security
120
+
121
+ - **E2E Encryption**: All terminal I/O encrypted with NaCl SecretBox (XSalsa20-Poly1305)
122
+ - **Key Exchange**: X25519 Diffie-Hellman during QR code scanning
123
+ - **Auth**: One-time boot token → session token with IP binding
124
+ - **WebSocket**: Ticket-based auth (one-time, 30s TTL, IP-bound)
125
+ - **Rate Limiting**: Per-endpoint via slowapi
126
+ - **IP Auto-Ban**: After repeated auth failures
127
+
128
+ See [SECURITY.md](SECURITY.md) for full details and vulnerability reporting.
129
+
130
+ ## API
131
+
132
+ | Endpoint | Auth | Description |
133
+ |----------|------|-------------|
134
+ | `GET /api/health` | No | Health check |
135
+ | `POST /api/e2e/exchange` | No | E2E key exchange |
136
+ | `POST /api/claim` | Boot token | Exchange boot token for session token |
137
+ | `GET /api/browse` | Session | Directory browser |
138
+ | `GET /api/check-agents` | Session | List available AI agents |
139
+ | `POST /api/ws-ticket` | Session | Get WebSocket connection ticket |
140
+ | `WS /api/terminal/ws` | Ticket | Interactive terminal session |
141
+
142
+ Internal API (localhost:8001):
143
+
144
+ | Endpoint | Description |
145
+ |----------|-------------|
146
+ | `GET /audit` | View audit logs |
147
+ | `GET /deeplog` | View deep (plaintext) logs |
148
+ | `GET /terminal/status` | Terminal session status |
149
+
150
+ ## Environment Variables
151
+
152
+ | Variable | Default | Description |
153
+ |----------|---------|-------------|
154
+ | `AGENT_TOKEN` | Auto-generated | Bearer token for API auth |
155
+ | `ALLOWED_DIRS` | Home + CWD | Comma-separated allowed directories |
156
+ | `AUDIT_LOG_DIR` | `./logs` | Audit log directory |
157
+ | `PUBLIC_PORT` | `8000` | Public API port |
158
+ | `INTERNAL_PORT` | `8001` | Internal admin port |
159
+
160
+ ## Development
161
+
162
+ ```bash
163
+ git clone https://github.com/maliozturk/CorvusTunnel.git
164
+ cd CorvusTunnel
165
+ pip install -e ".[dev]"
166
+ AGENT_TOKEN=dev-token corvustunnel start --verbose
167
+ ```
168
+
169
+ ## Contributing
170
+
171
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
172
+
173
+ ## License
174
+
175
+ MIT — see [LICENSE](LICENSE)
176
+
177
+ ## Links
178
+
179
+ - **Website**: [corvustunnel.com](https://corvustunnel.com)
180
+ - **GitHub**: [github.com/maliozturk/CorvusTunnel](https://github.com/maliozturk/CorvusTunnel)
@@ -0,0 +1,130 @@
1
+ # CorvusTunnel
2
+
3
+ **Control AI Agents from Your Phone**
4
+
5
+ Self-hosted remote control for Claude Code, Codex, and Antigravity. Free, open source, E2E encrypted.
6
+
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
9
+ [![PyPI](https://img.shields.io/pypi/v/corvustunnel.svg)](https://pypi.org/project/corvustunnel/)
10
+
11
+ ---
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ # Install
17
+ pip install corvustunnel
18
+
19
+ # Start
20
+ corvustunnel start
21
+
22
+ # With a specific workspace
23
+ corvustunnel start --workspace /path/to/your/project
24
+ ```
25
+
26
+ Scan the QR code with your phone → you're connected.
27
+
28
+ ## Features
29
+
30
+ - **Multi-agent support** — Claude Code, Codex CLI, Antigravity
31
+ - **End-to-end encryption** — NaCl/libsodium, key exchange via QR code
32
+ - **QR code connect** — scan to connect, no manual URL typing
33
+ - **Real-time streaming** — WebSocket-based terminal I/O via xterm.js
34
+ - **Push notifications** — Web Push API alerts for agent events
35
+ - **PWA support** — install on your phone like a native app
36
+ - **Smart suggestions** — context-aware action chips (approve, reject, continue)
37
+ - **Command favorites** — pin frequently used prompts for one-tap access
38
+ - **Audit logging** — JSONL forensic logs of all sessions
39
+ - **Security hardened** — IP ban, rate limiting, body size limits, CORS, security headers
40
+ - **Self-hosted** — runs on your machine, your data stays with you
41
+ - **Unlimited** — no session limits, no cooldowns, no restrictions
42
+
43
+ ## How It Works
44
+
45
+ ```
46
+ Your Phone Your Computer
47
+ ┌─────────┐ ┌──────────────────┐
48
+ │ │ WebSocket │ CorvusTunnel │
49
+ │ Scan │ ◄──────────────► │ ├── FastAPI │
50
+ │ QR │ (E2E encrypted)│ ├── PTY/pexpect │
51
+ │ Code │ │ └── Agent │
52
+ │ │ │ (claude/ │
53
+ │ Send │ │ codex/ │
54
+ │ prompts│ │ agy) │
55
+ └─────────┘ └──────────────────┘
56
+ ▲ ▲
57
+ │ Cloudflare Relay │
58
+ └──────────────────────────────┘
59
+ (TLS + E2E encryption)
60
+ ```
61
+
62
+ ## Installation
63
+
64
+ ```bash
65
+ pip install corvustunnel
66
+ corvustunnel start
67
+ ```
68
+
69
+ ## Security
70
+
71
+ - **E2E Encryption**: All terminal I/O encrypted with NaCl SecretBox (XSalsa20-Poly1305)
72
+ - **Key Exchange**: X25519 Diffie-Hellman during QR code scanning
73
+ - **Auth**: One-time boot token → session token with IP binding
74
+ - **WebSocket**: Ticket-based auth (one-time, 30s TTL, IP-bound)
75
+ - **Rate Limiting**: Per-endpoint via slowapi
76
+ - **IP Auto-Ban**: After repeated auth failures
77
+
78
+ See [SECURITY.md](SECURITY.md) for full details and vulnerability reporting.
79
+
80
+ ## API
81
+
82
+ | Endpoint | Auth | Description |
83
+ |----------|------|-------------|
84
+ | `GET /api/health` | No | Health check |
85
+ | `POST /api/e2e/exchange` | No | E2E key exchange |
86
+ | `POST /api/claim` | Boot token | Exchange boot token for session token |
87
+ | `GET /api/browse` | Session | Directory browser |
88
+ | `GET /api/check-agents` | Session | List available AI agents |
89
+ | `POST /api/ws-ticket` | Session | Get WebSocket connection ticket |
90
+ | `WS /api/terminal/ws` | Ticket | Interactive terminal session |
91
+
92
+ Internal API (localhost:8001):
93
+
94
+ | Endpoint | Description |
95
+ |----------|-------------|
96
+ | `GET /audit` | View audit logs |
97
+ | `GET /deeplog` | View deep (plaintext) logs |
98
+ | `GET /terminal/status` | Terminal session status |
99
+
100
+ ## Environment Variables
101
+
102
+ | Variable | Default | Description |
103
+ |----------|---------|-------------|
104
+ | `AGENT_TOKEN` | Auto-generated | Bearer token for API auth |
105
+ | `ALLOWED_DIRS` | Home + CWD | Comma-separated allowed directories |
106
+ | `AUDIT_LOG_DIR` | `./logs` | Audit log directory |
107
+ | `PUBLIC_PORT` | `8000` | Public API port |
108
+ | `INTERNAL_PORT` | `8001` | Internal admin port |
109
+
110
+ ## Development
111
+
112
+ ```bash
113
+ git clone https://github.com/maliozturk/CorvusTunnel.git
114
+ cd CorvusTunnel
115
+ pip install -e ".[dev]"
116
+ AGENT_TOKEN=dev-token corvustunnel start --verbose
117
+ ```
118
+
119
+ ## Contributing
120
+
121
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
122
+
123
+ ## License
124
+
125
+ MIT — see [LICENSE](LICENSE)
126
+
127
+ ## Links
128
+
129
+ - **Website**: [corvustunnel.com](https://corvustunnel.com)
130
+ - **GitHub**: [github.com/maliozturk/CorvusTunnel](https://github.com/maliozturk/CorvusTunnel)
@@ -0,0 +1 @@
1
+ """CorvusTunnel audit package."""
@@ -0,0 +1,270 @@
1
+ """
2
+ CorvusTunnel Deep Logger — Full forensic trail of every action.
3
+
4
+ Unlike the audit logger (which hashes prompts), the deep logger records
5
+ everything in plaintext: prompts, full outputs, paths browsed, auth
6
+ events, errors, and timing. Files are stored locally with daily
7
+ rotation and are NEVER exposed through the public API.
8
+
9
+ Usage:
10
+ from audit.deep_logger import deep_log
11
+
12
+ deep_log("prompt_submitted", prompt="list files", job_id="abc123",
13
+ client_ip="198.41.200.1", work_dir="C:\\projects\\demo")
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import platform
21
+ import sys
22
+ import threading
23
+ import time
24
+ import traceback
25
+ from datetime import datetime, timezone
26
+ from functools import lru_cache
27
+ from pathlib import Path
28
+ from typing import Any
29
+
30
+
31
+ class DeepLogger:
32
+ """Append-only JSONL deep logger with daily rotation and thread safety.
33
+
34
+ Every event includes:
35
+ - Monotonic and wall-clock timestamps
36
+ - Event category and action name
37
+ - Full payload (prompts, outputs, paths — never hashed)
38
+ - System context on first boot event
39
+
40
+ Files are named ``deep_YYYY-MM-DD.jsonl`` inside the log directory.
41
+ """
42
+
43
+ _BOOT_LOGGED = False
44
+
45
+ def __init__(self, log_dir: str = "./logs"):
46
+ self._log_dir = Path(log_dir) / "deep"
47
+ self._log_dir.mkdir(parents=True, exist_ok=True)
48
+ self._lock = threading.Lock()
49
+ self._seq = 0 # Monotonic event counter
50
+
51
+ if not DeepLogger._BOOT_LOGGED:
52
+ DeepLogger._BOOT_LOGGED = True
53
+ self._log_boot()
54
+
55
+ # ── Public API ────────────────────────────────────────────────────
56
+
57
+ def log(
58
+ self,
59
+ action: str,
60
+ *,
61
+ category: str = "general",
62
+ **kwargs: Any,
63
+ ) -> None:
64
+ """Append an event to the deep log.
65
+
66
+ Args:
67
+ action: Short verb, e.g. ``prompt_submitted``, ``job_approved``
68
+ category: Grouping tag — ``auth``, ``job``, ``browse``, ``exec``, ``system``
69
+ **kwargs: Arbitrary payload (prompts, outputs, paths, IPs, etc.)
70
+ """
71
+ with self._lock:
72
+ self._seq += 1
73
+ seq = self._seq
74
+
75
+ event: dict[str, Any] = {
76
+ "seq": seq,
77
+ "ts": time.time(),
78
+ "iso": datetime.now(timezone.utc).isoformat(),
79
+ "category": category,
80
+ "action": action,
81
+ }
82
+ # Flatten payload — skip None values to keep logs clean
83
+ for k, v in kwargs.items():
84
+ if v is not None:
85
+ event[k] = v
86
+
87
+ self._write(event)
88
+
89
+ # ── Convenience shortcuts ─────────────────────────────────────────
90
+
91
+ def auth_success(self, client_ip: str) -> None:
92
+ self.log("auth_success", category="auth", client_ip=client_ip)
93
+
94
+ def auth_fail(self, client_ip: str, reason: str = "") -> None:
95
+ self.log("auth_fail", category="auth", client_ip=client_ip, reason=reason)
96
+
97
+ def prompt_submitted(
98
+ self,
99
+ job_id: str,
100
+ prompt: str,
101
+ target: str,
102
+ client_ip: str,
103
+ work_dir: str | None = None,
104
+ require_approval: bool = True,
105
+ is_shell: bool = False,
106
+ ) -> None:
107
+ self.log(
108
+ "prompt_submitted",
109
+ category="job",
110
+ job_id=job_id,
111
+ prompt=prompt,
112
+ target=target,
113
+ client_ip=client_ip,
114
+ work_dir=work_dir,
115
+ require_approval=require_approval,
116
+ is_shell=is_shell,
117
+ )
118
+
119
+ def job_approved(self, job_id: str) -> None:
120
+ self.log("job_approved", category="job", job_id=job_id)
121
+
122
+ def job_rejected(self, job_id: str) -> None:
123
+ self.log("job_rejected", category="job", job_id=job_id)
124
+
125
+ def exec_start(
126
+ self, job_id: str, command: str, work_dir: str, mode: str = "agy"
127
+ ) -> None:
128
+ self.log(
129
+ "exec_start",
130
+ category="exec",
131
+ job_id=job_id,
132
+ command=command,
133
+ work_dir=work_dir,
134
+ mode=mode,
135
+ )
136
+
137
+ def exec_done(
138
+ self,
139
+ job_id: str,
140
+ return_code: int | None,
141
+ duration_s: float,
142
+ stdout: str | None = None,
143
+ stderr: str | None = None,
144
+ error: str | None = None,
145
+ timed_out: bool = False,
146
+ ) -> None:
147
+ self.log(
148
+ "exec_done",
149
+ category="exec",
150
+ job_id=job_id,
151
+ return_code=return_code,
152
+ duration_s=round(duration_s, 3),
153
+ stdout=stdout,
154
+ stderr=stderr,
155
+ error=error,
156
+ timed_out=timed_out,
157
+ )
158
+
159
+ def browse(self, client_ip: str, path: str | None, result_count: int) -> None:
160
+ self.log(
161
+ "browse",
162
+ category="browse",
163
+ client_ip=client_ip,
164
+ path=path,
165
+ result_count=result_count,
166
+ )
167
+
168
+ def browse_blocked(self, client_ip: str, path: str, reason: str) -> None:
169
+ self.log(
170
+ "browse_blocked",
171
+ category="browse",
172
+ client_ip=client_ip,
173
+ path=path,
174
+ reason=reason,
175
+ )
176
+
177
+ def error(self, action: str, error: str, **ctx: Any) -> None:
178
+ self.log(
179
+ action,
180
+ category="error",
181
+ error=error,
182
+ traceback=traceback.format_exc() if sys.exc_info()[0] else None,
183
+ **ctx,
184
+ )
185
+
186
+ # ── Reading (for the internal admin endpoint) ─────────────────────
187
+
188
+ def read_recent(self, limit: int = 100, date: str | None = None) -> list[dict]:
189
+ """Read recent deep log entries.
190
+
191
+ Args:
192
+ limit: Max entries to return (newest first)
193
+ date: ``YYYY-MM-DD`` string; defaults to today
194
+ """
195
+ if date is None:
196
+ date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
197
+ log_path = self._log_dir / f"deep_{date}.jsonl"
198
+ if not log_path.exists():
199
+ return []
200
+
201
+ entries: list[dict] = []
202
+ try:
203
+ with open(log_path, "r", encoding="utf-8") as f:
204
+ for line in f:
205
+ line = line.strip()
206
+ if line:
207
+ try:
208
+ entries.append(json.loads(line))
209
+ except json.JSONDecodeError:
210
+ continue
211
+ except OSError:
212
+ return []
213
+
214
+ return entries[-limit:]
215
+
216
+ def list_dates(self) -> list[str]:
217
+ """Return available log dates (newest first)."""
218
+ dates = []
219
+ for p in sorted(self._log_dir.glob("deep_*.jsonl"), reverse=True):
220
+ dates.append(p.stem.replace("deep_", ""))
221
+ return dates
222
+
223
+ # ── Internal ──────────────────────────────────────────────────────
224
+
225
+ def _log_boot(self) -> None:
226
+ """Log a boot event with system info on first init."""
227
+ self.log(
228
+ "server_boot",
229
+ category="system",
230
+ python=sys.version,
231
+ platform=platform.platform(),
232
+ pid=os.getpid(),
233
+ cwd=os.getcwd(),
234
+ )
235
+
236
+ def _get_log_path(self) -> Path:
237
+ today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
238
+ return self._log_dir / f"deep_{today}.jsonl"
239
+
240
+ def _write(self, event: dict[str, Any]) -> None:
241
+ log_path = self._get_log_path()
242
+ try:
243
+ with self._lock:
244
+ with open(log_path, "a", encoding="utf-8") as f:
245
+ f.write(json.dumps(event, ensure_ascii=False, default=str) + "\n")
246
+ except OSError:
247
+ print(
248
+ f"[DEEP LOG FALLBACK] {json.dumps(event, default=str)}",
249
+ file=sys.stderr,
250
+ )
251
+
252
+
253
+ # ── Singleton ────────────────────────────────────────────────────────
254
+
255
+ @lru_cache(maxsize=1)
256
+ def get_deep_logger() -> DeepLogger:
257
+ """Return the singleton DeepLogger.
258
+
259
+ Logs are stored at ``~/.corvustunnel/logs/deep/`` by default —
260
+ outside any ALLOWED_DIRS path so they are unreachable from the tunnel.
261
+ """
262
+ from config.settings import get_settings
263
+
264
+ settings = get_settings()
265
+ return DeepLogger(log_dir=settings.resolved_deep_log_dir)
266
+
267
+
268
+ def deep_log(action: str, *, category: str = "general", **kwargs: Any) -> None:
269
+ """Module-level shortcut for quick logging."""
270
+ get_deep_logger().log(action, category=category, **kwargs)