Skip to content

Commit c39a4dc

Browse files
committed
refactor(sandbox): center provider-owned metadata
1 parent 70db417 commit c39a4dc

6 files changed

Lines changed: 92 additions & 50 deletions

File tree

astrbot/core/computer/computer_client.py

Lines changed: 18 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from astrbot.api import logger
1212
from astrbot.core.computer.cua_registry import CuaSandboxRegistry
13+
from astrbot.core.computer.cua_sandbox_provider import CuaSandboxProvider
1314
from astrbot.core.computer.sandbox_manager import SANDBOX_LEASE_SECONDS, SandboxManager
1415
from astrbot.core.skills.skill_manager import SANDBOX_SKILLS_ROOT, SkillManager
1516
from astrbot.core.star.context import Context
@@ -38,29 +39,7 @@ class _CUAIdleState:
3839
cua_idle_state: dict[str, _CUAIdleState] = {}
3940

4041

41-
class _ComputerClientCuaProvider:
42-
provider_id = "cua"
43-
44-
def build_create_config(self, context: Context, session_id: str) -> dict:
45-
from .booters.cua import build_cua_booter_kwargs
46-
47-
config = context.get_config(umo=session_id)
48-
sandbox_cfg = config.get("provider_settings", {}).get("sandbox", {})
49-
return build_cua_booter_kwargs(sandbox_cfg)
50-
51-
async def create_booter(
52-
self, context: Context, session_id: str, sandbox_id: str, config: dict
53-
) -> ComputerBooter:
54-
return await _boot_managed_cua_sandbox(context, session_id, sandbox_id, config)
55-
56-
async def destroy_booter(self, booter: ComputerBooter, record: dict) -> None:
57-
await booter.shutdown()
58-
59-
60-
sandbox_manager = SandboxManager(
61-
registry=cua_registry,
62-
providers={"cua": _ComputerClientCuaProvider()},
63-
)
42+
sandbox_manager: SandboxManager
6443

6544

6645
def _sync_sandbox_manager_refs() -> None:
@@ -70,9 +49,6 @@ def _sync_sandbox_manager_refs() -> None:
7049
sandbox_manager.boot_locks = _cua_boot_locks
7150

7251

73-
_sync_sandbox_manager_refs()
74-
75-
7652
def _save_cua_registry() -> None:
7753
_sync_sandbox_manager_refs()
7854
sandbox_manager.save_registry()
@@ -184,6 +160,22 @@ async def _boot_managed_cua_sandbox(
184160
return client
185161

186162

163+
async def _boot_managed_cua_sandbox_hook(
164+
context: Context,
165+
session_id: str,
166+
sandbox_id: str,
167+
cua_kwargs: dict,
168+
) -> ComputerBooter:
169+
return await _boot_managed_cua_sandbox(context, session_id, sandbox_id, cua_kwargs)
170+
171+
172+
sandbox_manager = SandboxManager(
173+
registry=cua_registry,
174+
providers={"cua": CuaSandboxProvider(boot_hook=_boot_managed_cua_sandbox_hook)},
175+
)
176+
_sync_sandbox_manager_refs()
177+
178+
187179
def _new_managed_cua_sandbox_id() -> str:
188180
return f"cua-{uuid.uuid4().hex[:12]}"
189181

astrbot/core/computer/cua_sandbox_provider.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
import time
44
import uuid
5+
from collections.abc import Awaitable, Callable
56

67
from astrbot.api import logger
78
from astrbot.core.computer.booters import cua as cua_booter
89
from astrbot.core.computer.booters.base import ComputerBooter
910
from astrbot.core.star.context import Context
1011

12+
BootHook = Callable[[Context, str, str, dict], Awaitable[ComputerBooter]]
13+
1114

1215
async def _sync_skills_to_sandbox(booter: ComputerBooter) -> None:
1316
from astrbot.core.computer.computer_client import _sync_skills_to_sandbox as sync
@@ -18,18 +21,41 @@ async def _sync_skills_to_sandbox(booter: ComputerBooter) -> None:
1821
class CuaSandboxProvider:
1922
provider_id = "cua"
2023

24+
def __init__(self, boot_hook: BootHook | None = None) -> None:
25+
self._boot_hook = boot_hook
26+
2127
def build_create_config(self, context: Context, session_id: str) -> dict:
2228
config = context.get_config(umo=session_id)
2329
sandbox_cfg = config.get("provider_settings", {}).get("sandbox", {})
2430
return cua_booter.build_cua_booter_kwargs(sandbox_cfg)
2531

32+
def build_connect_info(self, sandbox_name: str, config: dict) -> dict:
33+
return {
34+
"name": sandbox_name,
35+
"local": config.get("local", True),
36+
"image": config.get("image"),
37+
"os_type": config.get("os_type"),
38+
}
39+
40+
def get_idle_timeout(self, context: Context, session_id: str) -> float:
41+
config = context.get_config(umo=session_id)
42+
sandbox_cfg = config.get("provider_settings", {}).get("sandbox", {})
43+
value = sandbox_cfg.get("cua_idle_timeout", 0)
44+
try:
45+
timeout = float(value)
46+
except (TypeError, ValueError):
47+
return 0.0
48+
return max(timeout, 0.0)
49+
2650
async def create_booter(
2751
self,
2852
context: Context,
2953
session_id: str,
3054
sandbox_id: str,
3155
config: dict,
3256
) -> ComputerBooter:
57+
if self._boot_hook is not None:
58+
return await self._boot_hook(context, session_id, sandbox_id, config)
3359
uuid_str = uuid.uuid5(uuid.NAMESPACE_DNS, session_id).hex
3460
client = cua_booter.CuaBooter(**config)
3561
started_at = time.monotonic()

astrbot/core/computer/sandbox_manager.py

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,8 @@ def drop_boot_lock(self, sandbox_id: str) -> None:
5050
self.boot_locks.pop(sandbox_id, None)
5151

5252
def get_idle_timeout(self, config: dict, provider_id: str) -> float:
53-
sandbox_cfg = config.get("provider_settings", {}).get("sandbox", {})
54-
value = sandbox_cfg.get(f"{provider_id}_idle_timeout", 0)
55-
try:
56-
timeout = float(value)
57-
except (TypeError, ValueError):
58-
return 0.0
59-
return max(timeout, 0.0)
53+
_ = config, provider_id
54+
return 0.0
6055

6156
def build_record_payload(
6257
self,
@@ -67,8 +62,10 @@ def build_record_payload(
6762
provider_id: str,
6863
config: dict,
6964
idle_timeout: float,
65+
connect_info: dict,
7066
is_default: bool = False,
7167
) -> dict:
68+
_ = config
7269
return {
7370
"sandbox_id": sandbox_id,
7471
"sandbox_name": sandbox_name,
@@ -78,12 +75,7 @@ def build_record_payload(
7875
"created_by_astrbot": True,
7976
"owner_user_id": session_id,
8077
"owner_session_id": session_id,
81-
"connect_info": {
82-
"name": sandbox_name,
83-
"local": config.get("local", True),
84-
"image": config.get("image"),
85-
"os_type": config.get("os_type"),
86-
},
78+
"connect_info": connect_info,
8779
"is_default": is_default,
8880
"idle_timeout": idle_timeout,
8981
}
@@ -153,6 +145,7 @@ async def get_or_create_booter(
153145
) -> ComputerBooter:
154146
provider = self.get_provider(provider_id)
155147
create_config = provider.build_create_config(context, session_id)
148+
idle_timeout = provider.get_idle_timeout(context, session_id)
156149

157150
current_sandbox_id = self.registry.get_current_sandbox_id(session_id)
158151
current_record = (
@@ -186,9 +179,7 @@ async def get_or_create_booter(
186179
self.save_registry()
187180
self.schedule_idle_cleanup(
188181
current_sandbox_id,
189-
self.get_idle_timeout(
190-
context.get_config(umo=session_id), provider_id
191-
),
182+
idle_timeout,
192183
)
193184
return booter
194185
self.session_booter.pop(current_sandbox_id, None)
@@ -205,8 +196,9 @@ async def get_or_create_booter(
205196
session_id=session_id,
206197
provider_id=provider_id,
207198
config=create_config,
208-
idle_timeout=self.get_idle_timeout(
209-
context.get_config(umo=session_id), provider_id
199+
idle_timeout=idle_timeout,
200+
connect_info=provider.build_connect_info(
201+
target_sandbox_id, create_config
210202
),
211203
is_default=True,
212204
)
@@ -285,7 +277,7 @@ async def get_or_create_booter(
285277
self.save_registry()
286278
self.schedule_idle_cleanup(
287279
target_sandbox_id,
288-
self.get_idle_timeout(context.get_config(umo=session_id), provider_id),
280+
idle_timeout,
289281
)
290282
return self.session_booter[target_sandbox_id]
291283

@@ -298,10 +290,9 @@ async def create_sandbox_uncontrolled(
298290
) -> dict:
299291
provider = self.get_provider(provider_id)
300292
create_config = provider.build_create_config(context, session_id)
301-
config = context.get_config(umo=session_id)
302293
sandbox_id = self.new_sandbox_id(provider_id)
303294
sandbox_name = sandbox_name or sandbox_id
304-
idle_timeout = self.get_idle_timeout(config, provider_id)
295+
idle_timeout = provider.get_idle_timeout(context, session_id)
305296
record = self.registry.upsert_sandbox(
306297
**self.build_record_payload(
307298
sandbox_id=sandbox_id,
@@ -310,6 +301,7 @@ async def create_sandbox_uncontrolled(
310301
provider_id=provider_id,
311302
config=create_config,
312303
idle_timeout=idle_timeout,
304+
connect_info=provider.build_connect_info(sandbox_name, create_config),
313305
)
314306
)
315307
try:
@@ -391,6 +383,7 @@ def update_sandbox_config(
391383
def _upsert_new_sandbox_record(
392384
self, context: Context, session_id: str, provider_id: str, create_config: dict
393385
) -> str:
386+
provider = self.get_provider(provider_id)
394387
sandbox_id = self.new_sandbox_id(provider_id)
395388
self.registry.upsert_sandbox(
396389
**self.build_record_payload(
@@ -399,9 +392,8 @@ def _upsert_new_sandbox_record(
399392
session_id=session_id,
400393
provider_id=provider_id,
401394
config=create_config,
402-
idle_timeout=self.get_idle_timeout(
403-
context.get_config(umo=session_id), provider_id
404-
),
395+
idle_timeout=provider.get_idle_timeout(context, session_id),
396+
connect_info=provider.build_connect_info(sandbox_id, create_config),
405397
)
406398
)
407399
self.save_registry()

astrbot/core/computer/sandbox_provider.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ class SandboxProvider(Protocol):
1111

1212
def build_create_config(self, context: Context, session_id: str) -> dict: ...
1313

14+
def build_connect_info(self, sandbox_name: str, config: dict) -> dict: ...
15+
16+
def get_idle_timeout(self, context: Context, session_id: str) -> float: ...
17+
1418
async def create_booter(
1519
self,
1620
context: Context,

astrbot/core/computer/sandbox_registry.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import Any
88

99
from astrbot.api import logger
10+
from astrbot.core.computer.sandbox_models import SandboxRecord
1011
from astrbot.core.utils.astrbot_path import get_astrbot_data_path
1112

1213
_UNSET = object()
@@ -104,6 +105,7 @@ def upsert_sandbox(
104105
"notes": notes,
105106
}
106107
)
108+
record = SandboxRecord.from_dict(record).to_dict()
107109
self._payload["sandboxes"][sandbox_id] = record
108110
if is_default or (managed and self._payload["default_sandbox_id"] is None):
109111
self.set_default_sandbox_id(sandbox_id)
@@ -250,9 +252,24 @@ def load(self) -> None:
250252
self._payload = _default_registry_payload()
251253
self._payload.update(payload)
252254
sandboxes = self._payload.get("sandboxes", {})
255+
valid_sandboxes = {}
253256
for record in sandboxes.values():
257+
try:
258+
normalized = SandboxRecord.from_dict(record).to_dict()
259+
except (KeyError, TypeError, ValueError) as exc:
260+
logger.warning(
261+
"[Computer] Skip invalid sandbox registry record: %s",
262+
exc,
263+
)
264+
continue
265+
record = normalized
254266
if not record.get("managed"):
267+
valid_sandboxes[record["sandbox_id"]] = record
255268
continue
256269
record["controller_user_id"] = None
257270
record["controller_session_id"] = None
258271
record["lease_expires_at"] = None
272+
valid_sandboxes[record["sandbox_id"]] = record
273+
self._payload["sandboxes"] = valid_sandboxes
274+
if self._payload["default_sandbox_id"] not in valid_sandboxes:
275+
self._payload["default_sandbox_id"] = None

tests/unit/test_sandbox_manager.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ def __init__(self):
2525
def build_create_config(self, context, session_id):
2626
return {"image": "fake"}
2727

28+
def build_connect_info(self, sandbox_name, config):
29+
return {"kind": "fake", "name": sandbox_name}
30+
31+
def get_idle_timeout(self, context, session_id):
32+
return 7.0
33+
2834
async def create_booter(self, context, session_id, sandbox_id, config):
2935
self.boots.append((session_id, sandbox_id, config))
3036
return FakeBooter(sandbox_id, self)
@@ -99,6 +105,11 @@ async def test_sandbox_manager_creates_default_and_current_sandbox(tmp_path):
99105

100106
assert booter.sandbox_id == registry.default_sandbox_id
101107
assert registry.get_current_sandbox_id("session-a") == booter.sandbox_id
108+
assert registry.get_sandbox(booter.sandbox_id)["connect_info"] == {
109+
"kind": "fake",
110+
"name": booter.sandbox_id,
111+
}
112+
assert registry.get_sandbox(booter.sandbox_id)["idle_timeout"] == 7.0
102113
assert provider.boots == [("session-a", booter.sandbox_id, {"image": "fake"})]
103114

104115

0 commit comments

Comments
 (0)