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.
- bazaar_compute_node/__init__.py +3 -0
- bazaar_compute_node/app/__init__.py +1 -0
- bazaar_compute_node/app/application.py +398 -0
- bazaar_compute_node/app/attachments.py +154 -0
- bazaar_compute_node/app/command.py +342 -0
- bazaar_compute_node/app/config.py +121 -0
- bazaar_compute_node/app/registry.py +120 -0
- bazaar_compute_node/app/transport.py +264 -0
- bazaar_compute_node/app/windows_pipe.py +463 -0
- bazaar_compute_node/app/wrapper.py +63 -0
- bazaar_compute_node/bcc.py +524 -0
- bazaar_compute_node/cli.py +382 -0
- bazaar_compute_node/contrib/__init__.py +1 -0
- bazaar_compute_node/contrib/codex_app_server/__init__.py +63 -0
- bazaar_compute_node/contrib/codex_app_server/approval.py +168 -0
- bazaar_compute_node/contrib/codex_app_server/client.py +408 -0
- bazaar_compute_node/contrib/codex_app_server/events.py +431 -0
- bazaar_compute_node/contrib/codex_app_server/plugin.py +15 -0
- bazaar_compute_node/contrib/codex_app_server/process.py +583 -0
- bazaar_compute_node/contrib/codex_app_server/protocol.py +103 -0
- bazaar_compute_node/contrib/codex_app_server/runtime.py +513 -0
- bazaar_compute_node/contrib/logging/__init__.py +5 -0
- bazaar_compute_node/contrib/logging/audit.py +61 -0
- bazaar_compute_node/contrib/logging/plugin.py +11 -0
- bazaar_compute_node/contrib/sqlite/__init__.py +14 -0
- bazaar_compute_node/contrib/sqlite/codec.py +768 -0
- bazaar_compute_node/contrib/sqlite/database.py +282 -0
- bazaar_compute_node/contrib/sqlite/migrations.py +646 -0
- bazaar_compute_node/contrib/sqlite/plugin.py +11 -0
- bazaar_compute_node/contrib/sqlite/repository.py +1059 -0
- bazaar_compute_node/contrib/wecom/__init__.py +1 -0
- bazaar_compute_node/contrib/wecom/channel.py +960 -0
- bazaar_compute_node/contrib/wecom/markdown.py +146 -0
- bazaar_compute_node/contrib/wecom/plugin.py +29 -0
- bazaar_compute_node/core/__init__.py +5 -0
- bazaar_compute_node/core/approval.py +51 -0
- bazaar_compute_node/core/audit.py +101 -0
- bazaar_compute_node/core/channel.py +121 -0
- bazaar_compute_node/core/client.py +30 -0
- bazaar_compute_node/core/command.py +85 -0
- bazaar_compute_node/core/concurrency.py +29 -0
- bazaar_compute_node/core/correlation.py +48 -0
- bazaar_compute_node/core/instruction.py +224 -0
- bazaar_compute_node/core/lifecycle.py +48 -0
- bazaar_compute_node/core/models/__init__.py +63 -0
- bazaar_compute_node/core/models/entities.py +514 -0
- bazaar_compute_node/core/models/states.py +369 -0
- bazaar_compute_node/core/observability.py +47 -0
- bazaar_compute_node/core/orchestration/__init__.py +5 -0
- bazaar_compute_node/core/orchestration/command.py +614 -0
- bazaar_compute_node/core/orchestration/services.py +135 -0
- bazaar_compute_node/core/orchestration/session.py +891 -0
- bazaar_compute_node/core/orchestration/turn.py +451 -0
- bazaar_compute_node/core/outcomes.py +51 -0
- bazaar_compute_node/core/paths.py +19 -0
- bazaar_compute_node/core/runtime.py +118 -0
- bazaar_compute_node/core/storage.py +167 -0
- bazaar_compute_node-0.1.3.dist-info/METADATA +178 -0
- bazaar_compute_node-0.1.3.dist-info/RECORD +62 -0
- bazaar_compute_node-0.1.3.dist-info/WHEEL +4 -0
- bazaar_compute_node-0.1.3.dist-info/entry_points.txt +15 -0
- 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
|