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
- Sanitize the preset name before the call to only [A-Za-z0-9 _.:@-].
- If the stored preset genuinely has an invalid name, rename it in cookbook_state.json (or via the presets API) to an allowed name.
- 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
- Create presets only through the API so names are validated at save time.
- Keep preset names to slugs (letters, digits, dash) to avoid edge cases.
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.