Files
roboco/tests/unit/runtime/test_briefing_sandbox_line.py
T
47d78f50ee feat(sandbox): on-demand provisioning via request_sandbox verb (#338)
* feat(sandbox): on-demand request_sandbox verb replaces eager provisioning

Sandboxes were provisioned at every agent spawn for opted-in projects,
so every role paid the sidecar spin-up and a provisioning failure
refused the spawn. Provisioning now happens when an agent asks: the
request_sandbox do-verb (dev + QA) reaches the orchestrator through
ContentActionsDeps, ensure_sandbox provisions idempotently with an
in-memory per-agent cache (evicted at teardown and janitor sweep), and
creds return in the envelope payload including ready-to-export
ROBOCO_TEST_* values. Spawn now only injects a marker env naming the
available services plus a briefing line; sandbox failures can no longer
refuse a spawn. Teardown lifecycle unchanged.

* feat(sandbox): harden request_sandbox + Phase 3 wiring proof and docs

Hardening from adversarial review: ensure_sandbox now provisions the
project's full opted-in set on first request (a later superset can
never tear down a live sandbox mid-use), serializes per-agent behind an
asyncio lock (a client timeout-retry no longer races its own in-flight
provision), and verifies container liveness on every cache hit (a dead
sandbox evicts and re-provisions instead of serving dead creds). MCP
client budget 720->1080s for the full-set cold case. Phase 3: e2e smoke
wiring test (manifest grants + guard-chain envelopes over the real
API), sandbox-db/tools/map docs and CLAUDE.md rewritten for on-demand.

* feat(sandbox): release sandboxes when the agent's work ends

CEO directive: sidecars must not dangle once the agent is done. The six
work-ending verbs (i_am_done, unclaim, i_am_idle, pass_review,
fail_review, i_documented) now release the caller's sandbox best-effort
on their success path via release_sandbox (lock + teardown + cache
evict; a no-sandbox agent costs a dict lookup). Container removal and
the janitor remain the backstop; a re-request provisions fresh.

* test(sandbox): monkeypatch the release hook instead of method assignment

mypy method-assign rejected the direct AsyncMock assignments; the prior
static gate ran before this test file landed.

* test(sandbox): guard envelope evidence for mypy in verb tests

* chore(prompts): regenerate verb tables for request_sandbox

* chore: resolve merge with master (breadcrumbs + statement budget)

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-08 16:40:02 +02:00

41 lines
1.3 KiB
Python

"""`_write_agent_briefing`'s sandbox availability line.
Names the `request_sandbox` verb for an opted-in project (spec's "name it —
cheap and kills a discovery failure mode" default) and is silent otherwise.
"""
from __future__ import annotations
from unittest.mock import patch
import pytest
from roboco.runtime.orchestrator import AgentOrchestrator
def _orch() -> AgentOrchestrator:
with patch.object(AgentOrchestrator, "__init__", return_value=None):
orch = AgentOrchestrator.__new__(AgentOrchestrator)
object.__setattr__(orch, "_TOOL_LOAD_CACHE", {})
return orch
@pytest.mark.asyncio
async def test_briefing_names_request_sandbox_when_opted_in(tmp_path: object) -> None:
orch = _orch()
path = await orch._write_agent_briefing(
"dev-1", None, str(tmp_path), ["postgres", "redis"]
)
assert path is not None
content = path.read_text()
assert "request_sandbox()" in content
assert "postgres, redis" in content
@pytest.mark.asyncio
async def test_briefing_omits_sandbox_line_when_not_opted_in(tmp_path: object) -> None:
orch = _orch()
path = await orch._write_agent_briefing("dev-1", None, str(tmp_path), [])
assert path is not None
content = path.read_text()
assert "request_sandbox()" not in content