mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
feat(runtime): stamp spawned containers with the stack's own compose-project labels
Agent spawns (all five provider paths), intake/secretary chats, and sandbox sidecars now carry com.docker.compose.project/service/oneoff/ config-hash labels copied from the orchestrator's own compose project, so a Docker UI (UGOS) groups them under the stack's project and they die with the stack: bare compose stop/restart affects them, compose down removes them (the config-hash label must be PRESENT for down to even see the container — compose filters its API listing on that key before the orphan predicate runs, verified live), and up -d deliberately does not resurrect them since the orchestrator respawns its own agents. Self-discovery reads the orchestrator's own container id from /proc/self/mountinfo keyed on the root-independent /containers/<id>/ segment — the UGREEN NAS data-root is /volume1/@docker on btrfs, so its mountinfo reads /@docker/containers/..., never the textbook /var/lib/docker path (verified against the live NAS) — with a HOSTNAME short-id fallback, then one docker inspect cached per process. Only definitive outcomes cache; a transient inspect failure logs and retries on the next spawn. Outside compose the helper yields nothing and every spawn command is byte-for-byte unchanged.
This commit is contained in:
@@ -189,6 +189,34 @@ async def test_gemini_spawn_wires_gateway_env_and_image_last() -> None:
|
||||
)
|
||||
|
||||
|
||||
async def test_gemini_spawn_adds_compose_labels_before_image(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""When the orchestrator resolves its own compose project, the sibling
|
||||
agent container carries it too (UGOS grouping + `down --remove-orphans`)
|
||||
— and the labels land BEFORE the image, never after (docker parses
|
||||
anything past the image as the container command, not a flag)."""
|
||||
|
||||
async def _fake_label_args(service: str) -> list[str]:
|
||||
return ["--label", f"com.docker.compose.service={service}"]
|
||||
|
||||
monkeypatch.setattr(
|
||||
"roboco.llm.providers.gemini.compose_label_args", _fake_label_args
|
||||
)
|
||||
host = _FakeHost()
|
||||
provider = GeminiCliProvider(host, image="roboco-agent-gemini:test")
|
||||
with patch(
|
||||
"asyncio.create_subprocess_exec", AsyncMock(return_value=_proc())
|
||||
) as exec_mock:
|
||||
await provider.spawn(_config())
|
||||
cmd = list(exec_mock.call_args.args)
|
||||
assert "com.docker.compose.service=be-dev-1" in cmd
|
||||
assert cmd.index("com.docker.compose.service=be-dev-1") < cmd.index(
|
||||
"roboco-agent-gemini:test"
|
||||
)
|
||||
assert cmd[-1] == "roboco-agent-gemini:test"
|
||||
|
||||
|
||||
async def test_gemini_spawn_mounts_auth_when_present(
|
||||
_isolate_gemini_auth: Path,
|
||||
) -> None:
|
||||
|
||||
@@ -266,6 +266,34 @@ async def test_grok_spawn_wires_gateway_env_and_image_last() -> None:
|
||||
)
|
||||
|
||||
|
||||
async def test_grok_spawn_adds_compose_labels_before_image(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""When the orchestrator resolves its own compose project, the sibling
|
||||
agent container carries it too (UGOS grouping + `down --remove-orphans`)
|
||||
— and the labels land BEFORE the image, never after (docker parses
|
||||
anything past the image as the container command, not a flag)."""
|
||||
|
||||
async def _fake_label_args(service: str) -> list[str]:
|
||||
return ["--label", f"com.docker.compose.service={service}"]
|
||||
|
||||
monkeypatch.setattr(
|
||||
"roboco.llm.providers.grok.compose_label_args", _fake_label_args
|
||||
)
|
||||
host = _FakeHost()
|
||||
provider = GrokCliProvider(host, image="roboco-agent-grok:test")
|
||||
with patch(
|
||||
"asyncio.create_subprocess_exec", AsyncMock(return_value=_proc())
|
||||
) as exec_mock:
|
||||
await provider.spawn(_config())
|
||||
cmd = list(exec_mock.call_args.args)
|
||||
assert "com.docker.compose.service=be-dev-1" in cmd
|
||||
assert cmd.index("com.docker.compose.service=be-dev-1") < cmd.index(
|
||||
"roboco-agent-grok:test"
|
||||
)
|
||||
assert cmd[-1] == "roboco-agent-grok:test"
|
||||
|
||||
|
||||
async def test_grok_spawn_mounts_auth_when_present(_isolate_grok_auth: Path) -> None:
|
||||
(_isolate_grok_auth / "auth.json").write_text("{}", encoding="utf-8")
|
||||
host = _FakeHost()
|
||||
@@ -421,6 +449,29 @@ async def test_codex_spawn_wires_gateway_env_and_image_last() -> None:
|
||||
)
|
||||
|
||||
|
||||
async def test_codex_spawn_adds_compose_labels_before_image(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def _fake_label_args(service: str) -> list[str]:
|
||||
return ["--label", f"com.docker.compose.service={service}"]
|
||||
|
||||
monkeypatch.setattr(
|
||||
"roboco.llm.providers.codex.compose_label_args", _fake_label_args
|
||||
)
|
||||
host = _FakeHost()
|
||||
provider = CodexCliProvider(host, image="roboco-agent-codex:test")
|
||||
with patch(
|
||||
"asyncio.create_subprocess_exec", AsyncMock(return_value=_proc())
|
||||
) as exec_mock:
|
||||
await provider.spawn(_codex_config())
|
||||
cmd = list(exec_mock.call_args.args)
|
||||
assert "com.docker.compose.service=be-dev-1" in cmd
|
||||
assert cmd.index("com.docker.compose.service=be-dev-1") < cmd.index(
|
||||
"roboco-agent-codex:test"
|
||||
)
|
||||
assert cmd[-1] == "roboco-agent-codex:test"
|
||||
|
||||
|
||||
async def test_codex_spawn_mounts_auth_when_present(
|
||||
_isolate_codex_auth: Path,
|
||||
) -> None:
|
||||
@@ -571,6 +622,29 @@ async def test_kimi_spawn_wires_gateway_env_and_image_last() -> None:
|
||||
)
|
||||
|
||||
|
||||
async def test_kimi_spawn_adds_compose_labels_before_image(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
async def _fake_label_args(service: str) -> list[str]:
|
||||
return ["--label", f"com.docker.compose.service={service}"]
|
||||
|
||||
monkeypatch.setattr(
|
||||
"roboco.llm.providers.kimi.compose_label_args", _fake_label_args
|
||||
)
|
||||
host = _FakeHost()
|
||||
provider = KimiCliProvider(host, image="roboco-agent-kimi:test")
|
||||
with patch(
|
||||
"asyncio.create_subprocess_exec", AsyncMock(return_value=_proc())
|
||||
) as exec_mock:
|
||||
await provider.spawn(_kimi_config())
|
||||
cmd = list(exec_mock.call_args.args)
|
||||
assert "com.docker.compose.service=be-dev-1" in cmd
|
||||
assert cmd.index("com.docker.compose.service=be-dev-1") < cmd.index(
|
||||
"roboco-agent-kimi:test"
|
||||
)
|
||||
assert cmd[-1] == "roboco-agent-kimi:test"
|
||||
|
||||
|
||||
async def test_kimi_spawn_mounts_auth_when_present(_isolate_kimi_auth: Path) -> None:
|
||||
creds_dir = _isolate_kimi_auth / "credentials"
|
||||
creds_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
Reference in New Issue
Block a user