Host <-> container path handling: the one place that knows a host path's flavour.
The server runs in a Linux container while the Docker host may be Windows, macOS or Linux,
so a host path is parsed in its own flavour (:func:flavour), never with os.path.
desktop/src/shared/paths.ts holds the same rules; both suites read
tests/fixtures/host_paths.json so they cannot drift.
Where this server's project directory lives on the host, when that can be known:
GET /api/project reports container_path (what the server itself sees, e.g.
/mnt/000) and host_path (/Users/me/datasets/000). Only the second one lets a
client turn the container paths every artifact carries into something the host's file
manager, a docker run -v for a sibling container, or a "Reveal in Finder" can use.
Until now host_path was os.environ["LOCAL_PROJECT_DIR"] and nothing else, and the v3
compose stack does not put that variable inside the container (the root
docker-compose.yml interpolates it into the volumes: entry only), so every v3 server
answered host_path: null -- while the very same container carried the answer twice over,
in its tit.host_project_dir label and in the bind mount that produced /mnt/<name>.
That forced every client that needs a host path to run docker inspect itself
(dev/smoke.sh, docs/dev/DECISIONS.md ยง 2026-09-03 (One Docker image and a real development loop)).
So: the environment variable stays authoritative when it is set, and when it is not, the
server asks the Docker Engine about its own container through the socket the stack already
mounts for DooD (/var/run/docker.sock) and reads the answer off the container's own
definition:
- the
Mounts entry whose Destination is (or contains) the project directory -- its
Source is the host path, and this also covers a project dir nested inside a mount;
- failing that, the
tit.host_project_dir label the desktop app stamps on every container
it creates (desktop/src/shared/compose.ts's LABEL_HOST_DIR).
Everything here is best-effort by construction: no socket, no /proc, an engine error, a
server running outside a container at all -- every one of them yields None, the same
answer the route gave before, never an exception on a request path. The lookup result is
cached per project directory, so a running server makes at most one Engine call for it.
clear_cache
Forget the memoised lookup (tests; a server never needs it).
Source code in tit/host_path.py
| def clear_cache() -> None:
"""Forget the memoised lookup (tests; a server never needs it)."""
_cache.clear()
|
host_project_dir
host_project_dir(container_path: str) -> str | None
The host directory container_path is mounted from, or None if unknowable.
LOCAL_PROJECT_DIR wins when set (it is what the loader and the packaged app pass, and
it is the documented contract of Project.host_path); otherwise the container's own
definition is consulted once and the result -- including None -- is cached.
Source code in tit/host_path.py
| def host_project_dir(container_path: str) -> str | None:
"""The host directory *container_path* is mounted from, or ``None`` if unknowable.
``LOCAL_PROJECT_DIR`` wins when set (it is what the loader and the packaged app pass, and
it is the documented contract of ``Project.host_path``); otherwise the container's own
definition is consulted once and the result -- including ``None`` -- is cached.
"""
from_env = os.environ.get(const.ENV_LOCAL_PROJECT_DIR)
if from_env:
return from_env
if not container_path:
return None
if container_path in _cache:
return _cache[container_path]
resolved = _from_own_container(container_path)
_cache[container_path] = resolved
return resolved
|
flavour
path in the flavour it is written in: a drive letter or a UNC prefix (\\ or
//) is Windows, anything else POSIX, so joins and is_absolute follow the host.
Source code in tit/host_path.py
| def flavour(path: str) -> PurePath:
"""*path* in the flavour it is written in: a drive letter or a UNC prefix (``\\\\`` or
``//``) is Windows, anything else POSIX, so joins and ``is_absolute`` follow the host.
"""
if _WINDOWS_RE.match(path):
return PureWindowsPath(path)
return PurePosixPath(path)
|
project_dir_name
project_dir_name(host_dir: str) -> str
The <name> in /mnt/<name> the compose stack mounts host_dir at.
Raises ValueError for a drive or share root, which has no name to mount under.
Source code in tit/host_path.py
| def project_dir_name(host_dir: str) -> str:
"""The ``<name>`` in ``/mnt/<name>`` the compose stack mounts *host_dir* at.
Raises ``ValueError`` for a drive or share root, which has no name to mount under.
"""
name = flavour(host_dir).name
if not name:
raise ValueError(f"Project directory has no name: {host_dir!r}")
return name
|
to_host
to_host(container_path: str, container_root: str, host_root: str) -> str | None
container_path re-rooted onto host_root in the host's separator, or None when
it is not under container_root (/mnt/000x is not under /mnt/000).
Source code in tit/host_path.py
| def to_host(container_path: str, container_root: str, host_root: str) -> str | None:
"""*container_path* re-rooted onto *host_root* in the host's separator, or ``None`` when
it is not under *container_root* (``/mnt/000x`` is not under ``/mnt/000``)."""
try:
rest = PurePosixPath(container_path).relative_to(container_root).parts
except ValueError:
return None
if ".." in rest:
return None
return str(flavour(host_root).joinpath(*rest))
|
to_container
to_container(host: str, host_root: str, container_root: str) -> str | None
host re-rooted onto container_root, or None when it is not under host_root.
A Windows root compares case-insensitively (c:\users is C:\Users).
Source code in tit/host_path.py
| def to_container(host: str, host_root: str, container_root: str) -> str | None:
"""*host* re-rooted onto *container_root*, or ``None`` when it is not under *host_root*.
A Windows root compares case-insensitively (``c:\\users`` is ``C:\\Users``).
"""
root, target = flavour(host_root), flavour(host)
if type(root) is not type(target):
return None
try:
rest = target.relative_to(root).parts
except ValueError:
return None
if ".." in rest:
return None
return str(PurePosixPath(container_root).joinpath(*rest))
|
get_host_project_dir
get_host_project_dir() -> str
The host's project directory (LOCAL_PROJECT_DIR), for sibling-container mounts.
Raises ValueError if it is unset or not an absolute host path.
Source code in tit/host_path.py
| def get_host_project_dir() -> str:
"""The host's project directory (``LOCAL_PROJECT_DIR``), for sibling-container mounts.
Raises ``ValueError`` if it is unset or not an absolute host path.
"""
# Never stripped: a folder whose name ends in a space is a real folder, and the mount source.
value = os.environ.get(const.ENV_LOCAL_PROJECT_DIR, "")
if not value.strip():
raise ValueError(
f"{const.ENV_LOCAL_PROJECT_DIR} environment variable is not set. "
"This is required for spawning sibling Docker containers."
)
if not flavour(value).is_absolute():
raise ValueError(
f"{const.ENV_LOCAL_PROJECT_DIR} must be an absolute host path, got {value!r}."
)
return value
|
bind_mount
docker run argv for one bind mount: --mount, not -v, whose colon-separated
form is ambiguous with a Windows C:\ source; CSV quoting covers a comma in a path.
Source code in tit/host_path.py
| def bind_mount(source: object, target: object, *, readonly: bool = False) -> list[str]:
"""``docker run`` argv for one bind mount: ``--mount``, not ``-v``, whose colon-separated
form is ambiguous with a Windows ``C:\\`` source; CSV quoting covers a comma in a path.
"""
fields = ["type=bind", f"source={source}", f"target={target}"]
if readonly:
fields.append("readonly")
buf = io.StringIO()
csv.writer(buf, lineterminator="").writerow(fields)
return ["--mount", buf.getvalue()]
|
host_dir_from_inspect
Pure half of the lookup: a container's inspect payload -> the host path of container_path.
Prefers the bind mount that actually produced the directory (exact destination, or the
closest enclosing one, so /mnt/000 resolves even when the server was pointed at
/mnt/000/sub-project), and falls back to the tit.host_project_dir label.
Source code in tit/host_path.py
| def host_dir_from_inspect(info: dict[str, Any], container_path: str) -> str | None:
"""Pure half of the lookup: a container's inspect payload -> the host path of *container_path*.
Prefers the bind mount that actually produced the directory (exact destination, or the
closest enclosing one, so ``/mnt/000`` resolves even when the server was pointed at
``/mnt/000/sub-project``), and falls back to the ``tit.host_project_dir`` label.
"""
target = container_path.rstrip("/") or "/"
best: tuple[int, str] | None = None
for mount in info.get("Mounts") or []:
if not isinstance(mount, dict):
continue
# Bind mounts only. A named volume's Source is a path inside the Docker VM
# (/var/lib/docker/volumes/...), which is not a host path any client could open.
if mount.get("Type") not in (None, "bind"):
continue
source = mount.get("Source")
destination = (mount.get("Destination") or "").rstrip("/")
if not source or not destination:
continue
host = to_host(target, destination, source)
# The longest matching destination wins: with both `/mnt` and `/mnt/000` mounted, the
# project dir belongs to `/mnt/000`.
if host is not None and (best is None or len(destination) > best[0]):
best = (len(destination), host)
if best is not None:
return best[1]
labels = (info.get("Config") or {}).get("Labels") or {}
label = labels.get(HOST_DIR_LABEL)
return label or None
|
own_container_id
own_container_id() -> str | None
This process's own container id, or None when not running in one.
Source code in tit/host_path.py
| def own_container_id() -> str | None:
"""This process's own container id, or ``None`` when not running in one."""
try:
with open("/proc/self/mountinfo", encoding="utf-8") as fh:
match = _MOUNTINFO_CONTAINER_RE.search(fh.read())
except OSError:
match = None
if match:
return match.group(1)
# Docker sets the container's hostname to its own short id unless the caller overrode it;
# an override just makes the inspect below 404, which is handled like every other miss.
hostname = os.environ.get("HOSTNAME", "")
return hostname if _SHORT_ID_RE.match(hostname) else None
|