{"record":{"id":"8ca0ac8a07a8a2c1","repo":"usestrix/strix","slug":"instruction-must-be-a-string","errorCode":null,"errorMessage":"instruction must be a string","messagePattern":"instruction must be a string","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"strix/interface/tui/backend/controller.py","lineNumber":293,"sourceCode":"        handler = handlers.get(command)\n        if handler is None:\n            raise ValueError(f\"Unknown command: {command}\")\n        result = await handler(payload)\n        self.notify_changed()\n        return result\n\n    async def _add_target(self, payload: dict[str, Any]) -> dict[str, Any]:\n        self._require_setup_mutable()\n        target = self._required_string(payload, \"target\")\n        if target not in self.targets:\n            self.targets.append(target)\n        return {\"target\": target, \"total\": len(self.targets)}\n\n    async def _set_instruction(self, payload: dict[str, Any]) -> dict[str, Any]:\n        self._require_setup_mutable()\n        instruction = payload.get(\"instruction\", \"\")\n        if not isinstance(instruction, str):\n            raise TypeError(\"instruction must be a string\")\n        self.instruction = instruction.strip()\n        return {\"instruction\": self.instruction}\n\n    async def _start(self, payload: dict[str, Any]) -> dict[str, Any]:\n        if self.scan_started or self._start_in_progress:\n            raise RuntimeError(\"Scan is already starting or running\")\n        # A bare prompt launches optimistically, like a coding agent: it skips\n        # the network model preflight and surfaces any model error live. A named\n        # target keeps the preflight so a real scan does not commit blind.\n        verify = payload.get(\"verify\", True)\n        if not isinstance(verify, bool):\n            raise TypeError(\"verify must be a boolean\")\n        # Launching with no target mounts the working directory, so it requires\n        # the user's explicit confirmation rather than happening silently.\n        mount_working_dir = payload.get(\"mount_working_dir\", False)\n        if not isinstance(mount_working_dir, bool):\n            raise TypeError(\"mount_working_dir must be a boolean\")\n        model = (load_settings().llm.model or \"\").strip()","sourceCodeStart":275,"sourceCodeEnd":311,"githubUrl":"https://github.com/usestrix/strix/blob/85513391305171ecc6faffe03da4a8bda5e3febb/strix/interface/tui/backend/controller.py#L275-L311","documentation":"Raised by the TUI controller's _set_instruction handler (strix/interface/tui/backend/controller.py:293) when the payload's 'instruction' field is present but not a str (e.g. a number, dict, or null-ish sentinel passed through JSON). Python's isinstance check is the type boundary between the JSON-speaking frontend and the controller's state, so a non-string is rejected as TypeError before it can pollute the scan instruction.","triggerScenarios":"Sending {\"command\": \"setup.set_instruction\", \"payload\": {\"instruction\": 123}} or any non-string JSON value; a frontend bug serializing the textbox content as an object/null; scripted clients reusing a parsed JSON value of the wrong type.","commonSituations":"Frontend regression after input-handling changes; automated drivers building payloads from untyped data (e.g. passing parsed YAML values straight through).","solutions":["Ensure the frontend/caller always sends instruction as a JSON string, coercing with String(value) / str(value) before dispatch.","Default to omitting the key (the handler falls back to \"\") rather than sending null.","Add a payload unit test asserting type str for every set_instruction dispatch."],"exampleFix":"// before\nsend(\"setup.set_instruction\", { instruction: null });\n\n// after\nsend(\"setup.set_instruction\", { instruction: String(textbox.value ?? \"\") });","handlingStrategy":"type-guard","validationCode":"instruction = payload.get(\"instruction\", \"\")\nif not isinstance(instruction, str):\n    payload[\"instruction\"] = str(instruction)","typeGuard":"def is_string_instruction(payload: dict) -> bool:\n    instruction = payload.get(\"instruction\", \"\")\n    return instruction is None or isinstance(instruction, str)","tryCatchPattern":"try:\n    await controller.handle(\"setup.set_instruction\", payload)\nexcept TypeError as exc:\n    if 'instruction must be a string' in str(exc):\n        payload[\"instruction\"] = str(payload.get(\"instruction\") or \"\")\n        await controller.handle(\"setup.set_instruction\", payload)\n    else:\n        raise","preventionTips":["Coerce textbox values to strings at the frontend boundary.","Omit the key rather than sending null.","Add payload contract tests for every command."],"tags":["tui","type-validation","payload","instruction"],"backgroundTag":null,"analyzedSha":"85513391305171ecc6faffe03da4a8bda5e3febb","analyzedAt":"2026-08-15T05:03:57.275Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}