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,419 @@
1
+ """Real network client for the AgentTeams execution layer.
2
+
3
+ The client implements the ``AgentTeamClient`` contract (see
4
+ ``execution_adapter``) over a real TCP connection, so the team -> developer
5
+ loop works across processes and hosts, not just in-memory.
6
+
7
+ Network protocol (JSON lines over a persistent TCP session):
8
+
9
+ - ``{"action": "auth", "token": <str>}`` -> ``{"ok": true}``
10
+ - ``{"action": "dispatch", "task": <TeamTask>}`` -> ``{"ok": true}``
11
+ - ``{"action": "list-pending"}`` -> ``{"ok": true, "tasks": [...]}``
12
+ - ``{"action": "store-result", "result": <...>}``-> ``{"ok": true}``
13
+ - ``{"action": "collect"}`` -> ``{"ok": true, "results": [...]}``
14
+
15
+ Config comes from environment variables so the client deploys unchanged:
16
+
17
+ - ``OWP_TEAM_ENDPOINT`` — ``host:port`` (default ``127.0.0.1:18742``)
18
+ - ``OWP_TEAM_TOKEN`` — shared auth token (empty disables auth)
19
+ - ``OWP_TEAM_TIMEOUT`` — socket timeout seconds (default 5.0)
20
+ - ``OWP_TEAM_MAX_RETRIES`` — connect/request retries (default 3)
21
+
22
+ Note on the Alibaba Cloud AgentTeams SDK: a public SDK exists
23
+ (``@alicloud/agentteams20260605``) but currently ships only TypeScript/Java/
24
+ Swift/PHP, and its API surface is the governance/management plane
25
+ (workspaces, identity, policy) rather than task dispatch/recovery. Until a
26
+ Python SDK with an execution-plane API is published, this module provides the
27
+ real network transport for the task lifecycle over a documented protocol; the
28
+ adapter boundary in ``execution_adapter.py`` is where a future SDK would plug
29
+ in without changing the business layer.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import json
35
+ import logging
36
+ import os
37
+ from pathlib import Path
38
+ import socket
39
+ import threading
40
+ import time
41
+ from typing import Any
42
+
43
+ from openworkproof.execution_adapter import (
44
+ TaskState,
45
+ TeamTask,
46
+ TeamTaskResult,
47
+ )
48
+
49
+ _logger = logging.getLogger("openworkproof.team_network_client")
50
+
51
+ DEFAULT_HOST = "127.0.0.1"
52
+ DEFAULT_PORT = 18742
53
+ DEFAULT_TIMEOUT = 5.0
54
+ DEFAULT_MAX_RETRIES = 3
55
+ DEFAULT_BACKOFF = 0.2
56
+
57
+ _ACTION_PATHS = frozenset(
58
+ {"auth", "dispatch", "list-pending", "store-result", "collect"}
59
+ )
60
+
61
+
62
+ class TeamNetworkError(Exception):
63
+ """Base class for network-layer team client failures."""
64
+
65
+
66
+ class TeamAuthenticationError(TeamNetworkError):
67
+ """The remote team service rejected the auth token."""
68
+
69
+
70
+ class TeamTimeoutError(TeamNetworkError):
71
+ """A request exceeded the configured timeout."""
72
+
73
+
74
+ class TeamProtocolError(TeamNetworkError):
75
+ """The remote service returned a malformed or refused response."""
76
+
77
+
78
+ class TeamNetworkConfig:
79
+ """Resolved client configuration (env-overridable)."""
80
+
81
+ def __init__(
82
+ self,
83
+ *,
84
+ host: str = DEFAULT_HOST,
85
+ port: int = DEFAULT_PORT,
86
+ token: str = "",
87
+ timeout: float = DEFAULT_TIMEOUT,
88
+ max_retries: int = DEFAULT_MAX_RETRIES,
89
+ backoff: float = DEFAULT_BACKOFF,
90
+ ) -> None:
91
+ if not isinstance(host, str) or not host:
92
+ raise TeamNetworkError("team endpoint host is invalid")
93
+ if type(port) is not int or not 1 <= port <= 65535:
94
+ raise TeamNetworkError("team endpoint port is invalid")
95
+ if type(timeout) not in {int, float} or timeout <= 0:
96
+ raise TeamNetworkError("team timeout is invalid")
97
+ if type(max_retries) is not int or max_retries < 0:
98
+ raise TeamNetworkError("team max retries is invalid")
99
+ if type(backoff) not in {int, float} or backoff < 0:
100
+ raise TeamNetworkError("team backoff is invalid")
101
+ self.host = host
102
+ self.port = port
103
+ self.token = token
104
+ self.timeout = timeout
105
+ self.max_retries = max_retries
106
+ self.backoff = backoff
107
+
108
+ @classmethod
109
+ def from_env(cls) -> "TeamNetworkConfig":
110
+ endpoint = os.environ.get("OWP_TEAM_ENDPOINT")
111
+ host, port = DEFAULT_HOST, DEFAULT_PORT
112
+ if endpoint:
113
+ parts = endpoint.rsplit(":", 1)
114
+ if len(parts) == 2 and parts[1].isdigit():
115
+ host, port = parts[0], int(parts[1])
116
+ return cls(
117
+ host=host,
118
+ port=port,
119
+ token=os.environ.get("OWP_TEAM_TOKEN", ""),
120
+ timeout=float(os.environ.get("OWP_TEAM_TIMEOUT", str(DEFAULT_TIMEOUT))),
121
+ max_retries=int(
122
+ os.environ.get("OWP_TEAM_MAX_RETRIES", str(DEFAULT_MAX_RETRIES))
123
+ ),
124
+ backoff=float(os.environ.get("OWP_TEAM_BACKOFF", str(DEFAULT_BACKOFF))),
125
+ )
126
+
127
+
128
+ class TeamNetworkClient:
129
+ """Real TCP client implementing the AgentTeamClient contract."""
130
+
131
+ def __init__(self, config: TeamNetworkConfig | None = None) -> None:
132
+ self._config = config or TeamNetworkConfig.from_env()
133
+ self._conn: socket.socket | None = None
134
+ self._lock = threading.Lock()
135
+
136
+ # ---- lifecycle ---------------------------------------------------------
137
+
138
+ def connect(self) -> None:
139
+ """Open the TCP session and perform the auth handshake."""
140
+ self._ensure_connected()
141
+ if self._config.token:
142
+ self._request({"action": "auth", "token": self._config.token}, authed=False)
143
+
144
+ def disconnect(self) -> None:
145
+ with self._lock:
146
+ if self._conn is not None:
147
+ try:
148
+ self._conn.close()
149
+ except OSError:
150
+ pass
151
+ self._conn = None
152
+
153
+ # ---- AgentTeamClient contract -------------------------------------------
154
+
155
+ def dispatch(self, task: TeamTask) -> None:
156
+ self._with_retry(
157
+ lambda: self._request(
158
+ {"action": "dispatch", "task": _task_to_dict(task)}
159
+ )
160
+ )
161
+
162
+ def list_pending(self) -> tuple[TeamTask, ...]:
163
+ response = self._with_retry(
164
+ lambda: self._request({"action": "list-pending"})
165
+ )
166
+ return tuple(_task_from_dict(item) for item in response["tasks"])
167
+
168
+ def store_result(self, result: TeamTaskResult) -> None:
169
+ self._with_retry(
170
+ lambda: self._request(
171
+ {"action": "store-result", "result": _result_to_dict(result)}
172
+ )
173
+ )
174
+
175
+ def collect(self) -> tuple[TeamTaskResult, ...]:
176
+ response = self._with_retry(lambda: self._request({"action": "collect"}))
177
+ return tuple(_result_from_dict(item) for item in response["results"])
178
+
179
+ # ---- internals ----------------------------------------------------------
180
+
181
+ def _ensure_connected(self) -> socket.socket:
182
+ with self._lock:
183
+ if self._conn is not None:
184
+ return self._conn
185
+ _logger.info(
186
+ "connecting to team service %s:%s",
187
+ self._config.host,
188
+ self._config.port,
189
+ )
190
+ try:
191
+ conn = socket.create_connection(
192
+ (self._config.host, self._config.port),
193
+ timeout=self._config.timeout,
194
+ )
195
+ except OSError as error:
196
+ raise TeamNetworkError(
197
+ f"cannot connect to team service: {error}"
198
+ ) from error
199
+ conn.settimeout(self._config.timeout)
200
+ self._conn = conn
201
+ return conn
202
+
203
+ def _request(
204
+ self, request: dict[str, Any], *, authed: bool = True
205
+ ) -> dict[str, Any]:
206
+ conn = self._ensure_connected()
207
+ try:
208
+ conn.sendall((json.dumps(request) + "\n").encode("utf-8"))
209
+ data = conn.recv(65536)
210
+ except socket.timeout as error:
211
+ self.disconnect()
212
+ raise TeamTimeoutError("team request timed out") from error
213
+ except OSError as error:
214
+ self.disconnect()
215
+ raise TeamNetworkError(f"team request failed: {error}") from error
216
+ if not data:
217
+ self.disconnect()
218
+ raise TeamNetworkError("team service closed the connection")
219
+ try:
220
+ response = json.loads(data.decode("utf-8"))
221
+ except (ValueError, UnicodeDecodeError) as error:
222
+ self.disconnect()
223
+ raise TeamProtocolError("team response is not JSON") from error
224
+ if authed and not response.get("ok"):
225
+ raise TeamProtocolError(response.get("error", "team refused the request"))
226
+ return response
227
+
228
+ def _with_retry(self, operation):
229
+ last_error: Exception | None = None
230
+ for attempt in range(self._config.max_retries + 1):
231
+ try:
232
+ return operation()
233
+ except (TeamNetworkError, TeamTimeoutError) as error:
234
+ last_error = error
235
+ if attempt < self._config.max_retries:
236
+ self.disconnect()
237
+ delay = self._config.backoff * (2**attempt)
238
+ _logger.warning(
239
+ "team request attempt %d/%d failed: %s; retrying in %.2fs",
240
+ attempt + 1,
241
+ self._config.max_retries + 1,
242
+ error,
243
+ delay,
244
+ )
245
+ time.sleep(delay)
246
+ assert last_error is not None
247
+ raise last_error
248
+
249
+
250
+ def _task_to_dict(task: TeamTask) -> dict[str, Any]:
251
+ return {
252
+ "task_id": task.task_id,
253
+ "kind": task.kind,
254
+ "ledger": task.ledger,
255
+ "payload": task.payload,
256
+ "state": task.state.value,
257
+ "created_at": task.created_at,
258
+ }
259
+
260
+
261
+ def _task_from_dict(data: dict[str, Any]) -> TeamTask:
262
+ return TeamTask(
263
+ task_id=data["task_id"],
264
+ kind=data["kind"],
265
+ ledger=data["ledger"],
266
+ payload=data.get("payload", {}),
267
+ state=TaskState(data["state"]),
268
+ created_at=data.get("created_at", ""),
269
+ )
270
+
271
+
272
+ def _result_to_dict(result: TeamTaskResult) -> dict[str, Any]:
273
+ return {
274
+ "task_id": result.task_id,
275
+ "kind": result.kind,
276
+ "status": result.status.value,
277
+ "result": result.result,
278
+ "error": result.error,
279
+ }
280
+
281
+
282
+ def _result_from_dict(data: dict[str, Any]) -> TeamTaskResult:
283
+ return TeamTaskResult(
284
+ task_id=data["task_id"],
285
+ kind=data["kind"],
286
+ status=TaskState(data["status"]),
287
+ result=data.get("result", {}),
288
+ error=data.get("error"),
289
+ )
290
+
291
+
292
+ class TeamNetworkService:
293
+ """Reference TCP team service implementing the documented protocol."""
294
+
295
+ def __init__(
296
+ self,
297
+ *,
298
+ host: str = DEFAULT_HOST,
299
+ port: int = DEFAULT_PORT,
300
+ token: str = "",
301
+ ) -> None:
302
+ self._host = host
303
+ self._port = port
304
+ self._token = token
305
+ self._queue: list[TeamTask] = []
306
+ self._results: dict[str, TeamTaskResult] = {}
307
+ self._socket: socket.socket | None = None
308
+ self._thread: threading.Thread | None = None
309
+
310
+ @property
311
+ def port(self) -> int:
312
+ return self._port
313
+
314
+ def start(self) -> None:
315
+ self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
316
+ self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
317
+ self._socket.bind((self._host, self._port))
318
+ self._socket.listen(4)
319
+ self._thread = threading.Thread(target=self._serve, daemon=True)
320
+ self._thread.start()
321
+
322
+ def stop(self) -> None:
323
+ if self._socket is not None:
324
+ try:
325
+ self._socket.close()
326
+ except OSError:
327
+ pass
328
+ self._socket = None
329
+
330
+ def _serve(self) -> None:
331
+ assert self._socket is not None
332
+ while True:
333
+ try:
334
+ conn, _ = self._socket.accept()
335
+ except OSError:
336
+ return
337
+ try:
338
+ self._handle(conn)
339
+ finally:
340
+ try:
341
+ conn.close()
342
+ except OSError:
343
+ pass
344
+
345
+ def _handle(self, conn: socket.socket) -> None:
346
+ authed = not self._token
347
+ while True:
348
+ try:
349
+ data = conn.recv(65536)
350
+ except OSError:
351
+ return
352
+ if not data:
353
+ return
354
+ for line in data.decode("utf-8").splitlines():
355
+ if not line:
356
+ continue
357
+ try:
358
+ request = json.loads(line)
359
+ except ValueError:
360
+ self._send(conn, {"ok": False, "error": "bad json"})
361
+ continue
362
+ if request.get("action") == "auth":
363
+ authed = request.get("token") == self._token
364
+ self._send(conn, {"ok": authed})
365
+ continue
366
+ if not authed:
367
+ self._send(conn, {"ok": False, "error": "not authenticated"})
368
+ continue
369
+ self._send(conn, self._dispatch(request))
370
+
371
+ def _dispatch(self, request: dict[str, Any]) -> dict[str, Any]:
372
+ action = request.get("action")
373
+ if action == "dispatch":
374
+ task = _task_from_dict(request["task"])
375
+ self._queue.append(task)
376
+ _logger.debug("service dispatched task %s", task.task_id)
377
+ return {"ok": True}
378
+ if action == "list-pending":
379
+ return {
380
+ "ok": True,
381
+ "tasks": [_task_to_dict(task) for task in self._queue],
382
+ }
383
+ if action == "store-result":
384
+ result = _result_from_dict(request["result"])
385
+ self._results[result.task_id] = result
386
+ return {"ok": True}
387
+ if action == "collect":
388
+ results = tuple(self._results.values())
389
+ self._results.clear()
390
+ return {
391
+ "ok": True,
392
+ "results": [_result_to_dict(item) for item in results],
393
+ }
394
+ return {"ok": False, "error": f"unknown action: {action}"}
395
+
396
+ @staticmethod
397
+ def _send(conn: socket.socket, payload: dict[str, Any]) -> None:
398
+ try:
399
+ conn.sendall((json.dumps(payload) + "\n").encode("utf-8"))
400
+ except OSError:
401
+ pass
402
+
403
+
404
+ def main() -> int:
405
+ """Subprocess entry: ``python -m openworkproof.team_network_client``."""
406
+ endpoint = os.environ.get("OWP_TEAM_ENDPOINT", f"{DEFAULT_HOST}:{DEFAULT_PORT}")
407
+ host, port = endpoint.rsplit(":", 1)
408
+ service = TeamNetworkService(
409
+ host=host,
410
+ port=int(port),
411
+ token=os.environ.get("OWP_TEAM_TOKEN", ""),
412
+ )
413
+ service.start()
414
+ threading.Event().wait()
415
+ return 0
416
+
417
+
418
+ if __name__ == "__main__":
419
+ raise SystemExit(main())
@@ -0,0 +1,269 @@
1
+ """Fixed trusted-helper request dispatcher."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import base64
6
+ import hashlib
7
+ import json
8
+ from pathlib import Path
9
+ import re
10
+ import sys
11
+ from typing import BinaryIO, Sequence
12
+
13
+ import rfc8785
14
+
15
+ from openworkproof import repo_tools
16
+ from openworkproof.models import RepoReadOutput
17
+
18
+
19
+ REQUEST_SCHEMA = "openworkproof-trusted-helper-request/0.1"
20
+ RESPONSE_SCHEMA = "openworkproof-trusted-helper-response/0.1"
21
+ MAX_REQUEST_BYTES = 8_192
22
+ RUNTIME_ROOT = Path("/runtime")
23
+ EXIT_BY_CODE = {
24
+ "REQUEST_INVALID": 64,
25
+ "RECOVERY_REQUIRED": 65,
26
+ "PATH_DENIED": 66,
27
+ "FILE_CHANGED": 67,
28
+ "INTERNAL_ERROR": 70,
29
+ }
30
+ _INTERNAL_ERROR_BYTES = (
31
+ b'{"code":"INTERNAL_ERROR","schema_version":'
32
+ b'"openworkproof-trusted-helper-response/0.1","status":"error"}'
33
+ )
34
+
35
+ _REQUEST_KEYS = frozenset(
36
+ {
37
+ "schema_version",
38
+ "operation",
39
+ "workspace_id",
40
+ "source_artifact_sha256",
41
+ "expected_head_commit",
42
+ "expected_workspace_manifest_digest",
43
+ "path",
44
+ }
45
+ )
46
+ _CANDIDATE_ERROR_CODES = (
47
+ "RECOVERY_REQUIRED",
48
+ "PATH_DENIED",
49
+ "FILE_CHANGED",
50
+ )
51
+ _DIGEST_PATTERN = re.compile(r"^[0-9a-f]{64}$")
52
+ _COMMIT_PATTERN = re.compile(r"^[0-9a-f]{40}$")
53
+ _RESULT_KEYS = frozenset(
54
+ {
55
+ "path",
56
+ "content_sha256",
57
+ "size_bytes",
58
+ "workspace_manifest_digest",
59
+ }
60
+ )
61
+
62
+
63
+ class _RequestInvalid(ValueError):
64
+ """The request is not the frozen canonical dispatcher representation."""
65
+
66
+
67
+ def _object_without_duplicate_keys(
68
+ pairs: list[tuple[str, object]],
69
+ ) -> dict[str, object]:
70
+ value: dict[str, object] = {}
71
+ for key, item in pairs:
72
+ if key in value:
73
+ raise _RequestInvalid
74
+ value[key] = item
75
+ return value
76
+
77
+
78
+ def _reject_json_number(unused: str) -> None:
79
+ raise _RequestInvalid
80
+
81
+
82
+ def _parse_request(raw: bytes, runtime_root: Path) -> repo_tools.CandidateReadRequest:
83
+ if type(raw) is not bytes or not 1 <= len(raw) <= MAX_REQUEST_BYTES:
84
+ raise _RequestInvalid
85
+ try:
86
+ value = json.loads(
87
+ raw,
88
+ object_pairs_hook=_object_without_duplicate_keys,
89
+ parse_float=_reject_json_number,
90
+ parse_int=_reject_json_number,
91
+ parse_constant=_reject_json_number,
92
+ )
93
+ canonical = rfc8785.dumps(value)
94
+ if type(canonical) is not bytes:
95
+ raise TypeError("canonicalizer did not return bytes")
96
+ if canonical != raw:
97
+ raise _RequestInvalid
98
+ except (
99
+ _RequestInvalid,
100
+ json.JSONDecodeError,
101
+ UnicodeDecodeError,
102
+ RecursionError,
103
+ rfc8785.CanonicalizationError,
104
+ ):
105
+ raise _RequestInvalid from None
106
+ if type(value) is not dict or frozenset(value) != _REQUEST_KEYS:
107
+ raise _RequestInvalid
108
+ if any(type(value[key]) is not str for key in _REQUEST_KEYS):
109
+ raise _RequestInvalid
110
+ if (
111
+ value["schema_version"] != REQUEST_SCHEMA
112
+ or value["operation"] != "repo_read"
113
+ or _DIGEST_PATTERN.fullmatch(value["workspace_id"]) is None
114
+ or _DIGEST_PATTERN.fullmatch(value["source_artifact_sha256"]) is None
115
+ or _COMMIT_PATTERN.fullmatch(value["expected_head_commit"]) is None
116
+ or _DIGEST_PATTERN.fullmatch(
117
+ value["expected_workspace_manifest_digest"]
118
+ )
119
+ is None
120
+ ):
121
+ raise _RequestInvalid
122
+ try:
123
+ repo_tools.validate_canonical_relative_path(value["path"])
124
+ except repo_tools.PathError:
125
+ raise _RequestInvalid from None
126
+ return repo_tools.CandidateReadRequest(
127
+ runtime_root=runtime_root,
128
+ workspace_id=value["workspace_id"],
129
+ source_artifact_sha256=value["source_artifact_sha256"],
130
+ expected_head_commit=value["expected_head_commit"],
131
+ expected_workspace_manifest_digest=(
132
+ value["expected_workspace_manifest_digest"]
133
+ ),
134
+ path=value["path"],
135
+ )
136
+
137
+
138
+ def _error_response(code: str) -> dict[str, str]:
139
+ return {
140
+ "schema_version": RESPONSE_SCHEMA,
141
+ "status": "error",
142
+ "code": code,
143
+ }
144
+
145
+
146
+ def _closed_candidate_error_code(
147
+ error: repo_tools.CandidateReadError,
148
+ ) -> str:
149
+ try:
150
+ code = error.code
151
+ except BaseException:
152
+ return "INTERNAL_ERROR"
153
+ if type(code) is str and code in _CANDIDATE_ERROR_CODES:
154
+ return code
155
+ return "INTERNAL_ERROR"
156
+
157
+
158
+ def _exit_for_code(code: str) -> int:
159
+ if code == "REQUEST_INVALID":
160
+ return 64
161
+ if code == "RECOVERY_REQUIRED":
162
+ return 65
163
+ if code == "PATH_DENIED":
164
+ return 66
165
+ if code == "FILE_CHANGED":
166
+ return 67
167
+ return 70
168
+
169
+
170
+ def _validated_success_response(
171
+ result: object,
172
+ request: repo_tools.CandidateReadRequest,
173
+ ) -> dict[str, object]:
174
+ if type(result) is not repo_tools.CandidateReadResult:
175
+ raise TypeError("candidate read result is not closed")
176
+ if type(result.content) is not bytes:
177
+ raise TypeError("candidate read content is not closed")
178
+ if type(result.output) is not RepoReadOutput:
179
+ raise TypeError("candidate read output is not closed")
180
+ content_sha256 = hashlib.sha256(result.content).hexdigest()
181
+ if (
182
+ result.output.path != request.path
183
+ or result.output.workspace_manifest_digest
184
+ != request.expected_workspace_manifest_digest
185
+ or result.output.size_bytes != len(result.content)
186
+ or result.output.content_sha256 != content_sha256
187
+ ):
188
+ raise ValueError("candidate read result binding mismatch")
189
+ dumped = result.output.model_dump(mode="json")
190
+ if type(dumped) is not dict or frozenset(dumped) != _RESULT_KEYS:
191
+ raise TypeError("candidate read output dump is not closed")
192
+ if (
193
+ type(dumped["path"]) is not str
194
+ or dumped["path"] != request.path
195
+ or type(dumped["content_sha256"]) is not str
196
+ or dumped["content_sha256"] != content_sha256
197
+ or type(dumped["size_bytes"]) is not int
198
+ or dumped["size_bytes"] != len(result.content)
199
+ or type(dumped["workspace_manifest_digest"]) is not str
200
+ or dumped["workspace_manifest_digest"]
201
+ != request.expected_workspace_manifest_digest
202
+ ):
203
+ raise ValueError("candidate read output dump binding mismatch")
204
+ return {
205
+ "schema_version": RESPONSE_SCHEMA,
206
+ "status": "ok",
207
+ "result": dumped,
208
+ "content_b64url": base64.urlsafe_b64encode(result.content)
209
+ .decode("ascii")
210
+ .rstrip("="),
211
+ }
212
+
213
+
214
+ def _write_response(
215
+ stdout: BinaryIO,
216
+ response: dict[str, object],
217
+ exit_code: int,
218
+ ) -> int:
219
+ try:
220
+ encoded = rfc8785.dumps(response)
221
+ if type(encoded) is not bytes:
222
+ raise TypeError("response canonicalizer did not return bytes")
223
+ except BaseException:
224
+ encoded = _INTERNAL_ERROR_BYTES
225
+ exit_code = 70
226
+ try:
227
+ if stdout.write(encoded) != len(encoded):
228
+ return 70
229
+ except BaseException:
230
+ return 70
231
+ return exit_code
232
+
233
+
234
+ def main(
235
+ argv: Sequence[str],
236
+ stdin: BinaryIO,
237
+ stdout: BinaryIO,
238
+ runtime_root: Path,
239
+ ) -> int:
240
+ try:
241
+ invalid_argv = (
242
+ not isinstance(argv, Sequence)
243
+ or isinstance(argv, (str, bytes, bytearray))
244
+ or len(argv) != 0
245
+ )
246
+ if invalid_argv:
247
+ return _write_response(
248
+ stdout,
249
+ _error_response("REQUEST_INVALID"),
250
+ 64,
251
+ )
252
+ raw = stdin.read(MAX_REQUEST_BYTES + 1)
253
+ request = _parse_request(raw, runtime_root)
254
+ result = repo_tools.read_candidate_file(request)
255
+ response = _validated_success_response(result, request)
256
+ return _write_response(stdout, response, 0)
257
+ except _RequestInvalid:
258
+ code = "REQUEST_INVALID"
259
+ except repo_tools.CandidateReadError as error:
260
+ code = _closed_candidate_error_code(error)
261
+ except BaseException:
262
+ code = "INTERNAL_ERROR"
263
+ return _write_response(stdout, _error_response(code), _exit_for_code(code))
264
+
265
+
266
+ if __name__ == "__main__":
267
+ raise SystemExit(
268
+ main(sys.argv[1:], sys.stdin.buffer, sys.stdout.buffer, RUNTIME_ROOT)
269
+ )