bazaar-compute-node 0.1.4__tar.gz → 0.1.6__tar.gz
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-0.1.4 → bazaar_compute_node-0.1.6}/PKG-INFO +4 -2
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/README.md +3 -1
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/pyproject.toml +3 -2
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/pyproject.toml.orig +7 -6
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/app/application.py +10 -7
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/app/attachments.py +52 -16
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/app/command.py +19 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/app/config.py +12 -4
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/app/registry.py +9 -4
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/app/transport.py +16 -10
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/app/windows_pipe.py +4 -2
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/bcc.py +5 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/cli.py +46 -10
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/codex_app_server/__init__.py +4 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/codex_app_server/client.py +34 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/codex_app_server/runtime.py +52 -1
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/sqlite/codec.py +33 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/sqlite/database.py +16 -4
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/sqlite/migrations.py +12 -0
- bazaar_compute_node-0.1.6/src/bazaar_compute_node/contrib/sqlite/plugin.py +16 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/sqlite/repository.py +18 -1
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/wecom/channel.py +400 -45
- bazaar_compute_node-0.1.6/src/bazaar_compute_node/contrib/wecom/outbound.py +193 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/command.py +1 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/instruction.py +3 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/models/__init__.py +2 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/models/entities.py +40 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/orchestration/command.py +130 -18
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/orchestration/session.py +110 -4
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/orchestration/turn.py +55 -3
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/paths.py +14 -1
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/runtime.py +16 -0
- bazaar_compute_node-0.1.4/src/bazaar_compute_node/contrib/sqlite/plugin.py +0 -11
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/LICENSE +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/__init__.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/app/__init__.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/app/wrapper.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/__init__.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/codex_app_server/approval.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/codex_app_server/events.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/codex_app_server/plugin.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/codex_app_server/process.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/codex_app_server/protocol.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/logging/__init__.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/logging/audit.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/logging/plugin.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/sqlite/__init__.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/wecom/__init__.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/wecom/markdown.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/contrib/wecom/plugin.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/__init__.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/approval.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/audit.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/channel.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/client.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/concurrency.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/correlation.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/lifecycle.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/models/states.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/observability.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/orchestration/__init__.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/orchestration/services.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/outcomes.py +0 -0
- {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/core/storage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bazaar-compute-node
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: Runtime-agnostic computer node daemon for agents and channels.
|
|
5
5
|
License-Expression: AGPL-3.0-only
|
|
6
6
|
License-File: LICENSE
|
|
@@ -167,7 +167,9 @@ uvx --from 'git+https://github.com/yuchanns/bazaar-compute-node@main' bcn restar
|
|
|
167
167
|
uvx --from 'git+https://github.com/yuchanns/bazaar-compute-node@main' bcn --help
|
|
168
168
|
```
|
|
169
169
|
|
|
170
|
-
节点数据默认保存在 `~/.bcn
|
|
170
|
+
节点数据默认保存在 `~/.bcn`。可通过 `BCN_DATA_NAME` 指定 HOME 下的另一数据目录名称,
|
|
171
|
+
例如 `BCN_DATA_NAME=.bcn-test`;该值必须是单个目录名。停止节点会优雅关闭后台进程,
|
|
172
|
+
不会删除既有会话、工作区或消息记录。
|
|
171
173
|
|
|
172
174
|
## 许可证
|
|
173
175
|
|
|
@@ -154,7 +154,9 @@ uvx --from 'git+https://github.com/yuchanns/bazaar-compute-node@main' bcn restar
|
|
|
154
154
|
uvx --from 'git+https://github.com/yuchanns/bazaar-compute-node@main' bcn --help
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
-
节点数据默认保存在 `~/.bcn
|
|
157
|
+
节点数据默认保存在 `~/.bcn`。可通过 `BCN_DATA_NAME` 指定 HOME 下的另一数据目录名称,
|
|
158
|
+
例如 `BCN_DATA_NAME=.bcn-test`;该值必须是单个目录名。停止节点会优雅关闭后台进程,
|
|
159
|
+
不会删除既有会话、工作区或消息记录。
|
|
158
160
|
|
|
159
161
|
## 许可证
|
|
160
162
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "bazaar-compute-node"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.6"
|
|
4
4
|
description = "Runtime-agnostic computer node daemon for agents and channels."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "AGPL-3.0-only"
|
|
@@ -43,7 +43,8 @@ path = "tests/support"
|
|
|
43
43
|
editable = true
|
|
44
44
|
|
|
45
45
|
[tool.pytest.ini_options]
|
|
46
|
-
|
|
46
|
+
addopts = "-m 'not e2e'"
|
|
47
|
+
markers = ["e2e: requires a real external provider"]
|
|
47
48
|
|
|
48
49
|
[build-system]
|
|
49
50
|
requires = ["uv_build>=0.12.2,<0.13.0"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "bazaar-compute-node"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.6"
|
|
4
4
|
description = "Runtime-agnostic computer node daemon for agents and channels."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "AGPL-3.0-only"
|
|
@@ -41,11 +41,12 @@ dev = [
|
|
|
41
41
|
[tool.uv.sources]
|
|
42
42
|
bcn-test-support = { path = "tests/support", editable = true }
|
|
43
43
|
|
|
44
|
-
[build-system]
|
|
45
|
-
requires = ["uv_build>=0.12.2,<0.13.0"]
|
|
46
|
-
build-backend = "uv_build"
|
|
47
|
-
|
|
48
44
|
[tool.pytest.ini_options]
|
|
45
|
+
addopts = "-m 'not e2e'"
|
|
49
46
|
markers = [
|
|
50
|
-
"
|
|
47
|
+
"e2e: requires a real external provider",
|
|
51
48
|
]
|
|
49
|
+
|
|
50
|
+
[build-system]
|
|
51
|
+
requires = ["uv_build>=0.12.2,<0.13.0"]
|
|
52
|
+
build-backend = "uv_build"
|
{bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/app/application.py
RENAMED
|
@@ -114,6 +114,7 @@ class NodeApplication:
|
|
|
114
114
|
storage=self.storage,
|
|
115
115
|
audit=self.audit,
|
|
116
116
|
timeout_budget=self.timeout_budget,
|
|
117
|
+
workspace=self._workspace_path,
|
|
117
118
|
on_node_initialized=self._ensure_workspace,
|
|
118
119
|
)
|
|
119
120
|
self.command_service = self.orchestrator.command_service
|
|
@@ -139,10 +140,12 @@ class NodeApplication:
|
|
|
139
140
|
async def start(self) -> None:
|
|
140
141
|
if self._started:
|
|
141
142
|
return
|
|
142
|
-
self.data_dir.mkdir
|
|
143
|
+
await asyncio.to_thread(self.data_dir.mkdir, parents=True, exist_ok=True)
|
|
143
144
|
if os.name != "nt":
|
|
144
|
-
self.data_dir.chmod
|
|
145
|
-
self._wrapper_path =
|
|
145
|
+
await asyncio.to_thread(self.data_dir.chmod, 0o700)
|
|
146
|
+
self._wrapper_path = await asyncio.to_thread(
|
|
147
|
+
install_bcc_wrapper, self.data_dir / "bin"
|
|
148
|
+
)
|
|
146
149
|
try:
|
|
147
150
|
await self.command_server.start()
|
|
148
151
|
await self.orchestrator.start(
|
|
@@ -165,7 +168,7 @@ class NodeApplication:
|
|
|
165
168
|
try:
|
|
166
169
|
await self.command_server.stop()
|
|
167
170
|
finally:
|
|
168
|
-
self._cleanup_bcc_wrapper()
|
|
171
|
+
await self._cleanup_bcc_wrapper()
|
|
169
172
|
return
|
|
170
173
|
self._started = False
|
|
171
174
|
self.command_dispatcher.stop_accepting()
|
|
@@ -183,15 +186,15 @@ class NodeApplication:
|
|
|
183
186
|
await self.command_server.stop()
|
|
184
187
|
finally:
|
|
185
188
|
try:
|
|
186
|
-
self._cleanup_bcc_wrapper()
|
|
189
|
+
await self._cleanup_bcc_wrapper()
|
|
187
190
|
finally:
|
|
188
191
|
self._stopped.set()
|
|
189
192
|
|
|
190
|
-
def _cleanup_bcc_wrapper(self) -> None:
|
|
193
|
+
async def _cleanup_bcc_wrapper(self) -> None:
|
|
191
194
|
wrapper_path = self._wrapper_path
|
|
192
195
|
if wrapper_path is None:
|
|
193
196
|
return
|
|
194
|
-
remove_bcc_wrapper
|
|
197
|
+
await asyncio.to_thread(remove_bcc_wrapper, wrapper_path)
|
|
195
198
|
self._wrapper_path = None
|
|
196
199
|
|
|
197
200
|
async def wait(self) -> None:
|
{bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/app/attachments.py
RENAMED
|
@@ -33,13 +33,24 @@ class AttachmentMaterializer:
|
|
|
33
33
|
root = self._workspace() / "attachments"
|
|
34
34
|
staging = root / ".staging"
|
|
35
35
|
await asyncio.to_thread(staging.mkdir, parents=True, exist_ok=True, mode=0o700)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
staged_files = await asyncio.to_thread(
|
|
37
|
+
lambda: tuple(
|
|
38
|
+
path
|
|
39
|
+
for path in staging.iterdir()
|
|
40
|
+
if path.is_file() and not path.is_symlink()
|
|
41
|
+
)
|
|
42
|
+
)
|
|
43
|
+
for path in staged_files:
|
|
44
|
+
await asyncio.to_thread(path.unlink)
|
|
39
45
|
referenced = await self._referenced_paths()
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
46
|
+
attachment_directories = await asyncio.to_thread(
|
|
47
|
+
lambda: tuple(
|
|
48
|
+
path
|
|
49
|
+
for path in root.iterdir()
|
|
50
|
+
if path != staging and path.is_dir() and not path.is_symlink()
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
for path in attachment_directories:
|
|
43
54
|
try:
|
|
44
55
|
if UUID(path.name).version != 7:
|
|
45
56
|
continue
|
|
@@ -61,10 +72,7 @@ class AttachmentMaterializer:
|
|
|
61
72
|
if not name or not kind:
|
|
62
73
|
raise ValueError("attachment name and kind must be non-empty")
|
|
63
74
|
attachment_id = str(uuid7())
|
|
64
|
-
suffix =
|
|
65
|
-
if not _SAFE_SUFFIX.fullmatch(suffix):
|
|
66
|
-
guessed = mimetypes.guess_extension(media_type or "") or ".bin"
|
|
67
|
-
suffix = guessed if _SAFE_SUFFIX.fullmatch(guessed) else ".bin"
|
|
75
|
+
suffix = await asyncio.to_thread(_attachment_suffix, name, media_type)
|
|
68
76
|
relative = PurePosixPath(
|
|
69
77
|
"attachments", attachment_id, f"content{suffix.lower()}"
|
|
70
78
|
)
|
|
@@ -78,8 +86,15 @@ class AttachmentMaterializer:
|
|
|
78
86
|
)
|
|
79
87
|
current_size = await asyncio.to_thread(self._stored_size, root)
|
|
80
88
|
size = 0
|
|
89
|
+
descriptor: int | None = None
|
|
81
90
|
try:
|
|
82
|
-
|
|
91
|
+
descriptor = await asyncio.to_thread(
|
|
92
|
+
os.open,
|
|
93
|
+
temporary,
|
|
94
|
+
os.O_WRONLY | os.O_CREAT | os.O_EXCL,
|
|
95
|
+
0o600,
|
|
96
|
+
)
|
|
97
|
+
try:
|
|
83
98
|
if isinstance(source, bytes):
|
|
84
99
|
size = len(source)
|
|
85
100
|
if size > self._max_file_bytes:
|
|
@@ -88,7 +103,7 @@ class AttachmentMaterializer:
|
|
|
88
103
|
)
|
|
89
104
|
if current_size + size > self._max_workspace_bytes:
|
|
90
105
|
raise ValueError("attachment workspace quota exceeded")
|
|
91
|
-
await asyncio.to_thread(
|
|
106
|
+
await asyncio.to_thread(_write_all, descriptor, source)
|
|
92
107
|
else:
|
|
93
108
|
async for chunk in source:
|
|
94
109
|
if not isinstance(chunk, bytes):
|
|
@@ -100,9 +115,11 @@ class AttachmentMaterializer:
|
|
|
100
115
|
)
|
|
101
116
|
if current_size + size > self._max_workspace_bytes:
|
|
102
117
|
raise ValueError("attachment workspace quota exceeded")
|
|
103
|
-
await asyncio.to_thread(
|
|
104
|
-
await asyncio.to_thread(
|
|
105
|
-
|
|
118
|
+
await asyncio.to_thread(_write_all, descriptor, chunk)
|
|
119
|
+
await asyncio.to_thread(os.fsync, descriptor)
|
|
120
|
+
finally:
|
|
121
|
+
await asyncio.to_thread(os.close, descriptor)
|
|
122
|
+
descriptor = None
|
|
106
123
|
await asyncio.to_thread(
|
|
107
124
|
destination.parent.mkdir, parents=True, mode=0o700
|
|
108
125
|
)
|
|
@@ -110,7 +127,9 @@ class AttachmentMaterializer:
|
|
|
110
127
|
if os.name != "nt":
|
|
111
128
|
await asyncio.to_thread(destination.chmod, 0o600)
|
|
112
129
|
except BaseException:
|
|
113
|
-
if
|
|
130
|
+
if descriptor is not None:
|
|
131
|
+
await asyncio.to_thread(os.close, descriptor)
|
|
132
|
+
if await asyncio.to_thread(temporary.exists):
|
|
114
133
|
await asyncio.to_thread(temporary.unlink)
|
|
115
134
|
raise
|
|
116
135
|
return InboundAttachment(
|
|
@@ -151,4 +170,21 @@ class AttachmentMaterializer:
|
|
|
151
170
|
)
|
|
152
171
|
|
|
153
172
|
|
|
173
|
+
def _write_all(descriptor: int, payload: bytes) -> None:
|
|
174
|
+
view = memoryview(payload)
|
|
175
|
+
while view:
|
|
176
|
+
written = os.write(descriptor, view)
|
|
177
|
+
if written == 0:
|
|
178
|
+
raise OSError("attachment write made no progress")
|
|
179
|
+
view = view[written:]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _attachment_suffix(name: str, media_type: str | None) -> str:
|
|
183
|
+
suffix = Path(name).suffix
|
|
184
|
+
if _SAFE_SUFFIX.fullmatch(suffix):
|
|
185
|
+
return suffix
|
|
186
|
+
guessed = mimetypes.guess_extension(media_type or "") or ".bin"
|
|
187
|
+
return guessed if _SAFE_SUFFIX.fullmatch(guessed) else ".bin"
|
|
188
|
+
|
|
189
|
+
|
|
154
190
|
__all__ = ["AttachmentMaterializer"]
|
{bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/app/command.py
RENAMED
|
@@ -55,6 +55,16 @@ def serialize_outbound(message: OutboundMessage) -> dict[str, object]:
|
|
|
55
55
|
"target": message.target,
|
|
56
56
|
"reply_to_message_id": message.reply_to_message_id,
|
|
57
57
|
"body": message.body,
|
|
58
|
+
"attachments": [
|
|
59
|
+
{
|
|
60
|
+
"name": attachment.name,
|
|
61
|
+
"relative_path": attachment.relative_path,
|
|
62
|
+
"media_type": attachment.media_type,
|
|
63
|
+
"size_bytes": attachment.size_bytes,
|
|
64
|
+
"sha256": attachment.sha256,
|
|
65
|
+
}
|
|
66
|
+
for attachment in message.attachments
|
|
67
|
+
],
|
|
58
68
|
"state": message.state.value,
|
|
59
69
|
"fresh_check_state": message.fresh_check_state.value,
|
|
60
70
|
"created_at_ms": message.created_at_ms,
|
|
@@ -284,6 +294,7 @@ class CommandDispatcher:
|
|
|
284
294
|
target = request.get("target")
|
|
285
295
|
body = request.get("body")
|
|
286
296
|
command_id = request.get("command_id")
|
|
297
|
+
attachment_paths = request.get("attachment_paths", [])
|
|
287
298
|
reply_to_message_id = request.get("reply_to_message_id")
|
|
288
299
|
created_at_ms = request.get("created_at_ms", time_ns() // 1_000_000)
|
|
289
300
|
if not isinstance(target, str) or not target:
|
|
@@ -296,6 +307,13 @@ class CommandDispatcher:
|
|
|
296
307
|
raise CommandDispatchError(
|
|
297
308
|
"COMMAND_ID_REQUIRED", "command_id must be a non-empty string"
|
|
298
309
|
)
|
|
310
|
+
if not isinstance(attachment_paths, list) or not all(
|
|
311
|
+
isinstance(path, str) and path for path in attachment_paths
|
|
312
|
+
):
|
|
313
|
+
raise CommandDispatchError(
|
|
314
|
+
"INVALID_ATTACHMENTS",
|
|
315
|
+
"attachment_paths must be a list of non-empty strings",
|
|
316
|
+
)
|
|
299
317
|
if reply_to_message_id is not None and (
|
|
300
318
|
not isinstance(reply_to_message_id, str) or not reply_to_message_id
|
|
301
319
|
):
|
|
@@ -317,6 +335,7 @@ class CommandDispatcher:
|
|
|
317
335
|
target=target,
|
|
318
336
|
body=body,
|
|
319
337
|
created_at_ms=created_at_ms,
|
|
338
|
+
attachment_paths=tuple(attachment_paths),
|
|
320
339
|
reply_to_message_id=reply_to_message_id,
|
|
321
340
|
)
|
|
322
341
|
return {
|
{bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/app/config.py
RENAMED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import os
|
|
3
4
|
import tomllib
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
from pathlib import Path
|
|
@@ -19,6 +20,7 @@ class NodeConfiguration:
|
|
|
19
20
|
storage: str | None = None
|
|
20
21
|
audit: str | None = None
|
|
21
22
|
endpoint: str | None = None
|
|
23
|
+
database_name: str | None = None
|
|
22
24
|
model: str | None = None
|
|
23
25
|
effort: str | None = None
|
|
24
26
|
sandbox_mode: RuntimeSandboxMode = RuntimeSandboxMode.WORKSPACE_WRITE
|
|
@@ -36,13 +38,18 @@ def resolve_config_path() -> Path:
|
|
|
36
38
|
return resolve_data_dir() / CONFIG_FILENAME
|
|
37
39
|
|
|
38
40
|
|
|
39
|
-
def load_node_configuration() -> NodeConfiguration:
|
|
40
|
-
path = resolve_config_path()
|
|
41
|
+
def load_node_configuration(path: Path | None = None) -> NodeConfiguration:
|
|
42
|
+
path = path or resolve_config_path()
|
|
41
43
|
try:
|
|
44
|
+
path.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
45
|
+
try:
|
|
46
|
+
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
|
|
47
|
+
except FileExistsError:
|
|
48
|
+
pass
|
|
49
|
+
else:
|
|
50
|
+
os.close(descriptor)
|
|
42
51
|
with path.open("rb") as config_file:
|
|
43
52
|
payload = tomllib.load(config_file)
|
|
44
|
-
except FileNotFoundError:
|
|
45
|
-
return NodeConfiguration()
|
|
46
53
|
except (OSError, UnicodeDecodeError, tomllib.TOMLDecodeError) as error:
|
|
47
54
|
raise ConfigurationError(f"cannot read {path}: {error}") from error
|
|
48
55
|
|
|
@@ -80,6 +87,7 @@ def load_node_configuration() -> NodeConfiguration:
|
|
|
80
87
|
storage=_optional_text(node.get("storage"), "node.storage"),
|
|
81
88
|
audit=_optional_text(node.get("audit"), "node.audit"),
|
|
82
89
|
endpoint=_optional_text(node.get("endpoint"), "node.endpoint"),
|
|
90
|
+
database_name=_optional_text(node.get("database_name"), "node.database_name"),
|
|
83
91
|
model=_optional_text(runtime.get("model"), "runtime.model"),
|
|
84
92
|
effort=_optional_text(runtime.get("effort"), "runtime.effort"),
|
|
85
93
|
sandbox_mode=parsed_sandbox_mode,
|
{bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/app/registry.py
RENAMED
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from collections.abc import Callable, Mapping
|
|
4
4
|
from dataclasses import dataclass
|
|
5
|
+
from functools import partial
|
|
5
6
|
from importlib.metadata import EntryPoint, entry_points
|
|
6
7
|
from typing import Any, cast
|
|
7
8
|
|
|
@@ -46,11 +47,18 @@ class AdapterRegistry:
|
|
|
46
47
|
runtime: str,
|
|
47
48
|
storage: str = "sqlite",
|
|
48
49
|
audit: str = "logging",
|
|
50
|
+
storage_options: Mapping[str, object] | None = None,
|
|
49
51
|
) -> AdapterFactories:
|
|
50
52
|
control = self._load_optional(
|
|
51
53
|
CONTROL_ENTRY_POINT_GROUP,
|
|
52
54
|
f"{channel}+{runtime}+{storage}",
|
|
53
55
|
)
|
|
56
|
+
storage_factory = cast(
|
|
57
|
+
Callable[[Mapping[str, object]], IStorage] | StorageFactory,
|
|
58
|
+
self._load(STORAGE_ENTRY_POINT_GROUP, storage),
|
|
59
|
+
)
|
|
60
|
+
if storage_options:
|
|
61
|
+
storage_factory = partial(storage_factory, dict(storage_options))
|
|
54
62
|
return AdapterFactories(
|
|
55
63
|
channel=cast(
|
|
56
64
|
ChannelFactory,
|
|
@@ -60,10 +68,7 @@ class AdapterRegistry:
|
|
|
60
68
|
RuntimeFactory,
|
|
61
69
|
self._load(RUNTIME_ENTRY_POINT_GROUP, runtime),
|
|
62
70
|
),
|
|
63
|
-
storage=cast(
|
|
64
|
-
StorageFactory,
|
|
65
|
-
self._load(STORAGE_ENTRY_POINT_GROUP, storage),
|
|
66
|
-
),
|
|
71
|
+
storage=cast(StorageFactory, storage_factory),
|
|
67
72
|
audit=cast(
|
|
68
73
|
AuditFactory,
|
|
69
74
|
self._load(AUDIT_ENTRY_POINT_GROUP, audit),
|
{bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/app/transport.py
RENAMED
|
@@ -84,21 +84,23 @@ class LocalCommandServer:
|
|
|
84
84
|
|
|
85
85
|
path = self._endpoint_path
|
|
86
86
|
if path is None:
|
|
87
|
-
path = (
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
path = await asyncio.to_thread(
|
|
88
|
+
lambda: (
|
|
89
|
+
Path(tempfile.gettempdir())
|
|
90
|
+
/ f"bcn-{os.getpid()}-{secrets.token_hex(6)}.sock"
|
|
91
|
+
)
|
|
90
92
|
)
|
|
91
93
|
path = path.expanduser()
|
|
92
|
-
path.parent.mkdir
|
|
93
|
-
if path.exists
|
|
94
|
+
await asyncio.to_thread(path.parent.mkdir, parents=True, exist_ok=True)
|
|
95
|
+
if await asyncio.to_thread(path.exists):
|
|
94
96
|
raise FileExistsError(f"local command endpoint already exists: {path}")
|
|
95
97
|
self._server = await asyncio.start_unix_server(
|
|
96
98
|
self._handle_client,
|
|
97
99
|
path=str(path),
|
|
98
100
|
)
|
|
99
|
-
os.chmod
|
|
101
|
+
await asyncio.to_thread(os.chmod, path, 0o600)
|
|
100
102
|
self._unix_path = path
|
|
101
|
-
path_stat = path.stat
|
|
103
|
+
path_stat = await asyncio.to_thread(path.stat)
|
|
102
104
|
self._unix_identity = (path_stat.st_dev, path_stat.st_ino)
|
|
103
105
|
self._endpoint = f"unix://{path}"
|
|
104
106
|
|
|
@@ -122,10 +124,14 @@ class LocalCommandServer:
|
|
|
122
124
|
self._unix_path = None
|
|
123
125
|
identity = self._unix_identity
|
|
124
126
|
self._unix_identity = None
|
|
125
|
-
if
|
|
126
|
-
|
|
127
|
+
if (
|
|
128
|
+
path is not None
|
|
129
|
+
and identity is not None
|
|
130
|
+
and await asyncio.to_thread(path.exists)
|
|
131
|
+
):
|
|
132
|
+
path_stat = await asyncio.to_thread(path.stat)
|
|
127
133
|
if (path_stat.st_dev, path_stat.st_ino) == identity:
|
|
128
|
-
path.unlink
|
|
134
|
+
await asyncio.to_thread(path.unlink)
|
|
129
135
|
|
|
130
136
|
async def _handle_client(
|
|
131
137
|
self,
|
{bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.6}/src/bazaar_compute_node/app/windows_pipe.py
RENAMED
|
@@ -12,6 +12,8 @@ from pathlib import Path
|
|
|
12
12
|
from typing import Any
|
|
13
13
|
from urllib.parse import urlsplit
|
|
14
14
|
|
|
15
|
+
from bazaar_compute_node.core.paths import resolve_data_dir
|
|
16
|
+
|
|
15
17
|
RequestHandler = Callable[[Mapping[str, object]], Awaitable[Mapping[str, object]]]
|
|
16
18
|
|
|
17
19
|
_ERROR_ALREADY_EXISTS = 183
|
|
@@ -122,8 +124,8 @@ def _is_invalid_handle(handle: Any) -> bool:
|
|
|
122
124
|
|
|
123
125
|
|
|
124
126
|
def _pipe_name(endpoint_path: Path | None) -> str:
|
|
125
|
-
path = endpoint_path or (
|
|
126
|
-
identity = str(path.expanduser().
|
|
127
|
+
path = endpoint_path or (resolve_data_dir() / "bcn.sock")
|
|
128
|
+
identity = str(path.expanduser().absolute()).casefold()
|
|
127
129
|
digest = hashlib.sha256(identity.encode("utf-8")).hexdigest()[:32]
|
|
128
130
|
return f"bcn-{digest}"
|
|
129
131
|
|
|
@@ -5,6 +5,7 @@ import asyncio
|
|
|
5
5
|
import os
|
|
6
6
|
import sys
|
|
7
7
|
from collections.abc import Mapping, Sequence
|
|
8
|
+
from pathlib import Path
|
|
8
9
|
from typing import NoReturn
|
|
9
10
|
from uuid import uuid7
|
|
10
11
|
|
|
@@ -46,6 +47,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
46
47
|
send_parser = message_subparsers.add_parser("send")
|
|
47
48
|
send_parser.add_argument("--target", required=True)
|
|
48
49
|
send_parser.add_argument("--reply-to")
|
|
50
|
+
send_parser.add_argument("--attachment", action="append", default=[])
|
|
49
51
|
|
|
50
52
|
thread_parser = subparsers.add_parser("thread")
|
|
51
53
|
thread_subparsers = thread_parser.add_subparsers(dest="command", required=True)
|
|
@@ -100,6 +102,9 @@ async def _request(
|
|
|
100
102
|
request["body"] = body if body is not None else ""
|
|
101
103
|
request["command_id"] = f"bcc-{uuid7().hex}"
|
|
102
104
|
request["reply_to_message_id"] = args.reply_to
|
|
105
|
+
request["attachment_paths"] = await asyncio.to_thread(
|
|
106
|
+
lambda: [str(Path(path).absolute()) for path in args.attachment]
|
|
107
|
+
)
|
|
103
108
|
elif args.command == "unfollow":
|
|
104
109
|
request["target"] = args.target
|
|
105
110
|
|
|
@@ -66,6 +66,16 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
66
66
|
)
|
|
67
67
|
parser.add_argument("--storage")
|
|
68
68
|
parser.add_argument("--audit")
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--config",
|
|
71
|
+
type=Path,
|
|
72
|
+
help="Configuration file path; defaults to the node data directory.",
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--database-name",
|
|
76
|
+
type=_database_name,
|
|
77
|
+
help="SQLite database filename under the node data directory.",
|
|
78
|
+
)
|
|
69
79
|
parser.add_argument(
|
|
70
80
|
"--endpoint",
|
|
71
81
|
type=Path,
|
|
@@ -85,6 +95,14 @@ def _non_empty_option(value: str) -> str:
|
|
|
85
95
|
return value
|
|
86
96
|
|
|
87
97
|
|
|
98
|
+
def _database_name(value: str) -> str:
|
|
99
|
+
if not value or value in {".", ".."} or "/" in value or "\\" in value:
|
|
100
|
+
raise argparse.ArgumentTypeError(
|
|
101
|
+
"database name must be a single non-empty path component"
|
|
102
|
+
)
|
|
103
|
+
return value
|
|
104
|
+
|
|
105
|
+
|
|
88
106
|
def _endpoint_path(args: argparse.Namespace, data_dir: Path) -> Path:
|
|
89
107
|
return (args.endpoint or data_dir / "bcn.sock").expanduser()
|
|
90
108
|
|
|
@@ -107,6 +125,9 @@ def _load_factories(
|
|
|
107
125
|
runtime=args.runtime,
|
|
108
126
|
storage=args.storage,
|
|
109
127
|
audit=args.audit,
|
|
128
|
+
storage_options={"database_name": args.database_name}
|
|
129
|
+
if args.storage == "sqlite" and args.database_name is not None
|
|
130
|
+
else None,
|
|
110
131
|
)
|
|
111
132
|
except ProviderLoadError as error:
|
|
112
133
|
parser.error(str(error))
|
|
@@ -128,7 +149,7 @@ def _apply_runtime_configuration(
|
|
|
128
149
|
parser: argparse.ArgumentParser,
|
|
129
150
|
) -> None:
|
|
130
151
|
try:
|
|
131
|
-
configuration = load_node_configuration()
|
|
152
|
+
configuration = load_node_configuration(args.config)
|
|
132
153
|
except ConfigurationError as error:
|
|
133
154
|
parser.error(str(error))
|
|
134
155
|
for name in (
|
|
@@ -145,6 +166,8 @@ def _apply_runtime_configuration(
|
|
|
145
166
|
setattr(args, name, getattr(configuration, name))
|
|
146
167
|
if args.endpoint is None and configuration.endpoint is not None:
|
|
147
168
|
args.endpoint = Path(configuration.endpoint).expanduser()
|
|
169
|
+
if args.database_name is None:
|
|
170
|
+
args.database_name = configuration.database_name
|
|
148
171
|
if args.storage is None:
|
|
149
172
|
args.storage = DEFAULT_STORAGE
|
|
150
173
|
if args.audit is None:
|
|
@@ -157,7 +180,7 @@ def _apply_runtime_configuration(
|
|
|
157
180
|
|
|
158
181
|
|
|
159
182
|
async def _run_node(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
|
|
160
|
-
factories = _load_factories
|
|
183
|
+
factories = await asyncio.to_thread(_load_factories, args, parser)
|
|
161
184
|
|
|
162
185
|
data_dir = resolve_data_dir()
|
|
163
186
|
node = NodeApplication(
|
|
@@ -198,6 +221,10 @@ def _daemon_command(args: argparse.Namespace, data_dir: Path) -> list[str]:
|
|
|
198
221
|
"--endpoint",
|
|
199
222
|
str(_endpoint_path(args, data_dir)),
|
|
200
223
|
]
|
|
224
|
+
if args.config is not None:
|
|
225
|
+
command.extend(("--config", str(args.config)))
|
|
226
|
+
if args.database_name is not None:
|
|
227
|
+
command.extend(("--database-name", args.database_name))
|
|
201
228
|
for name in ("model", "effort", "sandbox_mode"):
|
|
202
229
|
value = getattr(args, name)
|
|
203
230
|
if value is not None:
|
|
@@ -284,14 +311,14 @@ async def _start_daemon(
|
|
|
284
311
|
args: argparse.Namespace,
|
|
285
312
|
parser: argparse.ArgumentParser,
|
|
286
313
|
) -> int:
|
|
287
|
-
_load_factories
|
|
288
|
-
data_dir = resolve_data_dir
|
|
289
|
-
data_dir.mkdir
|
|
314
|
+
await asyncio.to_thread(_load_factories, args, parser)
|
|
315
|
+
data_dir = await asyncio.to_thread(resolve_data_dir)
|
|
316
|
+
await asyncio.to_thread(data_dir.mkdir, parents=True, exist_ok=True)
|
|
290
317
|
endpoint_path = _endpoint_path(args, data_dir)
|
|
291
318
|
endpoint = local_endpoint_for_path(endpoint_path)
|
|
292
319
|
if os.name == "nt" and await _endpoint_is_reachable(endpoint, timeout=0.5):
|
|
293
320
|
parser.error("bcn is already running")
|
|
294
|
-
if os.name != "nt" and endpoint_path.exists
|
|
321
|
+
if os.name != "nt" and await asyncio.to_thread(endpoint_path.exists):
|
|
295
322
|
parser.error(f"bcn endpoint already exists: {endpoint_path}")
|
|
296
323
|
|
|
297
324
|
log_path = data_dir / "bcn.log"
|
|
@@ -316,7 +343,7 @@ async def _stop_daemon(
|
|
|
316
343
|
args: argparse.Namespace,
|
|
317
344
|
parser: argparse.ArgumentParser,
|
|
318
345
|
) -> int:
|
|
319
|
-
data_dir = resolve_data_dir
|
|
346
|
+
data_dir = await asyncio.to_thread(resolve_data_dir)
|
|
320
347
|
endpoint_path = _endpoint_path(args, data_dir)
|
|
321
348
|
endpoint = local_endpoint_for_path(endpoint_path)
|
|
322
349
|
if not await _endpoint_is_reachable(endpoint, timeout=0.5):
|
|
@@ -354,10 +381,8 @@ async def _restart_daemon(
|
|
|
354
381
|
|
|
355
382
|
|
|
356
383
|
async def async_main(argv: Sequence[str] | None = None) -> int:
|
|
357
|
-
parser =
|
|
358
|
-
args = parser.parse_args(argv)
|
|
384
|
+
parser, args = await asyncio.to_thread(_prepare_cli_arguments, argv)
|
|
359
385
|
command = args.command
|
|
360
|
-
_apply_runtime_configuration(args, parser)
|
|
361
386
|
if command is None:
|
|
362
387
|
if args.channel is None and args.runtime is None:
|
|
363
388
|
parser.print_help()
|
|
@@ -374,6 +399,17 @@ async def async_main(argv: Sequence[str] | None = None) -> int:
|
|
|
374
399
|
parser.error(f"unsupported command: {command}")
|
|
375
400
|
|
|
376
401
|
|
|
402
|
+
def _prepare_cli_arguments(
|
|
403
|
+
argv: Sequence[str] | None,
|
|
404
|
+
) -> tuple[argparse.ArgumentParser, argparse.Namespace]:
|
|
405
|
+
parser = build_parser()
|
|
406
|
+
args = parser.parse_args(argv)
|
|
407
|
+
if args.config is not None:
|
|
408
|
+
args.config = args.config.expanduser().resolve()
|
|
409
|
+
_apply_runtime_configuration(args, parser)
|
|
410
|
+
return parser, args
|
|
411
|
+
|
|
412
|
+
|
|
377
413
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
378
414
|
return asyncio.run(async_main(argv))
|
|
379
415
|
|
|
@@ -11,10 +11,12 @@ from .client import (
|
|
|
11
11
|
build_thread_start_params,
|
|
12
12
|
build_turn_interrupt_params,
|
|
13
13
|
build_turn_start_params,
|
|
14
|
+
build_turn_steer_params,
|
|
14
15
|
parse_error_notification,
|
|
15
16
|
parse_thread_response,
|
|
16
17
|
parse_turn_notification,
|
|
17
18
|
parse_turn_response,
|
|
19
|
+
parse_turn_steer_response,
|
|
18
20
|
)
|
|
19
21
|
from .events import CodexTurnEventStream
|
|
20
22
|
from .process import (
|
|
@@ -56,8 +58,10 @@ __all__ = [
|
|
|
56
58
|
"build_thread_start_params",
|
|
57
59
|
"build_turn_interrupt_params",
|
|
58
60
|
"build_turn_start_params",
|
|
61
|
+
"build_turn_steer_params",
|
|
59
62
|
"parse_error_notification",
|
|
60
63
|
"parse_thread_response",
|
|
61
64
|
"parse_turn_notification",
|
|
62
65
|
"parse_turn_response",
|
|
66
|
+
"parse_turn_steer_response",
|
|
63
67
|
]
|