fix: async wrapping, stdout pollution, URL policy scoping, sdist contents - #4
Merged
Conversation
…ents Four defects found by a launch-readiness audit of 0.3.1, each verified against the published wheel. - @shield_tool always built a sync wrapper, so inspect.iscoroutinefunction() returned False on a shielded async def. LangChain and MCP branch on that to decide whether to await, so the tool body never ran and the model received a coroutine repr as its observation. Coroutine functions and async generators are now wrapped in kind. - The decorator printed to stdout on every allowed call. stdout is the transport for MCP stdio servers, so this corrupted the JSON-RPC stream. Blocks now log at WARNING on the "modelfuzz" logger with structured modelfuzz_tool / modelfuzz_rule / modelfuzz_reason fields; allows log at DEBUG. Nothing touches stdout. - URLAllowList was applied to every argument, so http_post(url, body) blocked its own legitimate calls -- body is not a URL. It now returns None for values that are not URLs. While rewriting it: reject non-http(s) schemes (file://api.internal.com/etc/passwd previously passed) and compare hostnames case-insensitively via urlparse().hostname. - No [tool.hatch.build.targets.sdist] meant hatchling swept the working tree, so the 0.3.1 sdist shipped .claude/settings.local.json. Now an allowlist. PolicyResult carries the originating Violation so the log can name the rule. Tests 25 -> 60; decorator.py and engine.py to 100%.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the four code-level launch blockers from the 0.3.1 readiness audit. Each was reproduced against the published wheel before fixing and re-verified against the rebuilt one. Version bumped to
0.3.2.The README honesty pass (the keyword-filter framing) is deliberately not here — it's a separate PR.
1.
@shield_toolbroke async tools_wrapalways built a sync wrapper, sofunctools.wrapsleftinspect.iscoroutinefunction()returningFalseon a shieldedasync def. LangChain and MCP branch on exactly that to decide whether to await, so the body never ran and the model received a coroutinerepras its observation — with no exception, just aRuntimeWarningmost apps never surface.Coroutine functions and async generators are now wrapped in kind.
2. stdout pollution corrupted MCP stdio
print(f"ModelFuzz Intercepted: ...")fired on every allowed call. stdout is the JSON-RPC transport for MCP stdio servers, so the client died withInvalid JSON. The logging was also inverted: it announced allows and recorded nothing on deny.Now: allows log at
DEBUG, blocks log atWARNINGon themodelfuzzlogger with structured fields. Nothing writes to stdout.PolicyResultgained an optionalviolationfield so the log can name the rule — it was previously discarded inengine.py. Additive, non-breaking.3.
URLAllowListblocked every legitimate multi-argument callThe engine applies every policy to every argument, so
http_post(url, body)blocked its own valid calls —bodyisn't a URL. The rule now returnsNonefor values it doesn't govern.Slightly wider than asked, within the same function, because leaving a known fail-open in code I was rewriting wasn't defensible:
http/httpsdefault).file://api.internal.com/etc/passwdpreviously passed for an allowlisted host — a fail-open.urlparse().hostnameinstead of hand-rollednetloc.split(":")[0], soHTTPS://API.INTERNAL.COMand a trailing dot now work. DNS is case-insensitive; these previously failed closed.Known tradeoff, documented in the docstring: a schemeless bare host (
"evil.com") is not identifiable as a URL and now passes. Most HTTP clients reject a schemeless URL, but it is a real narrowing of the rule and the docstring says so.4. sdist shipped local artifacts
No
[tool.hatch.build.targets.sdist]meant hatchling swept the working tree — the published 0.3.1 sdist contains.claude/settings.local.json, and a rebuild would have added.coverageand.claude/settings.json. Now an explicit allowlist, plus.claude/,.coverage*,htmlcov/,.envadded to.gitignore.Verification
decorator.pyandengine.pyat 100%;rules.py97%.capsysstdout assertions, structured-log assertions, keyword arguments, and the bare@shield_toolform.Blocked: String contains sensitive keyword: 'secret'.demo.pyoutput unchanged.rules.pyare a defensiveexcept ValueErroraround.hostnamethat I could not trigger on any Python version tested.Note for review
Python's
lastResorthandler means a block prints to stderr when the host app hasn't configured logging. That's stderr not stdout, so MCP is unaffected, and a silent security denial seemed worse than a visible one — but it is a behavior choice worth a second opinion. Adding aNullHandlerwould silence it by default.Not addressed here (audit's "fix soon after"):
SensitiveDataFilterfails open on unrecognized types and never checks dict keys;ModelFuzzBlockErrorstill carries only a string; nopy.typed.