Skip to content

server

server

HTTP gate component wrapping git http-backend with token auth.

Composed by the per-container supervisor as one of its services. The gate serves a single task's repo out of the shared per-project bare mirror, gated on a single minted token.

Token validation

Each request must carry HTTP Basic Auth with the token as the username (password is ignored). The supervisor minted exactly one token for the task this container serves; the requested repo must match the token's scope.

Transport

The supervisor binds the gate on a per-container Unix socket inside container_runtime_dir (= the in-container /run/terok) in socket mode, or on a per-container 127.0.0.1 TCP port in TCP mode.

GIT_HTTP_BACKEND_HINT = 'Alpine: apk add git-daemon' module-attribute

GateServer(*, mirror_root, token, scope, socket_path=None, host=None, port=None, hooks_path=None, home_path=None)

Per-container git gate, composed by the supervisor alongside the vault.

Serves the task's repo out of the shared per-project bare mirror at mirror_root, gated on the single token (scoped to scope). Binds either a per-container Unix socket (socket_path) or a per-container 127.0.0.1 TCP port (host + port); exactly one transport must be supplied.

Stateless and self-contained — the only terok dependency is the SELinux socket-labelling helper the Unix listener needs.

Bind the gate's configuration; start brings the listener up.

hooks_path is the sandbox-owned hooks directory the composer prepared (see install_hooks); None serves with hooks disabled (core.hooksPath=/dev/null). home_path pins Git's home/config lookup to a composer-owned directory; None preserves the ambient home for standalone callers. Plain values by design — this component stays free of gate-model imports.

Source code in src/terok_sandbox/gate/server.py
def __init__(
    self,
    *,
    mirror_root: Path,
    token: str,
    scope: str,
    socket_path: Path | None = None,
    host: str | None = None,
    port: int | None = None,
    hooks_path: Path | None = None,
    home_path: Path | None = None,
) -> None:
    """Bind the gate's configuration; ``start`` brings the listener up.

    *hooks_path* is the sandbox-owned hooks directory the composer
    prepared (see [`install_hooks`][terok_sandbox.gate.hooks.install_hooks]);
    ``None`` serves with hooks disabled (``core.hooksPath=/dev/null``).
    *home_path* pins Git's home/config lookup to a composer-owned
    directory; ``None`` preserves the ambient home for standalone
    callers.  Plain values by design — this component stays free of
    gate-model imports.
    """
    self._mirror_root = mirror_root
    self._hooks_path = hooks_path
    self._home_path = home_path
    self._token = token
    self._scope = scope
    self._socket_path = socket_path
    self._host = host
    self._port = port
    self._server: HTTPServer | None = None
    self._thread: threading.Thread | None = None

start() async

Bind the listener and serve it on a daemon thread.

Source code in src/terok_sandbox/gate/server.py
async def start(self) -> None:
    """Bind the listener and serve it on a daemon thread."""
    import asyncio

    handler = _make_handler_class(
        self._mirror_root,
        _SingleTokenStore(self._token, self._scope),
        self._hooks_path,
        self._home_path,
    )
    if self._socket_path is not None:
        server: HTTPServer = await asyncio.get_running_loop().run_in_executor(
            None, _create_unix_server, handler, self._socket_path
        )
    elif self._host and self._port:
        server = _ThreadingHTTPServer((self._host, self._port), handler)
    else:
        raise ValueError("GateServer needs either socket_path or host+port")
    self._server = server
    self._thread = threading.Thread(target=server.serve_forever, daemon=True, name="terok-gate")
    self._thread.start()

stop() async

Stop the listener and join the serving thread.

shutdown() blocks until the accept loop exits, so it runs in an executor rather than inline on the event loop — calling it on the loop thread would deadlock.

Source code in src/terok_sandbox/gate/server.py
async def stop(self) -> None:
    """Stop the listener and join the serving thread.

    ``shutdown()`` blocks until the accept loop exits, so it runs in
    an executor rather than inline on the event loop — calling it on
    the loop thread would deadlock.
    """
    import asyncio

    if self._server is None:
        return
    loop = asyncio.get_running_loop()
    await loop.run_in_executor(None, self._server.shutdown)
    self._server.server_close()
    if self._thread is not None:
        self._thread.join(timeout=2.0)
    self._server = None
    self._thread = None

git_http_backend()

The executable CGI in git's exec path, or None without git or without it.

Spawning git http-backend cannot tell: git starts either way and reports an unknown subcommand when the CGI is absent.

Source code in src/terok_sandbox/gate/server.py
def git_http_backend() -> Path | None:
    """The executable CGI in git's exec path, or ``None`` without git or without it.

    Spawning ``git http-backend`` cannot tell: git starts either way and
    reports an unknown subcommand when the CGI is absent.
    """
    try:
        exec_path = subprocess.run(  # nosec B603 B607 — fixed argv, PATH lookup is the cross-distro contract
            ["git", "--exec-path"],
            executable=require_host_tool("git"),
            capture_output=True,
            text=True,
            check=False,
        ).stdout.strip()
    except OSError:
        return None
    backend = Path(exec_path) / "git-http-backend"
    return backend if os.access(backend, os.X_OK) else None