bazaar-compute-node 0.1.9__tar.gz → 0.1.10__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.9 → bazaar_compute_node-0.1.10}/PKG-INFO +10 -8
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/README.md +8 -7
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/pyproject.toml +2 -1
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/pyproject.toml.orig +2 -1
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/app/agent.py +505 -0
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/app/agent_management.py +217 -0
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/app/application.py +399 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/app/command.py +5 -1
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/app/config.py +607 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/app/registry.py +30 -21
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/app/transport.py +5 -1
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/app/wrapper.py +13 -4
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/bcc.py +21 -3
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/cli.py +185 -132
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/codex/runtime.py +31 -22
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/contrib/sqlite/__init__.py +10 -0
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/contrib/sqlite/agent_migration.py +230 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/sqlite/codec.py +6 -1
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/sqlite/database.py +39 -147
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/contrib/sqlite/reminder_database.py +74 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/sqlite/reminder_repository.py +218 -3
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/sqlite/repository.py +6 -5
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/contrib/sqlite/scoped_repository.py +778 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/telegram/approval.py +4 -2
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/telegram/channel.py +29 -13
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/telegram/outbound.py +4 -5
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/telegram/plugin.py +5 -2
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/wecom/channel.py +10 -6
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/wecom/plugin.py +8 -5
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/core/channel.py +278 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/instruction.py +19 -5
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/models/__init__.py +20 -14
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/models/entities.py +25 -3
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/core/models/reminder_owner.py +59 -0
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/core/models/states.py +396 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/orchestration/command.py +29 -104
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/core/orchestration/delivery.py +110 -0
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/core/orchestration/error_feedback.py +164 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/orchestration/reminder.py +55 -25
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/orchestration/services.py +30 -18
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/orchestration/session.py +190 -189
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/orchestration/turn.py +47 -42
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/core/outcomes.py +99 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/paths.py +6 -8
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/runtime.py +38 -56
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/core/storage.py +172 -0
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/i18n/__init__.py +8 -0
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/i18n/catalog.py +44 -0
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/i18n/english.py +10 -0
- bazaar_compute_node-0.1.10/src/bazaar_compute_node/i18n/schinese.py +10 -0
- bazaar_compute_node-0.1.9/src/bazaar_compute_node/app/application.py +0 -462
- bazaar_compute_node-0.1.9/src/bazaar_compute_node/app/config.py +0 -143
- bazaar_compute_node-0.1.9/src/bazaar_compute_node/contrib/sqlite/__init__.py +0 -15
- bazaar_compute_node-0.1.9/src/bazaar_compute_node/contrib/sqlite/reminder_database.py +0 -21
- bazaar_compute_node-0.1.9/src/bazaar_compute_node/core/channel.py +0 -166
- bazaar_compute_node-0.1.9/src/bazaar_compute_node/core/models/states.py +0 -382
- bazaar_compute_node-0.1.9/src/bazaar_compute_node/core/outcomes.py +0 -51
- bazaar_compute_node-0.1.9/src/bazaar_compute_node/core/storage.py +0 -237
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/LICENSE +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/__init__.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/app/__init__.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/app/attachments.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/app/reminder_dispatch.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/app/windows_pipe.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/__init__.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/codex/__init__.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/codex/approval.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/codex/client.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/codex/events.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/codex/plugin.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/codex/process.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/codex/protocol.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/logging/__init__.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/logging/audit.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/logging/plugin.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/sqlite/migrations.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/sqlite/plugin.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/sqlite/reminder_codec.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/sqlite/reminder_migration.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/telegram/__init__.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/telegram/api.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/telegram/attachments.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/telegram/identity.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/telegram/markdown.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/wecom/__init__.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/wecom/markdown.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/contrib/wecom/outbound.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/__init__.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/approval.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/audit.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/client.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/command.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/concurrency.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/correlation.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/lifecycle.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/models/reminder.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/observability.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/orchestration/__init__.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/orchestration/reminder_command.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/reminder.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/reminder_schedule.py +0 -0
- {bazaar_compute_node-0.1.9 → bazaar_compute_node-0.1.10}/src/bazaar_compute_node/core/timerwheel.py +0 -0
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bazaar-compute-node
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.10
|
|
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
|
|
7
7
|
Requires-Dist: aiohttp>=3.12.0
|
|
8
8
|
Requires-Dist: aiosqlite>=0.20.0
|
|
9
9
|
Requires-Dist: cryptography>=45.0.0
|
|
10
|
+
Requires-Dist: tzdata>=2024.1 ; sys_platform == 'win32'
|
|
10
11
|
Requires-Python: >=3.14
|
|
11
12
|
Project-URL: Repository, https://github.com/yuchanns/bazaar-compute-node
|
|
12
13
|
Description-Content-Type: text/markdown
|
|
@@ -41,14 +42,15 @@ bcn --version
|
|
|
41
42
|
# Upgrade
|
|
42
43
|
uv tool upgrade bazaar-compute-node
|
|
43
44
|
|
|
44
|
-
#
|
|
45
|
-
|
|
45
|
+
# Add an Agent
|
|
46
|
+
bcn agent add \
|
|
47
|
+
--name Tifa \
|
|
48
|
+
--channel telegram \
|
|
49
|
+
--runtime codex \
|
|
50
|
+
--set channel.token_env=BCN_TELEGRAM_TIFA_TOKEN
|
|
46
51
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
--sandbox-mode workspace-write \
|
|
50
|
-
--network-access true \
|
|
51
|
-
--idle-timeout 600
|
|
52
|
+
# Start daemon
|
|
53
|
+
bcn start
|
|
52
54
|
|
|
53
55
|
# Stop daemon
|
|
54
56
|
bcn stop
|
|
@@ -28,14 +28,15 @@ bcn --version
|
|
|
28
28
|
# Upgrade
|
|
29
29
|
uv tool upgrade bazaar-compute-node
|
|
30
30
|
|
|
31
|
-
#
|
|
32
|
-
|
|
31
|
+
# Add an Agent
|
|
32
|
+
bcn agent add \
|
|
33
|
+
--name Tifa \
|
|
34
|
+
--channel telegram \
|
|
35
|
+
--runtime codex \
|
|
36
|
+
--set channel.token_env=BCN_TELEGRAM_TIFA_TOKEN
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
--sandbox-mode workspace-write \
|
|
37
|
-
--network-access true \
|
|
38
|
-
--idle-timeout 600
|
|
38
|
+
# Start daemon
|
|
39
|
+
bcn start
|
|
39
40
|
|
|
40
41
|
# Stop daemon
|
|
41
42
|
bcn stop
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "bazaar-compute-node"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.10"
|
|
4
4
|
description = "Runtime-agnostic computer node daemon for agents and channels."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "AGPL-3.0-only"
|
|
@@ -10,6 +10,7 @@ dependencies = [
|
|
|
10
10
|
"aiohttp>=3.12.0",
|
|
11
11
|
"aiosqlite>=0.20.0",
|
|
12
12
|
"cryptography>=45.0.0",
|
|
13
|
+
"tzdata>=2024.1; sys_platform == 'win32'",
|
|
13
14
|
]
|
|
14
15
|
|
|
15
16
|
[project.urls]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "bazaar-compute-node"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.10"
|
|
4
4
|
description = "Runtime-agnostic computer node daemon for agents and channels."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = "AGPL-3.0-only"
|
|
@@ -10,6 +10,7 @@ dependencies = [
|
|
|
10
10
|
"aiohttp>=3.12.0",
|
|
11
11
|
"aiosqlite>=0.20.0",
|
|
12
12
|
"cryptography>=45.0.0",
|
|
13
|
+
"tzdata>=2024.1; sys_platform == 'win32'",
|
|
13
14
|
]
|
|
14
15
|
|
|
15
16
|
[project.urls]
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import hmac
|
|
5
|
+
import logging
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import secrets
|
|
10
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
from ..core.channel import AgentScopedChannel, ChannelContext, IChannel
|
|
15
|
+
from ..core.concurrency import ISessionConcurrency, SessionLockRegistry
|
|
16
|
+
from ..core.lifecycle import TimeoutBudget
|
|
17
|
+
from ..core.models import RuntimeSession
|
|
18
|
+
from ..core.observability import IAudit
|
|
19
|
+
from ..core.orchestration import SessionOrchestrator
|
|
20
|
+
from ..core.orchestration.reminder_command import ReminderCommandService
|
|
21
|
+
from ..core.paths import resolve_workspace_dir
|
|
22
|
+
from ..core.runtime import IRuntime, RuntimeCommandContext
|
|
23
|
+
from ..core.storage import IStorageScope
|
|
24
|
+
from ..core.timerwheel import TimerWheel
|
|
25
|
+
from ..i18n import Translator
|
|
26
|
+
from .attachments import AttachmentMaterializer
|
|
27
|
+
from .command import CommandDispatchError
|
|
28
|
+
from .config import AgentConfiguration
|
|
29
|
+
from .registry import AgentAdapterFactories
|
|
30
|
+
from .reminder_dispatch import CommandDispatcher
|
|
31
|
+
from .wrapper import install_bcc_wrapper, remove_bcc_wrapper
|
|
32
|
+
|
|
33
|
+
CommandRecord = tuple[str, tuple[str, ...]]
|
|
34
|
+
|
|
35
|
+
_ENVIRONMENT_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
36
|
+
_PLATFORM_ENVIRONMENT = {
|
|
37
|
+
"HOME",
|
|
38
|
+
"LANG",
|
|
39
|
+
"LC_ALL",
|
|
40
|
+
"PATH",
|
|
41
|
+
"TMPDIR",
|
|
42
|
+
"TEMP",
|
|
43
|
+
"TMP",
|
|
44
|
+
"SystemRoot",
|
|
45
|
+
"ComSpec",
|
|
46
|
+
"PATHEXT",
|
|
47
|
+
"USERPROFILE",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class _SessionCapabilityBinding:
|
|
53
|
+
agent_id: str
|
|
54
|
+
bcn_session_id: str
|
|
55
|
+
runtime_session_id: str
|
|
56
|
+
capability: str
|
|
57
|
+
token_values: tuple[str, ...]
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class AgentApplication:
|
|
61
|
+
"""Process-local composition for one configured BCN Agent."""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
*,
|
|
66
|
+
configuration: AgentConfiguration,
|
|
67
|
+
factories: AgentAdapterFactories,
|
|
68
|
+
storage: IStorageScope,
|
|
69
|
+
audit: IAudit,
|
|
70
|
+
timer_wheel: TimerWheel,
|
|
71
|
+
reminder_concurrency: ISessionConcurrency,
|
|
72
|
+
reminder_poke: Callable[[], None],
|
|
73
|
+
endpoint: Callable[[], str],
|
|
74
|
+
timeout_budget: TimeoutBudget,
|
|
75
|
+
translator: Translator,
|
|
76
|
+
) -> None:
|
|
77
|
+
self.configuration = configuration
|
|
78
|
+
self.agent_id = configuration.id
|
|
79
|
+
self.name = configuration.name
|
|
80
|
+
self.storage = storage
|
|
81
|
+
self.audit = audit
|
|
82
|
+
self.timer_wheel = timer_wheel
|
|
83
|
+
self.timeout_budget = timeout_budget
|
|
84
|
+
self.translator = translator
|
|
85
|
+
self._endpoint = endpoint
|
|
86
|
+
self._wrapper_path: Path | None = None
|
|
87
|
+
self.command_log: list[CommandRecord] = []
|
|
88
|
+
self._session_capabilities: dict[str, _SessionCapabilityBinding] = {}
|
|
89
|
+
self._concurrency = SessionLockRegistry()
|
|
90
|
+
self._started = False
|
|
91
|
+
self._stopping = False
|
|
92
|
+
self._runtime_environment_include = configuration.runtime.env_include
|
|
93
|
+
self._logger = logging.getLogger("bazaar_compute_node.application.agent")
|
|
94
|
+
self._attachment_materializer = AttachmentMaterializer(
|
|
95
|
+
self.workspace_path,
|
|
96
|
+
self._referenced_attachment_paths,
|
|
97
|
+
)
|
|
98
|
+
provider_channel = factories.channel.build(
|
|
99
|
+
ChannelContext(
|
|
100
|
+
agent_id=self.agent_id,
|
|
101
|
+
attachments=self._attachment_materializer,
|
|
102
|
+
options=dict(configuration.channel.options),
|
|
103
|
+
workspace=self.workspace_path,
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
self.channel: IChannel = AgentScopedChannel(self.agent_id, provider_channel)
|
|
107
|
+
runtime_options: dict[str, str] = {
|
|
108
|
+
key: value
|
|
109
|
+
for key, value in configuration.runtime.options.items()
|
|
110
|
+
if isinstance(value, str)
|
|
111
|
+
}
|
|
112
|
+
if configuration.runtime.model is not None:
|
|
113
|
+
runtime_options["model"] = configuration.runtime.model
|
|
114
|
+
if configuration.runtime.effort is not None:
|
|
115
|
+
runtime_options["effort"] = configuration.runtime.effort
|
|
116
|
+
self._runtime_context = RuntimeCommandContext(
|
|
117
|
+
run_command=self._run_runtime_command,
|
|
118
|
+
environment_for_session=self._runtime_environment,
|
|
119
|
+
agent_id=self.agent_id,
|
|
120
|
+
agent_name=self.name,
|
|
121
|
+
bot_name=self._bot_name,
|
|
122
|
+
runtime_options=runtime_options,
|
|
123
|
+
sandbox_mode=configuration.runtime.sandbox_mode,
|
|
124
|
+
network_access=configuration.runtime.network_access,
|
|
125
|
+
startup_timeout_seconds=timeout_budget.startup_seconds,
|
|
126
|
+
)
|
|
127
|
+
self.runtime: IRuntime = factories.runtime(self._runtime_context)
|
|
128
|
+
idle_timeout_seconds = configuration.runtime.idle_timeout_seconds
|
|
129
|
+
if (
|
|
130
|
+
isinstance(idle_timeout_seconds, bool)
|
|
131
|
+
or not isinstance(idle_timeout_seconds, int | float)
|
|
132
|
+
or not math.isfinite(idle_timeout_seconds)
|
|
133
|
+
):
|
|
134
|
+
raise ValueError("runtime idle timeout must be a finite number")
|
|
135
|
+
if (
|
|
136
|
+
idle_timeout_seconds > 0
|
|
137
|
+
and idle_timeout_seconds * 1_000 > timer_wheel.maximum_delay_ms
|
|
138
|
+
):
|
|
139
|
+
raise ValueError("runtime idle timeout exceeds the timer horizon")
|
|
140
|
+
runtime_idle_timeout_ms = (
|
|
141
|
+
math.ceil(idle_timeout_seconds * 1_000) if idle_timeout_seconds > 0 else 0
|
|
142
|
+
)
|
|
143
|
+
self.orchestrator = SessionOrchestrator(
|
|
144
|
+
agent_id=self.agent_id,
|
|
145
|
+
channel=self.channel,
|
|
146
|
+
runtime=self.runtime,
|
|
147
|
+
storage=self.storage,
|
|
148
|
+
audit=self.audit,
|
|
149
|
+
timeout_budget=self.timeout_budget,
|
|
150
|
+
timer_wheel=self.timer_wheel,
|
|
151
|
+
runtime_idle_timeout_ms=runtime_idle_timeout_ms,
|
|
152
|
+
workspace=self.workspace_path,
|
|
153
|
+
translator=self.translator,
|
|
154
|
+
error_feedback_detail=self._error_feedback_detail,
|
|
155
|
+
concurrency=self._concurrency,
|
|
156
|
+
)
|
|
157
|
+
self.reminder_service = ReminderCommandService(
|
|
158
|
+
storage=self.storage,
|
|
159
|
+
concurrency=reminder_concurrency,
|
|
160
|
+
poke=reminder_poke,
|
|
161
|
+
)
|
|
162
|
+
self._provider_control_handler = (
|
|
163
|
+
factories.control(self._adapter_context())
|
|
164
|
+
if factories.control is not None
|
|
165
|
+
else None
|
|
166
|
+
)
|
|
167
|
+
self.command_dispatcher = CommandDispatcher(
|
|
168
|
+
self.orchestrator.command_service,
|
|
169
|
+
reminder_service=self.reminder_service,
|
|
170
|
+
timeout_budget=self.timeout_budget,
|
|
171
|
+
control_handler=(
|
|
172
|
+
self._handle_control
|
|
173
|
+
if self._provider_control_handler is not None
|
|
174
|
+
else None
|
|
175
|
+
),
|
|
176
|
+
session_binding_validator=self._validate_session_binding,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def started(self) -> bool:
|
|
181
|
+
return self._started
|
|
182
|
+
|
|
183
|
+
def workspace_path(self) -> Path:
|
|
184
|
+
return resolve_workspace_dir(self.agent_id)
|
|
185
|
+
|
|
186
|
+
async def start(self) -> None:
|
|
187
|
+
if self._started:
|
|
188
|
+
return
|
|
189
|
+
if self._stopping:
|
|
190
|
+
raise RuntimeError("Agent application is stopping")
|
|
191
|
+
workspace = self.workspace_path()
|
|
192
|
+
await asyncio.to_thread(
|
|
193
|
+
workspace.mkdir,
|
|
194
|
+
parents=True,
|
|
195
|
+
exist_ok=True,
|
|
196
|
+
mode=0o700,
|
|
197
|
+
)
|
|
198
|
+
if os.name != "nt":
|
|
199
|
+
await asyncio.to_thread(workspace.chmod, 0o700)
|
|
200
|
+
try:
|
|
201
|
+
self._wrapper_path = await asyncio.to_thread(
|
|
202
|
+
install_bcc_wrapper,
|
|
203
|
+
workspace / ".bcn" / "bin",
|
|
204
|
+
agent_id=self.agent_id,
|
|
205
|
+
)
|
|
206
|
+
await self._attachment_materializer.reconcile()
|
|
207
|
+
await self.orchestrator.start(
|
|
208
|
+
timeout=self.timeout_budget.startup_seconds,
|
|
209
|
+
)
|
|
210
|
+
except BaseException:
|
|
211
|
+
await self._cleanup_partial_start()
|
|
212
|
+
raise
|
|
213
|
+
self._started = True
|
|
214
|
+
self.command_dispatcher.start_accepting()
|
|
215
|
+
|
|
216
|
+
def _bot_name(self) -> str | None:
|
|
217
|
+
identity = self.channel.get_identity()
|
|
218
|
+
if identity is not None:
|
|
219
|
+
if identity.name is not None:
|
|
220
|
+
return identity.name
|
|
221
|
+
if identity.id is not None:
|
|
222
|
+
return identity.id
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
async def stop(self) -> None:
|
|
226
|
+
if self._stopping:
|
|
227
|
+
return
|
|
228
|
+
self._stopping = True
|
|
229
|
+
self._started = False
|
|
230
|
+
self.command_dispatcher.stop_accepting()
|
|
231
|
+
errors: list[BaseException] = []
|
|
232
|
+
try:
|
|
233
|
+
await self.command_dispatcher.drain(
|
|
234
|
+
timeout=self.timeout_budget.shutdown_seconds,
|
|
235
|
+
)
|
|
236
|
+
except BaseException as error: # noqa: BLE001
|
|
237
|
+
errors.append(error)
|
|
238
|
+
try:
|
|
239
|
+
await self.orchestrator.stop(
|
|
240
|
+
timeout=self.timeout_budget.shutdown_seconds,
|
|
241
|
+
)
|
|
242
|
+
except BaseException as error: # noqa: BLE001
|
|
243
|
+
errors.append(error)
|
|
244
|
+
try:
|
|
245
|
+
await self._cleanup_bcc_wrapper()
|
|
246
|
+
except BaseException as error: # noqa: BLE001
|
|
247
|
+
errors.append(error)
|
|
248
|
+
self._session_capabilities.clear()
|
|
249
|
+
if errors:
|
|
250
|
+
raise RuntimeError(
|
|
251
|
+
"; ".join(type(error).__name__ for error in errors)
|
|
252
|
+
) from errors[0]
|
|
253
|
+
|
|
254
|
+
async def dispatch(
|
|
255
|
+
self,
|
|
256
|
+
request: Mapping[str, object],
|
|
257
|
+
) -> Mapping[str, object]:
|
|
258
|
+
return await self.command_dispatcher(request)
|
|
259
|
+
|
|
260
|
+
async def has_session(self, session_id: str) -> bool:
|
|
261
|
+
if not isinstance(session_id, str) or not session_id:
|
|
262
|
+
return False
|
|
263
|
+
async with self.storage.transaction() as transaction:
|
|
264
|
+
return await transaction.get_bcn_session(session_id) is not None
|
|
265
|
+
|
|
266
|
+
async def publish_reminder_wake(self, session_id: str) -> None:
|
|
267
|
+
if not self._started:
|
|
268
|
+
raise RuntimeError("Agent application is not started")
|
|
269
|
+
await self.orchestrator.publish_reminder_wake(session_id)
|
|
270
|
+
|
|
271
|
+
def health_record(self) -> dict[str, object]:
|
|
272
|
+
return {
|
|
273
|
+
"agent_id": self.agent_id,
|
|
274
|
+
"name": self.name,
|
|
275
|
+
"status": "started" if self.started else "stopped",
|
|
276
|
+
"channel": self.channel.name,
|
|
277
|
+
"runtime": self.runtime.name,
|
|
278
|
+
"channel_health": dict(self.channel.health),
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async def _cleanup_partial_start(self) -> None:
|
|
282
|
+
try:
|
|
283
|
+
await self.orchestrator.stop(
|
|
284
|
+
timeout=self.timeout_budget.shutdown_seconds,
|
|
285
|
+
)
|
|
286
|
+
except BaseException as error:
|
|
287
|
+
self._logger.debug("orchestrator cleanup failed", exc_info=error)
|
|
288
|
+
try:
|
|
289
|
+
await self._cleanup_bcc_wrapper()
|
|
290
|
+
except BaseException as error:
|
|
291
|
+
self._logger.debug("bcc wrapper cleanup failed", exc_info=error)
|
|
292
|
+
self._session_capabilities.clear()
|
|
293
|
+
|
|
294
|
+
async def _cleanup_bcc_wrapper(self) -> None:
|
|
295
|
+
wrapper_path = self._wrapper_path
|
|
296
|
+
if wrapper_path is None:
|
|
297
|
+
return
|
|
298
|
+
await asyncio.to_thread(remove_bcc_wrapper, wrapper_path)
|
|
299
|
+
self._wrapper_path = None
|
|
300
|
+
|
|
301
|
+
async def _referenced_attachment_paths(self) -> set[str]:
|
|
302
|
+
async with self.storage.transaction() as transaction:
|
|
303
|
+
return set(await transaction.list_ready_attachment_paths())
|
|
304
|
+
|
|
305
|
+
def _adapter_context(self) -> Mapping[str, object]:
|
|
306
|
+
return {
|
|
307
|
+
"agent_id": self.agent_id,
|
|
308
|
+
"agent_name": self.name,
|
|
309
|
+
"channel": self.channel,
|
|
310
|
+
"runtime": self.runtime,
|
|
311
|
+
"storage": self.storage,
|
|
312
|
+
"audit": self.audit,
|
|
313
|
+
"command_log": self.command_log,
|
|
314
|
+
"is_started": lambda: self._started,
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async def _handle_control(
|
|
318
|
+
self,
|
|
319
|
+
request: Mapping[str, object],
|
|
320
|
+
) -> Mapping[str, object]:
|
|
321
|
+
session_id = request.get("session_id")
|
|
322
|
+
if not isinstance(session_id, str) or not session_id:
|
|
323
|
+
raise CommandDispatchError(
|
|
324
|
+
"SESSION_REQUIRED",
|
|
325
|
+
"session_id must be a non-empty string",
|
|
326
|
+
)
|
|
327
|
+
await self._validate_session_binding(session_id, request)
|
|
328
|
+
handler = self._provider_control_handler
|
|
329
|
+
if handler is None:
|
|
330
|
+
raise CommandDispatchError(
|
|
331
|
+
"INVALID_COMMAND",
|
|
332
|
+
"control operation is not supported",
|
|
333
|
+
)
|
|
334
|
+
return await handler(request)
|
|
335
|
+
|
|
336
|
+
async def _validate_session_binding(
|
|
337
|
+
self,
|
|
338
|
+
session_id: str,
|
|
339
|
+
request: Mapping[str, object],
|
|
340
|
+
) -> None:
|
|
341
|
+
request_agent_id = request.get("agent_id")
|
|
342
|
+
if request_agent_id != self.agent_id:
|
|
343
|
+
raise CommandDispatchError(
|
|
344
|
+
"SESSION_BINDING_FAILED",
|
|
345
|
+
"Agent binding is invalid",
|
|
346
|
+
)
|
|
347
|
+
runtime_session_id = request.get("runtime_session_id")
|
|
348
|
+
session_capability = request.get("session_capability")
|
|
349
|
+
async with self.storage.transaction() as transaction:
|
|
350
|
+
if await transaction.get_bcn_session(session_id) is None:
|
|
351
|
+
raise CommandDispatchError(
|
|
352
|
+
"SESSION_NOT_FOUND",
|
|
353
|
+
f"unknown bcn session: {session_id}",
|
|
354
|
+
)
|
|
355
|
+
runtime_session = self.orchestrator.runtime_session(session_id)
|
|
356
|
+
if runtime_session is None or runtime_session_id != runtime_session.id:
|
|
357
|
+
raise CommandDispatchError(
|
|
358
|
+
"SESSION_BINDING_FAILED",
|
|
359
|
+
"runtime session binding is invalid",
|
|
360
|
+
)
|
|
361
|
+
binding = self._session_capabilities.get(session_id)
|
|
362
|
+
if (
|
|
363
|
+
binding is None
|
|
364
|
+
or binding.agent_id != self.agent_id
|
|
365
|
+
or binding.bcn_session_id != session_id
|
|
366
|
+
or binding.runtime_session_id != runtime_session.id
|
|
367
|
+
or not isinstance(session_capability, str)
|
|
368
|
+
or not hmac.compare_digest(
|
|
369
|
+
session_capability.encode(), binding.capability.encode()
|
|
370
|
+
)
|
|
371
|
+
):
|
|
372
|
+
raise CommandDispatchError(
|
|
373
|
+
"SESSION_BINDING_FAILED",
|
|
374
|
+
"session capability is invalid",
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
def _runtime_environment(self, session: RuntimeSession) -> Mapping[str, str]:
|
|
378
|
+
runtime_session = self.orchestrator.runtime_session(session.bcn_session_id)
|
|
379
|
+
if runtime_session is None or runtime_session.id != session.id:
|
|
380
|
+
raise RuntimeError("runtime session is not the current live binding")
|
|
381
|
+
return self._build_command_environment(
|
|
382
|
+
session.bcn_session_id,
|
|
383
|
+
session.id,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
def _build_command_environment(
|
|
387
|
+
self,
|
|
388
|
+
session_id: str,
|
|
389
|
+
runtime_session_id: str,
|
|
390
|
+
) -> dict[str, str]:
|
|
391
|
+
if not session_id:
|
|
392
|
+
raise ValueError("session_id must be a non-empty string")
|
|
393
|
+
if not runtime_session_id:
|
|
394
|
+
raise ValueError("runtime_session_id must be a non-empty string")
|
|
395
|
+
wrapper_path = self._wrapper_path
|
|
396
|
+
if wrapper_path is None:
|
|
397
|
+
raise RuntimeError("bcc wrapper is not installed")
|
|
398
|
+
binding = self._session_capabilities.get(session_id)
|
|
399
|
+
if binding is None or binding.runtime_session_id != runtime_session_id:
|
|
400
|
+
capability = secrets.token_urlsafe(32)
|
|
401
|
+
token_values = [capability]
|
|
402
|
+
for name in self._runtime_environment_include:
|
|
403
|
+
if name != "TOKEN" and not name.endswith("_TOKEN"):
|
|
404
|
+
continue
|
|
405
|
+
value = os.environ.get(name)
|
|
406
|
+
if value and value not in token_values:
|
|
407
|
+
token_values.append(value)
|
|
408
|
+
binding = _SessionCapabilityBinding(
|
|
409
|
+
agent_id=self.agent_id,
|
|
410
|
+
bcn_session_id=session_id,
|
|
411
|
+
runtime_session_id=runtime_session_id,
|
|
412
|
+
capability=capability,
|
|
413
|
+
token_values=tuple(token_values),
|
|
414
|
+
)
|
|
415
|
+
self._session_capabilities[session_id] = binding
|
|
416
|
+
allowed = set(_PLATFORM_ENVIRONMENT)
|
|
417
|
+
for name in self.runtime.environment_variable_names():
|
|
418
|
+
if not _ENVIRONMENT_NAME.fullmatch(name):
|
|
419
|
+
raise ValueError(f"runtime environment name is invalid: {name}")
|
|
420
|
+
allowed.add(name)
|
|
421
|
+
for name in self._runtime_environment_include:
|
|
422
|
+
if not _ENVIRONMENT_NAME.fullmatch(name):
|
|
423
|
+
raise ValueError(f"runtime environment name is invalid: {name}")
|
|
424
|
+
if name not in os.environ:
|
|
425
|
+
raise ValueError(f"runtime environment variable is missing: {name}")
|
|
426
|
+
allowed.add(name)
|
|
427
|
+
environment = {
|
|
428
|
+
name: os.environ[name] for name in sorted(allowed) if os.environ.get(name)
|
|
429
|
+
}
|
|
430
|
+
environment["PATH"] = os.pathsep.join(
|
|
431
|
+
(str(wrapper_path.parent), environment.get("PATH", os.defpath))
|
|
432
|
+
)
|
|
433
|
+
environment.update(
|
|
434
|
+
{
|
|
435
|
+
"BCN_AGENT_ID": self.agent_id,
|
|
436
|
+
"BCN_ENDPOINT": self._endpoint(),
|
|
437
|
+
"BCN_SESSION_ID": session_id,
|
|
438
|
+
"BCN_RUNTIME_SESSION_ID": runtime_session_id,
|
|
439
|
+
"BCN_COMMAND_CAPABILITY": binding.capability,
|
|
440
|
+
}
|
|
441
|
+
)
|
|
442
|
+
return environment
|
|
443
|
+
|
|
444
|
+
def _error_feedback_detail(
|
|
445
|
+
self,
|
|
446
|
+
session_id: str,
|
|
447
|
+
error_message: str,
|
|
448
|
+
) -> str:
|
|
449
|
+
if not isinstance(session_id, str) or not session_id:
|
|
450
|
+
raise ValueError("session_id must be a non-empty string")
|
|
451
|
+
if not isinstance(error_message, str):
|
|
452
|
+
raise TypeError("error_message must be text")
|
|
453
|
+
binding = self._session_capabilities.get(session_id)
|
|
454
|
+
if binding is None:
|
|
455
|
+
return error_message
|
|
456
|
+
for token in binding.token_values:
|
|
457
|
+
error_message = error_message.replace(token, "<redacted>")
|
|
458
|
+
return error_message
|
|
459
|
+
|
|
460
|
+
async def _run_runtime_command(
|
|
461
|
+
self,
|
|
462
|
+
session_id: str,
|
|
463
|
+
arguments: Sequence[str],
|
|
464
|
+
body: str | None,
|
|
465
|
+
) -> None:
|
|
466
|
+
if not session_id:
|
|
467
|
+
raise ValueError("session_id must be a non-empty string")
|
|
468
|
+
if not arguments or any(
|
|
469
|
+
not isinstance(argument, str) for argument in arguments
|
|
470
|
+
):
|
|
471
|
+
raise ValueError("runtime command arguments must be non-empty text")
|
|
472
|
+
wrapper_path = self._wrapper_path
|
|
473
|
+
if wrapper_path is None:
|
|
474
|
+
raise RuntimeError("bcc wrapper is not installed")
|
|
475
|
+
self.command_log.append((session_id, tuple(arguments)))
|
|
476
|
+
runtime_session = self.orchestrator.runtime_session(session_id)
|
|
477
|
+
if runtime_session is None:
|
|
478
|
+
raise RuntimeError("runtime session is not live")
|
|
479
|
+
environment = self._build_command_environment(
|
|
480
|
+
session_id,
|
|
481
|
+
runtime_session.id,
|
|
482
|
+
)
|
|
483
|
+
process = await asyncio.create_subprocess_exec(
|
|
484
|
+
str(wrapper_path),
|
|
485
|
+
*arguments,
|
|
486
|
+
stdin=asyncio.subprocess.PIPE,
|
|
487
|
+
stdout=asyncio.subprocess.PIPE,
|
|
488
|
+
stderr=asyncio.subprocess.PIPE,
|
|
489
|
+
env=environment,
|
|
490
|
+
)
|
|
491
|
+
input_data = body.encode() if body is not None else None
|
|
492
|
+
try:
|
|
493
|
+
_stdout, stderr = await process.communicate(input=input_data)
|
|
494
|
+
except asyncio.CancelledError:
|
|
495
|
+
if process.returncode is None:
|
|
496
|
+
process.terminate()
|
|
497
|
+
await process.wait()
|
|
498
|
+
raise
|
|
499
|
+
if process.returncode != 0:
|
|
500
|
+
error = stderr.decode(errors="replace").strip()
|
|
501
|
+
command = " ".join(arguments)
|
|
502
|
+
raise RuntimeError(f"bcc command failed ({command}): {error}")
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
__all__ = ["AgentApplication", "CommandRecord"]
|