Skip to content

Daemon

daemon

Per-container ACP host-proxy daemon.

Binds a Unix socket on the host that aggregates a container's in-image ACP agents (terok-{agent}-acp wrappers) behind a single endpoint. Lifetime is tied to the container: the daemon polls runtime.container(name).state and exits cleanly once the container is gone.

Standalone use::

terok-executor acp <container_name> <socket_path>

CONTAINER_POLL_INTERVAL_SEC = 2.0 module-attribute

How often the daemon checks whether the container is still alive.

Tuned to balance acp list freshness against polling overhead.

acp_socket_is_live(path)

Return True when a peer is currently accepting on path.

Distinguishes a live ACP daemon from a stale socket file left behind by a crash: a successful connect means a peer is listening, while ECONNREFUSED (and any other OSError) means the file is safe to unlink.

Source code in src/terok_executor/acp/daemon.py
def acp_socket_is_live(path: Path) -> bool:
    """Return ``True`` when a peer is currently accepting on *path*.

    Distinguishes a live ACP daemon from a stale socket file left
    behind by a crash: a successful ``connect`` means a peer is
    listening, while ``ECONNREFUSED`` (and any other ``OSError``)
    means the file is safe to unlink.
    """
    if not path.exists():
        return False
    try:
        with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as probe:
            probe.settimeout(0.2)
            probe.connect(str(path))
    except OSError:
        return False
    return True

serve_acp(container_name, socket_path, *, sandbox=None, poll_interval_sec=CONTAINER_POLL_INTERVAL_SEC)

Bind socket_path and run the ACP host-proxy until container_name stops.

Returns the process exit code. When sandbox is None, builds one from the layered config.yml + env (the same SandboxConfig() defaults executor uses everywhere).

Source code in src/terok_executor/acp/daemon.py
def serve_acp(
    container_name: str,
    socket_path: Path,
    *,
    sandbox: Sandbox | None = None,
    poll_interval_sec: float = CONTAINER_POLL_INTERVAL_SEC,
) -> int:
    """Bind *socket_path* and run the ACP host-proxy until *container_name* stops.

    Returns the process exit code.  When *sandbox* is ``None``, builds
    one from the layered ``config.yml`` + env (the same
    ``SandboxConfig()`` defaults executor uses everywhere).
    """
    if sandbox is None:
        sandbox = Sandbox(config=SandboxConfig())
    return asyncio.run(_run(container_name, socket_path, sandbox, poll_interval_sec))

main(argv=None)

Argv entry — terok-executor acp <container_name> <socket_path>.

Set TEROK_ACP_DEBUG=1 (or any non-empty value) to drop the log level to DEBUG — the proxy then traces every JSON-RPC frame in/out of the daemon, which is what you usually want when chasing a "client X did/didn't send method Y" mystery.

Source code in src/terok_executor/acp/daemon.py
def main(argv: list[str] | None = None) -> int:
    """Argv entry — ``terok-executor acp <container_name> <socket_path>``.

    Set ``TEROK_ACP_DEBUG=1`` (or any non-empty value) to drop the log
    level to ``DEBUG`` — the proxy then traces every JSON-RPC frame
    in/out of the daemon, which is what you usually want when chasing
    a "client X did/didn't send method Y" mystery.
    """
    args = sys.argv[1:] if argv is None else argv
    if len(args) != 2:
        print(
            "usage: terok-executor acp <container_name> <socket_path>",
            file=sys.stderr,
        )
        return 2
    cname, sock_str = args
    level = logging.DEBUG if os.environ.get("TEROK_ACP_DEBUG") else logging.INFO
    # ``force=True`` because some import path (pydantic, acp, asyncio,
    # sandbox) may have already configured the root logger by the time
    # we get here — without ``force`` the second ``basicConfig`` is a
    # silent no-op and the DEBUG knob does nothing.
    logging.basicConfig(level=level, format="acp[%(levelname)s] %(message)s", force=True)
    return serve_acp(cname, Path(sock_str))