Repository navigation
Releases: sushant-me/agentbound
Release list
v0.1.15 — precision on real code: 0.79 → 1.00
The last two false positives from the 14-framework survey, and the measurement that matters.
ci-agent-missing-author-association was reported CRITICAL against pydantic/pydantic-ai. The rule splits an if: on || and asks whether the arm carries the check. pydantic-ai wraps every arm in one group and chains the gate onto the group — (A || B) && gate — so no arm segment contains it. Two things had to be right and the first attempt got both wrong: it depended on _if_block_span, which returns None for that file, and it stopped at the first enclosing ) — the arm's own wrapper.
guard-name-normalization-asymmetry was reported MEDIUM against microsoft/agent-framework. _guarded_names scanned back 600 characters from any .strip() and accepted a set(...) in that window. In _skills.py it paired seen_names: set[str] = set() — a dedup accumulator — with a stray .strip(), and called a duplicate check a security guard.
| first run | v0.1.14 | v0.1.15 | |
|---|---|---|---|
| production findings | 19 | 17 | 15 |
| verified true | 15 | 15 | 15 |
| verified false | 4 | 2 | 0 |
| precision | 0.79 | 0.88 | 1.00 |
| tests | 129 | 139 | 140 |
No true positive lost at any step. google/adk-python still reports its 3 including the real gate, langchain still its 2, adk-go/adk-java unchanged. That is the check that these fixes narrowed the rules rather than silenced them.
v0.1.14 — two confirmation-gate false positives, two causes
Found by surveying 14 real agent frameworks and opening every finding in the source. Both confirmation-gate-fails-open false positives came from the same defect class as v0.1.13: a rule reading locally, with no requirement that the things it correlates belong together.
Cause 1 — cross-function correlation. The three patterns (x = inspect.signature(y), valid = x.parameters, a comprehension filtering on valid) were matched anywhere in the file. In microsoft/agent-framework a schema generator called inspect.signature(cls) and an unrelated comprehension filtered a set called valid_params. Fixed with a proximity requirement: _GATE_WINDOW = 1200 characters. The reference true positive spans ~110.
Cause 2 — a regex anchored on a string literal. _DICT_FILTER_USING_RE began \{[^{}]*?\bfor\b, which matched the { inside startswith("{") and spanned thirteen lines to an unrelated for field in common_fields: / if field in params: loop. A loop with a membership test is not a dict comprehension.
| before | after | |
|---|---|---|
| microsoft/agent-framework | 3 | 1 |
| total, 14-framework survey | 19 | 17 |
| google/adk-python | 3 | 3 (unchanged — the gate is still found) |
| tests | 137 | 139 |
Two regression tests carry the real shapes.
v0.1.13 — precision on real code: 21 findings, 3 real
The tool-reserved-name-shadowing rule read any constant containing "tool" and "name" as a reserved-name set. Run against google/adk-python that produced 21 production findings, 19 of them false positives — every one a DEFAULT_*_TOOL_NAME_PREFIX, FINISH_TASK_TOOL_NAME, _LIST_SKILLS_TOOL_NAME or RESERVED_TOOL_CALL_ERROR_TYPE.
The bar is now the word RESERVED. _RESERVED_TOOL_NAMES = frozenset({...}) is unaffected.
| repo | before | after |
|---|---|---|
| google/adk-python | 21 | 3 |
| google/adk-go | 2 | 2 |
| google/adk-java | 8 | 8 |
| openai/openai-agents-python | 0 | 0 |
Precision on adk-python production code: 0.14 → 1.00, no true positive lost. 133 tests pass; four regression tests carry the real constant names.
v0.1.12 — security fix: masking could hide findings
Security fix: masking hid real findings
The masker blanked every string that was the entire right-hand side of an assignment, on the reasoning that a constant holds documentation. A one-line constant is also how real code carries a message, and blanking it deleted the exact text a rule matches on:
MSG = "duplicate tool name, overwriting" -> MSG =
logging.warning(MSG)With the message gone, tool-dict-last-wins reported nothing for a duplicate registration, so a later tool silently shadowing an earlier one went unflagged. That is a false negative — which this module's own docstring calls worse than the false positive the masking was written to remove — and it broke the stated property that masking "never hides executable behaviour". A string bound to a name is data the program evaluates, not prose.
Only a multi-line assigned value is masked now, because that is what a documentation block actually looks like. The false positive that motivated the masking — a triple-quoted DOCSTRING constant holding an example — is still suppressed, and its existing test still passes unchanged.
Second, the rule required the word to sit literally inside .warning(...), so even with the constant intact an extracted message was invisible. A warning whose argument is a module-level string constant containing duplicate is now recognised — the same warning, after an ordinary refactor.
Two regression tests. 118 tests pass, and every CLI contract step holds: exit 1 on findings, the --fail-on threshold in all three directions, exit 2 on an unreadable path, a single file scanned rather than ignored, and the self-scan still clean at exit 0.
v0.1.11 — report the version this build actually is
The fix
agentbound --version printed 0.1.9 on the v0.1.10 release. pyproject.toml
carried the bump and agentbound/__init__.py did not, and nothing compared them —
so the CLI, the tag, the release name and pip show agentbound disagreed about
which build was installed.
That consumer exists: tool-boundary-corpus
pins this project by release and asserts the scores measured from that exact
build. A detector that misreports its own version makes that pin unverifiable.
The v0.1.10 artifact is immutable, so this is a new release rather than an edit.
The guard is a test comparing agentbound.__version__ to pyproject.toml — a
second version string no test reads is a second version string that drifts again.
Also in this release
Two tests imported scripts/recall_audit by module name, which resolves only when
the repository root is on sys.path. python -m pytest puts it there and a bare
pytest does not, so the suite was green in CI and red on a clean checkout for
the life of the file. They load it by path now, a test pins that mechanism, and CI
runs both invocations.
116 tests pass under both, up from 114.
v0.1.10 — a string bound to a name is documentation too
Reported by tool-boundary-corpus, a labelled corpus that measures this detector's precision.
The false positive. Case code-pattern-only-in-comments is a file where the vulnerable pattern appears only inside a module constant:
DOCSTRING = """
if tool.name in self.tools_dict:
logger.warning("duplicate")
self.tools_dict[tool.name] = tool
"""The scanner reported tool-dict-last-wins at the line inside that string. Untaken string content is no more executable than a comment. Precision on the corpus was 0.750; every other case passed.
The fix. masking.py already treated a bare string statement (a docstring) as prose, and deliberately kept string arguments visible because tool-dict-last-wins reads the logging message as evidence. A string assigned to a name fell between the two. The prose classifier now also covers a string that is the entire right-hand side of an assignment. Strings inside a literal stay visible — that is what the reserved-set rules read — and a logging message is still an argument and still evidence.
Measured after the change
| check | before | after |
|---|---|---|
| precision on tool-boundary-corpus | 0.750 | 1.000 |
| recall on tool-boundary-corpus | 1.000 | 1.000 |
agentbound scan agentbound (the CI contract) |
exit 0 | exit 0 |
| repository-root scan (fixtures on purpose) | 25 findings | 16 |
| tests | 114 | 114 (3 added) |
agentbound v0.1.9 — GitHub Action, and CI-agent rules rebuilt against real code
Static detector for AI-agent tool-boundary bugs. Eight rules across Python,
TypeScript/JavaScript, Go, Java and GitHub Actions.
Install
pip install "agentbound @ git+https://github.com/sushant-me/agentbound@v0.1.9"
agentbound scan /path/to/repo --fail-on highOr as a GitHub Action:
- uses: sushant-me/agentbound@v1
with:
fail-on: highRules
| rule | severity | detects |
|---|---|---|
tool-reserved-name-shadowing |
high | a reserved-name set omits a framework-owned tool the framework registers |
confirmation-gate-fails-open |
high | inspect.signature(predicate) filters args, so a generic predicate returns False and the gate opens |
ci-agent-untrusted-issue-content |
high | a workflow runs an agent and consumes issue/comment text |
ci-agent-write-scope-on-untrusted-trigger |
varies | an agent runs on an untrusted trigger and holds a write scope it can actually exercise |
ci-agent-missing-author-association |
critical | an issues-triggered arm with no author gate, while other arms have one |
tool-dict-last-wins |
medium | a tool dict assigned unconditionally, duplicates only logged |
tool-built-in-silent-replace |
high | a tool registers after a duplicate throw gated on !isInModelTool(...) |
tool-inmodel-name-unoccupied |
high | an in-model tool never registers its name, so a server tool shadows it |
What is new in this line of work
The two CI-agent rules were rebuilt against real code rather than fixtures, and
every change came from a measurement that disagreed with them:
- Validated against a published CVE.
CVE-2026-44246(nnU-Net, CVSS 7.2,
agentic workflow injection) fires on its own vector, and the rule separates the
CVE's fix commit from the commit that actually closed it — they are two
different revisions, and the first still reports. - The precondition is per job, not per file. A read-only job that gathers
with the agent beside apublishjob holdingissues: writeis the
recommended architecture; half of all high-severity findings across a 197-file
corpus were that one mistake. - Severity follows the agent's tool patterns, not the permissions block.
Bash(gh issue edit:*)reaches any issue;Bash(gh issue edit 1234:*)reaches
one. Two workflows can grant an identicalissues: writeand differ completely
in what the agent may point it at. - A named or empty opt-out is not an opt-out.
allow-users: "someone"and
allowed_non_write_users: ""both leave an arbitrary author unable to trigger
the run; only a*or an event-computed value reaches everyone.
CLI contract
Exit 0 clean, 1 when a finding meets --fail-on, 2 when the path does not
exist. --fail-on defaults to low (any finding, the original behaviour); the
Action defaults to high, because this tool reports low and medium findings
deliberately for the residual cases where the architecture is already right.
Verification
87 tests, green on Python 3.9 / 3.11 / 3.13, every rule mutation-tested against
its own failure. The Action is tested end-to-end in CI through the interface a
user gets, not just as a Python package.