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.
- corvustunnel-1.0.0/LICENSE +21 -0
- corvustunnel-1.0.0/PKG-INFO +180 -0
- corvustunnel-1.0.0/README.md +130 -0
- corvustunnel-1.0.0/audit/__init__.py +1 -0
- corvustunnel-1.0.0/audit/deep_logger.py +270 -0
- corvustunnel-1.0.0/audit/logger.py +119 -0
- corvustunnel-1.0.0/auth/__init__.py +1 -0
- corvustunnel-1.0.0/auth/bearer.py +266 -0
- corvustunnel-1.0.0/auth/dependencies.py +77 -0
- corvustunnel-1.0.0/boot.py +90 -0
- corvustunnel-1.0.0/cli.py +747 -0
- corvustunnel-1.0.0/config/__init__.py +1 -0
- corvustunnel-1.0.0/config/settings.py +104 -0
- corvustunnel-1.0.0/corvustunnel.egg-info/PKG-INFO +180 -0
- corvustunnel-1.0.0/corvustunnel.egg-info/SOURCES.txt +50 -0
- corvustunnel-1.0.0/corvustunnel.egg-info/dependency_links.txt +1 -0
- corvustunnel-1.0.0/corvustunnel.egg-info/entry_points.txt +2 -0
- corvustunnel-1.0.0/corvustunnel.egg-info/requires.txt +22 -0
- corvustunnel-1.0.0/corvustunnel.egg-info/top_level.txt +14 -0
- corvustunnel-1.0.0/crypto/__init__.py +3 -0
- corvustunnel-1.0.0/crypto/e2e.py +249 -0
- corvustunnel-1.0.0/executor/__init__.py +1 -0
- corvustunnel-1.0.0/executor/term_session.py +519 -0
- corvustunnel-1.0.0/history/__init__.py +8 -0
- corvustunnel-1.0.0/history/parsers.py +625 -0
- corvustunnel-1.0.0/history/service.py +227 -0
- corvustunnel-1.0.0/internal_app.py +40 -0
- corvustunnel-1.0.0/main.py +115 -0
- corvustunnel-1.0.0/middleware/__init__.py +1 -0
- corvustunnel-1.0.0/middleware/ip_ban.py +143 -0
- corvustunnel-1.0.0/middleware/rate_limit.py +40 -0
- corvustunnel-1.0.0/middleware/security_headers.py +53 -0
- corvustunnel-1.0.0/public_app.py +116 -0
- corvustunnel-1.0.0/pyproject.toml +108 -0
- corvustunnel-1.0.0/routers/__init__.py +1 -0
- corvustunnel-1.0.0/routers/internal.py +64 -0
- corvustunnel-1.0.0/routers/public.py +730 -0
- corvustunnel-1.0.0/setup.cfg +4 -0
- corvustunnel-1.0.0/static/assets/index-Bk9ltKwG.js +59 -0
- corvustunnel-1.0.0/static/assets/index-CGLFeCfu.css +1 -0
- corvustunnel-1.0.0/static/icons/icon-192.png +0 -0
- corvustunnel-1.0.0/static/icons/icon-512.png +0 -0
- corvustunnel-1.0.0/static/icons/icon.svg +37 -0
- corvustunnel-1.0.0/static/index.html +26 -0
- corvustunnel-1.0.0/static/manifest.json +31 -0
- corvustunnel-1.0.0/static/sw.js +136 -0
- corvustunnel-1.0.0/tests/test_api.py +277 -0
- corvustunnel-1.0.0/tests/test_auth.py +210 -0
- corvustunnel-1.0.0/tests/test_boot.py +104 -0
- corvustunnel-1.0.0/tests/test_cli.py +155 -0
- corvustunnel-1.0.0/tests/test_config.py +175 -0
- 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)
|
|
58
|
+
[](https://www.python.org/downloads/)
|
|
59
|
+
[](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)
|
|
8
|
+
[](https://www.python.org/downloads/)
|
|
9
|
+
[](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)
|