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.
Files changed (39) hide show
  1. openworkproof/__init__.py +1 -0
  2. openworkproof/acceptance.py +3006 -0
  3. openworkproof/cli.py +164 -0
  4. openworkproof/composition.py +832 -0
  5. openworkproof/evidence.py +8281 -0
  6. openworkproof/execution_adapter.py +256 -0
  7. openworkproof/external_acceptor.py +211 -0
  8. openworkproof/mcp_server.py +2939 -0
  9. openworkproof/mcp_transport.py +72 -0
  10. openworkproof/models.py +3380 -0
  11. openworkproof/policy.py +2494 -0
  12. openworkproof/predicates.py +333 -0
  13. openworkproof/repo_pipeline/__init__.py +74 -0
  14. openworkproof/repo_pipeline/analysis.py +152 -0
  15. openworkproof/repo_pipeline/errors.py +64 -0
  16. openworkproof/repo_pipeline/models.py +80 -0
  17. openworkproof/repo_pipeline/output.py +68 -0
  18. openworkproof/repo_pipeline/reader.py +47 -0
  19. openworkproof/repo_pipeline/sources.py +120 -0
  20. openworkproof/repo_pipeline/traversal.py +253 -0
  21. openworkproof/repo_tools.py +8382 -0
  22. openworkproof/runtime_context.py +189 -0
  23. openworkproof/schema_registry.py +623 -0
  24. openworkproof/schemas/v0.1/acceptance-receipt.schema.json +1 -0
  25. openworkproof/schemas/v0.1/acceptance-rejection-receipt.schema.json +1 -0
  26. openworkproof/schemas/v0.1/action-receipt.schema.json +1 -0
  27. openworkproof/schemas/v0.1/capability-grant.schema.json +1 -0
  28. openworkproof/schemas/v0.1/schema-registry.json +1 -0
  29. openworkproof/schemas/v0.1/work-order.schema.json +1 -0
  30. openworkproof/signing.py +421 -0
  31. openworkproof/state.py +765 -0
  32. openworkproof/team_network_client.py +419 -0
  33. openworkproof/trusted_helper.py +269 -0
  34. openworkproof-1.0.0.dist-info/METADATA +578 -0
  35. openworkproof-1.0.0.dist-info/RECORD +39 -0
  36. openworkproof-1.0.0.dist-info/WHEEL +5 -0
  37. openworkproof-1.0.0.dist-info/entry_points.txt +2 -0
  38. openworkproof-1.0.0.dist-info/licenses/LICENSE +202 -0
  39. 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())