mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-11-05 07:12:02 +08:00
fix(box): persist runtime ownership across instance restarts (#2860)
Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
co-authored by
multica-agent
parent
0a962769b9
commit
80775f03f2
@@ -204,6 +204,16 @@ In this repo:
|
||||
- `pkg/box/service.py` is the application-facing facade for exec, sessions, managed processes, status, reconnects, quotas, generic mounts, and sandbox profiles.
|
||||
- [Outbox export failures](dev-docs/BOX_OUTBOX_EXPORT.md) documents bounded reads, permission fallback, and cleanup after Runner acceptance.
|
||||
- `pkg/box/connector.py` connects to the Box Runtime over stdio, Windows subprocess+WebSocket, or remote WebSocket.
|
||||
- Core-launched Box subprocesses receive a stable `LANGBOT_BOX_INSTANCE_ID`:
|
||||
explicit `box.instance_id`, then the environment override, then Core's instance
|
||||
UUID. Standalone Box identity is fixed before RPC initialization; configure it
|
||||
at launch with `--instance-id` or `LANGBOT_BOX_INSTANCE_ID`, or persist its
|
||||
generated identity with `LANGBOT_BOX_DATA_DIR`. Compose and Kubernetes keep
|
||||
this directory under the shared Box root. Each deployment needs its own root
|
||||
and identity. The runtime holds an identity lock, so Kubernetes uses Recreate
|
||||
rather than overlapping two owners during rollout. INIT cannot reassign Box
|
||||
ownership. The SDK implementation requires the instance-scoped cleanup change;
|
||||
the pinned SDK version is unchanged here.
|
||||
- `pkg/provider/tools/loaders/native.py` is the Core orchestration seam: the
|
||||
Skill loader supplies generic read-only mounts to Box execution. `mcp_stdio.py`
|
||||
and execution-backed tools depend on Box availability.
|
||||
|
||||
@@ -63,6 +63,8 @@ services:
|
||||
restart: on-failure
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
- LANGBOT_BOX_DATA_DIR=${LANGBOT_BOX_ROOT:-${PWD}/data/box}/runtime
|
||||
- LANGBOT_BOX_INSTANCE_ID=${LANGBOT_BOX_INSTANCE_ID:-}
|
||||
# Optional shared control-plane secret used to authenticate both the RPC
|
||||
# socket and managed-process relay. Leave unset on both OSS services, or
|
||||
# generate one with ``openssl rand -hex 32`` and set the same value on
|
||||
|
||||
@@ -270,6 +270,8 @@ metadata:
|
||||
app: langbot-box
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app: langbot-box
|
||||
@@ -298,6 +300,8 @@ spec:
|
||||
name: box-rpc
|
||||
protocol: TCP
|
||||
env:
|
||||
- name: LANGBOT_BOX_DATA_DIR
|
||||
value: /app/data/box/runtime
|
||||
- name: TZ
|
||||
valueFrom:
|
||||
configMapKeyRef:
|
||||
|
||||
@@ -243,6 +243,11 @@ class BoxRuntimeConnector(ManagedRuntimeConnector):
|
||||
env = os.environ.copy()
|
||||
env[BOX_CONTROL_TOKEN_ENV] = self._control_token
|
||||
env[BOX_TRUSTED_INSTANCE_ENV] = self._trusted_instance_uuid
|
||||
env['LANGBOT_BOX_INSTANCE_ID'] = str(
|
||||
self._filtered_box_config.get('instance_id')
|
||||
or env.get('LANGBOT_BOX_INSTANCE_ID')
|
||||
or self._trusted_instance_uuid
|
||||
)
|
||||
if self._filtered_box_config:
|
||||
env['LANGBOT_BOX_CONFIG'] = json.dumps(self._filtered_box_config)
|
||||
|
||||
@@ -281,6 +286,11 @@ class BoxRuntimeConnector(ManagedRuntimeConnector):
|
||||
env = os.environ.copy()
|
||||
env[BOX_CONTROL_TOKEN_ENV] = self._control_token
|
||||
env[BOX_TRUSTED_INSTANCE_ENV] = self._trusted_instance_uuid
|
||||
env['LANGBOT_BOX_INSTANCE_ID'] = str(
|
||||
self._filtered_box_config.get('instance_id')
|
||||
or env.get('LANGBOT_BOX_INSTANCE_ID')
|
||||
or self._trusted_instance_uuid
|
||||
)
|
||||
if self._filtered_box_config:
|
||||
env['LANGBOT_BOX_CONFIG'] = json.dumps(self._filtered_box_config)
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def isolated_box_identity(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv('LANGBOT_BOX_DATA_DIR', str(tmp_path / 'runtime-data'))
|
||||
monkeypatch.delenv('LANGBOT_BOX_INSTANCE_ID', raising=False)
|
||||
@@ -332,10 +332,27 @@ def test_external_box_runtime_rejects_invalid_configured_control_token(monkeypat
|
||||
connector.get_control_headers()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('core_id', 'configured_id', 'environment_id', 'expected_id'),
|
||||
[
|
||||
('instance-a', None, None, 'instance-a'),
|
||||
('instance_12345678-abcd', None, None, 'instance_12345678-abcd'),
|
||||
('instance-a', None, 'env-id', 'env-id'),
|
||||
('instance-a', 'config-id', 'env-id', 'config-id'),
|
||||
],
|
||||
)
|
||||
async def test_local_stdio_injects_generated_token_and_trusted_instance(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
core_id,
|
||||
configured_id,
|
||||
environment_id,
|
||||
expected_id,
|
||||
):
|
||||
monkeypatch.delenv(BOX_CONTROL_TOKEN_ENV, raising=False)
|
||||
if environment_id:
|
||||
monkeypatch.setenv('LANGBOT_BOX_INSTANCE_ID', environment_id)
|
||||
else:
|
||||
monkeypatch.delenv('LANGBOT_BOX_INSTANCE_ID', raising=False)
|
||||
captured = {}
|
||||
|
||||
class FakeStdioClientController:
|
||||
@@ -350,7 +367,11 @@ async def test_local_stdio_injects_generated_token_and_trusted_instance(
|
||||
'langbot_plugin.runtime.io.controllers.stdio.client.StdioClientController',
|
||||
FakeStdioClientController,
|
||||
)
|
||||
connector = BoxRuntimeConnector(make_app(Mock()))
|
||||
app = make_app(Mock())
|
||||
app.workspace_service.instance_uuid = core_id
|
||||
if configured_id:
|
||||
app.instance_config.data['box']['instance_id'] = configured_id
|
||||
connector = BoxRuntimeConnector(app)
|
||||
|
||||
def fake_callback(_transport_name, connected, _connect_error, _generation):
|
||||
async def callback(_connection):
|
||||
@@ -363,7 +384,22 @@ async def test_local_stdio_injects_generated_token_and_trusted_instance(
|
||||
await connector._start_local_stdio()
|
||||
|
||||
assert len(captured['env'][BOX_CONTROL_TOKEN_ENV]) >= 32
|
||||
assert captured['env'][BOX_TRUSTED_INSTANCE_ENV] == 'instance-a'
|
||||
assert captured['env'][BOX_TRUSTED_INSTANCE_ENV] == core_id
|
||||
assert captured['env']['LANGBOT_BOX_INSTANCE_ID'] == expected_id
|
||||
|
||||
|
||||
async def test_windows_subprocess_receives_stable_identity(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.delenv('LANGBOT_BOX_INSTANCE_ID', raising=False)
|
||||
process = Mock(wait=AsyncMock(return_value=0))
|
||||
spawn = AsyncMock(return_value=process)
|
||||
monkeypatch.setattr(asyncio, 'create_subprocess_exec', spawn)
|
||||
connector = BoxRuntimeConnector(make_app(Mock()))
|
||||
monkeypatch.setattr(connector, '_connect_ws', AsyncMock())
|
||||
|
||||
await connector._start_subprocess_then_ws()
|
||||
await connector.runtime_subprocess_task
|
||||
|
||||
assert spawn.await_args.kwargs['env']['LANGBOT_BOX_INSTANCE_ID'] == 'instance-a'
|
||||
|
||||
|
||||
async def test_websocket_controller_receives_control_headers(monkeypatch: pytest.MonkeyPatch):
|
||||
|
||||
Reference in New Issue
Block a user