{"slug": "cast-bool-x-is-a-promise-to-the-type-checker-at-runtime-it-is-the-identity", "title": "cast(bool, x) is a promise to the type checker. At runtime it is the identity function.", "summary": "A developer found that in google/adk-python, the value determining whether a tool call requires human confirmation is passed through typing.cast(bool, await ...), which is the identity function at runtime, so an awaited expression resolving to None reaches the caller unchecked and the confirmation step is skipped. The same pattern was previously filed against openai-agents-python as issue #4845, though via coercion rather than declaration. The developer notes the finding is a claim about what the code permits, not an observed failure, since no live call path resolving to None was traced.", "body_md": "In `google/adk-python`, the value that decides whether a tool call needs human confirmation reaches its caller through `cast(bool, await ...)`.\n\n`typing.cast` returns its second argument. That is the entire implementation:\n\n``` python\ndef cast(typ, val):\n    \"\"\"Cast a value to a type. This returns the value unchanged.\"\"\"\n    return val\n```\n\nIt exists so a static checker will stop complaining, and it does nothing at all when the program runs. If the awaited expression produces `None`, the caller receives `None`. The caller then tests it for truth and skips the confirmation.\n\n``` python\n>>> from typing import cast\n>>> cast(bool, None) is None\nTrue\n>>> if cast(bool, None): print(\"confirm\")     # prints nothing\n```\n\nThat is the same ending as the coercion defect filed against `openai-agents-python` as issue #4845. Different route. This one arrives by declaration instead of by conversion.\n\nAsk a type checker what `cast(bool, x)` is and it answers `bool`. Correctly. That is what `cast` is for. The programmer asserted the type and the checker took the assertion. There is no diagnostic to emit and no line to highlight.\n\nA grep-shaped tool has a different problem: the pair is not on one line. Formatters split `cast(` from `bool,` across a newline, so a per-line pattern never sees them together:\n\n```\n        return cast(\n            bool,\n            await self._tool_confirmation(...),\n        )\n```\n\n`grep -n 'cast(bool'` returns nothing here. I had to look at a window of lines rather than at single lines.\n\nTwo things, and the second is the embarrassing one.\n\nI first recorded the sites as `function_tool.py:206` and `mcp_tool.py:474`. Those lines read `return bool(self._require_confirmation)`, which is the safe branch. The expression that matters is a few lines above each of them.\n\n|  | what I reported | what actually matters | \n|---|---|---|\n| `function_tool.py` | `:206` | `:202-205` | \n| `mcp_tool.py` | `:474` | `:470-473` | \n| the code there | `return bool(...)` , the safe branch | the multi-line `cast` | \n\nI recorded, and reported, the location of the code that was fine.\n\nThe reason I landed there is that my tool did find those functions, and it found them for a reason I never checked. A coercion signal had matched `bool(...)` on the safe branch. The function was on my list, the list was right, and my account of why it was on the list was wrong. A correct output with a wrong cause is harder to catch than a wrong output, because nothing looks broken.\n\nThe same tool was also printing a banner that named two active signals while three were running. I fixed that by printing the signal set the run actually used. A declaration with no behaviour behind it, inside the tool I built to find declarations with no behaviour behind them.\n\nWhether any caller in `adk-python` actually passes something that resolves to `None`. I did not trace the call graph to a live path, so this is a claim about what the code permits and not about an observed failure.\n\n`cast` is not the defect. A cast over a value that has already been checked is fine and common. The defect is the unchecked value, and my signal cannot tell those two apart, which is why its output is a reading list rather than a finding.\n\nI have not measured how common this spelling is across the ecosystem. One repository, one commit, two sites.\n\nRepository: [crates/gx-witness](https://github.com/TraceFold/tracefold/tree/main/crates/gx-witness) is the part of the project that exists because a claim about a value is not the same as evidence about it.\n\nRunnable reproductions for every framework named above, offline and pinned to a version: [https://github.com/mahirhir/unanswered-approval](https://github.com/mahirhir/unanswered-approval)", "url": "https://wpnews.pro/news/cast-bool-x-is-a-promise-to-the-type-checker-at-runtime-it-is-the-identity", "canonical_source": "https://dev.to/mahirhir/castbool-x-is-a-promise-to-the-type-checker-at-runtime-it-is-the-identity-function-3d0e", "published_at": "2026-09-12 15:08:59+00:00", "updated_at": "2026-09-12 15:44:44.132039+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "developer-tools", "ai-tools"], "entities": ["google/adk-python", "openai-agents-python", "typing.cast", "gx-witness", "TraceFold", "unanswered-approval"], "alternates": {"html": "https://wpnews.pro/news/cast-bool-x-is-a-promise-to-the-type-checker-at-runtime-it-is-the-identity", "markdown": "https://wpnews.pro/news/cast-bool-x-is-a-promise-to-the-type-checker-at-runtime-it-is-the-identity.md", "text": "https://wpnews.pro/news/cast-bool-x-is-a-promise-to-the-type-checker-at-runtime-it-is-the-identity.txt", "jsonld": "https://wpnews.pro/news/cast-bool-x-is-a-promise-to-the-type-checker-at-runtime-it-is-the-identity.jsonld"}}