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,282 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import os
5
+ from contextlib import AbstractAsyncContextManager
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from time import time_ns
9
+ from uuid import uuid7
10
+
11
+ import aiosqlite
12
+
13
+ from ...core.paths import resolve_data_dir
14
+ from ...core.storage import NodeIdentity
15
+ from .migrations import (
16
+ MigrationChecksumError,
17
+ MigrationError,
18
+ apply_migrations,
19
+ )
20
+ from .repository import SqliteTransaction
21
+
22
+ DATABASE_FILENAME = "bcn.sqlite3"
23
+ NODE_STATE_KEY = 1
24
+ DEFAULT_BUSY_TIMEOUT_MS = 5_000
25
+
26
+
27
+ class NodeIdentityError(MigrationError):
28
+ """The persistent node identity does not match the requested identity."""
29
+
30
+
31
+ @dataclass(frozen=True, slots=True)
32
+ class NodeState:
33
+ node_id: str
34
+ schema_version: int
35
+ workspace_id: str
36
+ created_at_ms: int
37
+ updated_at_ms: int
38
+ metadata_json: str
39
+
40
+
41
+ class SqliteDatabase:
42
+ """Persistent SQLite foundation used by the storage repository adapter."""
43
+
44
+ @property
45
+ def name(self) -> str:
46
+ return "sqlite"
47
+
48
+ def __init__(
49
+ self,
50
+ *,
51
+ busy_timeout_ms: int = DEFAULT_BUSY_TIMEOUT_MS,
52
+ ) -> None:
53
+ if (
54
+ isinstance(busy_timeout_ms, bool)
55
+ or not isinstance(busy_timeout_ms, int)
56
+ or busy_timeout_ms <= 0
57
+ ):
58
+ raise ValueError("busy_timeout_ms must be a positive integer")
59
+ self.data_dir = resolve_data_dir()
60
+ self.database_path = self.data_dir / DATABASE_FILENAME
61
+ self._busy_timeout_ms = busy_timeout_ms
62
+ self._connection: aiosqlite.Connection | None = None
63
+ self._node_state: NodeState | None = None
64
+ self._schema_version: int | None = None
65
+ self._lifecycle_lock = asyncio.Lock()
66
+ self._transaction_lock = asyncio.Lock()
67
+
68
+ @property
69
+ def node_state(self) -> NodeState:
70
+ if self._node_state is None:
71
+ raise RuntimeError("SQLite node identity has not been initialized")
72
+ return self._node_state
73
+
74
+ @property
75
+ def node_id(self) -> str:
76
+ return self.node_state.node_id
77
+
78
+ @property
79
+ def workspace_id(self) -> str:
80
+ return self.node_state.workspace_id
81
+
82
+ @property
83
+ def is_started(self) -> bool:
84
+ return self._connection is not None
85
+
86
+ async def start(self, *, timeout: float) -> None:
87
+ if timeout <= 0:
88
+ raise ValueError("timeout must be positive")
89
+ async with self._lifecycle_lock:
90
+ if self._connection is not None:
91
+ return
92
+ connection: aiosqlite.Connection | None = None
93
+ try:
94
+ async with asyncio.timeout(timeout):
95
+ self.data_dir.mkdir(
96
+ parents=True,
97
+ exist_ok=True,
98
+ mode=0o700,
99
+ )
100
+ _restrict_permissions(self.data_dir, 0o700)
101
+ connection = await aiosqlite.connect(
102
+ self.database_path,
103
+ timeout=self._busy_timeout_ms / 1000,
104
+ isolation_level=None,
105
+ )
106
+ connection.row_factory = aiosqlite.Row
107
+ await connection.execute("PRAGMA journal_mode = WAL")
108
+ await connection.execute("PRAGMA synchronous = NORMAL")
109
+ await connection.execute("PRAGMA foreign_keys = ON")
110
+ await connection.execute(
111
+ f"PRAGMA busy_timeout = {self._busy_timeout_ms}"
112
+ )
113
+ _restrict_permissions(self.database_path, 0o600)
114
+ self._connection = connection
115
+ async with SqliteTransaction(self) as transaction:
116
+ self._schema_version = await apply_migrations(
117
+ transaction,
118
+ clock=_current_time_ms,
119
+ )
120
+ except BaseException:
121
+ self._connection = None
122
+ self._node_state = None
123
+ self._schema_version = None
124
+ if connection is not None:
125
+ await connection.close()
126
+ raise
127
+
128
+ async def stop(self, *, timeout: float) -> None:
129
+ if timeout <= 0:
130
+ raise ValueError("timeout must be positive")
131
+ async with self._lifecycle_lock:
132
+ connection = self._connection
133
+ if connection is None:
134
+ return
135
+ try:
136
+ async with asyncio.timeout(timeout):
137
+ async with self._transaction_lock:
138
+ await connection.close()
139
+ finally:
140
+ self._connection = None
141
+ self._node_state = None
142
+ self._schema_version = None
143
+
144
+ async def initialize(
145
+ self,
146
+ *,
147
+ node_id: str | None = None,
148
+ workspace_id: str | None = None,
149
+ ) -> NodeIdentity:
150
+ if node_id is not None and (not isinstance(node_id, str) or not node_id):
151
+ raise ValueError("node_id must be a non-empty string")
152
+ if workspace_id is not None and (
153
+ not isinstance(workspace_id, str) or not workspace_id
154
+ ):
155
+ raise ValueError("workspace_id must be a non-empty string")
156
+ async with self._lifecycle_lock:
157
+ self._require_connection()
158
+ schema_version = self._schema_version
159
+ if schema_version is None:
160
+ raise RuntimeError("SQLite schema has not been initialized")
161
+ state: NodeState | None = None
162
+ async with SqliteTransaction(self) as transaction:
163
+ state = await self._ensure_node_state(
164
+ transaction,
165
+ schema_version,
166
+ requested_node_id=node_id,
167
+ requested_workspace_id=workspace_id,
168
+ )
169
+ if state is None:
170
+ raise RuntimeError("SQLite node initialization did not create state")
171
+ self._node_state = state
172
+ return NodeIdentity(
173
+ node_id=state.node_id,
174
+ workspace_id=state.workspace_id,
175
+ )
176
+
177
+ def transaction(self) -> AbstractAsyncContextManager[SqliteTransaction]:
178
+ return SqliteTransaction(self)
179
+
180
+ def _require_connection(self) -> aiosqlite.Connection:
181
+ if self._connection is None:
182
+ raise RuntimeError("SQLite database has not been started")
183
+ return self._connection
184
+
185
+ async def _ensure_node_state(
186
+ self,
187
+ transaction: SqliteTransaction,
188
+ schema_version: int,
189
+ *,
190
+ requested_node_id: str | None,
191
+ requested_workspace_id: str | None,
192
+ ) -> NodeState:
193
+ row = await transaction.fetchone(
194
+ "SELECT node_id, schema_version, workspace_id, created_at_ms, "
195
+ "updated_at_ms, metadata_json FROM node_state "
196
+ "WHERE singleton_key = ?",
197
+ (NODE_STATE_KEY,),
198
+ )
199
+ now_ms = _current_time_ms()
200
+ if row is None:
201
+ node_id = requested_node_id or f"bcn-node-{uuid7()}"
202
+ workspace_id = requested_workspace_id or str(uuid7())
203
+ metadata_json = "{}"
204
+ await transaction.execute(
205
+ "INSERT INTO node_state "
206
+ "(singleton_key, node_id, schema_version, workspace_id, "
207
+ "created_at_ms, updated_at_ms, metadata_json) "
208
+ "VALUES (?, ?, ?, ?, ?, ?, ?)",
209
+ (
210
+ NODE_STATE_KEY,
211
+ node_id,
212
+ schema_version,
213
+ workspace_id,
214
+ now_ms,
215
+ now_ms,
216
+ metadata_json,
217
+ ),
218
+ )
219
+ return NodeState(
220
+ node_id=node_id,
221
+ schema_version=schema_version,
222
+ workspace_id=workspace_id,
223
+ created_at_ms=now_ms,
224
+ updated_at_ms=now_ms,
225
+ metadata_json=metadata_json,
226
+ )
227
+
228
+ node_id = row["node_id"]
229
+ workspace_id = row["workspace_id"]
230
+ if not isinstance(node_id, str) or not node_id:
231
+ raise NodeIdentityError("persistent node_id is missing")
232
+ if not isinstance(workspace_id, str) or not workspace_id:
233
+ raise NodeIdentityError("persistent workspace_id is missing")
234
+ if requested_node_id is not None and node_id != requested_node_id:
235
+ raise NodeIdentityError(
236
+ f"requested node_id does not match persisted node_id: {node_id}"
237
+ )
238
+ if (
239
+ requested_workspace_id is not None
240
+ and workspace_id != requested_workspace_id
241
+ ):
242
+ raise NodeIdentityError(
243
+ "requested workspace_id does not match the persisted workspace_id"
244
+ )
245
+ if row["schema_version"] != schema_version:
246
+ await transaction.execute(
247
+ "UPDATE node_state SET schema_version = ?, updated_at_ms = ? "
248
+ "WHERE singleton_key = ?",
249
+ (schema_version, now_ms, NODE_STATE_KEY),
250
+ )
251
+ updated_at_ms = now_ms
252
+ else:
253
+ updated_at_ms = int(row["updated_at_ms"])
254
+ return NodeState(
255
+ node_id=node_id,
256
+ schema_version=schema_version,
257
+ workspace_id=workspace_id,
258
+ created_at_ms=int(row["created_at_ms"]),
259
+ updated_at_ms=updated_at_ms,
260
+ metadata_json=row["metadata_json"] or "{}",
261
+ )
262
+
263
+
264
+ def _current_time_ms() -> int:
265
+ return time_ns() // 1_000_000
266
+
267
+
268
+ def _restrict_permissions(path: Path, mode: int) -> None:
269
+ if os.name != "nt":
270
+ path.chmod(mode)
271
+
272
+
273
+ __all__ = [
274
+ "DATABASE_FILENAME",
275
+ "DEFAULT_BUSY_TIMEOUT_MS",
276
+ "MigrationChecksumError",
277
+ "MigrationError",
278
+ "NodeIdentityError",
279
+ "NodeState",
280
+ "SqliteDatabase",
281
+ "SqliteTransaction",
282
+ ]