Skip to content

Releases: sushant-me/agentbound

v0.1.15 — precision on real code: 0.79 → 1.00

Choose a tag to compare

@sushant-me sushant-me released this 28 Sep 14:40

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

Choose a tag to compare

@sushant-me sushant-me released this 28 Sep 14:35

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

Choose a tag to compare

@sushant-me sushant-me released this 28 Sep 13:36

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

Choose a tag to compare

@sushant-me sushant-me released this 20 Sep 20:22

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

Choose a tag to compare

@sushant-me sushant-me released this 19 Sep 19:45

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

Choose a tag to compare

@sushant-me sushant-me released this 19 Sep 19:35

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

Choose a tag to compare

@sushant-me sushant-me released this 16 Sep 22:03

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 high

Or as a GitHub Action:

- uses: sushant-me/agentbound@v1
  with:
    fail-on: high

Rules

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 a publish job holding issues: write is 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 identical issues: write and 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.