Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/advanced/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,11 @@ When remote vars are set, Strix dual-writes telemetry to both local JSONL and th
</ParamField>

<ParamField path="DOCKER_HOST" type="string">
Docker daemon address, for example `unix:///var/run/docker.sock` or `tcp://10.0.0.5:2375`. When not set, Strix connects to the same daemon as the `docker` CLI: the current docker context (`docker context use`), then the default socket. Docker Desktop, OrbStack and Colima register their sockets as contexts, so no extra configuration is needed for them.
Docker daemon address, for example `unix:///var/run/docker.sock` or `tcp://10.0.0.5:2375`. When not set, Strix uses `DOCKER_CONTEXT`, then the saved current context (`docker context use`), then the default socket. Docker Desktop, OrbStack and Colima register their sockets as contexts, so no extra configuration is needed for them.
</ParamField>

<ParamField path="DOCKER_CONTEXT" type="string">
Select a Docker context for this shell without changing the saved current context. Used when `DOCKER_HOST` is unset. A missing context is an error; Strix does not silently fall back to another daemon. Set to `default` to use the SDK's default connection settings.
</ParamField>

<ParamField path="STRIX_RUNTIME_BACKEND" default="docker" type="string">
Expand Down
4 changes: 2 additions & 2 deletions strix/runtime/docker_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ def detail(self) -> str:


def resolve_docker_endpoint(environ: dict[str, str] | None = None) -> DockerEndpoint:
"""``DOCKER_HOST``, else the current docker context, else the SDK default."""
"""``DOCKER_HOST``, shell/saved context, then the SDK default."""
env = os.environ if environ is None else environ
host = env.get("DOCKER_HOST", "").strip()
if host:
return DockerEndpoint(host, "DOCKER_HOST")

name = get_current_context_name()
name = env.get("DOCKER_CONTEXT", "").strip() or get_current_context_name()
if name == DEFAULT_CONTEXT:
return DockerEndpoint(None, "default socket")

Expand Down
67 changes: 65 additions & 2 deletions tests/test_docker_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

from __future__ import annotations

import hashlib
import importlib
import json
from types import SimpleNamespace
from typing import Any, cast
from typing import TYPE_CHECKING, Any, cast

import pytest
from docker.errors import DockerException
Expand All @@ -19,6 +21,10 @@
)


if TYPE_CHECKING:
from pathlib import Path


cli_utils: Any = importlib.import_module("strix.interface.utils")


Expand All @@ -34,7 +40,9 @@ def _sdk_error(inner: BaseException) -> DockerException:

def test_docker_host_wins_over_context(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(docker_connection, "get_current_context_name", lambda: "desktop-linux")
endpoint = resolve_docker_endpoint({"DOCKER_HOST": "tcp://10.0.0.5:2375"})
endpoint = resolve_docker_endpoint(
{"DOCKER_HOST": "tcp://10.0.0.5:2375", "DOCKER_CONTEXT": "selected"}
)
assert endpoint == DockerEndpoint("tcp://10.0.0.5:2375", "DOCKER_HOST")


Expand All @@ -56,6 +64,61 @@ def test_default_context_uses_the_sdk_default(monkeypatch: pytest.MonkeyPatch) -
assert resolve_docker_endpoint({}) == DockerEndpoint(None, "default socket")


@pytest.mark.parametrize(
("selected", "expected_host"),
[
("selected", "unix:///tmp/strix-selected.sock"),
("default", None),
("", "unix:///tmp/strix-saved.sock"),
],
)
def test_shell_context_overrides_the_saved_context(
monkeypatch: pytest.MonkeyPatch,
tmp_path: Path,
selected: str,
expected_host: str | None,
) -> None:
"""Read a real Docker context store, without contacting any daemon."""
(tmp_path / "config.json").write_text(json.dumps({"currentContext": "saved"}))
for name in ("saved", "selected"):
context_id = hashlib.sha256(name.encode()).hexdigest()
metadata = tmp_path / "contexts" / "meta" / context_id / "meta.json"
metadata.parent.mkdir(parents=True)
metadata.write_text(
json.dumps(
{
"Name": name,
"Metadata": {},
"Endpoints": {
"docker": {
"Host": f"unix:///tmp/strix-{name}.sock",
"SkipTLSVerify": True,
}
},
}
)
)
monkeypatch.setenv("DOCKER_CONFIG", str(tmp_path))
monkeypatch.setenv("DOCKER_CONTEXT", selected)
monkeypatch.delenv("DOCKER_HOST", raising=False)

assert resolve_docker_endpoint().host == expected_host


def test_missing_shell_context_does_not_fall_back_to_saved_context(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
(tmp_path / "config.json").write_text(json.dumps({"currentContext": "default"}))
monkeypatch.setenv("DOCKER_CONFIG", str(tmp_path))
monkeypatch.setenv("DOCKER_CONTEXT", "missing")
monkeypatch.delenv("DOCKER_HOST", raising=False)

with pytest.raises(
DockerConnectionError, match="docker context 'missing' is selected but does not exist"
):
resolve_docker_endpoint()


def test_missing_or_broken_current_context_is_an_error(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(docker_connection, "get_current_context_name", lambda: "gone")
monkeypatch.setattr(
Expand Down