{# QA-047 (2026-06-02) — Recon findings panel. Inserted on the Overview tab between the KPI strip and the Findings-by-severity / ASI radar two-column row. Renders the recon-derived view-model assembled by ``dashboard_view.py``'s ``_assemble_recon_summary`` from the latest ``record_type=fingerprint`` line in ``memory.jsonl`` (plus any ``agent_skipped`` rows). Payload (``recon_summary``): - has_data — render-disabled flag (False ⇒ pending) - framework_family — e.g. ``"langchain"``, ``"Unknown"`` - target_model / target_ref — adapter ref (URL / module path / id) - target_mode — ``"prompt"|"code"|"http"|"framework"`` - has_tools / tool_count / tool_sample / discovered_tools (list) - has_memory / memory_keys (list) - is_multi_agent / touches_pii - inferred_goal / domain - sensitive_actions / declared_guardrails - profile_source / profile_confidence - skipped_agents — list[{agent, asi, reason}] When ``has_data`` is False the panel renders a single pending line ("Recon agent has not finished its sweep yet.") so the operator isn't staring at empty
s during the first few seconds of a scan. #}

RECON

Recon findings about this agent

{# QA-049 (2026-06-02) — the verbose subtitle (`What the recon agent learned… profile source X · confidence Y`) was dropped: the same provenance data is already surfaced in the panel rows below, so the subtitle was duplicating information without adding signal. The pending-state prose stays — the operator otherwise stares at an empty panel for the first few seconds of a scan. #} {% if not recon_summary.has_data %}

Recon agent has not finished its sweep yet. Capability fingerprint, declared tools, and skipped-agent reasons will populate here as soon as the recon record is written.

{% endif %}
{% if recon_summary.has_data %}
{# Framework family — the LLM scaffold / agent framework the recon agent detected. QA-057 (2026-06-03) — only render the row when the recon agent confirmed a framework (e.g. LangGraph / OpenAI Agents). Plain HTTP targets expose no framework hint, so `framework_family == "Unknown"` (the honest default from `_assemble_recon_summary`) is noise: the row was reading as a known signal when it was actually "no signal", which routinely confused operators. #} {% if recon_summary.framework_family and recon_summary.framework_family not in ("Unknown", "—") %}
Framework family
{{ recon_summary.framework_family }}
{% endif %} {# Endpoint — mode + adapter ref. Mode is the transport (prompt / code / http / framework); ref is the resolvable pointer (URL / module path / prompt id). QA-049 (2026-06-02) — the transport badge (`HTTP` / `PROMPT` / `CODE` / `FRAMEWORK`) and the ref are wrapped in a single inline `exec-recon__target-cell` so they cling together as one logical value rather than reading as two unrelated chips. Row shape now matches FRAMEWORK FAMILY / INFERRED GOAL — label + single value cell. QA-056 (2026-06-03) — label relabelled from "Target" to "Endpoint" (one-word). The HTTP-mode badge keeps clinging to the URL inside the same target-cell wrapper. #}
Endpoint
{{ recon_summary.target_mode }} {{ recon_summary.target_ref }}
{# Inferred goal + domain — the recon agent's read on what the agent is for. Drives the goal-relevant attack scenarios. #} {% if recon_summary.inferred_goal or recon_summary.domain %}
Inferred goal
{% if recon_summary.domain %} {{ recon_summary.domain }} {% endif %} {% if recon_summary.inferred_goal %} {{ recon_summary.inferred_goal }} {% endif %}
{% endif %} {# Capabilities — three boolean signals + count + sample chips. These map 1:1 to the four-signal tiering input (has_tools, has_memory, touches_pii, is_multi_agent) used by ObservedSurface. #}
Capabilities
tools {% if recon_summary.has_tools %} {{ recon_summary.tool_count }} declared {% endif %} memory {% if recon_summary.has_memory and recon_summary.memory_keys %} {{ recon_summary.memory_keys|length }} key(s) {% elif recon_summary.has_memory %} conversational {% endif %} multi-agent {% if recon_summary.is_multi_agent %} orchestrator detected {% endif %} touches PII
{# Discovered tools — collapsible chip strip. Sample chips render eagerly; the full list lives behind a
drawer so a long tool catalogue doesn't crowd the page. #} {% if recon_summary.discovered_tools %}
Discovered tools
{% for tool in recon_summary.tool_sample %} {{ tool }} {% endfor %} {% if recon_summary.tool_count > recon_summary.tool_sample|length %} {# No-JS expander. The checkbox + extra chips + toggle label are flex siblings of the sample chips above; the label is ordered LAST in CSS so it trails the whole chip flow instead of sitting between the sample row and the extra chips. Only the tools NOT already in the sample are listed (no duplicates). #} {% for tool in recon_summary.discovered_tools if tool not in recon_summary.tool_sample %} {{ tool }} {% endfor %} {% endif %}
{% endif %} {# Sensitive actions — high-blast-radius tool names the recon agent flagged. Renders as a separate row so the operator can see them at a glance even when the full tool list is collapsed. #} {% if recon_summary.sensitive_actions %}
Sensitive actions
{% for action in recon_summary.sensitive_actions %} {{ action }} {% endfor %}
{% endif %} {# Declared guardrails — what the agent told us it WON'T do. These become refusal-baseline anchors: the evaluator uses them to grade whether an attack genuinely escalated past the agent's own stated limits. #} {% if recon_summary.declared_guardrails %}
Declared guardrails
    {% for g in recon_summary.declared_guardrails %}
  • {{ g }}
  • {% endfor %}
{% endif %} {# QA-059 (2026-06-03) — the legacy "Recon context" row (and its former "System-prompt clues" wording) has been removed. The field surfaced `TargetFingerprint.notes` verbatim: scan mode, transport, and shape detected during recon. Operators read it as either a leaked system prompt (the old label) OR as duplicative recon metadata (the QA-049 relabel) — neither framing carried weight. The capability chips + endpoint row already surface the same provenance signal. The `system_prompt_clues` / `system_prompt_clues_full` fields were dropped from `_assemble_recon_summary` in the same change; no other template consumes them. #} {# Skipped agents — mirror the swarm INFO log row that said "ASIxx skipped — reason". Surfaced here so the operator understands why their per-category coverage may be < 10. #} {% if recon_summary.skipped_agents %}
Skipped agents
    {% for s in recon_summary.skipped_agents %}
  • {{ s.asi }} {{ s.agent }} {{ s.reason }}
  • {% endfor %}
{% endif %}
{% endif %}