Static-Analysis Confidence Gap · 3 of 10L3data science
The Agent-Framework Modeling Gap
The most fixable reason a scanner misses an agent bug is not the model — it is the mundane framework glue between source and sink that the analyzer was never taught to trace.
Abstract
Static analysis finds a security flaw by connecting an untrusted source to a dangerous sink through a chain of edges it can reconstruct from code. This piece argues that in agent software the chain is most often broken not at the model, which is genuinely opaque, but at ordinary framework glue — dynamic dispatch, tool registries, callbacks, configuration-driven wiring, serialization boundaries, and reflection — where a real edge exists in the program but is absent from the analyzer's reconstructed call graph. The contribution is a taxonomy of six severance patterns, a worked account of how a declarative flow model reconnects a severed path, and the argument that agent frameworks are unusually severing precisely because they are built out of these patterns. The practical consequence is that a large share of missed detections is a modeling gap that can be closed deliberately, and that the residual — the part no model can reach — is the true boundary where static analysis stops and other assurance must begin.
When a scanner comes back clean on an agent, the intuitive explanation is that the model in the middle is a black box, and there is truth in that. But it hides a more mundane and more actionable cause. Long before the data reaches the model, and again after the model emits an action, it travels through framework code — a decorator that registers a tool, a registry that looks the tool up by name, a callback that fires it, a queue that ships a message to another process. Each of these is ordinary software the analyzer could in principle follow, and each is a place where, in practice, it silently loses the thread. The path from a poisoned input to a dangerous call is right there in the program; it is just not in the graph the analyzer built. This piece is about that gap — why framework glue severs the trace, how a small declarative model reconnects it, and why agent frameworks make the problem acute.
The break is in the glue, not only the model
A source is a program location where untrusted data enters; a sink is a location where data reaching it can cause harm; and a finding is a path the analyzer can trace from one to the other. The whole method rests on the analyzer's ability to reconstruct that path from the code, edge by edge. When it reports nothing, the honest question is whether no path exists or whether a real path was simply not in the reconstruction — and for agents the second case is dominant and, unlike the model, fixable.
It is tempting to attribute every agent miss to the language model in the middle, which static analysis genuinely cannot see through. But a great deal of the severance happens in plain, traceable code on either side of the model. A tool is registered under a string name by a decorator; later the agent core looks that name up in a registry and invokes whatever it finds. Both halves are ordinary Python or TypeScript the analyzer reads fine — yet the edge that connects them runs through a lookup keyed on a runtime value, and that is exactly the kind of edge a static call graph tends to drop.
The distinction matters because it changes what a clean report means and what to do about it. If the miss is the model, no scanner improvement recovers it and the risk must move to runtime controls. If the miss is glue, it is a modeling gap: the analyzer can be taught the framework's shape and the path reappears. Separating these two is the difference between accepting a blind spot and closing it, and most teams never make the separation because a clean report looks identical in both cases.
How an analyzer connects source to sink
To see why glue breaks the trace, it helps to state how the trace is built. An analyzer constructs a call graph — a map of which functions can call which — and a data-flow graph that follows values through assignments, arguments, and returns. Taint tracking then asks whether any path in these graphs carries a value from a source to a sink. Where the analyzer cannot see inside a called function, it relies on a summary: a compact statement that says, for this function, input flows to output, or this argument reaches a dangerous operation. These summaries are what let analysis scale without re-deriving the world at every call.
Two of these mechanisms are where framework glue does its damage. The call graph must resolve which concrete function a call site invokes; when the target is chosen at runtime — a virtual method, a function fetched from a dictionary, a handler selected by a string — the analyzer must either prove the target set or conservatively guess, and a guess that is too narrow drops the real edge while a guess that is too wide drowns the result in noise. And a summary only exists for a function the analyzer has a model of; an unmodeled framework call is a wall the value disappears into.
None of this is a defect unique to one tool. Deciding the exact target of a dynamic call in general is undecidable, so every analyzer approximates the call graph, and every analyzer ships models for the libraries its authors had time to write. The modeling gap is the predictable shadow of those two necessities: the places where the approximate call graph and the finite set of models fail to cover a path the program actually has.
Six patterns that sever the trace
The ways framework glue breaks a path are not infinite; they fall into a small number of recognizable patterns, and naming them turns a vague sense of 'the tool missed it' into a checklist of where to look. Each pattern shares one feature: a real control- or data-flow edge exists, but it is realized through an indirection the static reconstruction does not follow.
The first is dynamic dispatch: a call whose concrete target is a runtime value — a virtual method on an unknown subtype, or a function stored in a variable. The second is registry lookup: a tool or handler registered under a name and later fetched from a map and invoked, so the caller-to-callee edge exists only through a string key. The third is callbacks and middleware: higher-order functions where the flow passes through a function handed in as data, common in request pipelines and event handlers. The fourth is configuration-driven wiring: decorators, dependency injection, or a manifest that connects components at load time, so the wiring lives in configuration rather than in a call the analyzer reads.
The fifth is the serialization or process boundary: a value written to a queue, a database, or another process and read back elsewhere, where the flow crosses a gap no in-memory graph spans. The sixth is reflection and generated code: targets constructed from strings, or code produced at build or deploy time that the analyzer never saw. The table gathers these with the mechanism that breaks the edge and the kind of model that restores it, and the remaining sections walk the most agent-relevant one in depth.
| Pattern | Mechanism that breaks the edge | Model that restores it |
|---|---|---|
| Dynamic dispatch | Target is a runtime value | Type narrowing or a call summary |
| Registry lookup | Callee fetched by string key | A flow step from register to invoke |
| Callbacks / middleware | Flow passes through passed-in function | Higher-order flow summary |
| Config wiring | Components joined at load time | Model the decorator or injector |
| Serialization gap | Value crosses queue or process | A source/sink pair at the boundary |
| Reflection / codegen | Target built from strings or generated | Model the factory; analyze generated code |
A worked case: the tool registry
The registry pattern is worth walking because it is the beating heart of most agent frameworks and it severs the exact path that matters. Consider a support agent whose tools are registered by name and dispatched from the model's chosen action. In sanitized pseudocode, a decorator adds a function to a registry under a string, and the agent core, given a name the model produced, looks it up and calls it. Read top to bottom, a human sees the connection instantly. The analyzer does not, because the call site invokes whatever the registry returns for a value it cannot pin down.
The consequence is precise. The taint from a poisoned ticket flows into the model step and out as a tool name and arguments; the arguments then flow into send_email through the registry dispatch. The first leg is the opaque model, which no scanner traces. But the second leg — arguments reaching send_email — is ordinary code, and it is dropped only because the dispatch edge is keyed on a runtime string. A tool that modeled the registry would reconnect that leg and surface the argument-to-sink flow even though the model in the middle stays dark.
This is the crux of the whole gap. Some of what a scanner misses on an agent is genuinely beyond static analysis, and some is a registry edge one declaration away from being visible. Treating them the same — shrugging at the clean report because 'agents are hard' — leaves recoverable findings on the floor. The registry case is the emblem of the recoverable kind.
registry = {}
def tool(name): # decorator: register by string
def wrap(fn): registry[name] = fn; return fn
return wrap
@tool("send_email")
def send_email(to, body): ... # the consequential sink
def run(action): # action came from the model
fn = registry[action.name] # callee chosen by runtime string
return fn(**action.args) # <-- edge to send_email dropped hereClosing the gap with declarative flow models
The remedy for a severed edge is not a better guess at the call graph; it is a model — a declarative statement, external to the analyzed code, that tells the analyzer how a framework moves data. In the general form used by modern analyzers, a model is a small row of data: it names a function and declares that an input reaches an output, that an argument is a sink of a given kind, or that a value flowing in becomes a source. These declarations are added without changing the target program and are consumed by the same taint engine that handles built-in libraries.
For the registry, the model is a flow step that says: a value passed to a registered function reaches the function of that name. For a serialization boundary, it is a paired declaration that the write is a sink-like exit and the matching read is a source-like entry, stitching the two sides into one flow. For a callback-based pipeline, it is a higher-order summary that carries taint through the handler. In each case a single, auditable row restores a path the reconstruction dropped, and the analyzer's recall on that pattern rises without touching its queries.
The important property is that these models are data, not code, so they can be reviewed, versioned, and shared as the artifact that encodes a team's knowledge of its own framework. That also makes their coverage measurable: one can count how many of a framework's flow-bearing constructs are modeled and treat the uncovered remainder as known blind spots rather than silent ones. The next sections argue why agents need this more than most software, and what to do with the part no model can reach.
Why agent frameworks are unusually severing
Ordinary applications contain some dynamic dispatch and some configuration wiring, but agent frameworks are built almost entirely out of the severance patterns. Their whole purpose is to let a model choose, at runtime, which tool to call with which arguments, so dynamic dispatch and registry lookup are not incidental — they are the architecture. Tools are typically declared with decorators, discovered through a registry, invoked through a generic dispatcher, and increasingly reached across a protocol boundary to a separate tool server, which adds a serialization gap on top of the dispatch.
The effect compounds. A single agentic flow can pass through a decorator that the analyzer must model to see the registration, a registry lookup it must model to see the dispatch, and a message boundary it must model to see the cross-process hop — three severances stacked on one path. Any one unmodeled link breaks the whole trace, so the probability that a stock analyzer follows an end-to-end agent flow with no custom models is low, not because the flow is exotic but because it threads several blind spots in series.
This reframes the pessimism about scanning agents. The frequent conclusion that static analysis simply does not work on agents conflates the genuinely opaque model with the merely unmodeled glue. The glue is the larger share and it is addressable; the frameworks are severing by design, but the design is made of patterns that declarative models are built to handle. The pessimism is warranted only for the true residual, which the next section isolates.
Measuring what is glue and what is truly opaque
To act on the gap rather than lament it, a team needs to know, for a missed flow, which of three things happened: the code was never extracted into the analyzer's model at all, the code was extracted but a framework construct on the path was not modeled, or the path was fully modeled and the miss lies elsewhere. These are distinguishable with modest effort — extraction is reported by the tool, and modeling coverage can be assessed by checking whether each flow-bearing construct on a known path has a model — and the distinction is what converts a vague blind spot into a work item.
The construct to track is modeling coverage: of the framework mechanisms that carry data on the paths you care about, what fraction does the analyzer model? A low coverage number is good news disguised as bad, because it means recoverable recall is being left on the table and points exactly at which decorators, registries, and boundaries to declare. Only once coverage is high does an unresolved miss become evidence of the genuine residual — the model step itself, where the transformation from data to instruction lives.
That residual is where this whole line of reasoning lands. After the glue is modeled, what remains unreachable is the opaque model boundary and the runtime choice of action, and those are not modeling gaps to be closed but structural limits to be respected. The value of measuring is that it draws the line honestly: here is the recoverable region a model recovers, and here is the boundary beyond which threat modeling, runtime brokering, and adversarial testing must carry the assurance.
What to do with the gap
The operational takeaway is to treat framework modeling as a first-class part of configuring static analysis for an agent, not an afterthought. Before trusting a clean report, inventory the framework constructs on the paths that matter — the decorators that register tools, the registry that dispatches them, the callbacks in the request pipeline, the boundaries to any external tool server — and confirm each has a model. The declarations are small, auditable rows of data, and they encode exactly the knowledge of your own system that a generic tool cannot have.
Doing this changes what a subsequent clean report proves. With the glue modeled, silence on a class carries real weight, because the path would have been visible had the flaw been present. Without it, silence is nearly meaningless, and the earlier discipline of naming what an empty report can and cannot support applies with full force. The modeling work is what moves a flow from the invisible column into the searched column, which is the only column a clean report is entitled to speak about.
The honest close is that modeling narrows the gap but does not erase it. Some of an agent's most important behavior — the model turning content into an instruction, the runtime selection of a tool — sits past every model, and pretending otherwise trades one false comfort for another. The right posture is neither to dismiss static analysis on agents nor to over-trust it, but to model the glue aggressively, measure what remains, and assign that residual to the controls built for it.
Key takeaways
- A scanner reports a flaw only if it can reconstruct a path from source to sink; on agents the path most often breaks at framework glue, not at the model.
- Six severance patterns drop a real edge: dynamic dispatch, registry lookup, callbacks, configuration wiring, serialization boundaries, and reflection or generated code.
- The tool-registry pattern severs the exact agentic path — the argument-to-sink leg is ordinary code, dropped only because dispatch is keyed on a runtime string.
- A declarative flow model — a small auditable row of data — reconnects a severed edge without changing the program or the queries, raising recall on that pattern.
- Agent frameworks are unusually severing because they are built out of these patterns and stack several on one path, so a single unmodeled link hides the whole flow.
- Triage every miss into not-extracted, not-modeled, or genuinely opaque; only the last is beyond static analysis, and it marks where other assurance must take over.
Practitioner Toolkit
Copy-paste, strictly defensive artifacts you can use today. Nothing here attacks a real system.
Walk each path that matters and confirm the analyzer can actually traverse its framework constructs.
- List the decorators or registration calls that add tools or handlers on this path.
- Identify the registry or dispatcher that selects a callee by a runtime name, and confirm it is modeled.
- Find callbacks and middleware the flow passes through; confirm a higher-order summary carries taint.
- Locate any serialization or process boundary (queue, database, external tool server) and confirm a source/sink pair spans it.
- Flag reflection or generated code on the path; model the factory or add the generated code to analysis.
- For each construct with no model, record it as a known blind spot — not a clean result.
Vendor-neutral shape of the data rows that reconnect a severed edge; adapt to your analyzer's model format.
# summary: input reaches output through a framework call
framework.registry.invoke ; input=Argument ; output=ReturnValue ; kind=taint
# sink: an argument is a consequential operation
framework.tools.send_email ; input=Argument[body] ; kind=external-effect
# boundary pair: a value crossing a queue re-enters as a source
framework.queue.put ; input=Argument ; kind=sink-exit
framework.queue.get ; output=ReturnValue ; kind=source-entryThe shortest path to a clean report that actually means something on an agent.
- Model the tool registry first — it severs the exact argument-to-sink leg that matters.
- Add a source/sink pair for each external tool-server boundary the agent crosses.
- Re-run and confirm a known-vulnerable path now surfaces; if not, the glue is still unmodeled.
- Track modeling coverage and treat every uncovered construct as an explicit blind spot.
Glossary
- Call graph
- A map of which functions can call which; the analyzer must resolve a call site's concrete targets to trace flow through it.
- Data-flow graph
- A representation that follows values through assignments, arguments, and returns so taint can be tracked from a source to a sink.
- Flow summary
- A compact statement of how a function moves data — input to output, or argument to a sink — that lets analysis scale without re-deriving the callee.
- Dynamic dispatch
- A call whose concrete target is decided at runtime, such as a virtual method or a function fetched from a map, which an approximate call graph may drop.
- Registry lookup
- Registering a handler under a name and later fetching and invoking it by that name, so the caller-to-callee edge exists only through a string key.
- Declarative flow model
- An external, data-form statement telling the analyzer how a framework moves data, consumed by the same taint engine as built-in library models.
- Modeling coverage
- The fraction of flow-bearing framework constructs on the paths of interest that the analyzer has a model for; the uncovered remainder is a known blind spot.
References
- Livshits & Lam, Finding Security Vulnerabilities in Java Applications with Static Analysis (USENIX Security 2005)
- Cousot & Cousot, Abstract Interpretation: A Unified Lattice Model for Static Analysis of Programs (POPL 1977)
- Rice, Classes of Recursively Enumerable Sets and Their Decision Problems (Trans. AMS, 1953)
- MITRE Common Weakness Enumeration (CWE)
- NIST SP 800-218, Secure Software Development Framework (SSDF) v1.1
- OWASP Agentic AI — Threats and Mitigations