offwork 0.1.2__tar.gz → 0.1.4__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.
- {offwork-0.1.2 → offwork-0.1.4}/PKG-INFO +46 -40
- offwork-0.1.4/README.md +119 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/__init__.py +34 -6
- {offwork-0.1.2 → offwork-0.1.4}/offwork/__main__.py +115 -10
- offwork-0.1.4/offwork/core/clients.py +142 -0
- offwork-0.1.4/offwork/core/ed25519.py +162 -0
- offwork-0.1.4/offwork/core/envelope.py +178 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/core/errors.py +22 -1
- offwork-0.1.4/offwork/core/identity.py +111 -0
- offwork-0.1.4/offwork/core/signing.py +88 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/core/task.py +18 -58
- {offwork-0.1.2 → offwork-0.1.4}/offwork/core/token.py +18 -25
- offwork-0.1.4/offwork/core/version.py +35 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/graph/tracing.py +12 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/typing.py +2 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/worker/backends/base.py +11 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/worker/backends/http.py +6 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/worker/deps.py +8 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/worker/remote.py +130 -50
- {offwork-0.1.2 → offwork-0.1.4}/offwork/worker/result.py +60 -24
- offwork-0.1.4/offwork/worker/schedule.py +53 -0
- {offwork-0.1.2 → offwork-0.1.4}/pyproject.toml +1 -1
- offwork-0.1.2/README.md +0 -113
- offwork-0.1.2/offwork/core/signing.py +0 -91
- offwork-0.1.2/offwork/core/version.py +0 -10
- offwork-0.1.2/offwork/worker/schedule.py +0 -26
- {offwork-0.1.2 → offwork-0.1.4}/LICENSE +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/_venv.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/core/__init__.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/core/models.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/core/pairing.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/core/progress.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/graph/__init__.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/graph/analyzer.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/graph/decorator.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/graph/graph.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/graph/store.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/py.typed +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/worker/__init__.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/worker/backends/__init__.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/worker/backends/local.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/worker/backends/rabbitmq.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/worker/backends/redis.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/worker/sandbox/Dockerfile +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/worker/sandbox/__init__.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/worker/sandbox/_protocol.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/worker/sandbox/docker.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/worker/sandbox/guest_agent.py +0 -0
- {offwork-0.1.2 → offwork-0.1.4}/offwork/worker/worker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: offwork
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Distributed Python task execution via automatic function serialization
|
|
5
5
|
License: AGPL-3.0-only
|
|
6
6
|
License-File: LICENSE
|
|
@@ -27,32 +27,35 @@ Description-Content-Type: text/markdown
|
|
|
27
27
|
|
|
28
28
|
# offwork
|
|
29
29
|
|
|
30
|
-
**Run any Python function on a remote worker — zero setup, zero deployment.**
|
|
31
|
-
|
|
32
30
|
[](https://pypi.org/project/offwork/)
|
|
33
31
|
[](https://www.python.org/downloads/)
|
|
34
32
|
[](LICENSE)
|
|
35
33
|
[](https://mypy-lang.org/)
|
|
36
34
|
[]()
|
|
37
35
|
|
|
38
|
-
|
|
36
|
+
**Run any Python function on a remote worker with just two lines of code.**
|
|
37
|
+
|
|
38
|
+
Put `.connect()` somewhere at the start of your script, add `@offwork.task` to your function, that's it.
|
|
39
|
+
You can now run it remotely — no shared codebase, no deployment pipeline.
|
|
40
|
+
|
|
41
|
+
`offwork` captures its entire dependency graph (helpers, imports, closures, constants) and ships it to the worker as a self-contained payload. The worker doesn't need to have any prior knowledge of your code.
|
|
39
42
|
|
|
40
43
|
## Quick start
|
|
41
44
|
|
|
42
45
|
```bash
|
|
43
46
|
pip install offwork
|
|
47
|
+
offwork worker --backend local://localhost:9748 --tmp # start a worker in a temp venv
|
|
44
48
|
```
|
|
45
49
|
|
|
46
50
|
```python
|
|
47
51
|
import asyncio, math, offwork
|
|
48
|
-
import offwork
|
|
49
52
|
|
|
50
53
|
offwork.connect("local://localhost:9748")
|
|
51
54
|
|
|
52
|
-
def add(a, b):
|
|
55
|
+
def add(a: float, b: float) -> float:
|
|
53
56
|
return a + b
|
|
54
57
|
|
|
55
|
-
@offwork.task
|
|
58
|
+
@offwork.task # only the entry point needs this - add() is captured automatically
|
|
56
59
|
def hypotenuse(a: float, b: float) -> float:
|
|
57
60
|
return math.sqrt(add(a**2, b**2))
|
|
58
61
|
|
|
@@ -62,66 +65,69 @@ async def main():
|
|
|
62
65
|
asyncio.run(main())
|
|
63
66
|
```
|
|
64
67
|
|
|
65
|
-
|
|
68
|
+
`.run()` serializes the function graph, submits it to the worker, and returns the result. The worker reconstructs source, installs any missing packages, and executes.
|
|
69
|
+
|
|
70
|
+
### Multi-machine
|
|
71
|
+
|
|
72
|
+
Swap `local://` for Redis or RabbitMQ to run on a remote worker:
|
|
66
73
|
|
|
67
74
|
```bash
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
pip install offwork[redis]
|
|
76
|
+
offwork worker --backend redis://other-machine:6379
|
|
70
77
|
```
|
|
71
78
|
|
|
72
|
-
|
|
79
|
+
See [Features](docs/FEATURES.md) for the full API.
|
|
73
80
|
|
|
74
81
|
## Features
|
|
75
82
|
|
|
76
83
|
| | |
|
|
77
84
|
|-|-|
|
|
78
|
-
| **
|
|
79
|
-
| **
|
|
80
|
-
| **Async-native** | `.run()`, `.start()`, `.map()`, `asyncio.gather` |
|
|
85
|
+
| **Async-native** | `.run()`, `.start()`, `.map()`, `asyncio.gather` — all coroutines |
|
|
86
|
+
| **Scheduling** | `.run_in(delay)`, `.run_at(datetime)`, `.run_every(interval)` with cancellation |
|
|
81
87
|
| **Retry & timeout** | `@offwork.task(timeout=30, retries=3)` with exponential backoff |
|
|
82
|
-
| **Scheduling** | `.run_in(delay)`, `.run_at(datetime)`, `.run_every(freq)` with cancellation |
|
|
83
88
|
| **Throttling** | `@offwork.task(throttle=timedelta(hours=24)/50)` — rate-limit executions |
|
|
84
89
|
| **Progress & cancellation** | `offwork.progress(3, 10)` inside tasks; `await future.cancel()` on client |
|
|
85
|
-
| **Heartbeat & stall detection** | Workers heartbeat; clients raise `TaskStalled` on silence |
|
|
86
|
-
| **
|
|
87
|
-
| **
|
|
88
|
-
| **Docker sandbox** | Container isolation, transparent to clients |
|
|
90
|
+
| **Heartbeat & stall detection** | Workers heartbeat every second; clients raise `TaskStalled` on silence |
|
|
91
|
+
| **Package auto-install** | Workers `pip install` missing packages before execution |
|
|
92
|
+
| **Docker sandbox** | Optional container isolation, fully transparent to clients |
|
|
89
93
|
| **Signed execution** | Pre-shared token or PIN pairing + HMAC-SHA256 task authentication |
|
|
90
|
-
| **Graceful shutdown** | Ctrl+C drains in-flight tasks; second Ctrl+C force-quits |
|
|
91
94
|
|
|
92
|
-
|
|
95
|
+
### Security
|
|
96
|
+
|
|
97
|
+
#### Signing
|
|
93
98
|
|
|
94
|
-
|
|
99
|
+
To make sure your worker only executes trusted code, use `--require-signing`.<br>
|
|
100
|
+
Configure a shared token or pair interactively with a PIN. See [Signing & Pairing](docs/SIGNING.md) for details.
|
|
95
101
|
|
|
96
102
|
```bash
|
|
97
|
-
|
|
98
|
-
offwork
|
|
103
|
+
# Token (CI/CD)
|
|
104
|
+
offwork token generate
|
|
105
|
+
export OFFWORK_SIGNING_TOKEN=<token> # client & worker
|
|
106
|
+
offwork worker --backend redis://localhost:6379 --require-signing
|
|
107
|
+
|
|
108
|
+
# PIN pairing (interactive)
|
|
109
|
+
offwork worker --backend redis://localhost:6379 --pair # shows 6-digit PIN
|
|
110
|
+
offwork pair --backend redis://localhost:6379 # client: enter PIN
|
|
99
111
|
```
|
|
100
112
|
|
|
101
|
-
See [
|
|
113
|
+
After pairing, tasks are signed automatically with no client code changes. See [Signing & Pairing](docs/SIGNING.md).
|
|
102
114
|
|
|
103
|
-
|
|
115
|
+
#### Sandbox
|
|
104
116
|
|
|
105
|
-
|
|
117
|
+
To avoid side-effects on the worker machine, run tasks inside Docker containers:
|
|
106
118
|
|
|
107
119
|
```bash
|
|
108
|
-
#
|
|
109
|
-
offwork
|
|
110
|
-
export OFFWORK_SIGNING_TOKEN=<token> # set on client & worker
|
|
111
|
-
offwork worker --backend redis://localhost:6379 --require-signing
|
|
112
|
-
|
|
113
|
-
# PIN-based pairing (interactive)
|
|
114
|
-
offwork worker --backend redis://localhost:6379 --pair # displays a 6-digit PIN
|
|
115
|
-
offwork pair --backend redis://localhost:6379 # on client: enter the PIN
|
|
120
|
+
offwork sandbox setup # build image (once)
|
|
121
|
+
offwork worker --backend redis://localhost:6379 --sandbox # run with isolation
|
|
116
122
|
```
|
|
117
123
|
|
|
118
|
-
|
|
124
|
+
See [Sandbox](docs/SANDBOX.md) for configuration.
|
|
119
125
|
|
|
120
126
|
## Documentation
|
|
121
127
|
|
|
122
128
|
| | |
|
|
123
129
|
|-|-|
|
|
124
|
-
| **[
|
|
130
|
+
| **[Features](docs/FEATURES.md)** | Full feature guide and API walkthrough |
|
|
125
131
|
| **[Technical Overview](docs/TECHNICAL_OVERVIEW.md)** | Architecture, serialization format, internals |
|
|
126
132
|
| **[Signing & Pairing](docs/SIGNING.md)** | Cryptographic task signing protocol |
|
|
127
133
|
| **[Sandbox](docs/SANDBOX.md)** | Docker container isolation |
|
|
@@ -129,11 +135,11 @@ After setup, tasks are signed automatically. No client-side code changes. See [S
|
|
|
129
135
|
## Examples
|
|
130
136
|
|
|
131
137
|
```bash
|
|
132
|
-
offwork worker --backend local://localhost:9748 --tmp
|
|
133
|
-
offwork run examples/remote_execution.py
|
|
138
|
+
offwork worker --backend local://localhost:9748 --tmp # start worker
|
|
139
|
+
offwork run examples/remote_execution.py # run any example
|
|
134
140
|
```
|
|
135
141
|
|
|
136
|
-
|
|
142
|
+
See [examples/README.md](examples/README.md) for a guide to all examples.
|
|
137
143
|
|
|
138
144
|
## License
|
|
139
145
|
|
offwork-0.1.4/README.md
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# offwork
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/offwork/)
|
|
4
|
+
[](https://www.python.org/downloads/)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://mypy-lang.org/)
|
|
7
|
+
[]()
|
|
8
|
+
|
|
9
|
+
**Run any Python function on a remote worker with just two lines of code.**
|
|
10
|
+
|
|
11
|
+
Put `.connect()` somewhere at the start of your script, add `@offwork.task` to your function, that's it.
|
|
12
|
+
You can now run it remotely — no shared codebase, no deployment pipeline.
|
|
13
|
+
|
|
14
|
+
`offwork` captures its entire dependency graph (helpers, imports, closures, constants) and ships it to the worker as a self-contained payload. The worker doesn't need to have any prior knowledge of your code.
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install offwork
|
|
20
|
+
offwork worker --backend local://localhost:9748 --tmp # start a worker in a temp venv
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import asyncio, math, offwork
|
|
25
|
+
|
|
26
|
+
offwork.connect("local://localhost:9748")
|
|
27
|
+
|
|
28
|
+
def add(a: float, b: float) -> float:
|
|
29
|
+
return a + b
|
|
30
|
+
|
|
31
|
+
@offwork.task # only the entry point needs this - add() is captured automatically
|
|
32
|
+
def hypotenuse(a: float, b: float) -> float:
|
|
33
|
+
return math.sqrt(add(a**2, b**2))
|
|
34
|
+
|
|
35
|
+
async def main():
|
|
36
|
+
print(await hypotenuse.run(3.0, 4.0)) # 5.0
|
|
37
|
+
|
|
38
|
+
asyncio.run(main())
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`.run()` serializes the function graph, submits it to the worker, and returns the result. The worker reconstructs source, installs any missing packages, and executes.
|
|
42
|
+
|
|
43
|
+
### Multi-machine
|
|
44
|
+
|
|
45
|
+
Swap `local://` for Redis or RabbitMQ to run on a remote worker:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install offwork[redis]
|
|
49
|
+
offwork worker --backend redis://other-machine:6379
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
See [Features](docs/FEATURES.md) for the full API.
|
|
53
|
+
|
|
54
|
+
## Features
|
|
55
|
+
|
|
56
|
+
| | |
|
|
57
|
+
|-|-|
|
|
58
|
+
| **Async-native** | `.run()`, `.start()`, `.map()`, `asyncio.gather` — all coroutines |
|
|
59
|
+
| **Scheduling** | `.run_in(delay)`, `.run_at(datetime)`, `.run_every(interval)` with cancellation |
|
|
60
|
+
| **Retry & timeout** | `@offwork.task(timeout=30, retries=3)` with exponential backoff |
|
|
61
|
+
| **Throttling** | `@offwork.task(throttle=timedelta(hours=24)/50)` — rate-limit executions |
|
|
62
|
+
| **Progress & cancellation** | `offwork.progress(3, 10)` inside tasks; `await future.cancel()` on client |
|
|
63
|
+
| **Heartbeat & stall detection** | Workers heartbeat every second; clients raise `TaskStalled` on silence |
|
|
64
|
+
| **Package auto-install** | Workers `pip install` missing packages before execution |
|
|
65
|
+
| **Docker sandbox** | Optional container isolation, fully transparent to clients |
|
|
66
|
+
| **Signed execution** | Pre-shared token or PIN pairing + HMAC-SHA256 task authentication |
|
|
67
|
+
|
|
68
|
+
### Security
|
|
69
|
+
|
|
70
|
+
#### Signing
|
|
71
|
+
|
|
72
|
+
To make sure your worker only executes trusted code, use `--require-signing`.<br>
|
|
73
|
+
Configure a shared token or pair interactively with a PIN. See [Signing & Pairing](docs/SIGNING.md) for details.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Token (CI/CD)
|
|
77
|
+
offwork token generate
|
|
78
|
+
export OFFWORK_SIGNING_TOKEN=<token> # client & worker
|
|
79
|
+
offwork worker --backend redis://localhost:6379 --require-signing
|
|
80
|
+
|
|
81
|
+
# PIN pairing (interactive)
|
|
82
|
+
offwork worker --backend redis://localhost:6379 --pair # shows 6-digit PIN
|
|
83
|
+
offwork pair --backend redis://localhost:6379 # client: enter PIN
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
After pairing, tasks are signed automatically with no client code changes. See [Signing & Pairing](docs/SIGNING.md).
|
|
87
|
+
|
|
88
|
+
#### Sandbox
|
|
89
|
+
|
|
90
|
+
To avoid side-effects on the worker machine, run tasks inside Docker containers:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
offwork sandbox setup # build image (once)
|
|
94
|
+
offwork worker --backend redis://localhost:6379 --sandbox # run with isolation
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
See [Sandbox](docs/SANDBOX.md) for configuration.
|
|
98
|
+
|
|
99
|
+
## Documentation
|
|
100
|
+
|
|
101
|
+
| | |
|
|
102
|
+
|-|-|
|
|
103
|
+
| **[Features](docs/FEATURES.md)** | Full feature guide and API walkthrough |
|
|
104
|
+
| **[Technical Overview](docs/TECHNICAL_OVERVIEW.md)** | Architecture, serialization format, internals |
|
|
105
|
+
| **[Signing & Pairing](docs/SIGNING.md)** | Cryptographic task signing protocol |
|
|
106
|
+
| **[Sandbox](docs/SANDBOX.md)** | Docker container isolation |
|
|
107
|
+
|
|
108
|
+
## Examples
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
offwork worker --backend local://localhost:9748 --tmp # start worker
|
|
112
|
+
offwork run examples/remote_execution.py # run any example
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
See [examples/README.md](examples/README.md) for a guide to all examples.
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
[AGPL-3.0](LICENSE)
|
|
@@ -8,14 +8,18 @@ from offwork.core.task import Task
|
|
|
8
8
|
from offwork.core.errors import (
|
|
9
9
|
Error,
|
|
10
10
|
RemoteError,
|
|
11
|
+
ReplayError,
|
|
11
12
|
TaskStalled,
|
|
12
13
|
WorkerError,
|
|
13
14
|
PairingError,
|
|
15
|
+
StaleTaskError,
|
|
14
16
|
TaskCancelled,
|
|
15
17
|
SignatureError,
|
|
16
18
|
DependencyError,
|
|
17
19
|
ThrottleError,
|
|
18
20
|
WorkerOnlyError,
|
|
21
|
+
ClientRevokedError,
|
|
22
|
+
IdentityMismatchError,
|
|
19
23
|
)
|
|
20
24
|
from offwork.core.models import ImportInfo, FunctionNode
|
|
21
25
|
from offwork.graph.graph import Graph
|
|
@@ -31,18 +35,29 @@ from offwork.core.pairing import (
|
|
|
31
35
|
respond_to_pairing,
|
|
32
36
|
)
|
|
33
37
|
from offwork.core.signing import (
|
|
34
|
-
|
|
38
|
+
NonceLRU,
|
|
35
39
|
derive_key,
|
|
36
40
|
verify_signature,
|
|
37
41
|
compute_signature,
|
|
38
|
-
verify_and_load_json,
|
|
39
42
|
)
|
|
40
43
|
from offwork.core.token import (
|
|
41
44
|
load_token,
|
|
42
45
|
save_token,
|
|
43
46
|
clear_token,
|
|
44
47
|
generate_token,
|
|
45
|
-
|
|
48
|
+
resolve_root_token,
|
|
49
|
+
)
|
|
50
|
+
from offwork.core.identity import (
|
|
51
|
+
get_client_id,
|
|
52
|
+
get_public_key,
|
|
53
|
+
clear_identity,
|
|
54
|
+
get_identity_seed,
|
|
55
|
+
get_identity_fingerprint,
|
|
56
|
+
)
|
|
57
|
+
from offwork.core.clients import KnownClients, ClientEntry
|
|
58
|
+
from offwork.core.envelope import (
|
|
59
|
+
build_signed_envelope,
|
|
60
|
+
verify_task_envelope,
|
|
46
61
|
)
|
|
47
62
|
from offwork.core.version import _VERSION
|
|
48
63
|
from offwork.core.progress import ProgressInfo
|
|
@@ -131,6 +146,10 @@ __all__ = [
|
|
|
131
146
|
"TaskCancelled",
|
|
132
147
|
"ThrottleError",
|
|
133
148
|
"SignatureError",
|
|
149
|
+
"ReplayError",
|
|
150
|
+
"StaleTaskError",
|
|
151
|
+
"ClientRevokedError",
|
|
152
|
+
"IdentityMismatchError",
|
|
134
153
|
"PairingError",
|
|
135
154
|
"WorkerOnlyError",
|
|
136
155
|
# Graph
|
|
@@ -139,15 +158,24 @@ __all__ = [
|
|
|
139
158
|
# Signing
|
|
140
159
|
"compute_signature",
|
|
141
160
|
"verify_signature",
|
|
142
|
-
"sign_json",
|
|
143
|
-
"verify_and_load_json",
|
|
144
161
|
"derive_key",
|
|
162
|
+
"NonceLRU",
|
|
163
|
+
"build_signed_envelope",
|
|
164
|
+
"verify_task_envelope",
|
|
145
165
|
# Token
|
|
146
166
|
"generate_token",
|
|
147
167
|
"save_token",
|
|
148
168
|
"load_token",
|
|
149
169
|
"clear_token",
|
|
150
|
-
"
|
|
170
|
+
"resolve_root_token",
|
|
171
|
+
# Identity / clients
|
|
172
|
+
"get_client_id",
|
|
173
|
+
"get_identity_seed",
|
|
174
|
+
"get_public_key",
|
|
175
|
+
"get_identity_fingerprint",
|
|
176
|
+
"clear_identity",
|
|
177
|
+
"KnownClients",
|
|
178
|
+
"ClientEntry",
|
|
151
179
|
# Pairing
|
|
152
180
|
"generate_pin",
|
|
153
181
|
"save_shared_key",
|
|
@@ -44,6 +44,8 @@ def _build_worker_cmd(python: str, args: argparse.Namespace) -> list[str]:
|
|
|
44
44
|
cmd.extend(["--log-level", args.log_level])
|
|
45
45
|
if args.require_signing:
|
|
46
46
|
cmd.append("--require-signing")
|
|
47
|
+
if args.clock_skew != 300.0:
|
|
48
|
+
cmd.extend(["--clock-skew", str(args.clock_skew)])
|
|
47
49
|
if args.sandbox:
|
|
48
50
|
cmd.append("--sandbox")
|
|
49
51
|
return cmd
|
|
@@ -108,6 +110,7 @@ def _cmd_worker(args: argparse.Namespace) -> None:
|
|
|
108
110
|
auto_install=not args.no_auto_install,
|
|
109
111
|
sandbox=bool(args.sandbox),
|
|
110
112
|
require_signing=bool(args.require_signing),
|
|
113
|
+
clock_skew=float(args.clock_skew),
|
|
111
114
|
))
|
|
112
115
|
|
|
113
116
|
|
|
@@ -142,6 +145,7 @@ async def _pair_then_serve(args: argparse.Namespace) -> None:
|
|
|
142
145
|
auto_install=not args.no_auto_install,
|
|
143
146
|
sandbox=bool(args.sandbox),
|
|
144
147
|
require_signing=True,
|
|
148
|
+
clock_skew=float(args.clock_skew),
|
|
145
149
|
)
|
|
146
150
|
|
|
147
151
|
|
|
@@ -395,7 +399,9 @@ def _add_worker_parser(sub: "argparse._SubParsersAction[argparse.ArgumentParser]
|
|
|
395
399
|
p.add_argument("--sandbox", action="store_true", default=False,
|
|
396
400
|
help="Run function execution inside an isolated Docker sandbox.")
|
|
397
401
|
p.add_argument("--require-signing", action="store_true", default=False,
|
|
398
|
-
help="Only accept tasks with valid
|
|
402
|
+
help="Only accept tasks with valid signed envelopes (per-client HMAC + Ed25519).")
|
|
403
|
+
p.add_argument("--clock-skew", type=float, default=300.0, metavar="SECONDS",
|
|
404
|
+
help="Maximum clock skew between client and worker (default: 300s)")
|
|
399
405
|
p.add_argument("--pair", action="store_true", default=False,
|
|
400
406
|
help="Generate a pairing PIN, wait for a client to pair, then start "
|
|
401
407
|
"serving with signing enabled.")
|
|
@@ -649,6 +655,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
649
655
|
_add_sandbox_parser(sub)
|
|
650
656
|
_add_pair_parser(sub)
|
|
651
657
|
_add_token_parser(sub)
|
|
658
|
+
_add_clients_parser(sub)
|
|
652
659
|
return parser
|
|
653
660
|
|
|
654
661
|
|
|
@@ -703,22 +710,27 @@ def _cmd_token_generate(args: argparse.Namespace) -> None:
|
|
|
703
710
|
|
|
704
711
|
|
|
705
712
|
def _cmd_token_show(_args: argparse.Namespace) -> None:
|
|
706
|
-
"""Show the current signing token status."""
|
|
713
|
+
"""Show the current signing token + local identity status."""
|
|
707
714
|
from offwork.core.token import load_token, _TOKEN_ENV_VAR
|
|
715
|
+
from offwork.core.identity import get_client_id, get_identity_fingerprint
|
|
708
716
|
|
|
709
717
|
env_val = os.environ.get(_TOKEN_ENV_VAR)
|
|
710
718
|
if env_val is not None:
|
|
711
719
|
print(f" Source: {_TOKEN_ENV_VAR} environment variable")
|
|
712
720
|
print(f" Token: {env_val.strip()[:16]}... (truncated)")
|
|
713
|
-
return
|
|
714
|
-
|
|
715
|
-
token = load_token()
|
|
716
|
-
if token is not None:
|
|
717
|
-
print(f" Source: ~/.offwork/token")
|
|
718
|
-
print(f" Token: {token[:16]}... (truncated)")
|
|
719
721
|
else:
|
|
720
|
-
|
|
721
|
-
|
|
722
|
+
token = load_token()
|
|
723
|
+
if token is not None:
|
|
724
|
+
print(f" Source: ~/.offwork/token")
|
|
725
|
+
print(f" Token: {token[:16]}... (truncated)")
|
|
726
|
+
else:
|
|
727
|
+
print(" No signing token configured.")
|
|
728
|
+
print(" Generate one with: offwork token generate")
|
|
729
|
+
|
|
730
|
+
print()
|
|
731
|
+
print(" Local identity:")
|
|
732
|
+
print(f" client_id: {get_client_id()}")
|
|
733
|
+
print(f" fingerprint: {get_identity_fingerprint()}")
|
|
722
734
|
|
|
723
735
|
|
|
724
736
|
def _cmd_token_clear(_args: argparse.Namespace) -> None:
|
|
@@ -749,6 +761,98 @@ def _add_token_parser(sub: "argparse._SubParsersAction[argparse.ArgumentParser]"
|
|
|
749
761
|
token_sub.add_parser("clear", help="Remove the saved token")
|
|
750
762
|
|
|
751
763
|
|
|
764
|
+
# -- Clients (known-clients registry) ---------------------------------------
|
|
765
|
+
|
|
766
|
+
|
|
767
|
+
def _cmd_clients(args: argparse.Namespace) -> None:
|
|
768
|
+
"""Handle ``offwork clients`` subcommands."""
|
|
769
|
+
action = getattr(args, "clients_action", None)
|
|
770
|
+
if action is None:
|
|
771
|
+
print("Usage: offwork clients {list|show|revoke|approve}", file=sys.stderr)
|
|
772
|
+
sys.exit(1)
|
|
773
|
+
handlers = {
|
|
774
|
+
"list": _cmd_clients_list,
|
|
775
|
+
"show": _cmd_clients_show,
|
|
776
|
+
"revoke": _cmd_clients_revoke,
|
|
777
|
+
"approve": _cmd_clients_approve,
|
|
778
|
+
}
|
|
779
|
+
handlers[action](args)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def _format_ts(ts: float) -> str:
|
|
783
|
+
import datetime as _dt
|
|
784
|
+
|
|
785
|
+
return _dt.datetime.fromtimestamp(ts).strftime("%Y-%m-%d %H:%M:%S")
|
|
786
|
+
|
|
787
|
+
|
|
788
|
+
def _cmd_clients_list(_args: argparse.Namespace) -> None:
|
|
789
|
+
from offwork.core.clients import KnownClients
|
|
790
|
+
|
|
791
|
+
entries = KnownClients().list_clients()
|
|
792
|
+
if not entries:
|
|
793
|
+
print("No known clients.")
|
|
794
|
+
return
|
|
795
|
+
print(f"{'CLIENT_ID':<34} {'FINGERPRINT':<18} "
|
|
796
|
+
f"{'FIRST_SEEN':<20} {'LAST_SEEN':<20} STATE")
|
|
797
|
+
import hashlib
|
|
798
|
+
for e in entries:
|
|
799
|
+
fp = hashlib.sha256(bytes.fromhex(e.pubkey)).hexdigest()[:16]
|
|
800
|
+
state = "revoked" if e.revoked else "active"
|
|
801
|
+
print(f"{e.client_id:<34} {fp:<18} "
|
|
802
|
+
f"{_format_ts(e.first_seen):<20} {_format_ts(e.last_seen):<20} {state}")
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def _cmd_clients_show(args: argparse.Namespace) -> None:
|
|
806
|
+
import hashlib
|
|
807
|
+
|
|
808
|
+
from offwork.core.clients import KnownClients
|
|
809
|
+
|
|
810
|
+
entry = KnownClients().get(args.client_id)
|
|
811
|
+
if entry is None:
|
|
812
|
+
print(f"No client with id {args.client_id!r}.", file=sys.stderr)
|
|
813
|
+
sys.exit(1)
|
|
814
|
+
fp = hashlib.sha256(bytes.fromhex(entry.pubkey)).hexdigest()[:16]
|
|
815
|
+
print(f" client_id: {entry.client_id}")
|
|
816
|
+
print(f" pubkey: {entry.pubkey}")
|
|
817
|
+
print(f" fingerprint: {fp}")
|
|
818
|
+
print(f" first_seen: {_format_ts(entry.first_seen)}")
|
|
819
|
+
print(f" last_seen: {_format_ts(entry.last_seen)}")
|
|
820
|
+
print(f" revoked: {entry.revoked}")
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def _cmd_clients_revoke(args: argparse.Namespace) -> None:
|
|
824
|
+
from offwork.core.clients import KnownClients
|
|
825
|
+
|
|
826
|
+
if KnownClients().revoke(args.client_id):
|
|
827
|
+
print(f"Revoked client {args.client_id}.")
|
|
828
|
+
else:
|
|
829
|
+
print(f"Client {args.client_id} not found or already revoked.")
|
|
830
|
+
|
|
831
|
+
|
|
832
|
+
def _cmd_clients_approve(args: argparse.Namespace) -> None:
|
|
833
|
+
from offwork.core.clients import KnownClients
|
|
834
|
+
|
|
835
|
+
if KnownClients().approve(args.client_id):
|
|
836
|
+
print(f"Un-revoked client {args.client_id}.")
|
|
837
|
+
else:
|
|
838
|
+
print(f"Client {args.client_id} not found or not revoked.")
|
|
839
|
+
|
|
840
|
+
|
|
841
|
+
def _add_clients_parser(sub: "argparse._SubParsersAction[argparse.ArgumentParser]") -> None:
|
|
842
|
+
p = sub.add_parser(
|
|
843
|
+
"clients",
|
|
844
|
+
help="Inspect and manage the known-clients registry (worker side)",
|
|
845
|
+
)
|
|
846
|
+
clients_sub = p.add_subparsers(dest="clients_action")
|
|
847
|
+
clients_sub.add_parser("list", help="List all known clients")
|
|
848
|
+
show = clients_sub.add_parser("show", help="Show details for one client")
|
|
849
|
+
show.add_argument("client_id", help="Client id to show")
|
|
850
|
+
rev = clients_sub.add_parser("revoke", help="Revoke a client")
|
|
851
|
+
rev.add_argument("client_id", help="Client id to revoke")
|
|
852
|
+
appr = clients_sub.add_parser("approve", help="Un-revoke a client")
|
|
853
|
+
appr.add_argument("client_id", help="Client id to un-revoke")
|
|
854
|
+
|
|
855
|
+
|
|
752
856
|
_COMMAND_HANDLERS: dict[str, Callable[[argparse.Namespace], None]] = {
|
|
753
857
|
"worker": _cmd_worker,
|
|
754
858
|
"run": _cmd_run,
|
|
@@ -758,6 +862,7 @@ _COMMAND_HANDLERS: dict[str, Callable[[argparse.Namespace], None]] = {
|
|
|
758
862
|
"sandbox": _cmd_sandbox,
|
|
759
863
|
"pair": _dispatch_pair,
|
|
760
864
|
"token": _cmd_token,
|
|
865
|
+
"clients": _cmd_clients,
|
|
761
866
|
}
|
|
762
867
|
|
|
763
868
|
|