I was following the agent checklist in sdk/README.md with a default Guard: scan every model response with guard.scan_output(text), then check side-effect tools before running them. On current dev (cf0b0c7), scanning an ordinary benign model response marks the shared session as tainted, so the very next side-effect call that was allowed before the scan now comes back as REVIEW. That makes the taint review fire on sessions where nothing untrusted was ever ingested.
Observed behavior
scan_output() scans model output with Source.TOOL_OUTPUT, and afterwards guard._maybe_mark_session_tainted_from_scan() adds scan:tool_output to the shared context's taint triggers. This happens even when the output is completely benign and no retrieved or external content entered the session.
The README's own checklist sets up the conflict:
- Step 5 says to scan agent output: "Scan agent output.
guard.scan_output(text)."
- The same page explains that "A tainted session plus a side-effect tool returns
REVIEW."
So a host doing exactly what the docs say ends up with every side-effect tool held for review after the first model response, benign or not. It also dilutes the signal of the review itself: if the session is always tainted, REVIEW stops meaning "untrusted data reached the session".
Minimal reproduction
Run on dev at cf0b0c7, Python 3.13, Windows 11, installed via uv sync --all-extras --dev:
from unplug import Guard
from unplug.api.enums import Action, Source
# Control: a clean session allows a representative side-effect tool.
guard = Guard()
assert guard.context.is_session_tainted is False
baseline = guard.check_tool_call("shell", {"command": "echo hello"})
assert baseline.action == Action.ALLOW
# Documented host step: scan an ordinary model response.
result = guard.scan_output("The answer is 42.")
assert result.safe is True
assert result.action == Action.ALLOW
# Benign output scan mutated shared taint state.
assert guard.context.is_session_tainted is True
assert any("scan:tool_output" in t for t in guard.context.taint_triggers)
# The same call is now held for review solely because of that output scan.
review = guard.check_tool_call("shell", {"command": "echo hello"})
assert review.action == Action.REVIEW
assert any(f.subcategory == "session_taint_side_effect" for f in review.findings)
All assertions pass on current dev. Observed trigger: ['scan:tool_output'].
The streaming path behaves differently
The equivalent chunked output scan is isolated and does not touch shared state:
stream_guard = Guard()
stream_result = stream_guard.scan_stream(["The answer is 42."], source=Source.TOOL_OUTPUT)
assert stream_result.safe is True
assert stream_guard.context.is_session_tainted is False
So non-streaming and streaming output scans have different session side effects for the same content. scan_stream() uses isolated=True internally; scan_output() does not.
Positive controls still work
I checked that real ingestion paths still taint and gate correctly, so this is not a broken tool gate:
guard.notify_taint_source("web_fetch") taints the session and check_tool_call("shell", ...) returns REVIEW.
guard.scan("Some RAG chunk content here.", source=Source.RETRIEVED) taints via scan:retrieved and the same side-effect call returns REVIEW.
Expected behavior
Output scanning should keep its leakage/redaction protection, but scanning my own model response should not mark the shared session as influenced by untrusted data. Session taint should come from actual untrusted ingestion: retrieved/external content or configured taint-source tools.
One possible direction, not a mandate: separate "scan this text for leakage before it leaves" from "the session has been influenced by untrusted data". Regression coverage would need both cases: benign output leaving the session clean, and real ingestion still tainting. Happy either way if maintainers would rather treat the current behavior as intended CaMeL-style conservatism and document it instead.
Environment: Python 3.13.13, Windows 11, unplug built from dev at cf0b0c7.
I was following the agent checklist in
sdk/README.mdwith a defaultGuard: scan every model response withguard.scan_output(text), then check side-effect tools before running them. On currentdev(cf0b0c7), scanning an ordinary benign model response marks the shared session as tainted, so the very next side-effect call that was allowed before the scan now comes back as REVIEW. That makes the taint review fire on sessions where nothing untrusted was ever ingested.Observed behavior
scan_output()scans model output withSource.TOOL_OUTPUT, and afterwardsguard._maybe_mark_session_tainted_from_scan()addsscan:tool_outputto the shared context's taint triggers. This happens even when the output is completely benign and no retrieved or external content entered the session.The README's own checklist sets up the conflict:
guard.scan_output(text)."REVIEW."So a host doing exactly what the docs say ends up with every side-effect tool held for review after the first model response, benign or not. It also dilutes the signal of the review itself: if the session is always tainted, REVIEW stops meaning "untrusted data reached the session".
Minimal reproduction
Run on
devatcf0b0c7, Python 3.13, Windows 11, installed viauv sync --all-extras --dev:All assertions pass on current
dev. Observed trigger:['scan:tool_output'].The streaming path behaves differently
The equivalent chunked output scan is isolated and does not touch shared state:
So non-streaming and streaming output scans have different session side effects for the same content.
scan_stream()usesisolated=Trueinternally;scan_output()does not.Positive controls still work
I checked that real ingestion paths still taint and gate correctly, so this is not a broken tool gate:
guard.notify_taint_source("web_fetch")taints the session andcheck_tool_call("shell", ...)returns REVIEW.guard.scan("Some RAG chunk content here.", source=Source.RETRIEVED)taints viascan:retrievedand the same side-effect call returns REVIEW.Expected behavior
Output scanning should keep its leakage/redaction protection, but scanning my own model response should not mark the shared session as influenced by untrusted data. Session taint should come from actual untrusted ingestion: retrieved/external content or configured taint-source tools.
One possible direction, not a mandate: separate "scan this text for leakage before it leaves" from "the session has been influenced by untrusted data". Regression coverage would need both cases: benign output leaving the session clean, and real ingestion still tainting. Happy either way if maintainers would rather treat the current behavior as intended CaMeL-style conservatism and document it instead.
Environment: Python 3.13.13, Windows 11, unplug built from
devatcf0b0c7.