odysseus-dev/odysseus · warning · HTTPException

Invalid preset name

Error message

Invalid preset name

What it means

Raised by POST /api/codex/cookbook/preset/{name} when the preset name contains characters outside [A-Za-z0-9 _.:@-]. The check is a hard prerequisite before any state lookup, so an invalid name never reaches preset matching. It exists to keep the path parameter safe for downstream shell/ssh usage.

Source

Thrown at routes/codex_routes.py:782

            if not isinstance(p, dict):
                continue
            out.append({
                "name": p.get("name"),
                "model": p.get("model") or p.get("modelId"),
                "host": p.get("host") or p.get("remoteHost"),
                "port": p.get("port"),
                "cmd": p.get("cmd"),
            })
        return {"presets": out, "default_host": (state.get("env") or {}).get("defaultServer", "")}

    @router.post("/cookbook/preset/{name}")
    async def codex_cookbook_serve_preset(request: Request, name: str):
        """Launch a saved preset by name. Reuses the working cmd + host the
        user already saved, avoiding the cmd-allowlist trial-and-error loop."""
        _require_cookbook_scope(request, COOKBOOK_LAUNCH_SCOPES)
        import re as _re
        if not _re.fullmatch(r"[A-Za-z0-9 _.:@\-]+", name):
            raise HTTPException(400, "Invalid preset name")
        state = _read_cookbook_state()
        presets = state.get("presets") or []
        lname = name.lower().strip()
        chosen = next(
            (p for p in presets if isinstance(p, dict) and (p.get("name") or "").lower() == lname),
            None,
        )
        if chosen is None:
            chosen = next(
                (p for p in presets if isinstance(p, dict) and lname in (p.get("name") or "").lower()),
                None,
            )
        if chosen is None:
            raise HTTPException(404, f"No preset matching {name!r}")
        repo_id = chosen.get("model") or chosen.get("modelId") or ""
        cmd = (chosen.get("cmd") or "").strip()
        host = chosen.get("host") or chosen.get("remoteHost") or ""
        if not repo_id or not cmd or cmd.startswith("(adopted"):

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Sanitize the preset name before the call to only [A-Za-z0-9 _.:@-].
  2. If the stored preset genuinely has an invalid name, rename it in cookbook_state.json (or via the presets API) to an allowed name.
  3. URL-encode the name properly so no character is mangled in transit.

Example fix

// before
await post(`/cookbook/preset/${rawName}`)  // rawName = "my/preset:1"

// after
const safe = rawName.replace(/[^A-Za-z0-9 _.:@-]/g, "-")
await post(`/cookbook/preset/${encodeURIComponent(safe)}`)
Defensive patterns

Strategy: validation

Validate before calling

import re
SAFE = re.compile(r'^[A-Za-z0-9 _.:@-]+$')
assert SAFE.fullmatch(name), f'preset name {name!r} has disallowed chars'

Type guard

const isSafePresetName = (n: string) => /^[A-Za-z0-9 _.:@-]+$/.test(n)

Prevention

When it happens

Trigger: Calling POST /cookbook/preset/{name} with a name containing '/', ',', ';', quotes, unicode, or other disallowed characters; also an empty or whitespace-only name after URL decoding.

Common situations: Client passes a URL-encoded name with %2F or special punctuation; preset was created via a different tool with arbitrary names; copy-paste of names with commas or parentheses.

Related errors


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