Skip to content

toad

toad

Toad web-TUI task runner.

task_run_toad launches the Toad multi-agent TUI behind Caddy for token-gated browser access; _resume_toad_container is the fast path for a toad task whose container already exists. The token/URL helpers (_ensure_toad_token, _toad_browser_url, _rehydrate_toad_token) are also consumed by the restart runner.

__all__ = ['task_run_toad'] module-attribute

task_run_toad(project_id, task_id, agents=None, preset=None, unrestricted=None)

Launch the Toad multi-agent TUI behind Caddy for token-gated browser access.

Same CLI image as interactive tasks, but the container entrypoint is terok-toad-entry: it starts Caddy on the published port, toad on an internal loopback port, and emits TEROK_READY once both are listening. Caddy enforces the per-task token (see _ensure_toad_token) on every request.

Source code in src/terok/lib/orchestration/task_runners/toad.py
def task_run_toad(
    project_id: str,
    task_id: str,
    agents: list[str] | None = None,
    preset: str | None = None,
    unrestricted: bool | None = None,
) -> None:
    """Launch the Toad multi-agent TUI behind Caddy for token-gated browser access.

    Same CLI image as interactive tasks, but the container entrypoint is
    ``terok-toad-entry``: it starts Caddy on the published port, toad on
    an internal loopback port, and emits ``TEROK_READY`` once both are
    listening.  Caddy enforces the per-task token (see
    `_ensure_toad_token`) on every request.
    """
    project = load_project(project_id)
    meta, meta_path = load_task_meta(project.id, task_id, "toad")

    cname = container_name(project.id, "toad", task_id)
    container_state = _rt.resolve_runtime(project).container(cname).state

    pub_host = get_public_host()

    if container_state is not None:
        _resume_toad_container(
            project=project,
            task_id=task_id,
            cname=cname,
            container_state=container_state,
            meta=meta,
            meta_path=meta_path,
            pub_host=pub_host,
        )
        return

    # New container — allocate a fresh port.
    port = assign_web_port(project.id, task_id)
    meta["web_port"] = port

    env, volumes = build_task_env_and_volumes(project, task_id)

    agent_config_dir = _prepare_agent_config(project, project_id, task_id, agents, preset)
    volumes.append(VolumeSpec(agent_config_dir, CONTAINER_TEROK_CONFIG, sharing=Sharing.PRIVATE))

    token = _ensure_toad_token(agent_config_dir)
    meta["web_token"] = token

    env["TOAD_PUBLIC_PORT"] = str(_TOAD_PUBLIC_PORT)
    env["TOAD_INTERNAL_PORT"] = str(_TOAD_INTERNAL_PORT)

    # Resolve unrestricted mode: CLI flag → config → default (True)
    if unrestricted is None:
        _effective = resolve_agent_config(
            project_id,
            agent_config=project.agent_config,
            project_root=project.root,
            preset=preset,
        )
        _cfg_val = resolve_provider_value(
            "unrestricted", _effective, project.default_agent or "claude"
        )
        unrestricted = _cfg_val is None or _str_to_bool(_cfg_val)
    if unrestricted:
        _apply_unrestricted_env(env)

    meta["mode"] = "toad"
    meta["unrestricted"] = unrestricted
    if preset:
        meta["preset"] = preset
    write_task_meta(meta_path, meta)

    # Preserve the address family when the public host is a loopback — binding
    # ::1 to 127.0.0.1 would make the URL we print (``http://[::1]:…``)
    # unreachable.  LAN exposure still goes to ``0.0.0.0``.
    if pub_host == "::1":
        bind_addr = "[::1]"
    elif pub_host in _LOOPBACK_HOSTS:
        bind_addr = _LOCALHOST
    else:
        bind_addr = "0.0.0.0"  # nosec B104

    task_dir = project.tasks_root / str(task_id)
    # ``terok-toad-entry`` (from the caddy/toad roster entries) owns the
    # in-container choreography: it starts Caddy on ``_TOAD_PUBLIC_PORT``,
    # launches toad on loopback ``_TOAD_INTERNAL_PORT``, waits for both to
    # bind, and emits the ``TEROK_READY`` readiness marker.
    toad_cmd = f"terok-toad-entry --public-url http://{url_host(pub_host)}:{port} /workspace"
    run_hook(
        "pre_start",
        project.hook_pre_start,
        project_id=project.id,
        task_id=task_id,
        mode="toad",
        cname=cname,
        web_port=port,
        task_dir=task_dir,
        meta_path=meta_path,
    )
    _run_container(
        cname=cname,
        image=project_cli_image(project.id),
        env=env,
        volumes=volumes,
        project=project,
        task_id=task_id,
        task_dir=task_dir,
        extra_args=["-p", f"{bind_addr}:{port}:{_TOAD_PUBLIC_PORT}"],
        command=["bash", "-lc", toad_cmd],
    )
    _apply_shield_policy(project, cname, task_dir, is_restart=False)
    run_hook(
        "post_start",
        project.hook_post_start,
        project_id=project.id,
        task_id=task_id,
        mode="toad",
        cname=cname,
        web_port=port,
        task_dir=task_dir,
        meta_path=meta_path,
    )

    def _toad_ready(line: str) -> bool:
        """Return True when the supervisor wrapper reports both listeners are up."""
        return "TEROK_READY" in line

    runtime = _rt.resolve_runtime(project)
    ready = runtime.container(cname).stream_initial_logs(
        ready_check=_toad_ready,
        timeout_sec=None,
    )

    if not ready or not runtime.container(cname).running:
        print(f"Toad failed to start. Check logs: podman logs {cname}")
        raise SystemExit(1)

    run_hook(
        "post_ready",
        project.hook_post_ready,
        project_id=project.id,
        task_id=task_id,
        mode="toad",
        cname=cname,
        web_port=port,
        task_dir=task_dir,
        meta_path=meta_path,
    )

    meta["ready_at"] = datetime.now(UTC).isoformat()
    write_task_meta(meta_path, meta)

    color_enabled = _supports_color()
    url = _toad_browser_url(pub_host, port, token)
    print(
        f"\n>> Toad is serving."
        f"\n- Name: {_green(cname, color_enabled)}"
        f"\n- URL:  {_hyperlink(_blue(url, color_enabled), url, enabled=color_enabled)}"
        f"\n- Logs: {_yellow(f'podman logs -f {cname}', color_enabled)}"
        f"\n- Stop: {_red(f'podman stop {cname}', color_enabled)}"
    )