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.

GateServer(*, mirror_root, token, scope, socket_path=None, host=None, port=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.

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,
) -> None:
    """Bind the gate's configuration; ``start`` brings the listener up."""
    self._mirror_root = mirror_root
    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)
    )
    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