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)}"
)