bazaar-compute-node 0.1.3__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 (62) hide show
  1. bazaar_compute_node/__init__.py +3 -0
  2. bazaar_compute_node/app/__init__.py +1 -0
  3. bazaar_compute_node/app/application.py +398 -0
  4. bazaar_compute_node/app/attachments.py +154 -0
  5. bazaar_compute_node/app/command.py +342 -0
  6. bazaar_compute_node/app/config.py +121 -0
  7. bazaar_compute_node/app/registry.py +120 -0
  8. bazaar_compute_node/app/transport.py +264 -0
  9. bazaar_compute_node/app/windows_pipe.py +463 -0
  10. bazaar_compute_node/app/wrapper.py +63 -0
  11. bazaar_compute_node/bcc.py +524 -0
  12. bazaar_compute_node/cli.py +382 -0
  13. bazaar_compute_node/contrib/__init__.py +1 -0
  14. bazaar_compute_node/contrib/codex_app_server/__init__.py +63 -0
  15. bazaar_compute_node/contrib/codex_app_server/approval.py +168 -0
  16. bazaar_compute_node/contrib/codex_app_server/client.py +408 -0
  17. bazaar_compute_node/contrib/codex_app_server/events.py +431 -0
  18. bazaar_compute_node/contrib/codex_app_server/plugin.py +15 -0
  19. bazaar_compute_node/contrib/codex_app_server/process.py +583 -0
  20. bazaar_compute_node/contrib/codex_app_server/protocol.py +103 -0
  21. bazaar_compute_node/contrib/codex_app_server/runtime.py +513 -0
  22. bazaar_compute_node/contrib/logging/__init__.py +5 -0
  23. bazaar_compute_node/contrib/logging/audit.py +61 -0
  24. bazaar_compute_node/contrib/logging/plugin.py +11 -0
  25. bazaar_compute_node/contrib/sqlite/__init__.py +14 -0
  26. bazaar_compute_node/contrib/sqlite/codec.py +768 -0
  27. bazaar_compute_node/contrib/sqlite/database.py +282 -0
  28. bazaar_compute_node/contrib/sqlite/migrations.py +646 -0
  29. bazaar_compute_node/contrib/sqlite/plugin.py +11 -0
  30. bazaar_compute_node/contrib/sqlite/repository.py +1059 -0
  31. bazaar_compute_node/contrib/wecom/__init__.py +1 -0
  32. bazaar_compute_node/contrib/wecom/channel.py +960 -0
  33. bazaar_compute_node/contrib/wecom/markdown.py +146 -0
  34. bazaar_compute_node/contrib/wecom/plugin.py +29 -0
  35. bazaar_compute_node/core/__init__.py +5 -0
  36. bazaar_compute_node/core/approval.py +51 -0
  37. bazaar_compute_node/core/audit.py +101 -0
  38. bazaar_compute_node/core/channel.py +121 -0
  39. bazaar_compute_node/core/client.py +30 -0
  40. bazaar_compute_node/core/command.py +85 -0
  41. bazaar_compute_node/core/concurrency.py +29 -0
  42. bazaar_compute_node/core/correlation.py +48 -0
  43. bazaar_compute_node/core/instruction.py +224 -0
  44. bazaar_compute_node/core/lifecycle.py +48 -0
  45. bazaar_compute_node/core/models/__init__.py +63 -0
  46. bazaar_compute_node/core/models/entities.py +514 -0
  47. bazaar_compute_node/core/models/states.py +369 -0
  48. bazaar_compute_node/core/observability.py +47 -0
  49. bazaar_compute_node/core/orchestration/__init__.py +5 -0
  50. bazaar_compute_node/core/orchestration/command.py +614 -0
  51. bazaar_compute_node/core/orchestration/services.py +135 -0
  52. bazaar_compute_node/core/orchestration/session.py +891 -0
  53. bazaar_compute_node/core/orchestration/turn.py +451 -0
  54. bazaar_compute_node/core/outcomes.py +51 -0
  55. bazaar_compute_node/core/paths.py +19 -0
  56. bazaar_compute_node/core/runtime.py +118 -0
  57. bazaar_compute_node/core/storage.py +167 -0
  58. bazaar_compute_node-0.1.3.dist-info/METADATA +178 -0
  59. bazaar_compute_node-0.1.3.dist-info/RECORD +62 -0
  60. bazaar_compute_node-0.1.3.dist-info/WHEEL +4 -0
  61. bazaar_compute_node-0.1.3.dist-info/entry_points.txt +15 -0
  62. bazaar_compute_node-0.1.3.dist-info/licenses/LICENSE +613 -0
@@ -0,0 +1,264 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import os
6
+ import secrets
7
+ import sys
8
+ import tempfile
9
+ from collections.abc import Awaitable, Callable, Mapping
10
+ from pathlib import Path
11
+ from typing import Any
12
+ from urllib.parse import parse_qs, urlsplit
13
+
14
+ RequestHandler = Callable[[Mapping[str, object]], Awaitable[Mapping[str, object]]]
15
+ StreamPair = tuple[asyncio.StreamReader, asyncio.StreamWriter]
16
+
17
+
18
+ def local_endpoint_for_path(endpoint_path: Path) -> str:
19
+ """Return the stable local endpoint represented by one configured path."""
20
+
21
+ path = endpoint_path.expanduser()
22
+ if sys.platform == "win32":
23
+ from .windows_pipe import named_pipe_endpoint
24
+
25
+ return named_pipe_endpoint(path)
26
+ return f"unix://{path}"
27
+
28
+
29
+ if sys.platform == "win32":
30
+
31
+ async def _open_unix_connection(path: str) -> StreamPair:
32
+ raise ValueError(f"Unix command endpoints are not supported on Windows: {path}")
33
+
34
+ else:
35
+
36
+ async def _open_unix_connection(path: str) -> StreamPair:
37
+ return await asyncio.open_unix_connection(path)
38
+
39
+
40
+ class LocalCommandServer:
41
+ """Serve one request per local JSONL connection."""
42
+
43
+ def __init__(
44
+ self,
45
+ handler: RequestHandler | None = None,
46
+ *,
47
+ endpoint_path: Path | None = None,
48
+ ) -> None:
49
+ self._handler = handler
50
+ self._endpoint_path = endpoint_path
51
+ self._server: asyncio.AbstractServer | None = None
52
+ self._unix_path: Path | None = None
53
+ self._unix_identity: tuple[int, int] | None = None
54
+ self._windows_server: Any | None = None
55
+ self._capability: str | None = None
56
+ self._endpoint: str | None = None
57
+
58
+ def set_handler(self, handler: RequestHandler) -> None:
59
+ if self._server is not None or self._windows_server is not None:
60
+ raise RuntimeError("local command server handler is already active")
61
+ self._handler = handler
62
+
63
+ @property
64
+ def endpoint(self) -> str:
65
+ if self._endpoint is None:
66
+ raise RuntimeError("local command server is not started")
67
+ return self._endpoint
68
+
69
+ async def start(self) -> None:
70
+ if self._server is not None or self._windows_server is not None:
71
+ return
72
+
73
+ if sys.platform == "win32":
74
+ from .windows_pipe import WindowsNamedPipeServer
75
+
76
+ windows_server = WindowsNamedPipeServer(
77
+ self._dispatch,
78
+ endpoint_path=self._endpoint_path,
79
+ )
80
+ await windows_server.start()
81
+ self._windows_server = windows_server
82
+ self._endpoint = windows_server.endpoint
83
+ return
84
+
85
+ path = self._endpoint_path
86
+ if path is None:
87
+ path = (
88
+ Path(tempfile.gettempdir())
89
+ / f"bcn-{os.getpid()}-{secrets.token_hex(6)}.sock"
90
+ )
91
+ path = path.expanduser()
92
+ path.parent.mkdir(parents=True, exist_ok=True)
93
+ if path.exists():
94
+ raise FileExistsError(f"local command endpoint already exists: {path}")
95
+ self._server = await asyncio.start_unix_server(
96
+ self._handle_client,
97
+ path=str(path),
98
+ )
99
+ os.chmod(path, 0o600)
100
+ self._unix_path = path
101
+ path_stat = path.stat()
102
+ self._unix_identity = (path_stat.st_dev, path_stat.st_ino)
103
+ self._endpoint = f"unix://{path}"
104
+
105
+ async def stop(self) -> None:
106
+ windows_server = self._windows_server
107
+ self._windows_server = None
108
+ if windows_server is not None:
109
+ await windows_server.stop()
110
+ self._endpoint = None
111
+ self._capability = None
112
+ return
113
+
114
+ server = self._server
115
+ self._server = None
116
+ self._endpoint = None
117
+ self._capability = None
118
+ if server is not None:
119
+ server.close()
120
+ await server.wait_closed()
121
+ path = self._unix_path
122
+ self._unix_path = None
123
+ identity = self._unix_identity
124
+ self._unix_identity = None
125
+ if path is not None and identity is not None and path.exists():
126
+ path_stat = path.stat()
127
+ if (path_stat.st_dev, path_stat.st_ino) == identity:
128
+ path.unlink()
129
+
130
+ async def _handle_client(
131
+ self,
132
+ reader: asyncio.StreamReader,
133
+ writer: asyncio.StreamWriter,
134
+ ) -> None:
135
+ try:
136
+ line = await reader.readline()
137
+ if not line:
138
+ return
139
+ payload = json.loads(line)
140
+ if not isinstance(payload, dict):
141
+ raise TypeError("request must be a JSON object")
142
+ response = await self._dispatch(payload)
143
+ except asyncio.CancelledError:
144
+ raise
145
+ except json.JSONDecodeError as error:
146
+ response = {
147
+ "ok": False,
148
+ "code": "INVALID_REQUEST",
149
+ "error": f"invalid JSON request: {error.msg}",
150
+ }
151
+ except ValueError as error:
152
+ response = {
153
+ "ok": False,
154
+ "code": "INVALID_REQUEST",
155
+ "error": str(error),
156
+ }
157
+ except Exception as error: # noqa: BLE001
158
+ response = {
159
+ "ok": False,
160
+ "code": "COMMAND_FAILED",
161
+ "error": str(error),
162
+ }
163
+ try:
164
+ writer.write(
165
+ json.dumps(response, ensure_ascii=False, separators=(",", ":")).encode()
166
+ + b"\n"
167
+ )
168
+ await writer.drain()
169
+ finally:
170
+ writer.close()
171
+ try:
172
+ await writer.wait_closed()
173
+ except OSError:
174
+ pass
175
+
176
+ async def _dispatch(self, payload: Mapping[str, object]) -> Mapping[str, object]:
177
+ if self._handler is None:
178
+ raise RuntimeError("local command server is not ready")
179
+ request = dict(payload)
180
+ if self._capability is not None:
181
+ capability = request.pop("capability", None)
182
+ if capability != self._capability:
183
+ return {
184
+ "ok": False,
185
+ "code": "LOCAL_AUTH_FAILED",
186
+ "error": "local command capability is invalid",
187
+ }
188
+ return await self._handler(request)
189
+
190
+
191
+ class LocalCommandClient:
192
+ """Open a fresh local connection for each command."""
193
+
194
+ @staticmethod
195
+ async def request(
196
+ endpoint: str,
197
+ payload: Mapping[str, object],
198
+ *,
199
+ timeout: float = 10,
200
+ ) -> Mapping[str, object]:
201
+ if timeout <= 0:
202
+ raise ValueError("timeout must be positive")
203
+ return await asyncio.wait_for(
204
+ LocalCommandClient._request(endpoint, payload),
205
+ timeout=timeout,
206
+ )
207
+
208
+ @staticmethod
209
+ async def _request(
210
+ endpoint: str,
211
+ payload: Mapping[str, object],
212
+ ) -> Mapping[str, object]:
213
+ parsed = urlsplit(endpoint)
214
+ request = dict(payload)
215
+ if parsed.scheme == "pipe":
216
+ if sys.platform != "win32":
217
+ raise ValueError(
218
+ "Windows named pipe endpoints are not supported on this platform"
219
+ )
220
+ from .windows_pipe import request_named_pipe
221
+
222
+ return await asyncio.to_thread(request_named_pipe, endpoint, payload)
223
+ if parsed.scheme == "unix":
224
+ reader, writer = await _open_unix_connection(parsed.path)
225
+ elif parsed.scheme == "tcp":
226
+ query = parse_qs(parsed.query)
227
+ token_values = query.get("token")
228
+ if (
229
+ set(query) != {"token"}
230
+ or token_values is None
231
+ or len(token_values) != 1
232
+ ):
233
+ raise ValueError("TCP command endpoint has no capability token")
234
+ if parsed.hostname != "127.0.0.1":
235
+ raise ValueError("TCP command endpoint must use loopback")
236
+ request["capability"] = token_values[0]
237
+ if parsed.hostname is None or parsed.port is None:
238
+ raise ValueError("TCP command endpoint is invalid")
239
+ reader, writer = await asyncio.open_connection(
240
+ parsed.hostname,
241
+ parsed.port,
242
+ )
243
+ else:
244
+ raise ValueError(f"unsupported local command endpoint: {endpoint}")
245
+
246
+ try:
247
+ writer.write(
248
+ json.dumps(request, ensure_ascii=False, separators=(",", ":")).encode()
249
+ + b"\n"
250
+ )
251
+ await writer.drain()
252
+ line = await reader.readline()
253
+ if not line:
254
+ raise ConnectionError("local command server closed without a response")
255
+ response = json.loads(line)
256
+ if not isinstance(response, dict):
257
+ raise TypeError("local command response must be a JSON object")
258
+ return response
259
+ finally:
260
+ writer.close()
261
+ try:
262
+ await writer.wait_closed()
263
+ except OSError:
264
+ pass