openadapt-desktop 0.2.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.
- engine/__init__.py +24 -0
- engine/__main__.py +5 -0
- engine/audit.py +125 -0
- engine/backends/__init__.py +12 -0
- engine/backends/federated.py +117 -0
- engine/backends/huggingface.py +160 -0
- engine/backends/protocol.py +134 -0
- engine/backends/s3.py +173 -0
- engine/backends/wormhole.py +99 -0
- engine/cli.py +482 -0
- engine/config.py +127 -0
- engine/controller.py +309 -0
- engine/db.py +205 -0
- engine/ipc.py +134 -0
- engine/main.py +82 -0
- engine/monitor.py +148 -0
- engine/review.py +199 -0
- engine/scrubber.py +234 -0
- engine/storage_manager.py +277 -0
- engine/upload_manager.py +187 -0
- openadapt_desktop-0.2.0.dist-info/METADATA +44 -0
- openadapt_desktop-0.2.0.dist-info/RECORD +25 -0
- openadapt_desktop-0.2.0.dist-info/WHEEL +4 -0
- openadapt_desktop-0.2.0.dist-info/entry_points.txt +2 -0
- openadapt_desktop-0.2.0.dist-info/licenses/LICENSE +21 -0
engine/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""OpenAdapt Desktop Engine -- Python sidecar for the Tauri shell.
|
|
2
|
+
|
|
3
|
+
This package implements the recording engine, PII scrubbing, storage management,
|
|
4
|
+
upload backends, and health monitoring. It runs as a standalone process that
|
|
5
|
+
communicates with the Tauri shell via JSON-over-stdin/stdout IPC.
|
|
6
|
+
|
|
7
|
+
Architecture:
|
|
8
|
+
Tauri Shell (Rust + WebView)
|
|
9
|
+
| IPC (JSON over stdin/stdout)
|
|
10
|
+
v
|
|
11
|
+
Python Engine (this package)
|
|
12
|
+
+-- controller.py Recording start/stop/pause
|
|
13
|
+
+-- ipc.py JSON line protocol handler
|
|
14
|
+
+-- storage_manager.py Storage tiers, cleanup, index
|
|
15
|
+
+-- upload_manager.py Multi-backend upload with queue
|
|
16
|
+
+-- scrubber.py PII scrubbing orchestration
|
|
17
|
+
+-- review.py Upload review state machine
|
|
18
|
+
+-- config.py Settings (pydantic-settings)
|
|
19
|
+
+-- monitor.py Health monitoring (memory, disk, watchdog)
|
|
20
|
+
+-- audit.py Network audit logging
|
|
21
|
+
+-- backends/ Storage backend plugins
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
__version__ = "0.2.0"
|
engine/__main__.py
ADDED
engine/audit.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Network audit logging -- append-only JSONL log of all outbound network activity.
|
|
2
|
+
|
|
3
|
+
Every outbound network request is logged to an append-only JSONL file at
|
|
4
|
+
~/.openadapt/audit.jsonl. This enables enterprise IT to:
|
|
5
|
+
- grep the audit log for unexpected destinations
|
|
6
|
+
- set firewall rules allowing only their S3 endpoint
|
|
7
|
+
- use a network proxy to independently verify traffic
|
|
8
|
+
- run OPENADAPT_STORAGE_MODE=air-gapped and confirm zero outbound traffic
|
|
9
|
+
|
|
10
|
+
Log format (from design doc Section 7.9):
|
|
11
|
+
{"ts":"2026-03-02T10:00:01Z","event":"startup","storage_mode":"enterprise","backends":["s3"],"excluded":["hf","r2","ipfs"]}
|
|
12
|
+
{"ts":"2026-03-02T10:05:00Z","event":"upload_start","backend":"s3","dest":"s3://bucket/rec.tar.zst","size_mb":142}
|
|
13
|
+
{"ts":"2026-03-02T10:05:32Z","event":"upload_complete","backend":"s3","dest":"s3://bucket/rec.tar.zst","bytes_sent":148897280}
|
|
14
|
+
|
|
15
|
+
All significant actions are logged:
|
|
16
|
+
- Recording started/stopped
|
|
17
|
+
- Upload initiated/completed/failed (with destination URL)
|
|
18
|
+
- Settings changed
|
|
19
|
+
- Data deleted (local or cloud)
|
|
20
|
+
- Update installed
|
|
21
|
+
- Permissions granted/denied
|
|
22
|
+
- Every outbound network request (destination, size, response code)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AuditLogger:
|
|
34
|
+
"""Append-only audit logger for network and significant events.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
log_path: Path to the audit.jsonl file.
|
|
38
|
+
enabled: Whether audit logging is active.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, log_path: Path, enabled: bool = True) -> None:
|
|
42
|
+
self.log_path = log_path
|
|
43
|
+
self.enabled = enabled
|
|
44
|
+
|
|
45
|
+
def log(self, event: str, **data: Any) -> None:
|
|
46
|
+
"""Write an audit log entry.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
event: Event type (e.g., "startup", "upload_start", "settings_changed").
|
|
50
|
+
**data: Additional event data as keyword arguments.
|
|
51
|
+
"""
|
|
52
|
+
if not self.enabled:
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
entry = {
|
|
56
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
57
|
+
"event": event,
|
|
58
|
+
**data,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
self.log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
line = json.dumps(entry)
|
|
63
|
+
with open(self.log_path, "a") as f:
|
|
64
|
+
f.write(line + "\n")
|
|
65
|
+
|
|
66
|
+
def log_startup(self, storage_mode: str, active_backends: list[str]) -> None:
|
|
67
|
+
"""Log engine startup with active configuration.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
storage_mode: Current storage mode (air-gapped, enterprise, community, full).
|
|
71
|
+
active_backends: List of active backend names.
|
|
72
|
+
"""
|
|
73
|
+
all_backends = {"s3", "r2", "hf", "wormhole", "federated"}
|
|
74
|
+
excluded = sorted(all_backends - set(active_backends))
|
|
75
|
+
self.log(
|
|
76
|
+
"startup",
|
|
77
|
+
storage_mode=storage_mode,
|
|
78
|
+
backends=active_backends,
|
|
79
|
+
excluded=excluded,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def log_upload_start(self, backend: str, dest: str, size_bytes: int) -> None:
|
|
83
|
+
"""Log the start of an upload operation.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
backend: Storage backend name.
|
|
87
|
+
dest: Destination URI (e.g., "s3://bucket/key").
|
|
88
|
+
size_bytes: Size of the data being uploaded.
|
|
89
|
+
"""
|
|
90
|
+
self.log(
|
|
91
|
+
"upload_start",
|
|
92
|
+
backend=backend,
|
|
93
|
+
dest=dest,
|
|
94
|
+
size_mb=round(size_bytes / (1024 * 1024), 1),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def log_upload_complete(self, backend: str, dest: str, bytes_sent: int) -> None:
|
|
98
|
+
"""Log the completion of an upload operation.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
backend: Storage backend name.
|
|
102
|
+
dest: Destination URI.
|
|
103
|
+
bytes_sent: Total bytes sent.
|
|
104
|
+
"""
|
|
105
|
+
self.log(
|
|
106
|
+
"upload_complete",
|
|
107
|
+
backend=backend,
|
|
108
|
+
dest=dest,
|
|
109
|
+
bytes_sent=bytes_sent,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def log_upload_failed(self, backend: str, dest: str, error: str) -> None:
|
|
113
|
+
"""Log a failed upload operation.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
backend: Storage backend name.
|
|
117
|
+
dest: Destination URI.
|
|
118
|
+
error: Error message.
|
|
119
|
+
"""
|
|
120
|
+
self.log(
|
|
121
|
+
"upload_failed",
|
|
122
|
+
backend=backend,
|
|
123
|
+
dest=dest,
|
|
124
|
+
error=error,
|
|
125
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Storage backend plugins for OpenAdapt Desktop.
|
|
2
|
+
|
|
3
|
+
Each backend implements the StorageBackend protocol defined in protocol.py.
|
|
4
|
+
Backends are loaded conditionally based on build-time feature flags and
|
|
5
|
+
runtime configuration.
|
|
6
|
+
|
|
7
|
+
Available backends:
|
|
8
|
+
s3 S3-compatible (AWS S3, Cloudflare R2, MinIO)
|
|
9
|
+
huggingface HuggingFace Hub (public/private datasets)
|
|
10
|
+
wormhole Magic Wormhole (P2P ephemeral transfer)
|
|
11
|
+
federated Federated learning gradient upload (Flower)
|
|
12
|
+
"""
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Federated learning gradient upload backend -- Flower-based federated learning.
|
|
2
|
+
|
|
3
|
+
Federated learning solves the tension between privacy and model improvement:
|
|
4
|
+
- Enterprise users get model improvements WITHOUT sharing any data
|
|
5
|
+
- Community users get model improvements WITHOUT seeing enterprise screens
|
|
6
|
+
- OpenAdapt trains better models without centralized data collection
|
|
7
|
+
|
|
8
|
+
What gets shared:
|
|
9
|
+
- Model weight deltas (compressed, ~1-10 MB per round)
|
|
10
|
+
- Participation metadata (sample count, training loss)
|
|
11
|
+
- Differentially private gradients (optionally)
|
|
12
|
+
|
|
13
|
+
What does NOT get shared:
|
|
14
|
+
- Raw screenshots
|
|
15
|
+
- Keyboard/mouse events
|
|
16
|
+
- Any recording content
|
|
17
|
+
|
|
18
|
+
Phasing (from design doc Section 9.8):
|
|
19
|
+
v0.1-v0.5: No federated (data collection focus)
|
|
20
|
+
v1.0: Custom gradient API (manual model update sharing)
|
|
21
|
+
v2.0: Flower integration (automated federated rounds)
|
|
22
|
+
v3.0: Secure aggregation + DP (enterprise-grade privacy)
|
|
23
|
+
|
|
24
|
+
Requires: flwr, torch (installed via federated or full extras)
|
|
25
|
+
|
|
26
|
+
See design doc Section 9 for the full federated learning design.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
from engine.backends.protocol import UploadRecord, UploadResult
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class FederatedBackend:
|
|
37
|
+
"""Federated learning gradient upload backend.
|
|
38
|
+
|
|
39
|
+
Uploads model gradients (NOT raw data) to a Flower aggregation server.
|
|
40
|
+
The server averages gradients from all participants to produce an
|
|
41
|
+
improved global model.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
server_url: Flower aggregation server URL.
|
|
45
|
+
rounds_per_day: How many federated rounds to participate in per day.
|
|
46
|
+
min_local_samples: Minimum number of local recordings before participating.
|
|
47
|
+
differential_privacy: Whether to add DP noise to gradients.
|
|
48
|
+
epsilon: Privacy budget (lower = more private, noisier gradients).
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
name: str = "federated"
|
|
52
|
+
supports_delete: bool = False
|
|
53
|
+
supports_list: bool = False
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
server_url: str = "https://fl.openadapt.ai",
|
|
58
|
+
rounds_per_day: int = 1,
|
|
59
|
+
min_local_samples: int = 100,
|
|
60
|
+
differential_privacy: bool = True,
|
|
61
|
+
epsilon: float = 1.0,
|
|
62
|
+
) -> None:
|
|
63
|
+
self.server_url = server_url
|
|
64
|
+
self.rounds_per_day = rounds_per_day
|
|
65
|
+
self.min_local_samples = min_local_samples
|
|
66
|
+
self.differential_privacy = differential_privacy
|
|
67
|
+
self.epsilon = epsilon
|
|
68
|
+
|
|
69
|
+
def upload(self, archive_path: Path, metadata: dict) -> UploadResult:
|
|
70
|
+
"""Upload model gradients (not raw data) to the FL server.
|
|
71
|
+
|
|
72
|
+
This method performs local training on the capture data and
|
|
73
|
+
uploads only the resulting model weight deltas.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
archive_path: Path to the capture (used for local training, not uploaded).
|
|
77
|
+
metadata: Capture metadata.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
UploadResult with gradient upload confirmation.
|
|
81
|
+
"""
|
|
82
|
+
# TODO: Load base model
|
|
83
|
+
# TODO: Fine-tune on local capture data
|
|
84
|
+
# TODO: Compute gradient deltas
|
|
85
|
+
# TODO: Apply differential privacy noise if enabled
|
|
86
|
+
# TODO: Upload gradients to Flower server
|
|
87
|
+
raise NotImplementedError
|
|
88
|
+
|
|
89
|
+
def delete(self, recording_id: str) -> bool:
|
|
90
|
+
"""Not supported -- gradients cannot be individually deleted from the aggregate."""
|
|
91
|
+
raise NotImplementedError(
|
|
92
|
+
"Federated gradients are aggregated and cannot be individually deleted"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def list_uploads(self) -> list[UploadRecord]:
|
|
96
|
+
"""Not supported for federated learning."""
|
|
97
|
+
raise NotImplementedError("Federated gradient uploads cannot be listed")
|
|
98
|
+
|
|
99
|
+
def verify_credentials(self) -> bool:
|
|
100
|
+
"""Verify connection to the Flower aggregation server.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
True if the server is reachable.
|
|
104
|
+
"""
|
|
105
|
+
# TODO: HTTP health check to server_url
|
|
106
|
+
raise NotImplementedError
|
|
107
|
+
|
|
108
|
+
def estimate_cost(self, size_bytes: int) -> float | None:
|
|
109
|
+
"""Federated learning has no per-upload cost for participants.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
size_bytes: Ignored (gradients are ~1-10 MB regardless of data size).
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Always 0.0 for participants.
|
|
116
|
+
"""
|
|
117
|
+
return 0.0
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""HuggingFace Hub storage backend -- uploads recordings as dataset shards.
|
|
2
|
+
|
|
3
|
+
Uploads capture archives to a HuggingFace dataset repository using the
|
|
4
|
+
huggingface_hub Python library. Recordings are stored as dataset shards
|
|
5
|
+
in a community dataset repo (e.g., OpenAdaptAI/desktop-recordings).
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Versioned via Git LFS
|
|
9
|
+
- Built-in dataset viewer lets community browse without downloading
|
|
10
|
+
- Supports both public and private (paid HF tier) datasets
|
|
11
|
+
|
|
12
|
+
Requires: huggingface_hub (installed via community or full extras)
|
|
13
|
+
|
|
14
|
+
See design doc Section 7.7 for implementation details.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from engine.backends.protocol import UploadRecord, UploadResult
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class HuggingFaceBackend:
|
|
25
|
+
"""HuggingFace Hub storage backend.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
repo: HuggingFace dataset repository (e.g., "OpenAdaptAI/desktop-recordings").
|
|
29
|
+
token: HuggingFace API token.
|
|
30
|
+
private: Whether to create a private dataset (requires paid HF tier).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
name: str = "huggingface"
|
|
34
|
+
supports_delete: bool = True
|
|
35
|
+
supports_list: bool = True
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
repo: str = "OpenAdaptAI/desktop-recordings",
|
|
40
|
+
token: str = "",
|
|
41
|
+
private: bool = False,
|
|
42
|
+
) -> None:
|
|
43
|
+
self.repo = repo
|
|
44
|
+
self.token = token
|
|
45
|
+
self.private = private
|
|
46
|
+
|
|
47
|
+
def upload(self, archive_path: Path, metadata: dict) -> UploadResult:
|
|
48
|
+
"""Upload a capture archive to HuggingFace Hub.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
archive_path: Path to the archive file.
|
|
52
|
+
metadata: Capture metadata.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
UploadResult with the HF Hub URL.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
from huggingface_hub import HfApi
|
|
59
|
+
|
|
60
|
+
api = HfApi(token=self.token)
|
|
61
|
+
capture_id = metadata.get("capture_id", "unknown")
|
|
62
|
+
path_in_repo = f"captures/{capture_id}/{archive_path.name}"
|
|
63
|
+
|
|
64
|
+
api.upload_file(
|
|
65
|
+
path_or_fileobj=str(archive_path),
|
|
66
|
+
path_in_repo=path_in_repo,
|
|
67
|
+
repo_id=self.repo,
|
|
68
|
+
repo_type="dataset",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
url = f"https://huggingface.co/datasets/{self.repo}/blob/main/{path_in_repo}"
|
|
72
|
+
return UploadResult(
|
|
73
|
+
success=True,
|
|
74
|
+
remote_url=url,
|
|
75
|
+
bytes_sent=archive_path.stat().st_size,
|
|
76
|
+
)
|
|
77
|
+
except ImportError:
|
|
78
|
+
return UploadResult(success=False, error="huggingface_hub not installed")
|
|
79
|
+
except Exception as e:
|
|
80
|
+
return UploadResult(success=False, error=str(e))
|
|
81
|
+
|
|
82
|
+
def delete(self, recording_id: str) -> bool:
|
|
83
|
+
"""Delete a recording from HuggingFace Hub.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
recording_id: The capture session ID.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
True if deletion succeeded.
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
from huggingface_hub import HfApi
|
|
93
|
+
|
|
94
|
+
api = HfApi(token=self.token)
|
|
95
|
+
api.delete_folder(
|
|
96
|
+
path_in_repo=f"captures/{recording_id}",
|
|
97
|
+
repo_id=self.repo,
|
|
98
|
+
repo_type="dataset",
|
|
99
|
+
)
|
|
100
|
+
return True
|
|
101
|
+
except Exception:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
def list_uploads(self) -> list[UploadRecord]:
|
|
105
|
+
"""List all uploads in the HuggingFace dataset repo.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
List of UploadRecord objects.
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
from huggingface_hub import HfApi
|
|
112
|
+
|
|
113
|
+
api = HfApi(token=self.token)
|
|
114
|
+
files = api.list_repo_files(repo_id=self.repo, repo_type="dataset")
|
|
115
|
+
records = []
|
|
116
|
+
for f in files:
|
|
117
|
+
if f.startswith("captures/"):
|
|
118
|
+
parts = f.split("/")
|
|
119
|
+
recording_id = parts[1] if len(parts) > 1 else "unknown"
|
|
120
|
+
records.append(UploadRecord(
|
|
121
|
+
recording_id=recording_id,
|
|
122
|
+
backend="huggingface",
|
|
123
|
+
remote_url=f"https://huggingface.co/datasets/{self.repo}/blob/main/{f}",
|
|
124
|
+
uploaded_at="",
|
|
125
|
+
size_bytes=0,
|
|
126
|
+
))
|
|
127
|
+
return records
|
|
128
|
+
except Exception:
|
|
129
|
+
return []
|
|
130
|
+
|
|
131
|
+
def verify_credentials(self) -> bool:
|
|
132
|
+
"""Verify HuggingFace token by calling the whoami endpoint.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
True if the token is valid.
|
|
136
|
+
"""
|
|
137
|
+
try:
|
|
138
|
+
from huggingface_hub import HfApi
|
|
139
|
+
|
|
140
|
+
api = HfApi(token=self.token)
|
|
141
|
+
api.whoami()
|
|
142
|
+
return True
|
|
143
|
+
except Exception:
|
|
144
|
+
return False
|
|
145
|
+
|
|
146
|
+
def estimate_cost(self, size_bytes: int) -> float | None:
|
|
147
|
+
"""Estimate cost for HuggingFace Hub storage.
|
|
148
|
+
|
|
149
|
+
Public datasets on HF Hub are free (unlimited storage).
|
|
150
|
+
Private datasets require a paid HF tier.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
size_bytes: Size of the data in bytes.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
0.0 for public datasets, None for private (varies by tier).
|
|
157
|
+
"""
|
|
158
|
+
if not self.private:
|
|
159
|
+
return 0.0
|
|
160
|
+
return None
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""StorageBackend protocol -- interface that all storage backends implement.
|
|
2
|
+
|
|
3
|
+
Every storage destination implements this protocol. The upload manager
|
|
4
|
+
dispatches uploads to backends through this uniform interface.
|
|
5
|
+
|
|
6
|
+
All backends share the same upload pipeline:
|
|
7
|
+
[User approves in review UI] -> Compress (tar.zst) -> Queue -> Upload Worker
|
|
8
|
+
|
|
|
9
|
+
Backend-specific:
|
|
10
|
+
- S3: multipart upload
|
|
11
|
+
- HF Hub: git lfs push
|
|
12
|
+
- R2: S3-compatible multipart
|
|
13
|
+
- Wormhole: P2P direct
|
|
14
|
+
|
|
15
|
+
See design doc Section 7.3 for the full protocol specification.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Protocol, runtime_checkable
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class UploadResult:
|
|
27
|
+
"""Result of an upload operation.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
success: Whether the upload succeeded.
|
|
31
|
+
remote_url: URL or URI of the uploaded resource (if applicable).
|
|
32
|
+
bytes_sent: Total bytes sent.
|
|
33
|
+
error: Error message if the upload failed.
|
|
34
|
+
metadata: Additional backend-specific metadata.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
success: bool
|
|
38
|
+
remote_url: str = ""
|
|
39
|
+
bytes_sent: int = 0
|
|
40
|
+
error: str = ""
|
|
41
|
+
metadata: dict = field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class UploadRecord:
|
|
46
|
+
"""Record of a past upload, for listing/auditing.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
recording_id: The capture session ID that was uploaded.
|
|
50
|
+
backend: Name of the storage backend used.
|
|
51
|
+
remote_url: URL or URI of the uploaded resource.
|
|
52
|
+
uploaded_at: ISO 8601 timestamp of the upload.
|
|
53
|
+
size_bytes: Size of the uploaded archive.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
recording_id: str
|
|
57
|
+
backend: str
|
|
58
|
+
remote_url: str
|
|
59
|
+
uploaded_at: str
|
|
60
|
+
size_bytes: int
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@runtime_checkable
|
|
64
|
+
class StorageBackend(Protocol):
|
|
65
|
+
"""Protocol that every storage backend must implement.
|
|
66
|
+
|
|
67
|
+
Backends provide upload, optional delete, optional list, credential
|
|
68
|
+
verification, and cost estimation.
|
|
69
|
+
|
|
70
|
+
Attributes:
|
|
71
|
+
name: Human-readable backend name (e.g., "s3", "huggingface").
|
|
72
|
+
supports_delete: Whether uploaded resources can be deleted.
|
|
73
|
+
supports_list: Whether uploaded resources can be listed.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
name: str
|
|
77
|
+
supports_delete: bool
|
|
78
|
+
supports_list: bool
|
|
79
|
+
|
|
80
|
+
def upload(self, archive_path: Path, metadata: dict) -> UploadResult:
|
|
81
|
+
"""Upload a capture archive to the backend.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
archive_path: Path to the tar.zst archive to upload.
|
|
85
|
+
metadata: Capture metadata (id, duration, event count, etc.).
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
UploadResult with success status and remote URL.
|
|
89
|
+
"""
|
|
90
|
+
...
|
|
91
|
+
|
|
92
|
+
def delete(self, recording_id: str) -> bool:
|
|
93
|
+
"""Delete a previously uploaded recording.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
recording_id: The capture session ID to delete.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
True if deletion succeeded.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
NotImplementedError: If the backend does not support deletion.
|
|
103
|
+
"""
|
|
104
|
+
...
|
|
105
|
+
|
|
106
|
+
def list_uploads(self) -> list[UploadRecord]:
|
|
107
|
+
"""List all uploads made through this backend.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
List of UploadRecord objects.
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
NotImplementedError: If the backend does not support listing.
|
|
114
|
+
"""
|
|
115
|
+
...
|
|
116
|
+
|
|
117
|
+
def verify_credentials(self) -> bool:
|
|
118
|
+
"""Verify that the backend credentials are valid.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
True if credentials are valid and the backend is reachable.
|
|
122
|
+
"""
|
|
123
|
+
...
|
|
124
|
+
|
|
125
|
+
def estimate_cost(self, size_bytes: int) -> float | None:
|
|
126
|
+
"""Estimate the cost of uploading/storing the given amount of data.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
size_bytes: Size of the data in bytes.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Estimated cost in USD, or None if cost estimation is not available.
|
|
133
|
+
"""
|
|
134
|
+
...
|