odysseus-dev/odysseus · warning · HTTPException

tmux_session required, [a-zA-Z0-9_-]+ only

Error message

tmux_session required, [a-zA-Z0-9_-]+ only

What it means

Raised by POST /api/codex/cookbook/adopt when the tmux_session field is missing or contains characters outside [a-zA-Z0-9_-]. The session name is later interpolated into a shell command (tmux has-session, locally or via ssh), so this validation is a command-injection guard and is strict by design.

Source

Thrown at routes/codex_routes.py:839

        if serve_endpoint is None:
            raise HTTPException(503, "model serve endpoint unavailable")
        return await serve_endpoint(request, req)

    @router.post("/cookbook/adopt")
    async def codex_cookbook_adopt(request: Request, body: dict[str, Any] = Body(default_factory=dict)):
        """Adopt an existing tmux session (one started via raw ssh+tmux) into
        cookbook tracking. Needed when serve_model rejects a cmd and the
        agent falls back to direct ssh — without adoption the session is
        invisible to the UI. Body: {tmux_session, model, host?, port?}."""
        _require_cookbook_scope(request, COOKBOOK_LAUNCH_SCOPES)
        norm = dict(body or {})
        sess = (norm.get("tmux_session") or norm.get("session_id") or "").strip()
        model = (norm.get("model") or norm.get("repo_id") or "").strip()
        host = validate_remote_host((norm.get("host") or norm.get("remote_host") or "").strip() or None) or ""
        port = norm.get("port") or 8000
        import re as _re
        if not sess or not _re.fullmatch(r"[a-zA-Z0-9_-]+", sess):
            raise HTTPException(400, "tmux_session required, [a-zA-Z0-9_-]+ only")
        if not model:
            raise HTTPException(400, "model required")
        # Verify the tmux session exists on the target host before adopting.
        import shlex
        if host:
            check = f"ssh {shlex.quote(host)} 'tmux has-session -t {shlex.quote(sess)}'"
        else:
            check = f"tmux has-session -t {shlex.quote(sess)}"
        chk = await _run_shell(check, timeout=8)
        if chk.get("exit_code") not in (0, None):
            raise HTTPException(404, f"tmux session {sess!r} not found on {host or 'local'}")
        # Write into cookbook_state.json.
        import time as _t, json as _json
        from core.atomic_io import atomic_write_json
        from pathlib import Path as _Path
        cookbook_state_path = _Path(COOKBOOK_STATE_FILE)
        try:
            state = _json.loads(cookbook_state_path.read_text(encoding="utf-8"))

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Rename the tmux session before adopting: tmux rename-session (or start with) a name using only [a-zA-Z0-9_-].
  2. Send the session name exactly as allowed; strip disallowed characters client-side only if the actual session name matches.
  3. If the session has a dot, create a new session with a safe name running the same command.

Example fix

# before
tmux new-session -d -s 'vllm.server'   # dot not allowed by adopt

# after
tmux new-session -d -s 'vllm-server'
# then POST /cookbook/adopt {"tmux_session":"vllm-server", ...}
Defensive patterns

Strategy: validation

Validate before calling

import re
assert re.fullmatch(r'[a-zA-Z0-9_-]+', session), 'rename tmux session first'

Type guard

const isAdoptableSession = (s: string) => /^[a-zA-Z0-9_-]+$/.test(s)

Prevention

When it happens

Trigger: Calling /cookbook/adopt with tmux_session/session_id empty, containing a dot, colon, space, or shell metacharacters. Note: tmux itself permits dots and colons in session names, but this API rejects them.

Common situations: Default tmux session names with dots (e.g. 'my.session') created outside the cookbook; client sends session_id instead of a sanitized value; name copied from 'tmux ls' output containing a dot or colon.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/3b6b45a8f925d4a6. Report an issue: GitHub.