{"record":{"id":"cb4d24542e87347f","repo":"odysseus-dev/odysseus","slug":"invalid-preset-name","errorCode":null,"errorMessage":"Invalid preset name","messagePattern":"Invalid preset name","errorType":"http","errorClass":"HTTPException","httpStatus":400,"severity":"warning","filePath":"routes/codex_routes.py","lineNumber":782,"sourceCode":"            if not isinstance(p, dict):\n                continue\n            out.append({\n                \"name\": p.get(\"name\"),\n                \"model\": p.get(\"model\") or p.get(\"modelId\"),\n                \"host\": p.get(\"host\") or p.get(\"remoteHost\"),\n                \"port\": p.get(\"port\"),\n                \"cmd\": p.get(\"cmd\"),\n            })\n        return {\"presets\": out, \"default_host\": (state.get(\"env\") or {}).get(\"defaultServer\", \"\")}\n\n    @router.post(\"/cookbook/preset/{name}\")\n    async def codex_cookbook_serve_preset(request: Request, name: str):\n        \"\"\"Launch a saved preset by name. Reuses the working cmd + host the\n        user already saved, avoiding the cmd-allowlist trial-and-error loop.\"\"\"\n        _require_cookbook_scope(request, COOKBOOK_LAUNCH_SCOPES)\n        import re as _re\n        if not _re.fullmatch(r\"[A-Za-z0-9 _.:@\\-]+\", name):\n            raise HTTPException(400, \"Invalid preset name\")\n        state = _read_cookbook_state()\n        presets = state.get(\"presets\") or []\n        lname = name.lower().strip()\n        chosen = next(\n            (p for p in presets if isinstance(p, dict) and (p.get(\"name\") or \"\").lower() == lname),\n            None,\n        )\n        if chosen is None:\n            chosen = next(\n                (p for p in presets if isinstance(p, dict) and lname in (p.get(\"name\") or \"\").lower()),\n                None,\n            )\n        if chosen is None:\n            raise HTTPException(404, f\"No preset matching {name!r}\")\n        repo_id = chosen.get(\"model\") or chosen.get(\"modelId\") or \"\"\n        cmd = (chosen.get(\"cmd\") or \"\").strip()\n        host = chosen.get(\"host\") or chosen.get(\"remoteHost\") or \"\"\n        if not repo_id or not cmd or cmd.startswith(\"(adopted\"):","sourceCodeStart":764,"sourceCodeEnd":800,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/codex_routes.py#L764-L800","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait post(`/cookbook/preset/${rawName}`)  // rawName = \"my/preset:1\"\n\n// after\nconst safe = rawName.replace(/[^A-Za-z0-9 _.:@-]/g, \"-\")\nawait post(`/cookbook/preset/${encodeURIComponent(safe)}`)","handlingStrategy":"validation","validationCode":"import re\nSAFE = re.compile(r'^[A-Za-z0-9 _.:@-]+$')\nassert SAFE.fullmatch(name), f'preset name {name!r} has disallowed chars'","typeGuard":"const isSafePresetName = (n: string) => /^[A-Za-z0-9 _.:@-]+$/.test(n)","tryCatchPattern":null,"preventionTips":["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."],"tags":["validation","preset-names","http-400"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}