Alpha status. The propose → verify → write-back loop, the offline, OpenAI/Codex, and Anthropic backends, and the verifier gate are implemented and running. The concurrency and type/struct roles are also implemented as deterministic, zero-dependency analyzer passes (see Specialized analyzers below). The remaining specialized agent roles (Oracle adjudicator, diff, behavioral verifier) are the intended next steps; the naming crew currently runs a single generic pass that routes through whichever backend is selected.
The core loop: propose → verify → write-back
run_agent_pass in src/warden/agents/crew.py drives one full sweep over a module version.
Gather hard facts
For every defined function in the version,
gather_facts assembles a FunctionFacts
object (the hallucination constraint). Every field is derived mechanically from the binary
and the KB; the backend sees only what is actually in the binary. See FunctionFacts below.Sort bottom-up
Functions are sorted by number of call targets, ascending. Leaf functions run first. As
callees acquire names, the next caller’s context is richer when the backend sees it.
Skip already-confident entries
If a function already has a symbol with
confidence >= 0.5 (the SKIP_CONFIDENCE
constant in crew.py), or if the symbol is locked (human or Oracle), the function is
skipped. This is the mechanism that makes re-running safe: the crew never overwrites
high-confidence or locked work.Backend proposes
The selected backend receives the
FunctionFacts and returns a Proposal (a name,
summary, confidence score, and optional refined type signature), or None to abstain.Verifier gate
verify_proposal runs cheap sanity checks before anything reaches the KB. It rejects
proposals with invalid identifiers, names shorter than two characters, confidence outside
[0, 1], or string-xref claims that aren’t backed by actual referenced strings. See
the verifier gate for detail.Write-back under the economy
Accepted proposals are submitted to
kb.upsert_symbol with provenance="agent". The
KB’s economy decides whether to actually write: an agent proposal may only overwrite a
lower-confidence prior agent entry. Human, Oracle, and higher-confidence agent entries are
never touched. Each write stores the provenance trail and evidence list alongside the
symbol.run_agent_pass returns an AgentRunResult with counters for considered,
proposed, written, rejected_by_verifier, rejected_by_economy, and skipped_existing.
FunctionFacts: the hallucination constraint
referenced_strings does not contain.
referenced_stringsis built by walking the function’si32.constinstructions and looking up each immediate in the module’s data-section string map.call_targetscontains direct call names (with imports resolved to their import names) and<indirect>forcall_indirectsites.type_signatureis the WASM type section entry. It is exact, not guessed.instruction_mnemonicscontains up to all opcodes in the function body; LLM backends truncate to the first 40 when building the user message.
The backends
Offline heuristic backend
The zero-dependency default. Deterministic, no API key, no network. Runs immediately afterpip install -e . with no extras. Applies three heuristics in priority order:
| Priority | Heuristic | Confidence | Trigger |
|---|---|---|---|
| 1 | String xref | 0.45 | referenced_strings is non-empty |
| 2 | Call-neighborhood | 0.30 | Function makes at least one direct call |
| 3 | Placeholder | 0.12 | Neither heuristic fires |
make_backend silently falls back to the offline backend.
OpenAI / Codex backend
Selected automatically whenOPENAI_API_KEY is set and the openai package is installed (pip install -e '.[agents]'). Uses the OpenAI Responses API with structured JSON output so the model returns {name, summary, confidence}.
- Default model:
gpt-5.3-codex(override withWARDEN_OPENAI_MODELorWARDEN_AGENT_MODEL). - Aliases:
--backend openai,--backend codex, and--backend oaiall select this backend. - Reasoning effort: defaults to
mediumand can be changed withWARDEN_OPENAI_REASONING_EFFORT. - System prompt: the same RE prompt used by the Anthropic backend.
- User message: contains only fields from
FunctionFacts: function index, type signature, export status, call targets, referenced strings, raw name hint, and up to 40 opcode mnemonics. - Output: the response is validated and clamped.
nameis run throughslugify;confidenceis clamped to[0.0, 1.0].
Anthropic backend
Selected automatically whenANTHROPIC_API_KEY is set, the anthropic package is installed (pip install -e '.[agents]'), and the OpenAI backend is not available. Uses the Anthropic Messages API with structured JSON output via a JSON schema constraint so the model always returns {name, summary, confidence} and nothing else.
- Default model:
claude-opus-4-8(override with theWARDEN_AGENT_MODELenvironment variable). - System prompt: instructs the model to act as a RE assistant, propose a concise snake_case C-style identifier, write a one-sentence purpose, and emit a calibrated confidence in
[0, 1]. The model is told explicitly to prefer low confidence when evidence is thin and never invent behavior unsupported by the facts. - User message: contains only fields from
FunctionFacts: function index, type signature, export status, call targets, referenced strings, raw name hint, and up to 40 opcode mnemonics. No content outside these facts is sent. - Output: the response is validated and clamped.
nameis run throughslugifyto guarantee a valid identifier;confidenceis clamped to[0.0, 1.0].
Backend selection
make_backend(prefer) in backends.py resolves which backend runs:
| Condition | Backend chosen |
|---|---|
--backend offline | OfflineHeuristicBackend |
--backend openai, --backend codex, or --backend oai | OpenAIBackend (falls back to offline if unavailable) |
--backend anthropic | AnthropicBackend (falls back to offline if unavailable) |
No flag, OPENAI_API_KEY set, openai installed | OpenAIBackend |
No flag, OpenAI unavailable, ANTHROPIC_API_KEY set, anthropic installed | AnthropicBackend |
| No flag, key missing or package absent | OfflineHeuristicBackend |
--backend flag always beats auto-detection.
Running the agent crew
Call-graph strategy
By default,run_agent_pass walks the call graph bottom-up instead of running a single flat sweep. Pass --strategy flat to get the original behavior.
concurrency parameter (default 8) caps how many proposals are in-flight at once within a single layer. Set it programmatically via run_agent_pass(..., concurrency=N).
How the call-graph walk works
Build the call graph
build_call_graph(module) in warden.analysis.callgraph constructs a CallGraph with
direct and indirect edges for every defined function. Direct call and return_call
instructions are exact. call_indirect and return_call_indirect instructions carry only a
type index at the static level, so their targets are over-approximated: every defined
function in the module’s element table whose type matches the call’s type index is included as
a potential callee. The resulting graph is a conservative static skeleton.Condense recursion into layers
strongly_connected_components (iterative Tarjan) groups mutually recursive functions into
SCCs. layered_schedule then condenses the SCC graph into a DAG and assigns a depth to each
component: layer 0 holds leaves, and every later layer holds functions whose defined callees
all appear in earlier layers. Mutual recursion lands in the same layer and is treated as
a single unit. All traversals are sorted, so the schedule is deterministic.Route to specialists
Before processing any layer,
_specialist_notes runs the concurrency and struct analyzers
(the same passes that warden analyze runs). Their findings are written to the KB and also
routed into per-function hint lists:- Atomic sites from the concurrency analyzer produce notes such as
"atomic i32.atomic.rmw.add at offset 8; likely a synchronization primitive". - Struct layouts from the struct analyzer produce notes describing which field offsets the function accesses through a base pointer.
FunctionFacts.notes so the backend sees them when proposing a name.Enrich each function with callee names
When a function is about to be processed,
_enrich looks up the KB names of all its direct
defined callees and attaches them as FunctionFacts.callee_names. Because layers are
processed bottom-up, the callees have already been named (or skipped) before the caller is
reached. A backend that sees callee_names=["parse_header", "validate_checksum"] has far
richer context than one that sees only raw opcodes.Propose each layer concurrently
All functions in a layer are independent (no intra-layer edges by construction), so their
proposals can safely run in parallel.
_propose_concurrently uses asyncio.gather with a
semaphore capped at concurrency. Backends that block (every current backend) are dispatched
via asyncio.to_thread so the event loop stays responsive. A single-function layer skips the
async path entirely and calls backend.propose directly.Write back under the economy
Proposals from each layer go through the same
verify_proposal gate and kb.upsert_symbol
call as the flat pass. Because functions in the same layer cannot be each other’s callees,
concurrent branches in one layer never share a callee that is being written at the same time.
The KB’s provenance/confidence economy rejects any write that would overwrite a higher-confidence
or locked entry, so concurrent branches are safe.FunctionFacts fields added by the call-graph strategy
The call-graph strategy attaches two fields that the flat pass leaves empty:| Field | Type | Source |
|---|---|---|
callee_names | list[str] | KB names of defined callees, looked up after each prior layer is written |
notes | list[str] | Per-function hints from the concurrency and struct analyzers |
FunctionFacts dataclass and are forwarded to the backend as additional context in the user message.
When to use each strategy
Use--strategy call-graph (the default) for any module where naming quality matters. The bottom-up order means callers are named in light of what their callees do, which is the main quality improvement over a flat pass.
Use --strategy flat when you want a quick, fully sequential sweep, for example in CI environments where deterministic single-threaded output is easier to diff, or when debugging the backend in isolation.
The verifier gate
verify_proposal(proposal, facts) in crew.py sits between the backend’s output and the KB. It returns (accepted, reason).
Currently it performs cheap structural checks:
- The name must match
^[A-Za-z_][A-Za-z0-9_]*$(valid C identifier). - The name must be at least two characters.
- Confidence must be in
[0.0, 1.0]. - A summary that claims string evidence must be backed by non-empty
facts.referenced_strings.
wasm2c, where a lifted C reconstruction is recompiled and executed against the original WASM under a fuzzer corpus. warden verify <wasm> reports whether the current environment has the toolchain needed to activate it.
The behavioral verifier (wasm2c differential re-execution) is scaffolded but not yet active.
The
verify_proposal call site is where it plugs in when a C toolchain is available.The provenance/confidence economy
Every write to the KB carries three fields that together make re-running the entire crew safe. The full economy is explained in core concepts; here is how the agent crew interacts with it.provenanceis set to"agent"for every crew write. This places agent output at the lowest authority tier, below human, Oracle, export, and string-xref entries.confidenceis the calibrated score returned by the backend. The offline backend emits 0.45, 0.30, or 0.12 depending on which heuristic fired. LLM backends are instructed to self-calibrate and their output is clamped to[0.0, 1.0].lockedis never set by the crew. Onlywarden set-name(human writes) setslocked=True, which makes an entry immutable to every automated actor.
- Before the backend is called: entries at
confidence >= 0.5or markedlockedare skipped. The thresholdSKIP_CONFIDENCE = 0.5is the boundary between “confident enough to leave alone” and “fair game.” - After the verifier passes:
kb.upsert_symbolenforces that an agent proposal may only land if no higher-confidence agent entry (or any higher-authority entry) already exists. The result is counted asrejected_by_economyand no write happens.
warden agent on a module that already has Oracle matches and a prior agent pass at confidence 0.45 will re-propose only the functions that are still below threshold, and only overwrite those where the new proposal is stronger.
Specialized analyzers
Beyond the naming crew, WARDEN ships two deterministic analyzers that populate first-class KB facts with no LLM and no API key. They cover the concurrency and type/struct roles from the intended crew architecture and run together under a single command:Concurrency analyzer
warden.analysis.concurrency.analyze_concurrency(module, kb, version_id) recovers the thread
model from three byte-level fossils that survive Emscripten stripping:
| Signal | What it means |
|---|---|
| Shared memory flag | The WASM limits field has the shared bit set (atomics require it) |
Atomic opcodes (0xFE family) | Every rmw, cmpxchg, wait, notify, or fence instruction is an atomic site |
| pthread-named imports/exports | pthread_*, emscripten_thread*, _emscripten_proxy*, atomic-tagged helpers surviving in the symbol table |
ConcurrencyReport with .shared_memory, .atomic_sites, .pthread_markers,
and .facts. When a KB and version ID are supplied, each atomic site is written to the
thread_model table via kb.add_thread_fact as kind='atomic', with the memarg offset as the
best-effort “guarded data” pointer and a confidence of 0.6. This is high enough to be a fact but below the
human/Oracle tier, because the exact guarded data is a best-effort guess.
A module is considered multithreaded when any of the three signals is present. Shared memory or
atomic opcodes are conclusive; pthread-named symbols are a weaker hint (a module may import them
without actually spawning threads), but they are still recorded.
Struct-layout analyzer
warden.analysis.structs.analyze_structs(module, kb, version_id) reconstructs candidate struct
shapes from memory-access patterns. Emscripten compiles a C struct field access into a recognizable
two-instruction sequence:
(base local, offset) pair is one candidate field; multiple accesses to the same offset are
deduped. Fields are grouped by base local into a StructLayout named
<func>_arg<N>_t, and the recovered fields are sorted by offset so the output is deterministic.
Each StructLayout has a .name, .fields (a list of StructField(offset, size, type, name)),
and .source_function. When a KB and version ID are supplied, every layout is persisted via
kb.upsert_struct at provenance agent, confidence 0.5, so the recovered shapes become
queryable KB facts and carry forward on the next ingest.
Running both passes
warden analyze <label> runs both analyzers in sequence and prints a summary:
warden agent run sees
thread_model and structs entries when assembling FunctionFacts) and to the HTML report
generator.
Intended crew architecture
The current implementation runs a single generic naming pass. The target architecture from the design is a crew of specialized agents, each owning a distinct domain:Oracle agent
Oracle agent
Adjudicates fuzzy Oracle matches and attaches upstream Emscripten/musl source links to matched symbols.
Concurrency agent
Concurrency agent
Owns atomic/lock/TLS analysis; labels lock-to-guarded-data relationships and worker entry points discovered via dynCall/elem tables. Implemented as a deterministic pass in
warden.analysis.concurrency. warden analyze runs it and persists findings to the thread_model table.Type/struct agent
Type/struct agent
Reconstructs struct layouts from memory access patterns and propagates types to callers. Implemented as a deterministic pass in
warden.analysis.structs. warden analyze runs it and persists findings to the structs table.Naming/summarization agent
Naming/summarization agent
Proposes human-readable names and writes pseudocode summaries. This is what the current implementation does.
Diff agent
Diff agent
On each new version, explains modified functions and writes the semantic changelog.
Verifier agent
Verifier agent
Builds differential test harnesses and triages mismatches between the WASM and its reconstruction.
warden mcp, so any MCP-capable model can drive the loop. The current warden agent command is the starting spine for this architecture.
Relation to other pipeline stages
- Oracle identification runs before the agent crew and pre-populates the KB with high-confidence names for runtime/libc functions. In a real Emscripten module, 40–80% of functions may already be named by the time the crew runs. Those are skipped, so the crew concentrates effort on the application-specific remainder.
- Diff carry-over runs on ingest of a new version and ports annotations from the previous version. After carry-over, only genuinely changed or new functions are below threshold, so the crew only touches what actually needs attention.
warden demoruns the full pipeline end-to-end offline and shows all three stages feeding each other (Oracle → agent (offline) → diff carry-over), with no API key.