openworkproof 1.0.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.
- openworkproof/__init__.py +1 -0
- openworkproof/acceptance.py +3006 -0
- openworkproof/cli.py +164 -0
- openworkproof/composition.py +832 -0
- openworkproof/evidence.py +8281 -0
- openworkproof/execution_adapter.py +256 -0
- openworkproof/external_acceptor.py +211 -0
- openworkproof/mcp_server.py +2939 -0
- openworkproof/mcp_transport.py +72 -0
- openworkproof/models.py +3380 -0
- openworkproof/policy.py +2494 -0
- openworkproof/predicates.py +333 -0
- openworkproof/repo_pipeline/__init__.py +74 -0
- openworkproof/repo_pipeline/analysis.py +152 -0
- openworkproof/repo_pipeline/errors.py +64 -0
- openworkproof/repo_pipeline/models.py +80 -0
- openworkproof/repo_pipeline/output.py +68 -0
- openworkproof/repo_pipeline/reader.py +47 -0
- openworkproof/repo_pipeline/sources.py +120 -0
- openworkproof/repo_pipeline/traversal.py +253 -0
- openworkproof/repo_tools.py +8382 -0
- openworkproof/runtime_context.py +189 -0
- openworkproof/schema_registry.py +623 -0
- openworkproof/schemas/v0.1/acceptance-receipt.schema.json +1 -0
- openworkproof/schemas/v0.1/acceptance-rejection-receipt.schema.json +1 -0
- openworkproof/schemas/v0.1/action-receipt.schema.json +1 -0
- openworkproof/schemas/v0.1/capability-grant.schema.json +1 -0
- openworkproof/schemas/v0.1/schema-registry.json +1 -0
- openworkproof/schemas/v0.1/work-order.schema.json +1 -0
- openworkproof/signing.py +421 -0
- openworkproof/state.py +765 -0
- openworkproof/team_network_client.py +419 -0
- openworkproof/trusted_helper.py +269 -0
- openworkproof-1.0.0.dist-info/METADATA +578 -0
- openworkproof-1.0.0.dist-info/RECORD +39 -0
- openworkproof-1.0.0.dist-info/WHEEL +5 -0
- openworkproof-1.0.0.dist-info/entry_points.txt +2 -0
- openworkproof-1.0.0.dist-info/licenses/LICENSE +202 -0
- openworkproof-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""Execution adapter layer between AgentTeams and the OpenWorkProof
|
|
2
|
+
Developer-mode coordinators.
|
|
3
|
+
|
|
4
|
+
The adapter closes the AgentTeams <-> Developer mode loop:
|
|
5
|
+
|
|
6
|
+
- **AgentTeams side**: a team client dispatches ``TeamTask`` objects and
|
|
7
|
+
collects ``TeamTaskResult`` objects (team collaboration: assign, execute,
|
|
8
|
+
gather).
|
|
9
|
+
- **Developer side**: the protocol coordinators execute the task payload
|
|
10
|
+
(repo read, developer-mode test run, ledger status).
|
|
11
|
+
- **Adapter**: converts task payloads into coordinator calls, adapts results
|
|
12
|
+
back into team results, keeps task state in sync (PENDING -> RUNNING ->
|
|
13
|
+
SUCCEEDED/FAILED), isolates handler failures into stable error codes, and
|
|
14
|
+
logs every transition.
|
|
15
|
+
|
|
16
|
+
The layer is deliberately transport-free: any team client implementing
|
|
17
|
+
``AgentTeamClient`` (local, network, or future AgentTeams SDK) plugs in
|
|
18
|
+
unchanged.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from collections import deque
|
|
24
|
+
from dataclasses import dataclass, field
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from enum import Enum
|
|
27
|
+
import logging
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Protocol
|
|
30
|
+
|
|
31
|
+
_logger = logging.getLogger("openworkproof.execution_adapter")
|
|
32
|
+
|
|
33
|
+
SUPPORTED_KINDS = frozenset({"repo_read", "run_tests", "status"})
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TaskState(Enum):
|
|
37
|
+
PENDING = "pending"
|
|
38
|
+
RUNNING = "running"
|
|
39
|
+
SUCCEEDED = "succeeded"
|
|
40
|
+
FAILED = "failed"
|
|
41
|
+
CANCELLED = "cancelled"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class ExecutionAdapterError(Exception):
|
|
45
|
+
"""Base class for adapter-layer failures."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class UnknownTaskKindError(ExecutionAdapterError):
|
|
49
|
+
"""The task kind is not supported by the adapter."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class InvalidTaskError(ExecutionAdapterError):
|
|
53
|
+
"""The task payload or ledger reference is malformed."""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class TaskDispatchError(ExecutionAdapterError):
|
|
57
|
+
"""The team client rejected the task or its result."""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _utc_now() -> str:
|
|
61
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, slots=True)
|
|
65
|
+
class TeamTask:
|
|
66
|
+
"""One unit of work assigned by a team to the Developer mode."""
|
|
67
|
+
|
|
68
|
+
task_id: str
|
|
69
|
+
kind: str
|
|
70
|
+
ledger: str
|
|
71
|
+
payload: dict[str, object] = field(default_factory=dict)
|
|
72
|
+
state: TaskState = TaskState.PENDING
|
|
73
|
+
created_at: str = field(default_factory=_utc_now)
|
|
74
|
+
|
|
75
|
+
def with_state(self, state: TaskState) -> "TeamTask":
|
|
76
|
+
return TeamTask(
|
|
77
|
+
task_id=self.task_id,
|
|
78
|
+
kind=self.kind,
|
|
79
|
+
ledger=self.ledger,
|
|
80
|
+
payload=self.payload,
|
|
81
|
+
state=state,
|
|
82
|
+
created_at=self.created_at,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True, slots=True)
|
|
87
|
+
class TeamTaskResult:
|
|
88
|
+
"""The outcome of one team task after adapter execution."""
|
|
89
|
+
|
|
90
|
+
task_id: str
|
|
91
|
+
kind: str
|
|
92
|
+
status: TaskState
|
|
93
|
+
result: dict[str, object] = field(default_factory=dict)
|
|
94
|
+
error: str | None = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class AgentTeamClient(Protocol):
|
|
98
|
+
"""Team-collaboration contract the adapter depends on."""
|
|
99
|
+
|
|
100
|
+
def dispatch(self, task: TeamTask) -> None: ...
|
|
101
|
+
|
|
102
|
+
def collect(self) -> tuple[TeamTaskResult, ...]: ...
|
|
103
|
+
|
|
104
|
+
def list_pending(self) -> tuple[TeamTask, ...]: ...
|
|
105
|
+
|
|
106
|
+
def store_result(self, result: TeamTaskResult) -> None: ...
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class LocalTeamClient:
|
|
110
|
+
"""In-process team client for local and single-host deployment.
|
|
111
|
+
|
|
112
|
+
Tasks are dispatched into a FIFO queue and results are collected after
|
|
113
|
+
the adapter runs them. This is the reference implementation of
|
|
114
|
+
``AgentTeamClient`` and is used by the adapter tests.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def __init__(self) -> None:
|
|
118
|
+
self._queue: deque[TeamTask] = deque()
|
|
119
|
+
self._results: dict[str, TeamTaskResult] = {}
|
|
120
|
+
|
|
121
|
+
def dispatch(self, task: TeamTask) -> None:
|
|
122
|
+
self._queue.append(task)
|
|
123
|
+
_logger.debug("dispatched task %s (%s)", task.task_id, task.kind)
|
|
124
|
+
|
|
125
|
+
def collect(self) -> tuple[TeamTaskResult, ...]:
|
|
126
|
+
results = tuple(self._results.values())
|
|
127
|
+
self._results.clear()
|
|
128
|
+
return results
|
|
129
|
+
|
|
130
|
+
def list_pending(self) -> tuple[TeamTask, ...]:
|
|
131
|
+
return tuple(self._queue)
|
|
132
|
+
|
|
133
|
+
def store_result(self, result: TeamTaskResult) -> None:
|
|
134
|
+
self._results[result.task_id] = result
|
|
135
|
+
|
|
136
|
+
def _pop_pending(self) -> TeamTask | None:
|
|
137
|
+
return self._queue.popleft() if self._queue else None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TeamExecutionAdapter:
|
|
141
|
+
"""Converts team tasks into Developer-mode coordinator calls and back.
|
|
142
|
+
|
|
143
|
+
Responsibilities:
|
|
144
|
+
|
|
145
|
+
- dispatch routing: task ``kind`` selects the coordinator call;
|
|
146
|
+
- data conversion: task payload (protocol JSON) -> coordinator input,
|
|
147
|
+
coordinator result -> ``TeamTaskResult``;
|
|
148
|
+
- state synchronisation: PENDING -> RUNNING -> SUCCEEDED / FAILED;
|
|
149
|
+
- error isolation: handler failures become stable error codes;
|
|
150
|
+
- graded logging of every transition.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
def __init__(self, client: AgentTeamClient) -> None:
|
|
154
|
+
if not callable(getattr(client, "dispatch", None)) or not callable(
|
|
155
|
+
getattr(client, "collect", None)
|
|
156
|
+
):
|
|
157
|
+
raise ExecutionAdapterError("team client contract is unavailable")
|
|
158
|
+
self._client = client
|
|
159
|
+
self._running: dict[str, TeamTask] = {}
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def client(self) -> AgentTeamClient:
|
|
163
|
+
return self._client
|
|
164
|
+
|
|
165
|
+
def execute_task(self, task: TeamTask) -> TeamTaskResult:
|
|
166
|
+
"""Execute one task through the Developer-mode coordinators."""
|
|
167
|
+
if task.kind not in SUPPORTED_KINDS:
|
|
168
|
+
raise UnknownTaskKindError(f"unsupported task kind: {task.kind}")
|
|
169
|
+
self._running[task.task_id] = task.with_state(TaskState.RUNNING)
|
|
170
|
+
_logger.info(
|
|
171
|
+
"task %s running (%s, ledger=%s)",
|
|
172
|
+
task.task_id,
|
|
173
|
+
task.kind,
|
|
174
|
+
task.ledger,
|
|
175
|
+
)
|
|
176
|
+
try:
|
|
177
|
+
if task.kind == "repo_read":
|
|
178
|
+
result = self._run_repo_read(task)
|
|
179
|
+
elif task.kind == "run_tests":
|
|
180
|
+
result = self._run_run_tests(task)
|
|
181
|
+
elif task.kind == "status":
|
|
182
|
+
result = self._run_status(task)
|
|
183
|
+
else: # pragma: no cover - guarded by SUPPORTED_KINDS
|
|
184
|
+
raise UnknownTaskKindError(f"unsupported task kind: {task.kind}")
|
|
185
|
+
except Exception as error: # noqa: BLE001
|
|
186
|
+
_logger.error("task %s failed: %s", task.task_id, error)
|
|
187
|
+
outcome = TeamTaskResult(
|
|
188
|
+
task_id=task.task_id,
|
|
189
|
+
kind=task.kind,
|
|
190
|
+
status=TaskState.FAILED,
|
|
191
|
+
error=_error_code(error),
|
|
192
|
+
)
|
|
193
|
+
else:
|
|
194
|
+
_logger.info("task %s succeeded", task.task_id)
|
|
195
|
+
outcome = TeamTaskResult(
|
|
196
|
+
task_id=task.task_id,
|
|
197
|
+
kind=task.kind,
|
|
198
|
+
status=TaskState.SUCCEEDED,
|
|
199
|
+
result=result,
|
|
200
|
+
)
|
|
201
|
+
self._running.pop(task.task_id, None)
|
|
202
|
+
return outcome
|
|
203
|
+
|
|
204
|
+
def run_pending_tasks(
|
|
205
|
+
self, *, max_tasks: int | None = None
|
|
206
|
+
) -> tuple[TeamTaskResult, ...]:
|
|
207
|
+
"""Drain the team queue: dispatch, execute, and stage results.
|
|
208
|
+
|
|
209
|
+
Returns the executed results after they are handed back to the team
|
|
210
|
+
client via ``collect``.
|
|
211
|
+
"""
|
|
212
|
+
pending = self._client.list_pending()
|
|
213
|
+
if max_tasks is not None:
|
|
214
|
+
pending = pending[:max_tasks]
|
|
215
|
+
outcomes: list[TeamTaskResult] = []
|
|
216
|
+
for task in pending:
|
|
217
|
+
outcome = self.execute_task(task)
|
|
218
|
+
if not callable(getattr(self._client, "store_result", None)):
|
|
219
|
+
raise TaskDispatchError("team client cannot store results")
|
|
220
|
+
self._client.store_result(outcome)
|
|
221
|
+
outcomes.append(outcome)
|
|
222
|
+
return tuple(outcomes)
|
|
223
|
+
|
|
224
|
+
# ---- coordinator routing -------------------------------------------------
|
|
225
|
+
|
|
226
|
+
def _run_repo_read(self, task: TeamTask) -> dict[str, object]:
|
|
227
|
+
from openworkproof import cli
|
|
228
|
+
|
|
229
|
+
return _strip_schema(cli.cli_repo_read(task.ledger, task.payload))
|
|
230
|
+
|
|
231
|
+
def _run_run_tests(self, task: TeamTask) -> dict[str, object]:
|
|
232
|
+
from openworkproof import cli
|
|
233
|
+
|
|
234
|
+
return _strip_schema(cli.cli_run_tests(task.ledger, task.payload))
|
|
235
|
+
|
|
236
|
+
def _run_status(self, task: TeamTask) -> dict[str, object]:
|
|
237
|
+
from openworkproof import cli
|
|
238
|
+
|
|
239
|
+
return _strip_schema(cli.cli_status(task.ledger))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _strip_schema(result: dict[str, object]) -> dict[str, object]:
|
|
243
|
+
return {key: value for key, value in result.items() if key != "schema_version"}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _error_code(error: Exception) -> str:
|
|
247
|
+
from openworkproof.cli import CliError
|
|
248
|
+
from openworkproof.mcp_server import HandlerCoordinationError
|
|
249
|
+
|
|
250
|
+
if isinstance(error, CliError):
|
|
251
|
+
return "CLI_TRANSPORT_ERROR"
|
|
252
|
+
if isinstance(error, HandlerCoordinationError):
|
|
253
|
+
return "HANDLER_COORDINATION_ERROR"
|
|
254
|
+
if isinstance(error, UnknownTaskKindError):
|
|
255
|
+
return "UNKNOWN_TASK_KIND"
|
|
256
|
+
return "INTERNAL_ERROR"
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Reference external Acceptor service and client for delivery validation.
|
|
2
|
+
|
|
3
|
+
The external Acceptor is a *separate process* that holds only the Acceptor
|
|
4
|
+
private key and the draft messages sent to it over a real TCP connection. It
|
|
5
|
+
never touches the system ledger, the system keys, or the evidence root.
|
|
6
|
+
|
|
7
|
+
Protocol (JSON lines over TCP):
|
|
8
|
+
|
|
9
|
+
- request ``{"action": "sign-acceptance", "draft": <AcceptanceReceipt json>}``
|
|
10
|
+
- request ``{"action": "sign-rejection", "rejection": <raw json>}``
|
|
11
|
+
- response ``{"ok": true, "receipt": <signed receipt json>}``
|
|
12
|
+
- response ``{"ok": false, "error": <str>}``
|
|
13
|
+
|
|
14
|
+
Behavioural fault injection (for V6/V7 verification):
|
|
15
|
+
|
|
16
|
+
- ``OWP_ACCEPTOR_DELAY_MS`` — respond after N ms (client timeout test)
|
|
17
|
+
- ``OWP_ACCEPTOR_DROP`` — close the connection without a response
|
|
18
|
+
- ``OWP_ACCEPTOR_REFUSE`` — reject with an error response
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
import socket
|
|
27
|
+
import sys
|
|
28
|
+
import threading
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
|
|
32
|
+
|
|
33
|
+
from openworkproof.models import AcceptanceReceipt, AcceptanceRejectionReceipt
|
|
34
|
+
from openworkproof.signing import sign_payload
|
|
35
|
+
|
|
36
|
+
DEFAULT_HOST = "127.0.0.1"
|
|
37
|
+
DEFAULT_PORT = 18741
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def load_acceptor_key(key_hex: str) -> Ed25519PrivateKey:
|
|
41
|
+
raw = bytes.fromhex(key_hex)
|
|
42
|
+
if len(raw) != 32:
|
|
43
|
+
raise ValueError("acceptor key must be 32 raw bytes")
|
|
44
|
+
return Ed25519PrivateKey.from_private_bytes(raw)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ExternalAcceptorServer:
|
|
48
|
+
"""A minimal TCP server that signs acceptance/rejection drafts."""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
private_key: Ed25519PrivateKey,
|
|
53
|
+
*,
|
|
54
|
+
host: str = DEFAULT_HOST,
|
|
55
|
+
port: int = DEFAULT_PORT,
|
|
56
|
+
delay_ms: int = 0,
|
|
57
|
+
drop: bool = False,
|
|
58
|
+
refuse: bool = False,
|
|
59
|
+
) -> None:
|
|
60
|
+
self._key = private_key
|
|
61
|
+
self._host = host
|
|
62
|
+
self._port = port
|
|
63
|
+
self._delay_ms = delay_ms
|
|
64
|
+
self._drop = drop
|
|
65
|
+
self._refuse = refuse
|
|
66
|
+
self._socket: socket.socket | None = None
|
|
67
|
+
self._thread: threading.Thread | None = None
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def port(self) -> int:
|
|
71
|
+
return self._port
|
|
72
|
+
|
|
73
|
+
def start(self) -> None:
|
|
74
|
+
self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
75
|
+
self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
76
|
+
self._socket.bind((self._host, self._port))
|
|
77
|
+
self._socket.listen(4)
|
|
78
|
+
self._thread = threading.Thread(target=self._serve, daemon=True)
|
|
79
|
+
self._thread.start()
|
|
80
|
+
|
|
81
|
+
def stop(self) -> None:
|
|
82
|
+
if self._socket is not None:
|
|
83
|
+
try:
|
|
84
|
+
self._socket.close()
|
|
85
|
+
except OSError:
|
|
86
|
+
pass
|
|
87
|
+
self._socket = None
|
|
88
|
+
|
|
89
|
+
def _serve(self) -> None:
|
|
90
|
+
assert self._socket is not None
|
|
91
|
+
while True:
|
|
92
|
+
try:
|
|
93
|
+
conn, _ = self._socket.accept()
|
|
94
|
+
except OSError:
|
|
95
|
+
return
|
|
96
|
+
try:
|
|
97
|
+
self._handle(conn)
|
|
98
|
+
finally:
|
|
99
|
+
try:
|
|
100
|
+
conn.close()
|
|
101
|
+
except OSError:
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
def _handle(self, conn: socket.socket) -> None:
|
|
105
|
+
try:
|
|
106
|
+
raw = conn.recv(65536)
|
|
107
|
+
except OSError:
|
|
108
|
+
return
|
|
109
|
+
if not raw:
|
|
110
|
+
return
|
|
111
|
+
if self._delay_ms:
|
|
112
|
+
threading.Event().wait(self._delay_ms / 1000.0)
|
|
113
|
+
if self._drop:
|
|
114
|
+
return
|
|
115
|
+
try:
|
|
116
|
+
request = json.loads(raw.decode("utf-8"))
|
|
117
|
+
if self._refuse:
|
|
118
|
+
response = {"ok": False, "error": "acceptor refused the request"}
|
|
119
|
+
else:
|
|
120
|
+
response = self._dispatch(request)
|
|
121
|
+
except Exception as error: # noqa: BLE001
|
|
122
|
+
response = {"ok": False, "error": repr(error)}
|
|
123
|
+
try:
|
|
124
|
+
conn.sendall((json.dumps(response) + "\n").encode("utf-8"))
|
|
125
|
+
except OSError:
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
def _dispatch(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
129
|
+
action = request.get("action")
|
|
130
|
+
if action == "sign-acceptance":
|
|
131
|
+
draft = request["draft"]
|
|
132
|
+
if type(draft) is not dict:
|
|
133
|
+
return {"ok": False, "error": "draft must be an object"}
|
|
134
|
+
signed = sign_payload("acceptance-receipt", draft, self._key)
|
|
135
|
+
return {"ok": True, "receipt": signed}
|
|
136
|
+
if action == "sign-rejection":
|
|
137
|
+
rejection = request["rejection"]
|
|
138
|
+
if type(rejection) is not dict:
|
|
139
|
+
return {"ok": False, "error": "rejection must be an object"}
|
|
140
|
+
signed = sign_payload(
|
|
141
|
+
"acceptance-rejection-receipt", rejection, self._key
|
|
142
|
+
)
|
|
143
|
+
return {"ok": True, "receipt": signed}
|
|
144
|
+
return {"ok": False, "error": "unknown action"}
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class ExternalAcceptorClient:
|
|
148
|
+
"""Client side: send drafts to the external Acceptor over TCP."""
|
|
149
|
+
|
|
150
|
+
def __init__(self, *, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT, timeout: float = 5.0):
|
|
151
|
+
self._host = host
|
|
152
|
+
self._port = port
|
|
153
|
+
self._timeout = timeout
|
|
154
|
+
|
|
155
|
+
def sign_acceptance(self, draft) -> AcceptanceReceipt:
|
|
156
|
+
"""Sign an externally signable acceptance draft over the socket."""
|
|
157
|
+
from openworkproof.acceptance import AcceptanceSigningDraft
|
|
158
|
+
|
|
159
|
+
if not isinstance(draft, AcceptanceSigningDraft):
|
|
160
|
+
raise TypeError("expected an AcceptanceSigningDraft")
|
|
161
|
+
request = {
|
|
162
|
+
"action": "sign-acceptance",
|
|
163
|
+
"draft": json.loads(draft.canonical_payload),
|
|
164
|
+
}
|
|
165
|
+
response = self._round_trip(request)
|
|
166
|
+
return AcceptanceReceipt.model_validate(response["receipt"])
|
|
167
|
+
|
|
168
|
+
def sign_rejection(self, rejection: dict[str, object]) -> AcceptanceRejectionReceipt:
|
|
169
|
+
request = {"action": "sign-rejection", "rejection": rejection}
|
|
170
|
+
response = self._round_trip(request)
|
|
171
|
+
return AcceptanceRejectionReceipt.model_validate(response["receipt"])
|
|
172
|
+
|
|
173
|
+
def _round_trip(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
174
|
+
with socket.create_connection(
|
|
175
|
+
(self._host, self._port), timeout=self._timeout
|
|
176
|
+
) as conn:
|
|
177
|
+
conn.settimeout(self._timeout)
|
|
178
|
+
conn.sendall((json.dumps(request) + "\n").encode("utf-8"))
|
|
179
|
+
data = conn.recv(65536)
|
|
180
|
+
if not data:
|
|
181
|
+
raise ConnectionError("external acceptor closed the connection without a response")
|
|
182
|
+
response = json.loads(data.decode("utf-8"))
|
|
183
|
+
if not response.get("ok"):
|
|
184
|
+
raise RuntimeError(response.get("error", "external acceptor refused"))
|
|
185
|
+
return response
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def main() -> int:
|
|
189
|
+
"""Subprocess entry point: ``python -m openworkproof.external_acceptor``."""
|
|
190
|
+
key_hex = os.environ["OWP_ACCEPTOR_KEY_HEX"]
|
|
191
|
+
host = os.environ.get("OWP_ACCEPTOR_HOST", DEFAULT_HOST)
|
|
192
|
+
port = int(os.environ.get("OWP_ACCEPTOR_PORT", str(DEFAULT_PORT)))
|
|
193
|
+
delay_ms = int(os.environ.get("OWP_ACCEPTOR_DELAY_MS", "0"))
|
|
194
|
+
drop = os.environ.get("OWP_ACCEPTOR_DROP", "") == "1"
|
|
195
|
+
refuse = os.environ.get("OWP_ACCEPTOR_REFUSE", "") == "1"
|
|
196
|
+
server = ExternalAcceptorServer(
|
|
197
|
+
load_acceptor_key(key_hex),
|
|
198
|
+
host=host,
|
|
199
|
+
port=port,
|
|
200
|
+
delay_ms=delay_ms,
|
|
201
|
+
drop=drop,
|
|
202
|
+
refuse=refuse,
|
|
203
|
+
)
|
|
204
|
+
server.start()
|
|
205
|
+
# Keep the process alive until terminated.
|
|
206
|
+
threading.Event().wait()
|
|
207
|
+
return 0
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
if __name__ == "__main__":
|
|
211
|
+
raise SystemExit(main())
|